/* ══════════════════════════════════════════════════════════════════════════════
   base.css — reset and design tokens
   ══════════════════════════════════════════════════════════════════════════════

   Layer 1 of 3. Load order is always base → components → layout → module.

   The previous implementation had 6,051 lines across six per-app stylesheets
   where the per-app boundary was fiction: `patients.css` was loaded by 26
   templates across 6 apps, `.primary-button` was defined in three files, and
   `.form-grid` in three. One of those collisions shipped a real bug —
   `.regimen-form-field { display: grid }` is an author rule and beats the UA
   stylesheet's `[hidden] { display: none }` at equal specificity, so setting
   `element.hidden = true` did nothing (15_FRONTEND_REVIEW §5).

   The fix is not "more discipline", it is a layer with an actual boundary:

     base.css        reset, tokens              <- this file, no components
     components.css  button, badge, card, table, form control, panel
     layout.css      shell, header, sidebar, workspace, footer
     <module>.css    only what is genuinely unique to one module

   A colour, a radius or a spacing step never appears as a literal outside this
   file. If a value is needed twice, it is a token.
   ────────────────────────────────────────────────────────────────────────── */

:root {
  /* ── Brand ────────────────────────────────────────────────────────────────
     Used for the brand card and for accents only. The sidebar deliberately
     does NOT use these — see below. */
  --brand: #0E6B63;
  --brand-dk: #0A4F49;
  --brand-lt: #12897E;

  /* ── Sidebar ──────────────────────────────────────────────────────────────
     §0.3: "The sidebar has its own color family, visually distinct from the
     brand card. It must not read as a downward extension of the ALICE·OIS
     block — separate surfaces, separate colors, a visible gap between them."
     This is why these are separate tokens rather than darker --brand values. */
  --side: #12403A;
  --side-dk: #092623;
  --side-txt: #B9CFC9;
  --side-hover: rgba(255, 255, 255, .07);
  --side-active: rgba(255, 255, 255, .11);

  /* ── Accent ───────────────────────────────────────────────────────────────
     Amber. Marks the active navigation item and calls-to-action that create
     something. Never used for status — status has its own scale below. */
  --accent: #C77D2E;
  --accent-dk: #A8621F;
  --accent-lt: #F5E5D2;

  /* ── Chronology node colours (§4.4) ───────────────────────────────────────
     §4.4 asks for major nodes "color-coded by type", and the legend on that
     screen is what makes the coding readable. A colour that carries a meaning
     belongs here rather than in the module that draws it — the rule that no
     colour literal appears outside this file.

     Three are existing tokens under a name that says what they mean here; only
     the violet is new. **Never a status colour**: a surgery is not a warning,
     and --ok / --warn / --err must keep meaning what they mean. */
  --node-diag: var(--brand);
  --node-eval: var(--info);
  --node-rcp: var(--accent);
  --node-loco: #6A5A8C;
  /* Build step 7. A minor node, so a quieter ink rather than a fifth hue: §4.4
     colour-codes the *major* nodes by type, and a consultation competing with
     them for attention is what turns a timeline back into a list. */
  --node-consult: var(--ink-3);
  /* Build step 12. Two minor nodes, on the same reasoning as --node-consult —
     a hospitalisation entry or discharge is a visit, not one of §4.4's four
     major event types. Two distinct inks, not one: the legend refuses two
     sources sharing a colour (`oncology.tests.test_every_source_declares_
     what_the_template_reads`), and entry/discharge are exactly the pair a
     reader most wants to tell apart at a glance. */
  --node-hosp-entry: #8C6A4A;
  --node-hosp-discharge: #4A7A6A;
  /* Session 56 — §4.4's ninth node, and a minor one for --node-consult's own
     reason. A warm rose, chosen to sit apart from both hospitalisation inks
     (a cure and a stay are the two things a reader most often scans a course
     for) and from --node-loco's violet, which is the other treatment. */
  --node-chemo: #A05A78;

  /* ── Ink ──────────────────────────────────────────────────────────────────
     Text, darkest to lightest. --ink-3 is the lightest that may carry meaning;
     --ink-4 is decoration only and must never be the sole carrier of a
     clinical fact. */
  --ink: #16211F;
  --ink-2: #3E4A47;
  --ink-3: #6B7773;
  --ink-4: #9BA5A1;

  /* ── Surfaces ─────────────────────────────────────────────────────────────*/
  --line: #DEE4E1;
  --line-2: #EDF1EF;
  --bg: #F1F4F3;
  --card: #FFFFFF;
  --surf: #F7FAF9;

  /* ── Status ───────────────────────────────────────────────────────────────
     Four states, each with a text colour and a background. Every one of these
     pairs meets WCAG AA for normal text on its own background — a status a
     clinician cannot read on a tablet in daylight is not a status.
     Colour is never the only signal: every badge carries a word as well. */
  --ok: #2E8B58;    --ok-bg: #E7F3EC;    --ok-line: #CBE5D6;
  --warn: #B7791F;  --warn-bg: #FBF1DF;  --warn-line: #EDD9AE;
  --err: #B03A3A;   --err-bg: #FAEAEA;   --err-line: #EFCFCF;
  --info: #3A6DA6;  --info-bg: #E9F1F9;  --info-line: #D2E2F1;

  /* ── Shape ────────────────────────────────────────────────────────────────*/
  --r: 11px;        /* cards and panels */
  --r-s: 8px;       /* controls and buttons */
  --r-pill: 20px;   /* badges and pill tabs */
  --sh: 0 1px 2px rgba(20, 45, 42, .05), 0 4px 14px -6px rgba(20, 45, 42, .13);

  /* ── Spacing ──────────────────────────────────────────────────────────────
     §0.3: "Lateral padding: 16px, not the current larger value. The horizontal
     space belongs to the workspace." --gutter is that 16px and is the only
     value the shell uses for its outer padding. */
  --gutter: 16px;
  --gap: 12px;
  --gap-s: 6px;
  /* Between two distinct regions of one screen — a filter card and the list
     it filters, a stepper and its body — rather than between fields inside
     one of them. Found missing on the ward list: `--gap` alone read as no
     separation at all once the filter card lost its own trailing button. */
  --gap-lg: 28px;

  /* ⚠️ **Accueil's floor** (D743, re-measured at tier 2 — D749): the least
     height ONE CARD may be divided down to before the fixed layout gives up
     and the workspace scrolls.

     ⚠️ **« one card », and the word is load-bearing.** This token is read in
     two places and only one of them was enough: `layout.css` puts it on the
     flex ROW, and `components.css` now puts it on the GRID row inside that
     flex row. Without the second, a row of three cards that has wrapped to two
     grid rows divides the floor between them — and 150px becomes 70px, which
     is less than the chrome, which is zero pixels of list. **A floor divided
     among an unknown number of rows is not a floor.**

     ⚠️ **149px is a SUM, not a round number**, and each part was measured on
     the tier-2 page with real rows in it:

         card padding            16 + 16 = 32
         .worklist-head + margin           38
         ONE whole .worklist-row           49
         .worklist-link + margin           30
                                          ───
                                          149  → 150, rounded UP

     ⚠️ **The rounding is not cosmetic**: at exactly 149 the scroller came out
     48px against a 49px row — the row's own bottom border — so the first row
     was still a pixel short of whole and `overflow-y: auto` still engaged on
     a single-row card. A floor is a minimum; it rounds up.

     ⚠️ **It was 120px, and that is 30px short of one row** — so the scroller
     inside the card was 19px tall against a 49px row. `.worklist-rows` is
     `overflow-y: auto` and it *did* scroll; a 19px window onto a 49px row is
     simply not something anybody can read or aim at, which is how the user
     reported it: « these 3 tabs are not scrollable ». **A scroller too small
     to use reads exactly like no scroller at all.**

     ⚠️ **120px was measured honestly and against the wrong page**: the tier-1
     Accueil had no donut and no ward list, so zone 2 cost nothing and the
     worklists had two cards, not three. §9 trap 52 from the layout side — a
     measurement taken against data that did not exist yet.

     A token rather than a literal because the number is a judgement about
     READABILITY, and the page it is judged against grows as tiers are
     deployed: `ALICE_TIER` decides how many zones Accueil has. ⚠️ It was a
     `@media (max-height: 760px)` breakpoint until D743, which could only ever
     be calibrated against one of those pages.
     ⚠️ Declared HERE and not only as a `var()` fallback: a token that exists
     nowhere is one nobody can set, and `sync.test_transport.
     CssVariablesExistTests` refuses it (§9 trap 49 — an unknown custom
     property is dropped in the same silence as an invalid length). */
  --home-card-floor: 150px;
  /* ⚠️ **The donut needs its OWN floor and it is not the list floor.**
     `--home-card-floor` is about a LIST card — a heading, a footer link and
     rows between them, which can honestly shrink to one row. The donut is a
     fixed 140px graphic plus a legend that wraps: measured at 207px of
     content in a 150px box on a tablet, where the circle was simply cut off.
     A floor that is right for one kind of card is not a floor for another. */
  --home-donut-floor: 210px;

  /* ── Type ─────────────────────────────────────────────────────────────────
     The three families the mockup uses, now **self-hosted** — see the
     @font-face block below the reset. The system faces stay in each stack as a
     fallback: a missing or corrupted .woff2 must degrade to a readable page,
     never to an unstyled one.

     Nothing outside this block names a font. That was true when these were
     fallback-only stacks and it is what made self-hosting a contained change;
     keep it true. */
  --font-ui: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --fs: 13.5px;      /* body */
  --fs-s: 12.3px;    /* dense tables, secondary text */
  --fs-xs: 10.8px;   /* labels, captions */
  --fs-h: 20px;      /* page title */
}

