/* ==========================================================================
   Aleris Design Tokens — Canonical Reference
   ==========================================================================
   Source of truth for all Aleris digital products.
   Three-layer architecture: Primitives → Semantic → Component.

   Figma is the upstream source for primitives. This file consolidates
   Figma exports with documented design decisions. Where values differ
   from Figma, a FIGMA-UPDATE comment marks what needs syncing back.

   Last updated: May 2026
   Maintainer: Torfinn Almers, Head of Design, Aleris Group
   ========================================================================== */

/* Companion stylesheet for Museo Sans @font-face declarations:
     baseline/tokens/aleris-fonts.css
   Load order: tokens, then fonts. Fonts file references --font-family-primary
   defined here. */


/* --------------------------------------------------------------------------
   LAYER 1: PRIMITIVES
   Raw values. No semantic meaning. Platform-independent.
   Naming convention: category.group.scale (100=light/tint, 300=mid, 500=full)
   -------------------------------------------------------------------------- */

:root {

  /* --- Colors: Brand --- */
  --color-petrol-100: #d9e1e2;   /* @usage Tinted backgrounds, selected row, inverted primary hover fill | @constraint Never as text color */
  --color-petrol-200: #abc7c9;   /* @usage Tinted backgrounds a step down from petrol-100 | @constraint Never as text color – too light */
  --color-petrol-300: #7fa9ae;   /* @usage Secondary accents, hover tints | @constraint Not for body text – too light. Never behind white text: 2.56:1 */
  --color-petrol-400: #4f868e;   /* @usage Mid petrol for structure and accents | @constraint Not for body text */
  --color-petrol-450: #0f6b73;   /* @usage The far stop of --surface-gradient-cold, and the petrol to reach for on any petrol ground light text sits on | @constraint Measured 2026-09-13, all four light roles clear 1.4.3's 4.5:1 body floor on it: white 6.23:1, petrol-100 4.69:1, sand-50 5.88:1, sand-100 5.31:1. VALUE TAKEN FROM sundviktlakemedel, which minted this token under this exact name on 2026-09-02, eleven days before Brand OS did, and for the same reason — a ghost Button inside its WaitPanel rendered at 1.00:1 against the top-left of its own gradient panel, so five of seven seeded patients had an unreachable action on their home screen. Brand OS independently added the same name at #437d85 on 2026-09-13, chosen as minimum headroom over white alone and placed on the petrol-500 → petrol-400 interpolation so the gradient's appearance would not change. That criterion was too narrow: at #437d85 petrol-100 measures 3.51:1 and a secondary line fails, which the consumer's value had already solved. Torfinn ruled for the consumer's value on 2026-09-13. IT IS NOT ON THE INTERPOLATION LINE — it is a deeper, more saturated teal, 52/18/18 in RGB from #437d85 — so the gradient does look different, and that is the cost of the wider floor rather than an oversight. */
  --color-petrol-500: #004851;   /* @usage Primary text, headings, structural elements | @constraint Default text color. Only specify when deviating */
  --color-petrol-700: #003238;   /* @usage Secondary/confirm button press fill | @constraint Interaction only, never decorative or print. White text 13.88:1. Added 2026-08-05, card 19 – hover no longer needs a colour step (card 16, the hover/focus merge), so only the press step was needed, not the petrol-600 the design return also proposed */

  --color-sand-50: #faf8f6;     /* @usage Page background for instrumental surfaces | @constraint Tools, admin, dashboards only. Communicative uses sand-100 */
  --color-sand-100: #f2ece4;    /* @usage Page background for communicative surfaces | @constraint Patient-facing, marketing, booking. Never use white as page bg */
  --color-sand-300: #e7ceb5;
  --color-sand-500: #d9b48f;

  /* --- Colors: Orange ---
       Seven steps. The ramp splits by job, not only by lightness: 100-500 are
       brand tints used in print and digital, 600-700 are interaction fills that
       exist to hold contrast on screen and have no print counterpart by design.
       SUPERSEDE 2026-07-29: orange-500 is no longer the primary button fill –
       white text on it fails AA. The interactive role moved to orange-600/700.
       orange-400 and orange-200 were briefly retired on 2026-07-28 and
       reinstated on 2026-07-29: both carry print specifications, so the
       retirement was decided on the digital half of the palette only. */
  --color-orange-100: #fde8df;
  --color-orange-200: #fbd1c0;   /* @usage Soft tint, backgrounds and surfaces. Print and digital | @constraint Decorative/background only, not for interactive states */
  --color-orange-300: #ffbe9f;   /* @usage Light accent backgrounds, soft orange tints | @constraint Decorative/background only, not for interactive states */
  --color-orange-400: #faaa8d;   /* @usage Accent tint, backgrounds and surfaces. Print and digital | @constraint Decorative/background only, not for interactive states. Never an interactive state – this is the exact hex the retired primary hover pointed at */
  --color-orange-500: #f58c61;   /* @usage Accent, decorative, print. The brand orange | @constraint Brand use only. Never an interactive state or interactive fill – use orange-600/700 */
  --color-orange-600: #d14811;   /* @usage Primary button fill, interactive orange | @constraint Interaction only, never decorative or print. White text 4.52:1. Never on a petrol surface */
  --color-orange-700: #b23c0e;   /* @usage Interaction hover and active fill | @constraint Interaction only, never decorative or print. White text 5.92:1 */

  /* --- Colors: Legacy ---
       Heritage brand colours kept in the palette at a legacy tier, not retired.
       In-set (so conformance treats them as named palette colours, not arbitrary
       hex) but flagged @tier legacy so new use warns rather than passes silently.
       Posture: replace on touch — swap for the core palette when a surface is next
       updated; no removal campaign. Other retired values may inherit this tier.
       NOTE: distinct from --color-chart-01-teal (#0f9081), an unrelated chart colour. */
  --color-legacy-teal: #00c6b2;  /* @usage Legacy / heritage brand colour (formerly "turquoise"); existing surfaces only, predominantly Norway | @constraint Not for new work; replace on touch. In-set legacy — conformance warns, never errors, on new use | @tier legacy */

  /* --- Colors: Neutral --- */
  --color-gray-100: #d7d2cb;
  --color-gray-300: #9e9281;
  --color-gray-500: #585044;

  --color-white: #ffffff;        /* @usage Card/surface backgrounds, inverse text bg | @constraint Never as page background. Pages are sand-100 or sand-50 */

  /* --- Colors: Feedback ---
       FIGMA-UPDATE: Figma has only warning-100 (#c14444). This file adds
       error-500 (consolidated from Figma's warning-100) and warning-500
       (from documented amber, fits sand family). Sync back to Figma. */
  --color-error-500: #c14444;    /* @usage Validation errors, destructive actions | @constraint State communication only. Never decoration */
  --color-warning-500: #d9b48f;  /* @usage Warning states, caution indicators | @constraint State communication only. Same hex as sand-500 — context differentiates */
  --color-info-500: #007bc7;     /* @usage Informational states — neutral notices that are neither a problem nor a completion | @constraint Indicator, or text on white only. Measured 2026-09-13: 4.51:1 on white, 4.26:1 on sand-50, 3.84:1 on sand-100 — so it clears AA for text on white and NOT on either sand ground, the same shape as confirm-500 and governed by the same rule (BASELINE.md § Forms, card 118: state text sits on a white card or an instrumental ground, never directly on the communicative one). Clears the 3:1 boundary floor on all three as a UI component. Added 2026-09-13, board card 156, because the design-system export carried an informational state the token set had no name for. Its value there was petrol-500, which is --text-primary — an info state in body-text colour does not read as a state, so this takes the blue already in the system as --color-goal-no-data rather than that. NOT an alias of that token: the goal set is constrained "Dashboard only, never patient-facing", and tying a general status to it would give one value two unrelated roles with nothing comparing them. Same hex, separate declarations, on the precedent warning-500/sand-500 already set. */

  /* --- Colors: Confirm ---
       Completion/confirmation action color. Distinct from goal-achieved
       (dashboard indicator) — this is for interactive "mark done" actions.
       Evidence: Hälsodeklarationer prototype "Klarmarkera" button.
       DECISION 2026-06-12: value aligned to Foundation F1 (#4f866e),
       replacing earlier #27ae60. White text on #4f866e = 4.23:1 — passes
       AA for large text/UI components; #27ae60 failed at 2.87:1.
       FIGMA-UPDATE: Not yet in Figma. Add as action color. */
  /* Narrowed 2026-07-31, applied here 2026-08-01 with the confirm sweep. The
     value is unchanged and the role is not: white on it is 4.23:1, so it never
     cleared the floor as an interactive fill. The confirm button variant was
     retired rather than recoloured – BASELINE.md hard rule 5 is the rule of
     record. Both greens are indicators now, told apart by what they indicate.
     The button tokens below now alias the secondary cluster (card 19,
     2026-08-05, applying card 16's decision) – this indicator value is no
     longer reachable through any --button-* fill. Card 23, the pairing rule,
     remains undecided and does not touch this: it would unwind the alias, not
     this indicator's value. */
  --color-confirm-500: #4f866e;  /* @usage Completion indicator – status dots, badges, chips | @constraint Indicator only, never an interactive fill: white on it is 4.23:1. Distinct from goal-achieved green, which indicates "target met" */

  /* --- Colors: Goal Status ---
       Traffic light indicators for KPI/goal dashboards only.
       Never use in patient-facing interfaces.
       MUST always pair with icon or shape. Reminder only – the rule lives at
       constitutional/accessibility-is-foundational.md */
  --color-goal-achieved: #2e8540;   /* @usage KPI target met indicator | @constraint Dashboard only. Always pair with icon/shape. Never for interactive confirm */
  --color-goal-borderline: #ffb81c; /* @usage KPI near-target indicator | @constraint Dashboard only. Always pair with icon/shape */
  --color-goal-missed: #d4351c;     /* @usage KPI target missed indicator | @constraint Dashboard only. Always pair with icon/shape */
  --color-goal-no-data: #007bc7;    /* @usage KPI no-data indicator | @constraint Dashboard only. Always pair with icon/shape */

  /* --- Colors: Data Visualization ---
       Dedicated chart/graph palette. Deliberately distinct from brand palette
       to prevent confusion between data points and interactive UI elements.
       Ordered by recommended usage sequence (start with 01, add as needed).
       Cool series (01-06) and warm series (07-12) can pair by index. */
  --color-chart-01-teal: #0f9081;
  --color-chart-02-mint: #94cbc4;
  --color-chart-03-blue: #577ba3;
  --color-chart-04-blue-light: #98c9ef;
  --color-chart-05-purple: #a078c2;
  --color-chart-06-purple-light: #d8b8ef;
  --color-chart-07-terracotta: #d77a61;
  --color-chart-08-terracotta-light: #f0c3b2;
  --color-chart-09-marine: #6b9495;
  --color-chart-10-olive: #bed0c0;
  --color-chart-11-warm-grey: #e4ded5;
  --color-chart-12-dark-sand: #d9b48f;
  /* Note: chart-12 dark-sand is same hex as sand-500 and warning-500.
     Context differentiates: sand-500 = brand primitive, warning-500 = UI state,
     chart-12 = data category. If chart-12 appears alongside a warning state
     in the same view, consider using a different chart color. */

  /* --- Spacing ---
       Modular scale: 1.5 ratio aligned to 4px grid.
       Shares ratio with type scale for cross-dimensional harmony.
       FIGMA-UPDATE: Replaces previous spacing scale. Sync to Figma. */
  --spacing-0: 0px;          /* @usage Zero gap, collapsed state | @constraint Use explicitly — don't omit spacing, set it to 0 */
  --spacing-3xs: 4px;       /* @usage Minimum gap, icon padding, label-to-field | @constraint Smallest usable gap. Never go below this */
  --spacing-2xs: 8px;       /* @usage Tight element spacing, inline gaps | @constraint Related inline elements */
  --spacing-xs: 12px;       /* @usage Related element spacing, input padding-y | @constraint Default vertical rhythm within components */
  --spacing-sm: 16px;       /* @usage Default component padding, grid margin mobile | @constraint Most common padding value */
  --spacing-md: 24px;       /* @usage Section spacing within components, card padding | @constraint Primary structural spacing */
  --spacing-lg: 36px;       /* @usage Between components, above h2 | @constraint Component-level separation */
  --spacing-xl: 48px;       /* @usage Between sections | @constraint Section-level separation */
  --spacing-2xl: 72px;      /* @usage Major section breaks | @constraint Communicative surfaces mainly */
  --spacing-3xl: 96px;      /* @usage Page-level spacing | @constraint Communicative surfaces only. Never on instrumental */

  /* --- Border Radius ---
       Four-tier system based on functional role:
       s (4px)    → general containers: panels, modals, tables
       l (16px)   → cards specifically
       m (8px)    → interactive: buttons, inputs, dropdowns, action cards
       full       → compact indicators: badges, tags, status dots

       radius-l retired 2026-08-07 at its old value (12px, sat reserved and
       unassigned from creation — no component ever took it, and the only
       document that assigned it was a stale reference table). Reinstated
       2026-08-10 at a new value, board card 45: computed styles measured
       live on aleris.se/.no/.dk showed every card shipping at 16px against
       Baseline's 4px, and the 4px container tier inverts the concentric-
       corner convention against anything nested flush inside it (an outer
       radius must be at least its inner radius plus the gap). Cards only,
       for now — panels, modals and tables were never measured live and stay
       --radius-s until they are; see BASELINE.md § Component shapes and
       workspace/archive/2026-08-07-radius-contradiction-and-nesting.md /
       2026-08-10-radius-verify-prototype-and-package.md. */
  --radius-0: 0px;            /* @usage Sharp corners, full-bleed edges | @constraint Explicit removal of rounding */
  --radius-s: 4px;            /* @usage General containers: panels, modals, tables | @constraint General container tier. Cards use --radius-l instead. Not for buttons or inputs */
  --radius-l: 16px;           /* @usage Cards only | @constraint Card tier, decided 2026-08-10 (board card 45) from a live production measurement. Not a general container value */
  --radius-m: 8px;            /* @usage Interactive: buttons, inputs, dropdowns | @constraint Interactive element tier. Not for containers */
  --radius-full: 100px;       /* @usage Compact indicators: badges, tags, avatars | @constraint Indicator tier only. No full-pill buttons exist in Aleris */

  /* --- Stroke --- */
  --stroke-0: 0px;
  --stroke-xs: 1px;
  --stroke-m: 2px;
  --stroke-xl: 4px;

  /* --- Typography: Font Family --- */
  --font-family-primary: 'Museo Sans', Arial, sans-serif;
  --font-family-fallback: Arial, sans-serif;
  --font-family-icons: 'Font Awesome 6 Pro';

  /* --- Typography: Font Size ---
       Perfect fifth modular scale. Ratio: 1.5. Base: 18px.
       Shares ratio with spacing scale for cross-dimensional harmony.
       Half-steps (sm, h4) use geometric means for pragmatic in-between sizes.

       CORRECTED 2026-08-05, card 18/19. Every rem value below used to assume
       a root font-size of 18px – this comment block said so directly, and
       `--font-size-xs` was commented "14px — accessibility floor". Nothing
       in `app/` has ever set the root to 18px: no `html { font-size: … }`
       exists anywhere, confirmed by search, and there is no Tailwind config
       to supply one. At the real, unset browser default (16px), every size
       rendered ~11% smaller than documented – including the floor itself,
       at 12.48px rather than 14px. One root cause, not seven: the px
       targets below (14/16/18/22/27/40/60) were always the right design
       values, only the rem numbers computed against a root that was never
       real. Rewritten to hit the same px targets at 16px instead – the
       modular ratios between sizes are unchanged, because every value
       scales by the same constant (16/18). See `tokens.test.ts`, "the 14px
       floor", and `baseline/conformance/index.html` § The 14px floor for
       the measurement that found this and now verifies the fix.

       FIGMA-UPDATE: Full scale replacement. Sync all sizes to Figma. */
  --font-size-xs: 0.875rem;     /* 14px @16px root — accessibility floor */
  --font-size-sm: 1rem;         /* 16px @16px root — labels, UI chrome */
  --font-size-base: 1.125rem;   /* 18px @16px root — body text. Scale anchor. */
  --font-size-md: 1.375rem;     /* 22px @16px root — h4 half-step */
  --font-size-lg: 1.6875rem;    /* 27px @16px root — base × 1.5 */
  --font-size-xl: 2.5rem;       /* 40px @16px root */
  --font-size-2xl: 3.75rem;     /* 60px @16px root */

  /* --- Typography: Font Weight ---
       Mapped to actual Museo Sans font files (woff2).
       Available weights: 100, 300, 500, 700, 900 (plus italics).
       IMPORTANT: Museo Sans has no 400 weight. Using font-weight: 400
       causes browser synthesis — always use these tokens instead.
       When falling back to Arial, 500 renders slightly heavier than
       Arial's 400 but is acceptable. */
  --font-weight-light: 300;     /* Museo Sans 300. @usage Large display text on communicative surfaces | @constraint Exceptional use only. Size must be xl (40px) or above */
  --font-weight-regular: 500;   /* Museo Sans 500 (Medium). @usage Body text, labels, buttons, UI chrome | @constraint The workhorse. Never use 400 — Museo Sans has no 400 weight */
  --font-weight-bold: 700;      /* Museo Sans 700. @usage Headings, emphasis, CTAs | @constraint The other workhorse. 500 and 700 are the system */
  --font-weight-black: 900;     /* Museo Sans 900. @usage Almost never | @constraint Exceptional use only. Try 700 at larger size first */

  /* --- Typography: Line Height ---
       Values chosen to resonate with the type and spacing scales.
       body 18px × 1.5 = 27px (= h3 font size)
       h3 27px × 1.33 = 36px (= spacing-lg)
       h2 40px × 1.2 = 48px (= spacing-xl) */
  --line-height-tight: 1.1;     /* Display text (60px) */
  --line-height-heading: 1.2;   /* h1, h2 */
  --line-height-subheading: 1.33; /* h3, h4 */
  --line-height-body: 1.5;      /* Body text — 18 × 1.5 = 27px = h3 size */

  /* --- Typography: Letter Spacing ---
       CORRECTED 2026-09-04. The inline comments below used to read "h2, h3" on
       --letter-spacing-tight and "h1" on --letter-spacing-tighter. The
       --type-*-letter-spacing composites in the Component layer have never
       assigned them that way: h1 takes tightest, h2 takes tighter, h3 takes
       tight, h4 and lead take normal. The comments described a scheme the file
       does not implement, and a reader who stopped here was told the wrong one.
       Found by an external consumer that read these four lines instead of the
       composites and filed a correction against its own — correct — values.
       Kept as one of a stale comment's costs: it produced a false finding
       downstream, silently, in the half of the file that looks authoritative.
       Asserted by tokens.test.ts § "letter-spacing roles match the composites
       that consume them", so the two halves cannot disagree again. */
  --letter-spacing-normal: 0;         /* h4, lead, body, labels */
  --letter-spacing-tight: -0.01em;    /* h3 (27px) */
  --letter-spacing-tighter: -0.015em; /* h2 (40px) */
  --letter-spacing-tightest: -0.02em; /* h1 (60px), the display size */

  /* --- Shadows ---
       Petrol-tinted for brand coherence. Mapped to Figma's e0–e3 scale.
       FIGMA-UPDATE: Figma uses black-based shadows. Update Figma to
       petrol-tinted values for brand consistency. */
  --shadow-e0: 0px 0px 0px rgba(0, 72, 81, 0);
  --shadow-e1: 0px 2px 8px rgba(0, 72, 81, 0.08);
  --shadow-e2: 0px 4px 16px rgba(0, 72, 81, 0.12);
  --shadow-e3: 0px 6px 24px rgba(0, 72, 81, 0.16);

  /* --- Layout Grid ---
       12-column grid. Gutters use spacing tokens.
       Max-width varies by surface temperature:
       communicative = capped for reading comfort,
       instrumental = wider or fluid for data density. */
  --grid-columns: 12;
  --grid-gutter: var(--spacing-md);
  --grid-margin-mobile: var(--spacing-sm);
  --grid-margin-desktop: var(--spacing-md);
  --grid-max-width-communicative: 1200px;
  --grid-max-width-instrumental: 1440px;

  /* --- Column ladders, decided 2026-09-05, board card 120 (Torfinn, by looking) ---
       Chosen from baseline/conformance/layout/index.html: three candidates per
       surface rendered at 320/390/640/768/1024/1280/1440 from this file and the
       licensed face, each frame reporting its own column width and characters
       per line. The decision is the shape; the numbers below are what the
       shape measures, stated so the next reader knows they were seen.

       Communicative: ONE column at every width, and it grows. 40rem (640px) up
       to a 48rem window, then a quarter of every further pixel, held at 48rem
       (768px) from an 80rem window up. Both anchors are --breakpoint-sm and
       --breakpoint-md — asserted, since a clamp() cannot read a custom
       property and these are literals only meaningfully the tokens' values
       while something checks. Promoted from sundviktlakemedel's patient view,
       where Torfinn chose "grow the one column" over a second column on
       2026-09-02. DELIBERATE DEPARTURE, recorded rather than hidden: at 18px
       body in Museo Sans (measured average 7.31px per character) this column
       holds 83 characters at 640, 88 at 768, 96 at 1024 and 105 at 1280 —
       past the 45–75 reading guideline from 640px up. A measure-capped
       alternative (548px, 75 characters) was rendered beside it and not
       chosen. "6–8 of 12 grid columns" in BASELINE.md was never a measure
       proxy either — 600–800px is 82–109 characters — and is superseded by
       this token.

       Instrumental: fluid, no breakpoints. Tiles are however many columns of
       --grid-tile-min-instrumental fit: measured one-up to 390, two at 640,
       three at 768, four at 1024, FIVE at 1280 and 1440 — so a row of four
       tiles has an empty fifth slot from 1280 up. Rendered, seen, chosen. The
       stepped alternatives (12→6→1, 12→1) were beside it and not chosen. */
  --grid-column-communicative: clamp(40rem, 40rem + (100vw - 48rem) / 4, 48rem); /* @usage The reading column on a communicative surface — max-width of the content column inside --grid-max-width-communicative | @constraint One column at every width. Anchors are --breakpoint-sm and --breakpoint-md and are asserted equal to them. Holds 88 characters of 18px body at 768 and 105 at 1280 — chosen knowing that (card 120) */
  --grid-tile-min-instrumental: 14rem;    /* @usage Minimum tile width for an instrumental grid: grid-template-columns: repeat(auto-fit, minmax(var(--grid-tile-min-instrumental), 1fr)) | @constraint No breakpoints; the column count falls out of the width. Four tiles land on five columns from 1280 up — chosen knowing that (card 120) */

  /* --- Breakpoints ---
       Practical values, not derived from the modular scale.
       Use container queries where possible; these cover viewport fallbacks. */
  --breakpoint-sm: 640px;
  --breakpoint-md: 768px;
  --breakpoint-lg: 1024px;
  --breakpoint-xl: 1280px;

  /* --- Icons ---
       Font Awesome 6 Pro (licensed). Available styles: solid, regular, light.
       Icon sizes follow a scale independent of typography.
       FIGMA-UPDATE: Move icon sizes out of text-size styles in Figma. */
  --icon-xs: 12px;       /* Inline indicators, badge icons */
  --icon-sm: 16px;       /* Button icons, form field icons */
  --icon-md: 18px;       /* Default inline icon (matches body text) */
  --icon-lg: 24px;       /* Navigation, card header icons */
  --icon-xl: 48px;       /* Feature icons, empty states */


  /* --------------------------------------------------------------------------
     LAYER 2: SEMANTIC TOKENS
     Map primitives to usage context. Carry meaning.
     These are the tokens product teams should reference in code.
     -------------------------------------------------------------------------- */

  /* --- Brand Role --- */
  --brand-primary: var(--color-petrol-500);
  --brand-accent: var(--color-orange-500);
  --brand-light: var(--color-white);

  /* --- Text ---
       --text-accent REPOINTED 2026-09-04, from --color-orange-500 to
       --color-petrol-400. It was a semantic *text* alias resolving to a value
       that fails at every size: orange-500 is 2.38:1 on white, below the
       4.5:1 normal-text floor and below the 3:1 large-text floor as well, so
       no font size or weight could have made it conformant. foundation/colour.md
       gives orange-500 as "Brand – accent, decorative, print. The brand orange"
       and never as text; the same table gives petrol-400 as "Structure and
       accents. Large text only, from 18 pt or 14 pt bold. Not for body text —
       4.09:1 on white". Foundation takes precedence over Baseline (BASELINE.md,
       second sentence), so this was Baseline contradicting Foundation rather
       than a decision either document had made.
       Repointed rather than retired because Foundation does specify an accent
       text colour and this is where it belongs; the constraint that makes it
       usable now travels with it, which is what was missing. Grepped before
       changing: zero consumers in this repo, so nothing rendered was wrong —
       this was a loaded trap, not a live failure. --brand-accent stays on
       orange-500, which is the role Foundation actually assigns it.
       Asserted by tokens.test.ts § "semantic text tokens are legible at the
       size they claim". */
  --text-primary: var(--color-petrol-500);
  --text-secondary: var(--color-gray-500);
  /* --text-tertiary RETIRED 2026-09-04, board card 115 (Torfinn's ruling).
     Written without a colon after the name on purpose. The parser guard in
     tokens.test.ts reads any name-then-colon-then-value sequence inside a
     comment as a real declaration, so a retirement note in that shape
     resurrects the token whose death it records — and this note tripped that
     guard on its first run, by quoting the offending shape as an example of
     the offending shape. It held --color-gray-300 (#9e9281): 3.05:1 white,
     2.88:1 sand-50, 2.60:1 sand-100 — failing AA for normal text on all three
     grounds and the 3:1 large-text floor on both sands, so it carried text at
     no size on a sand ground.
     Retired rather than repointed, because the value was never the defect.
     gray-300 is right for what it is actually used for, and --text-disabled
     and --state-disabled-text already hold it. The name was the defect: a
     token reading as "the third level of body text" is reached for as caption
     ink, and it was — three times in one hour while building the site (card
     115's own evidence), each time as a caption, each time failing. Renaming
     it would have kept a fourth name for one value; removing it closes the
     invitation at the source.
     A consumer survey ran before the removal rather than after: 849 files
     across patientguide, sundviktlakemedel, aleris-assistant,
     aleris-vibe-coding and this repo hold exactly one text use of it —
     patientguide's print stylesheet, on the (url) suffix after an external
     link, which moves to --text-secondary. Three uses of the primitive as a
     border colour in sundviktlakemedel are unaffected: non-text, and 3.05:1
     clears the 3:1 boundary floor.
     Deliberately NOT replaced by a caption tier. There is nothing between
     gray-300 and gray-500 (7.94/7.49/6.76); the lightest warm grey clearing
     4.5:1 on all three grounds is around #6d6456 (5.82/5.49/4.96), which sits
     about 1.4 ratio steps from gray-500 and would not read as a separate
     tier. A caption wants --text-secondary.
     Asserted by tokens.test.ts § "retired tokens do not come back". */
  --text-inverse: var(--color-white);
  --text-accent: var(--color-petrol-400);              /* @usage Accent text — a figure or short phrase lifted off the body | @constraint Large text only: 18px+ regular, or 14px+ bold. 4.09:1 white, 3.86:1 sand-50, 3.49:1 sand-100 — clears 3:1 on all three, clears 4.5:1 on none. Never body text */
  --text-link: var(--color-petrol-500);
  --text-disabled: var(--color-gray-300);
  --text-error: var(--color-error-500);                 /* @usage Error text — field validation, destructive-action warnings | @constraint 5.03:1 white, 4.74:1 sand-50, 4.28:1 sand-100 — normal-text-safe on white and sand-50, large-text-only on sand-100, which is the communicative page ground where a field error most often sits. Found 2026-09-04 by the check in tokens.test.ts § "semantic text aliases resolve to a colour that may carry text"; not repointed, because the value is Foundation's and what to do about the sand-100 case is a decision — board card 118 */

  /* --- Backgrounds / Surfaces ---
       Two page surfaces by temperature:
       Communicative (patient-facing, marketing, booking): sand-100.
         Cards are white on sand — warmth is the brand, cards provide editorial structure.
       Instrumental (tools, admin, documentation, dashboards): sand-50.
         Near-white with faint sand warmth. Content lives directly on the surface.
         Cards used sparingly — only when content genuinely needs visual grouping.
       The surface choice drives whether cards are structural (communicative)
       or optional (instrumental). */
  --surface-page: var(--color-sand-100);                /* @usage Communicative page background | @constraint Patient-facing, marketing, booking. Cards mandatory on this surface */
  --surface-page-instrumental: var(--color-sand-50);    /* @usage Instrumental page background | @constraint Tools, admin, dashboards. Cards optional on this surface */
  --surface-card: var(--color-white);                   /* @usage Card and panel backgrounds | @constraint Always white. Never sand on cards */
  --surface-elevated: var(--color-white);               /* @usage Dropdowns, popovers, tooltips | @constraint White with elevation shadow */
  --surface-overlay: var(--color-white);                /* @usage Modal and dialog backgrounds | @constraint White with highest elevation */

  /* Surface temperature: neutral/warm/cold variants.
     These support the communicative (warm) vs. instrumental (cold/neutral)
     surface modes described in the design governance document. */
  --surface-subtle-neutral: var(--color-sand-100);
  --surface-subtle-warm: var(--color-orange-100);
  --surface-subtle-cold: var(--color-petrol-100);
  --surface-strong-neutral: var(--color-sand-500);
  --surface-strong-warm: var(--color-orange-500);
  --surface-strong-cold: var(--color-petrol-500);

  /* Gradient grounds. Named for the axis this file already uses — cold, warm — rather than for
     the pigment, because the semantic layer names a role and --gradient-petrol would be
     primitive-layer thinking. Each starts on its own --surface-strong-* value and lightens one
     step, so the three cold surfaces are one family a reader can predict. Added 2026-09-13,
     board card 156, from the design system export; nothing canonical expressed a gradient before
     this, which is the gap board card 121 was opened on — a live action shipped invisible on
     five of seven seeded home screens because no control variant is defined against a branded
     ground. Adding the ground does not answer 121: which controls may sit on one, and how a
     check measures text over a gradient, are still that card's. */
  --surface-gradient-cold: linear-gradient(135deg, var(--color-petrol-500) 0%, var(--color-petrol-450) 100%);
  /* @usage Branded cold ground, and the only gradient that carries text | @constraint All four
     light text roles hold end to end, measured 2026-09-13: white 10.27 → 6.23, petrol-100
     7.73 → 4.69, sand-50 9.69 → 5.88, sand-100 8.75 → 5.31, every one clearing the 4.5:1 body
     floor at both stops. Dark text is not the question here — this is a dark ground.
     TWO RETUNES IN ONE DAY, and the second is the one that matters. It shipped running to
     petrol-400, where white is 4.09:1, and the design system's own painting puts 16px white body
     text on it eleven times, so a heading started legible and ended illegible along its own
     background. The first retune fixed white and left petrol-100 at 3.51:1. The second took
     sundviktlakemedel's value, which had already cleared both — a floor chosen against ONE text
     colour is how the second one keeps failing.
     `lib/gradient-contrast.test.ts` asserts all four rather than leaving this comment to be
     believed. */
  --surface-gradient-warm: linear-gradient(135deg, var(--color-orange-500) 0%, var(--color-orange-300) 100%);
  /* @usage Branded warm ground | @constraint DARK TEXT ONLY. Measured 2026-09-13: white is 2.38:1
     on the near stop and 1.60:1 on the far stop — nowhere near any floor, at either end, so white
     text on this ground is never permissible rather than conditionally so. Petrol-500 on the far
     stop is 6.41:1 and clears AA. This is a decorative ground, not a text surface. */

  /* --- Borders --- */
  --border-default: var(--color-gray-100);
  --border-strong: var(--color-gray-300);
  --border-focus: var(--color-petrol-500);
  --border-error: var(--color-error-500);

  /* --- Interactive States ---
       REVISED 2026-08-05 with card 19, applying board decisions 16 and 17.
       Two structural changes: hover stopped being a fill (card 16 – hover and
       focus merged into one ring-based signal, so every --button-*-hover-bg
       token and --state-hover are gone), and active became per-variant for
       buttons rather than falling back on a generic tint (card 17). */

  /* RETIRED 2026-08-05, card 16 (was --color-orange-700). Hover no longer
     changes a fill on any variant – see the dual ring below. A generic
     fallback for a state that no variant uses any more is dead weight rather
     than a safety net; reword this note if a future non-button component
     genuinely needs a hover colour rather than widening this one back out. */

  /* RETIRED 2026-07-31, card 17 (was --color-sand-500, also
     --surface-strong-neutral). A surface tint, never a button fill: white on
     it is 1.93:1. No variant defined its own active fill, so all six fell
     back on this token and failed without anything in the file saying so.
     Removed rather than repointed – a generic fallback for a per-variant
     state is exactly the mechanism that hid the failure. Buttons now use
     --button-*-active-bg plus --state-press-shade below. */

  /* Focus ring – dual, card 17. One petrol ring was specified for six variants
     on two surfaces: petrol-on-petrol (1.0:1) on secondary, petrol-on-petrol-
     page (1.0:1) on primary-inverse. A single colour cannot serve every
     ground, and a surface-aware ring fails silently when a button lands on
     the wrong one. Both rings are always drawn on focus – 2px inner, 2px
     outer – so whichever ground the button sits on, one ring contrasts. On
     petrol surfaces the two swap: see --button-primary-inverse-focus-ring*.
     On hover alone, card 16's merge draws the outer ring only, no fill
     change – see BASELINE.md § Buttons. */
  --state-focus-ring: var(--color-petrol-500);        /* Outer ring. Reads against light page surfaces */
  --state-focus-ring-inner: var(--color-white);       /* Inner ring. Reads against the button's own fill */
  --state-focus-ring-width: var(--stroke-m);          /* 2px per ring */

  /* Press affordance, card 17. Supplementary to the accessible state, which
     the fill and (on focus) the ring already carry – this is not asserted in
     the fitness check. Deliberately neutral rather than petrol-tinted: a
     petrol inset on a petrol fill is invisible, the same mistake the single
     focus ring made. */
  --state-press-shade: rgba(0, 0, 0, 0.4);
  --state-press-translate-y: 1px;   /* Dropped under prefers-reduced-motion; the inset shade stays */

  --state-disabled-bg: var(--color-gray-100);   /* Inputs only – buttons do not disable, see BASELINE.md § Buttons */
  --state-disabled-text: var(--color-gray-300);

  /* --- Feedback / Status ---
       Reserved for state communication only. Never decoration. */
  --status-error: var(--color-error-500);
  --status-warning: var(--color-warning-500);
  --status-confirm: var(--color-confirm-500);
  --status-info: var(--color-info-500);
  --status-background: var(--color-white);

  /* --- Goal Status (semantic) ---
       Dashboard KPI indicators. Instrumental surfaces only.
       Always pair with icon or shape. Reminder only – the rule lives at
       constitutional/accessibility-is-foundational.md */
  --goal-achieved: var(--color-goal-achieved);
  --goal-borderline: var(--color-goal-borderline);
  --goal-missed: var(--color-goal-missed);
  --goal-no-data: var(--color-goal-no-data);

  /* --- Data Visualization (semantic) ---
       chart-1 through chart-12 for sequential, ordered category assignment.
       chart-pair-N-a and chart-pair-N-b for comparisons, before/after, two-series charts.
       12 colors total — sufficient for most visualizations.
       If you need more than 6-8 in one chart, reconsider the visualization. */

  /* Sequential assignment — use in this order */
  --chart-1: var(--color-chart-01-teal);
  --chart-2: var(--color-chart-02-mint);
  --chart-3: var(--color-chart-03-blue);
  --chart-4: var(--color-chart-04-blue-light);
  --chart-5: var(--color-chart-05-purple);
  --chart-6: var(--color-chart-06-purple-light);
  --chart-7: var(--color-chart-07-terracotta);
  --chart-8: var(--color-chart-08-terracotta-light);
  --chart-9: var(--color-chart-09-marine);
  --chart-10: var(--color-chart-10-olive);
  --chart-11: var(--color-chart-11-warm-grey);
  --chart-12: var(--color-chart-12-dark-sand);

  /* Paired assignment — for comparisons, before/after, two-series charts */
  --chart-pair-1-a: var(--color-chart-01-teal);
  --chart-pair-1-b: var(--color-chart-07-terracotta);
  --chart-pair-2-a: var(--color-chart-03-blue);
  --chart-pair-2-b: var(--color-chart-08-terracotta-light);
  --chart-pair-3-a: var(--color-chart-05-purple);
  --chart-pair-3-b: var(--color-chart-10-olive);

  /* --- Typography Compositions ---
       Composite tokens for common text roles.
       Each bundles: size, weight, line-height, letter-spacing, color.
       Perfect fifth (1.5) modular scale from 18px base.
       Line-heights resonate with spacing scale:
         body 18×1.5 = 27px (h3 size)
         h3 27×1.33 = 36px (spacing-lg)
         h2 40×1.2 = 48px (spacing-xl) */

  /* Heading 1 / Display — communicative hero, landing pages (60px) */
  --type-h1-size: var(--font-size-2xl);
  --type-h1-weight: var(--font-weight-bold);
  --type-h1-line-height: var(--line-height-tight);
  --type-h1-letter-spacing: var(--letter-spacing-tightest);
  --type-h1-color: var(--text-primary);

  /* Heading 2 — page title, major section (40px) */
  --type-h2-size: var(--font-size-xl);
  --type-h2-weight: var(--font-weight-bold);
  --type-h2-line-height: var(--line-height-heading);
  --type-h2-letter-spacing: var(--letter-spacing-tighter);
  --type-h2-color: var(--text-primary);

  /* Heading 3 — section heading (27px) */
  --type-h3-size: var(--font-size-lg);
  --type-h3-weight: var(--font-weight-bold);
  --type-h3-line-height: var(--line-height-subheading);
  --type-h3-letter-spacing: var(--letter-spacing-tight);
  --type-h3-color: var(--text-primary);

  /* Heading 4 — card header, sub-subsection (22px) */
  --type-h4-size: var(--font-size-md);
  --type-h4-weight: var(--font-weight-bold);
  --type-h4-line-height: var(--line-height-subheading);
  --type-h4-letter-spacing: var(--letter-spacing-normal);
  --type-h4-color: var(--text-primary);

  /* Lead — introductory paragraph below a heading.
     Bridges heading and body: heading-sized but body-weight, secondary color.
     Not a heading — it doesn't structure, it orients.
     Use once per page/section, directly after the h1 or h2. */
  --type-lead-size: var(--font-size-lg);          /* 27px — same as h3 */
  --type-lead-weight: var(--font-weight-regular);  /* 500 — not bold */
  --type-lead-line-height: var(--line-height-subheading);  /* 1.33 */
  --type-lead-letter-spacing: var(--letter-spacing-normal);
  --type-lead-color: var(--text-secondary);        /* Lighter than headings */

  /* Body text (18px) */
  --type-body-size: var(--font-size-base);
  --type-body-weight: var(--font-weight-regular);
  --type-body-line-height: var(--line-height-body);
  --type-body-color: var(--text-primary);

  /* Body bold — emphasis within body (18px bold) */
  --type-body-bold-weight: var(--font-weight-bold);

  /* Label — form labels, UI chrome (16px) */
  --type-label-size: var(--font-size-sm);
  --type-label-weight: var(--font-weight-regular);
  --type-label-line-height: var(--line-height-body);
  --type-label-color: var(--text-primary);

  /* Small — captions, metadata, timestamps (14px) */
  --type-small-size: var(--font-size-xs);
  --type-small-weight: var(--font-weight-regular);
  --type-small-line-height: var(--line-height-body);
  --type-small-color: var(--text-secondary);

  /* --- Spacing: Semantic ---
       Spacing above headings proportional to heading visual weight:
       Above h2 (40px) → spacing-lg (36px)
       Above h3 (27px) → spacing-md (24px)
       Above h4 (22px) → spacing-sm (16px) */
  --spacing-page-padding: var(--spacing-sm);
  --spacing-card-padding: var(--spacing-md);
  --spacing-section-gap: var(--spacing-md);
  --spacing-element-gap: var(--spacing-xs);
  --spacing-label-gap: var(--spacing-3xs);

  /* --- Elevation --- */
  --elevation-flat: var(--shadow-e0);
  --elevation-card: var(--shadow-e1);
  --elevation-dropdown: var(--shadow-e2);
  --elevation-modal: var(--shadow-e3);


  /* --------------------------------------------------------------------------
     LAYER 3: COMPONENT TOKENS (STUBS)
     Specific to individual components. Extend as the system matures.
     These are starting points — not a complete component library.
     -------------------------------------------------------------------------- */

  /* --- Button ---
     State model rebuilt 2026-08-05, card 19, applying board decisions 16 and
     17. Hover is no longer a fill on any variant – it draws the outer focus
     ring only, no colour change (card 16, the hover/focus merge). Focus draws
     both rings. Press (the old "active") gets its own fill per filled
     variant rather than falling back on the retired --state-active. Buttons
     do not disable – see BASELINE.md § Buttons. */
  --button-primary-bg: var(--color-orange-600);        /* Repointed off --brand-accent (orange-500) 2026-07-29: white text on orange-500 fails AA. orange-600 = 4.52:1 */
  --button-primary-text: var(--color-white);
  --button-primary-active-bg: var(--color-orange-700);   /* Was the hover value until card 16's merge; orange-700 is the ramp's last step, so press reuses it rather than adding a colour. 5.92:1 */
  --button-primary-radius: var(--radius-m);
  --button-primary-padding-x: var(--spacing-md);
  --button-primary-padding-y: var(--spacing-xs);
  --button-primary-font-size: var(--font-size-base);
  --button-primary-font-weight: var(--font-weight-regular);
  --button-primary-shadow: var(--shadow-e1);

  /* Variant clusters alias padding / font-size / font-weight to the primary
     cluster so every variant has a complete contract. Override an alias if a
     specific variant truly needs different sizing — but the default is "use
     the primary cluster's spacing and type rules so all four variants match
     visually beyond the colour story." */

  /* Primary, inverted – the primary action ON a petrol surface.
     NEW VARIANT 2026-07-29. Surface-aware: orange-600 is specified for light
     surfaces, and it must never be placed on petrol. On a petrol ground the
     primary action inverts instead – white fill, petrol text. Rest 10.27:1,
     press 7.73:1. petrol-100 is reused, not added – it is also the outline/
     ghost press fill below.
     Focus ring swaps here, and only here: this variant sits on petrol, so the
     ring facing the page (the "outer" position) must read against petrol,
     and the ring facing the button's own white fill (the "inner" position)
     must read against white. The two --state-focus-ring* values are exactly
     backwards for that, hence the per-variant override – the token names
     keep their positional meaning, only the values assigned to them swap. */
  --button-primary-inverse-bg: var(--color-white);
  --button-primary-inverse-text: var(--color-petrol-500);
  --button-primary-inverse-active-bg: var(--color-petrol-100);   /* Was the hover value until card 16's merge. 7.73:1 */
  --button-primary-inverse-focus-ring: var(--state-focus-ring-inner);         /* white – reads against the petrol page */
  --button-primary-inverse-focus-ring-inner: var(--state-focus-ring);         /* petrol – reads against this button's white fill */
  --button-primary-inverse-radius: var(--button-primary-radius);
  --button-primary-inverse-padding-x: var(--button-primary-padding-x);
  --button-primary-inverse-padding-y: var(--button-primary-padding-y);
  --button-primary-inverse-font-size: var(--button-primary-font-size);
  --button-primary-inverse-font-weight: var(--button-primary-font-weight);

  --button-secondary-bg: var(--brand-primary);
  --button-secondary-text: var(--color-white);
  --button-secondary-active-bg: var(--color-petrol-700);   /* 13.88:1. First per-variant press fill this cluster has had – previously fell back on the retired --state-active at 1.93:1 */
  --button-secondary-radius: var(--radius-m);
  --button-secondary-padding-x: var(--button-primary-padding-x);
  --button-secondary-padding-y: var(--button-primary-padding-y);
  --button-secondary-font-size: var(--button-primary-font-size);
  --button-secondary-font-weight: var(--button-primary-font-weight);

  --button-outline-bg: transparent;
  --button-outline-text: var(--brand-primary);
  --button-outline-border: var(--brand-primary);
  --button-outline-border-rule: var(--stroke-xs) solid var(--button-outline-border);
  --button-outline-radius: var(--radius-m);
  --button-outline-padding-x: var(--button-primary-padding-x);
  --button-outline-padding-y: var(--button-primary-padding-y);
  --button-outline-font-size: var(--button-primary-font-size);
  --button-outline-font-weight: var(--button-primary-font-weight);
  --button-outline-active-bg: var(--color-petrol-100);   /* Declared directly now hover-bg is gone. Boundary is carried by the border (8.75:1), not this fill (1.13:1 alone) – outline's border does that job in every state */

  --button-ghost-bg: transparent;
  --button-ghost-text: var(--brand-primary);
  --button-ghost-radius: var(--radius-m);
  --button-ghost-padding-x: var(--button-primary-padding-x);
  --button-ghost-padding-y: var(--button-primary-padding-y);
  --button-ghost-font-size: var(--button-primary-font-size);
  --button-ghost-font-weight: var(--button-primary-font-weight);
  /* KNOWN BOUNDARY FAILURE, carried from the retired ghost/hover cell (card
     21) rather than fixed here: ghost has no border token, so petrol-100 on
     sand-100 (1.13:1) is the only thing marking the control in this state.
     Tracked in tokens.test.ts KNOWN_BOUNDARY_FAILURES['ghost/active']. Ghost
     has zero instances in app/ or components/ – not fixed unilaterally
     because a border changes what "ghost" means as a variant, which is
     Torfinn's call, not a value pick. */
  --button-ghost-active-bg: var(--color-petrol-100);

  /* Confirm button — completion/sign-off actions only.
     "Klarmarkera", "Godkänn", "Signera", "Markera som klar".
     Not for generic "yes/ok" in dialogs — use primary for those.
     Card 3, decided 2026-07-31: the confirm colour story is retired: confirm
     aliases the secondary cluster and adds a required check glyph, so "I'm
     done" is carried by the glyph and the verb rather than by a fill. The
     alias is by reference, not by value, so secondary's press fill (or any
     future change to it) reaches confirm automatically. */
  --button-confirm-bg: var(--button-secondary-bg);
  --button-confirm-text: var(--button-secondary-text);
  --button-confirm-active-bg: var(--button-secondary-active-bg);
  --button-confirm-icon: check;   /* Required, not optional – the variant's only remaining distinction from secondary. "check" is already in the F11 icon allowlist */
  --button-confirm-radius: var(--radius-m);

  /* Touch target, card 19. BASELINE.md § Accessibility has asserted a 44px
     minimum touch/click target since before this file existed; nothing
     enforced it. Global rather than per-variant – the floor does not vary by
     colour story. Not classified as a state suffix in tokens.test.ts: see
     GLOBAL_BUTTON_TOKENS there. */
  --button-min-height-touch: 44px;
  --button-padding-y-touch: var(--spacing-sm);

  /* Pointer target, card 63/OQ2, 2026-08-12. 44px is WCAG 2.5.5 (AAA);
     the AA floor is 2.5.8 at 24×24 CSS px. BASELINE.md § Accessibility
     relaxed the blanket 44px rule to this floor for dense instrumental
     surfaces on desktop pointer input – touch and patient-facing contexts
     keep --button-min-height-touch. */
  --button-min-height-pointer: 24px;   /* @usage Minimum height for buttons on dense instrumental surfaces under pointer input | @constraint A floor, not a size. No pointer padding token exists — the only padding-y here is the touch one — so a button honouring this floor with default padding and a body line box still renders at 44px. A compact control needs a padding value this file does not yet carry (sundviktlakemedel, 2026-08-25) */

  /* Small button variant */
  --button-small-font-size: var(--font-size-xs);
  --button-small-padding-x: var(--spacing-sm);
  --button-small-padding-y: var(--spacing-3xs);

  /* --- Tab Navigation ---
       Sticky horizontal tabs below page headings.
       Used for sub-navigation within a section (not primary nav).
       Active tab indicated by underline in accent color, not background change.
       Sticks below topbar on scroll (z-index between sticky and sidebar). */
  --tab-nav-bg: transparent;                                          /* Inherits page surface */
  --tab-nav-border-bottom: var(--border-default);                     /* Colour only */
  --tab-nav-border-bottom-rule: var(--stroke-xs) solid var(--tab-nav-border-bottom); /* Full shorthand */
  --tab-nav-padding-x: var(--spacing-sm);
  --tab-nav-padding-y: var(--spacing-xs);
  --tab-nav-gap: var(--spacing-md);                  /* Between tab items */
  --tab-nav-font-size: var(--font-size-sm);
  --tab-nav-font-weight: var(--font-weight-regular);
  --tab-nav-color: var(--text-secondary);
  --tab-nav-color-active: var(--text-primary);
  --tab-nav-color-hover: var(--text-primary);
  --tab-nav-indicator-color: var(--color-petrol-500); /* Underline on active tab */
  --tab-nav-indicator-width: 2px;
  --tab-nav-sticky-z: var(--z-sticky);               /* Sticks on scroll */

  /* --- Card ---
       Cards use border + shadow together on sand backgrounds.
       Border provides definition; shadow provides subtle lift.
       Evidence: Hälsodeklarationer prototype — "the subtle line
       that helps with contrast making them pop subtly."
       Radius 4px → 16px, 2026-08-10, board card 45 — see the --radius-l
       primitive comment for the full reasoning. */
  --card-bg: var(--surface-card);
  --card-radius: var(--radius-l);
  --card-padding: var(--spacing-card-padding);
  --card-border: var(--border-default);                                /* Colour only — for fine-grained composition */
  --card-border-rule: var(--stroke-xs) solid var(--card-border);       /* Full shorthand — `border: var(--card-border-rule)` Just Works */
  --card-shadow: var(--elevation-card);

  /* Flush top-of-card media — a hero image with zero clearance from the
     card's own top/left/right edges. Corrected 2026-08-10, same day as
     board card 45: this does not need its own radius value at all. The
     card already clips its contents to its own rounded shape, so a flush
     image with no radius on it is clipped to match the card's top corners
     for free, and its bottom edge is already square because it sits nowhere
     near the card's own bottom corners. The image needs zero radius
     handling; the card needs --card-media-overflow. An earlier version of
     this decision added --card-media-radius-top/-bottom to hand-match the
     image's corners to the card's — reverted the same day once it was
     pointed out that clipping already produces the identical result with
     nothing to keep in sync. Does not apply to an image inset with
     clearance on all sides — that stays --image-radius-default, unaffected.
     Scoped to card+media, not every card: applying this to all cards
     forecloses anything that intentionally overhangs a card's edge (a
     corner badge, a dropdown) — nothing in Baseline does that today, but a
     plain card should stay free to, without a second decision reopening
     this one. */
  --card-media-overflow: hidden;

  /* --- Input ---
       Label above field (Fixed). Mark optional fields, not required.
       Error: "what happened + what to do" below the field.
       Disabled vs read-only: see governance comments below. */

  /* Field box */
  --input-bg: var(--color-white);
  --input-border: var(--border-default);                               /* Colour only */
  --input-border-rule: var(--stroke-xs) solid var(--input-border);     /* Full shorthand */
  --input-border-focus: var(--border-focus);
  --input-border-error: var(--border-error);
  --input-radius: var(--radius-m);
  --input-padding-x: var(--spacing-sm);
  --input-padding-y: var(--spacing-xs);
  --input-font-size: var(--font-size-base);
  --input-text: var(--text-primary);
  --input-placeholder: var(--color-gray-300);            /* @usage Placeholder text inside a field | @constraint Not required to meet the AA text floor — it is not required content, and every field in this system carries a visible label (§ Forms), so nothing depends on the placeholder to be understood. Points at the primitive rather than at a semantic text alias since --text-tertiary was retired 2026-09-04 (card 115); a placeholder is not a text level */

  /* Label — always above the field */
  --input-label-size: var(--font-size-sm);
  --input-label-weight: var(--font-weight-bold);
  --input-label-color: var(--text-primary);
  --input-label-gap: var(--spacing-3xs);        /* 4px between label and field */

  /* Helper text — below field, replaced by error on validation failure */
  --input-helper-size: var(--font-size-xs);
  --input-helper-color: var(--text-secondary);

  /* Error — replaces helper text position on validation failure.
     Pattern: "what happened + what to do" (from den nära experten digital voice).
     Input gets aria-invalid="true", error msg linked via aria-describedby. */
  --input-error-color: var(--color-error-500);
  --input-error-size: var(--font-size-xs);
  --input-error-border: var(--border-error);     /* red ring on the input itself */

  /* Optional indicator — "(valfritt)" suffix in label.
     Mark optional fields, not required. In clinical forms 80%+ of fields
     are required — marking all of them creates noise. */
  --input-optional-color: var(--text-secondary);

  /* Focus — petrol ring. Consistent with keyboard navigation patterns.
     Uses focus-visible (not focus) to avoid showing ring on mouse click. */
  --input-focus-ring: var(--state-focus-ring);
  --input-focus-ring-offset: 2px;

  /* Disabled — use sparingly.
     Patient-facing: prefer explain-on-click over disabled state.
     Show what's possible, explain prerequisites to unlock it.
     Instrumental: disabled acceptable when workflow context is
     self-evident to the user (e.g. clinician knows they must
     complete the note before signing). */
  --input-disabled-bg: var(--color-gray-100);
  --input-disabled-text: var(--text-disabled);
  --input-disabled-border: var(--color-gray-100);

  /* Read-only — looks like content, not a greyed-out input.
     For data that can be unlocked to editable (role-based or
     state-based), present in an input-shaped container with a
     lock icon. Otherwise present as plain content text. */
  --input-readonly-bg: var(--surface-page);      /* sand — blends with page */
  --input-readonly-text: var(--text-primary);
  --input-readonly-border: transparent;

  /* --- Badge --- */
  --badge-radius: var(--radius-full);
  --badge-font-size: var(--font-size-xs);
  --badge-padding-x: var(--spacing-2xs);
  --badge-padding-y: var(--spacing-3xs);

  /* --- Table ---
       Two table types share the same token foundation:
       Read tables (display) and Work tables (interactive/editable).
       Density is user-selectable in instrumental surfaces. */

  /* Row density — three levels */
  --table-row-height-compact: 36px;
  --table-row-height-default: 48px;
  --table-row-height-comfortable: 64px;

  /* Cell spacing */
  --table-cell-padding-x: var(--spacing-sm);
  --table-cell-padding-y: var(--spacing-2xs);

  /* Header */
  --table-header-bg: var(--surface-subtle-cold);
  --table-header-text: var(--text-primary);
  --table-header-weight: var(--font-weight-bold);
  --table-header-size: var(--font-size-xs);
  --table-header-transform: none;
  --table-header-letter-spacing: 0.05em;

  /* Body */
  --table-body-size: var(--font-size-sm);
  --table-body-color: var(--text-primary);

  /* Row states */
  --table-row-hover-bg: var(--surface-subtle-warm);
  --table-row-selected-bg: var(--color-petrol-100);
  --table-row-border: var(--border-default);                          /* Colour only */
  --table-row-border-rule: var(--stroke-xs) solid var(--table-row-border); /* Full shorthand */

  /* Zebra striping — optional, off by default */
  --table-row-stripe-bg: var(--color-sand-100);

  /* --- Motion ---
       Animation serves feedback and state communication only, not decoration.
       Four duration levels, two easing curves. No more.
       Surface temperature selects from the same tokens: instrumental stays
       at instant/fast, communicative can use moderate/slow.
       Only animate transform and opacity (GPU-composited, no layout reflow).
       prefers-reduced-motion: reduce disables ALL animation (Fixed, a11y).
       See aleris-baseline-animation.md for full guidance. */

  /* Duration — four levels */
  --duration-instant: 100ms;    /* Focus rings, color shifts, checkbox toggles — feels immediate */
  --duration-fast: 200ms;       /* Buttons, hover, tooltips, badge updates — system reacting */
  --duration-moderate: 350ms;   /* Modals, accordions, sidebars, card flip — spatial tracking */
  --duration-slow: 500ms;       /* Page transitions, major layout shifts — used sparingly */

  /* Easing — two curves */
  --ease-out: cubic-bezier(0.0, 0.0, 0.2, 1);    /* Elements arriving: fast start, gentle stop */
  --ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1);  /* Elements moving: smooth at both ends */
  /* Never use ease-in alone — slow start reads as unresponsive. */

  /* Keyframe primitives (reference — apply via CSS @keyframes)
       fade-in:  opacity 0→1, translateY 4px→0  (ease-out, duration-fast)
       scale-in: opacity 0→1, scale 0.97→1       (ease-out, duration-fast)
     fade-in suits content entering the viewport (cards, list items, panels).
     scale-in suits overlays and modals (dialogs, dropdowns, popovers).
     Collapse is slightly faster than expand (300ms vs 350ms) — closing
     feels decisive, opening feels gradual. */

  /* --- Skeleton / Loading ---
       Shimmer over spinners. Products compose primitives (rectangle, circle,
       text-line) to match their content layouts.
       < 2s: shimmer only. > 2s: shimmer + progress. > 10s: navigate away + notify.
       No animation in email — all email layouts are static (Fixed). */
  --skeleton-bg: var(--color-gray-100);
  --skeleton-shimmer: linear-gradient(90deg, var(--color-gray-100) 0%, var(--color-sand-100) 50%, var(--color-gray-100) 100%);
  --skeleton-duration: 1400ms;                     /* @usage One shimmer sweep | @constraint A loop, not a transition — do not alias the --duration-* scale. Changed from var(--duration-moderate) (350ms) on 2026-09-13, board card 156. The four --duration-* steps are transition levels, annotated as such — "system reacting", "spatial tracking" — and a shimmer is an ambient loop, a different kind of motion that happens to be measured in the same unit. At 350ms it swept four times inside this section's own "< 2s: shimmer only" window, which reads as flicker rather than as waiting. 1400ms is the design system export's value and Torfinn adopted it; the alias was the defect, and the value only looked right because nothing separated the two kinds of duration. */
  --skeleton-easing: var(--ease-in-out);
  --skeleton-radius-rect: var(--radius-s);         /* Rectangle primitives */
  --skeleton-radius-circle: var(--radius-full);    /* Avatar/icon placeholders */

  /* --- Adaptive Responsive ---
       Component swap at breakpoints, not just scaling.
       The grid (12-col, container queries) handles layout.
       These tokens handle the stacking context. */

  /* Z-index hierarchy — canonical stacking order */
  --z-base: 0;
  --z-dropdown: 10;          /* Dropdowns, select menus, popovers */
  --z-sticky: 20;            /* Sticky table headers, fixed elements within scroll */
  --z-sidebar: 30;           /* Sidebar navigation (when overlaying on mobile) */
  --z-search: 40;            /* Command palette / search overlay */
  --z-topbar: 50;            /* TopBar — always on top of page content */
  --z-modal: 100;            /* Dialogs, sheets, modal overlays */
  --z-toast: 110;            /* Toast notifications — above modals */

  /* --- Table Density Persistence ---
       Density (compact/default/comfortable) is a user preference,
       not a per-view design decision. Persist via localStorage
       or user profile. Key: 'aleris-table-density'.
       Default if no preference stored: 'default' (48px rows). */
  --table-density-preference-key: 'aleris-table-density';

  /* --- Images ---
       Format strategy: AVIF → WebP → JPEG (progressive enhancement via <picture>).
       Component sets the aspect ratio; image fills via object-fit: cover.
       EXIF metadata stripped by default — GPS data from clinic photos is a privacy risk.
       See aleris-baseline-images.md for full guidance. */

  /* Aspect ratios — used with CSS aspect-ratio property */
  --image-ratio-hero: 16 / 9;       /* Heroes, banners, video thumbnails */
  --image-ratio-content: 3 / 2;     /* Articles, staff portraits, cards — most versatile */
  --image-ratio-square: 1 / 1;      /* Avatars, thumbnails, small functional images */
  --image-ratio-document: 4 / 3;    /* Documentation, clinical photography */

  /* Border radius on images. Does not apply to an image flush to a card's
     top edge with zero clearance — that image takes no radius at all; the
     card's own --card-media-overflow clips it to match (§ Card). This token
     is for an inset image with clearance on all sides. */
  --image-radius-default: var(--radius-m);    /* 8px — softened corners */
  --image-radius-avatar: var(--radius-full);  /* Circular for profile images */
  --image-radius-none: 0;                     /* Full-bleed images */
}