/* ── Fonts ──────────────────────────────────────────────────────────────────
   Self-hosted, because an offline-first hospital terminal cannot reach
   fonts.googleapis.com. All three families are OFL-1.1 licensed and
   redistributable; the licence text ships beside the files in
   core/static/core/fonts/, which is what the licence requires.

   Three things about these declarations are deliberate:

   1. **`font-display: swap`.** Text renders immediately in the fallback face
      and swaps when the web font arrives. On a slow terminal the alternative
      (`block`) is a page of invisible text — and a clinician cannot read a
      dose that has not painted yet.
   2. **Variable fonts for Inter and Fraunces**, so `font-weight: 100 900` is
      one file per subset rather than one per weight. IBM Plex Mono has no
      variable cut on Google Fonts, so it ships as the two weights actually
      used — 400 for identifiers in tables, 600 for the recovery code.
   3. **latin and latin-ext are separate files with `unicode-range`.** The
      browser downloads latin-ext only when a glyph needs it. French sits almost
      entirely in latin; latin-ext covers the rest of what a name at CHU
      Treichville can contain.

   The ranges below are Google Fonts' own, kept verbatim: they are what the
   files were subset to, and inventing a wider range would tell the browser a
   file contains glyphs it does not. */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/inter-100-900-latin.65850a373e25.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/inter-100-900-latin-ext.01ba6c2a184b.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/fraunces-100-900-latin.5d2835174326.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/fraunces-100-900-latin-ext.3592119408ee.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/ibm-plex-mono-400-latin.814fe1615d4d.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/ibm-plex-mono-400-latin-ext.17a5b2a15bf5.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/ibm-plex-mono-600-latin.e71a597e376e.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/ibm-plex-mono-600-latin-ext.6041b3192dc3.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ── Reset ──────────────────────────────────────────────────────────────────
   Minimal on purpose. Only what removes a browser default that would otherwise
   have to be undone repeatedly. */

*,
*::before,
*::after { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
  font-family: var(--font-ui);
  font-size: var(--fs);
  line-height: 1.45;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: 600; }
p { margin: 0 0 .6em; }
ul, ol { margin: 0; padding-left: 18px; }

a { color: var(--brand-dk); text-decoration: none; }
a:hover { text-decoration: underline; }

img, svg { max-width: 100%; }

button, input, select, textarea { font: inherit; color: inherit; }

/* Every interactive element gets a visible focus ring. Keyboard access is a
   §0.7 requirement for the select component, but a ring that exists on only
   some controls is worse than none — a keyboard user loses their place the
   moment they tab onto the one that has no ring. */
:focus-visible {
  outline: 2px solid var(--brand-lt);
  outline-offset: 2px;
}

.mono { font-family: var(--font-mono); }

/* Visible to a screen reader, not on screen. For labels that would be
   redundant sighted but are the only context a screen reader gets. */
.visually-hidden {
  position: absolute;
  /* ⚠ Pinned to the containing block's origin, and this is what stops the
     whole document scrolling (D159's real cause, measured rather than guessed).

     Without `top`/`left` an absolutely-positioned element sits at its **static
     position** — wherever it would have been in flow. A `<th><span
     class="visually-hidden">Actions</span></th>` low in an inner-scrolled panel
     therefore had `top: 1115px` on a 720px viewport. And an `overflow: hidden`
     ancestor only clips absolutely-positioned descendants when that ancestor is
     itself positioned: every ancestor up to `.shell` is `position: static`, so
     the span's containing block was the initial one, `.shell`'s clipping could
     not reach it, and the document grew to 1115px to contain it. The page then
     scrolled by the difference and the footer rose with background beneath —
     exactly the symptom reported, and exactly why it appeared on some screens
     and not others.

     `100dvh` and `overflow: hidden` on the root were the right fix for a
     different cause and were never enough for this one. Where the element sits
     is irrelevant to a screen reader, which follows DOM order. */
  top: 0; left: 0;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── Motion ─────────────────────────────────────────────────────────────────
   Honour a system-level request for reduced motion. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}
