/* ===== Design tokens — TWO LAYERS =====
   Normalised from DESIGN_AUDIT.md (112 literal values across the sheet,
   most of them one-offs).

   LAYER 1 — PRIMITIVES. Raw values, named for what they LOOK like
     (--gray-900, --blue-500). No opinion about where they are used. Colours
     were computed in OKLCH and written here as hex: OKLCH is what makes the
     ladders perceptually even, hex is what every browser reads without a
     fallback. The OKLCH coordinates are in the comments so the ladder can be
     extended without guessing.

   LAYER 2 — SEMANTICS. Roles (--surface-page, --text-primary,
     --accent-default). Components read ONLY these. Every one points at a
     primitive through var(), so a theme swaps meanings, never raw numbers.
     Themes live here and nowhere else: [data-theme="dark"] re-points the
     roles, it does not redefine any primitive.

   LAYER 3 — LEGACY BRIDGE at the bottom. The component sheets still use the
     old token names (--c-bg, --fs-300, --sp-3 …) and are deliberately NOT
     being touched in this pass, so those names are kept and re-pointed at
     the semantic layer. That way this change is one file and one revert.
     The bridge is temporary; it goes away when the components are migrated.

   ---------------------------------------------------------------------
   SYSTEM RULE — two hue families, one set of lightness steps.
   The light theme is warm (H ≈ 90, cream paper) and the dark theme is cool
   (H ≈ 286, slate ink). This is a rule, not a leftover: the site's light
   mode has always read as paper rather than as white, and forcing both
   themes onto one hue would cost that. What the two families DO share is
   the lightness ladder — identical L stops in both — so a role that is
   "one step down from the page" is the same perceptual step in either theme.
   Chroma is deliberately tiny (.008 warm / .010 cool): these are neutrals
   that lean, not colours.
   --------------------------------------------------------------------- */

/* =====================================================================
   LAYER 1 — PRIMITIVES
   ===================================================================== */
:root {
  /* --- neutral ladder, warm family — OKLCH H 90, C .008 --------------
     Steps: 100 96 72 45 37 32 26 20 12 %L. The gaps are uneven on purpose:
     they sit where the design actually needs stops (page, text, chrome),
     not on a mathematical curve nothing lands on. */
  --warm-000: #ffffff;   /* L 100 % — C forced to 0: white has no hue      */
  --warm-050: #f4f2ec;   /* L  96 % — the page                             */
  --warm-300: #a6a49f;   /* L  72 %                                        */
  --warm-500: #575550;   /* L  45 % — secondary text on light              */
  --warm-600: #41403b;   /* L  37 %                                        */
  --warm-700: #34332e;   /* L  32 %                                        */
  --warm-800: #252420;   /* L  26 %                                        */
  --warm-900: #171612;   /* L  20 % — primary text on light                */
  --warm-950: #070603;   /* L  12 %                                        */

  /* --- neutral ladder, cool family — OKLCH H 286, C .010 -------------
     Same nine L stops as the warm family. */
  --cool-000: #ffffff;   /* L 100 % */
  --cool-050: #f1f1f8;   /* L  96 % — primary text on dark */
  --cool-200: #c3c3ca;   /* L  82 % — see the note on --text-secondary below */
  --cool-300: #a4a4ab;   /* L  72 % */
  --cool-500: #55555b;   /* L  45 % */
  --cool-600: #3f3f45;   /* L  37 % */
  --cool-700: #323238;   /* L  32 % */
  --cool-800: #232329;   /* L  26 % */
  --cool-900: #15151a;   /* L  20 % */
  --cool-950: #050509;   /* L  12 % — the darkest page */

  /* --- background tints (the visitor-pickable shades) ----------------
     One rule: same L, same C, hue is the only variable. Light options sit
     at L 95 % / C .025, dark options take an L stop from the ladder at
     C .030 — high enough to read as a tint, low enough to stay a surface. */
  --violet-050: #f1ebfd;   /* L 95 % C .025 H 301 */
  --azure-050:  #e5efff;   /* L 95 % C .025 H 262 */

  --indigo-800: #232232;   /* L 26 % C .030 H 288 */
  --indigo-700: #323142;   /* L 32 % C .030 H 288 — surface = bg + 6 % L */

  /* EXCEPTION — noir keeps C .010, not the .030 the other tints share.
     It is the "clean black" option and that is its whole job; pulling it up
     to the shared chroma turns it violet and it stops being the neutral
     choice in the set. At C .010 it lands exactly on the cool neutral
     ladder, which is why it has no primitives of its own: noir IS the
     ladder (--cool-900 / --cool-800). Kept as an alias for readability. */
  --noir-900: var(--cool-900);
  --noir-800: var(--cool-800);

  /* --- accents ------------------------------------------------------
     Both normalised to L 70 % / C .18 so they carry the same visual weight;
     only hue separates them. */
  --coral-500: #fa6b52;   /* L 70 % C .18 H  32 */
  --blue-500:  #6997ff;   /* L 70 % C .18 H 266 — on dark backgrounds */
  --blue-600:  #527df1;   /* L 62 % C .18 H 266 — on light backgrounds */

  /* EXCEPTION — the logo's sun is NOT on the accent ladder.
     Its lightness is semantic: the sun is the light source in the mark, and
     at the accents' L 70 % it stops reading as emitted light and starts
     reading as an orange dot. Left at its measured L 84.5 % / C .139 / H 77.
     Decorative graphic, never a text background — see the contrast note in
     DESIGN_AUDIT.md. */
  --sun-400: #ffbf58;

  /* --- alpha ladder --------------------------------------------------
     Four steps replacing fourteen hand-picked opacities. Shadow alphas are
     NOT on this ladder — they are baked into the three elevation tokens
     below, where they are part of a ramp rather than independent values. */
  --black-a05: rgba(0, 0, 0, .05);
  --black-a10: rgba(0, 0, 0, .10);
  --black-a20: rgba(0, 0, 0, .20);
  --black-a45: rgba(0, 0, 0, .45);
  --white-a10: rgba(255, 255, 255, .10);
  --white-a20: rgba(255, 255, 255, .20);

  /* --- type scale — modular, 16px base, ratio 1.25 -------------------
     12.8 · 16 · 20 · 25 · 31.25 · 48.83 */
  /* One step BELOW xs, and not an exception: 12.8 / 1.25 = 10.24, so this is
     the ladder continued, not a value picked to fit something. It exists
     because dense labelling — a count in a pill, an index before a category
     name — sits under the smallest reading size and had nowhere to go. */
  --text-2xs: 0.64rem;     /* 10.24px */
  --text-xs:  0.8rem;      /* 12.8px */
  --text-md:  1rem;        /* 16px   — base */
  --text-lg:  1.25rem;     /* 20px   */
  --text-xl:  1.5625rem;   /* 25px   */
  --text-2xl: clamp(1.953rem, 6vw, 3.052rem);   /* 31.25 → 48.83px */

  /* EXCEPTION — off the modular scale, on purpose.
     14px sits between the scale's 12.8 and 16 and exists because the ratio
     is too coarse for dense UI: at 12.8 the nav tabs read as fine print, at
     16 the five of them stop fitting a 320px screen. Intended consumers:
     the nav tabs and the footer contact pills. */
  --text-sm: 0.875rem;     /* 14px */

  /* EXCEPTION — compositional size, does not obey the scale.
     The Hero headline is sized to the viewport and to the shape of the
     three lines, not to a type ramp: it is the page's largest graphic
     element, and the nearest scale step (76.3px) makes the third line
     stop balancing the first. Left at its measured 44 → 88px. */
  --text-display: clamp(2.75rem, 10vw, 5.5rem);   /* 44 → 88px */

  /* --- weight — three steps, where there were none -------------------
     Weights were written as bare numbers in the component sheets (card.css
     has a literal 600). Three is the whole range the site draws on, and
     naming them is what lets a component say which ROLE a weight plays
     rather than restating the number. */
  --weight-regular:  400;
  --weight-medium:   500;
  --weight-semibold: 600;

  /* --- tracking — two steps, where there were none --------------------
     In `em`, not px, so a step holds its proportion at whatever size it is
     used: small type needs the letters opened up and the amount is a share
     of the size, not a constant. caps is for uppercase labels, which need
     the most; micro is for the small numerals beside them. */
  --tracking-caps:  0.07em;
  --tracking-micro: 0.05em;

  /* --- leading — three roles, was seven near-identical numbers ------- */
  --leading-tight:   1.2;   /* display and headings */
  --leading-normal:  1.4;   /* UI rows, labels */
  --leading-relaxed: 1.6;   /* prose */

  /* --- spacing — every step a multiple of 4 at the default base ------
     In rem, not px: the type scale is already rem-based, and a user who
     raises the browser's base font size was getting text that grew while
     the padding around it did not. The two axes now scale together. The
     4px grid still holds at base 16; above it the whole grid scales. */
  --space-1:  0.25rem;   /*  4px at base 16 */
  --space-2:  0.5rem;    /*  8px */
  --space-3:  0.75rem;   /* 12px */
  --space-4:  1rem;      /* 16px */
  --space-5:  1.25rem;   /* 20px */
  --space-6:  1.5rem;    /* 24px */
  --space-8:  2rem;      /* 32px */
  --space-10: 2.5rem;    /* 40px */
  --space-12: 3rem;      /* 48px */
  --space-20: 5rem;      /* 80px */
  --space-24: 6rem;      /* 96px */

  /* --- radius — four steps ------------------------------------------
     `50%` stays literal in the components that need a circle: that is a
     shape, not a step on this scale. */
  --radius-sm:   8px;
  --radius-md:  16px;
  --radius-lg:  24px;
  --radius-full: 999px;

  /* --- motion — curves are tokenised, durations are not (yet) --------
     Inventoried in MOTION_AUDIT.md. Two curves cover everything on the site:

     --ease-out is the house curve, on 19 of 27 transitions: anything that
       appears, dims, tints or recolours.
     --ease-overshoot passes the target and comes back — the 1.4 control
       point is what takes it past 100 %. Reserved for things that TRAVEL:
       the nav highlight sliding between tabs, the sun rising out of the
       cloud. It was written longhand in two files before this pass.

     A third curve, cubic-bezier(.25, .8, .35, 1), used once on the logo
     wordmark's colour fade, was folded into --ease-out: over 350 ms on a
     colour change the two are not tellable apart.

     Durations are still literals in three files — see MOTION_AUDIT.md for
     the per-value breakdown; collapsing them is a separate decision. */
  --ease-out:       cubic-bezier(0.16, 1, 0.3, 1);
  --ease-overshoot: cubic-bezier(.34, 1.4, .5, 1);
  --dur-fast:  180ms;
  --dur:       320ms;
  --dur-slow:  600ms;
  --theme-dur: 420ms;

  /* EXCEPTION — a named duration, not a step on the scale.
     The Hero headline's word drop is a composed scene: 500ms is what the
     four overlapping steps were timed against, and the stagger between them
     is derived from it (500 × (1 − 0.85) = 75ms), not set independently.
     Moving it to --dur-slow would stretch the whole scene by ~150ms. It is
     compositional, like --text-display, and does not obey the scale.
     The stagger stays a derived number in hero.css — giving each delay its
     own token would cut the tie to the duration it is computed from. */
  --dur-hero-drop: 500ms;

  /* --- motion roles --------------------------------------------------
     Each role is a duration + curve pair, usable straight inside a
     transition: `transition: color var(--motion-hover)`. Components read
     these, not the primitives above.

     --motion-hover     pointer feedback and colour change
     --motion-enter     something appearing
     --motion-exit      something leaving
     --motion-emphasis  travel with an overshoot — the site's signature move

     TWO OF THE FOUR CURRENTLY HAVE NO CONSUMER, and that is reported rather
     than papered over:
     • exit — every appearing element on the site uses ONE symmetric
       transition for both directions, so nothing declares a separate
       leaving state yet. The role exists for when one does.
     • emphasis — its only two candidates are the nav capsule and the logo
       sun, and both are hand-tuned exceptions deliberately kept on their
       own literals (see MOTION_AUDIT.md). The pair below is the value a
       new consumer should start from, not a description of existing code.

     Theme cross-fades are NOT one of these roles: they already have their
     own semantic duration, --theme-dur, and they answer to a state change
     rather than to an interaction. */
  --motion-hover:    var(--dur-fast) var(--ease-out);
  --motion-enter:    var(--dur) var(--ease-out);
  --motion-exit:     var(--dur-fast) var(--ease-out);
  --motion-emphasis: var(--dur) var(--ease-overshoot);

  /* --- typefaces ----------------------------------------------------- */
  --font-display: "Georgia", "Times New Roman", serif;
  --font-body:    system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* A third family, and the only web font the site loads (self-hosted, see
     the @font-face block in work/iconography/iconography.css). It is here
     rather than scoped to its one consumer because a typeface is a role, not
     a local value — and because the reason it exists generalises: dense,
     numeric, tightly-tracked interface text needs metrics the system stack
     does not guarantee across platforms. */
  --font-ui:      "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* =====================================================================
   LAYER 2 — SEMANTICS
   Roles only. Components read these; primitives are an implementation
   detail they never touch.
   ===================================================================== */
:root {
  --surface-page:    var(--warm-050);
  --surface-raised:  var(--warm-000);
  --text-primary:    var(--warm-900);
  --text-secondary:  var(--warm-500);
  --border-subtle:   var(--black-a10);
  --grid-dot:        var(--black-a10);
  --overlay-scrim:   var(--black-a45);

  --accent-default:   var(--coral-500);
  /* Theme-dependent on purpose. The headline blue has to clear 3:1 (large
     text) against whatever page it sits on, and one lightness cannot do that
     on both a L 95 % page and a L 20 % one. Measured: blue-500 reaches only
     2.41–2.43:1 on the three light tints; blue-600 clears at 3.24:1 worst
     case. Dark keeps blue-500, which measures 3.76–6.47:1 there. */
  --accent-highlight: var(--blue-600);
  --brand-sun:        var(--sun-400);

  /* Elevation — three levels, replacing ten hand-written shadows.
     Tinted rather than black: over the tinted page a neutral-black shadow
     desaturates whatever it falls on and reads as grey grime. Alphas are
     lower and blurs wider than a black scale would need — a coloured shadow
     reads heavier at equal opacity. */
  --shadow-tint: 74, 60, 122;      /* muted indigo, as R,G,B for rgba() */
  --elev-1: 0 1px 2px  rgba(var(--shadow-tint), .05), 0 3px 10px rgba(var(--shadow-tint), .06);
  --elev-2: 0 2px 6px  rgba(var(--shadow-tint), .05), 0 18px 44px rgba(var(--shadow-tint), .12);
  --elev-3: 0 4px 10px rgba(var(--shadow-tint), .06), 0 24px 60px rgba(var(--shadow-tint), .16);

  /* Slightly see-through surface for things that SCROLL — cards, contact
     pills. They pass under the fixed nav dock, and two identical opaque
     whites merged into one shape at the overlap. Declared solid here and
     upgraded in the @supports block at the end of the file: consumers say
     var(--surface-veil), so if the token itself held a color-mix() a
     non-supporting browser would fail the substitution at computed-value
     time — which resets background to TRANSPARENT, not to any earlier
     declaration. The fallback has to live in the token. */
  --surface-veil: var(--surface-raised);
}

:root[data-theme="dark"] {
  --surface-page:    var(--cool-950);
  --surface-raised:  var(--cool-900);
  --text-primary:    var(--cool-050);
  /* Raised from cool-300 (L 72) to cool-200 (L 82): on the slate shade the
     card surface sat at L 43.7 after the veil, and cool-300 measured only
     3.22:1 there. cool-200 clears at 4.55:1 and is the first ladder step
     that does — the gap between 72 and 96 had no stop in it before.
     Slate has since been dropped from the picker, so the worst case is now
     indigo, which is darker and easier. The value STAYS: the remaining shades
     only gain contrast from it, and stepping back down would buy nothing. */
  --text-secondary:  var(--cool-200);
  --border-subtle:   var(--white-a10);
  --grid-dot:        var(--white-a10);

  --accent-highlight: var(--blue-500);

  /* Plain black, and heavier: there is no page tint left to protect on a
     near-black background, and a coloured glow there reads as a bug. */
  --shadow-tint: 0, 0, 0;
  --elev-1: 0 1px 2px  rgba(0, 0, 0, .04), 0 3px 10px rgba(0, 0, 0, .05);
  --elev-2: 0 2px 6px  rgba(0, 0, 0, .06), 0 18px 44px rgba(0, 0, 0, .16);
  --elev-3: 0 4px 10px rgba(0, 0, 0, .08), 0 24px 60px rgba(0, 0, 0, .22);
}

/* ===== Background shades =====
   A visitor-pickable tint for the page, driven by data-shade on <html>
   (js/modules/bg-shade.js). Light and dark get COMPLETELY DIFFERENT option
   sets — three each — rather than light/dark variants of one named shade,
   so each option is declared once in the theme block it belongs to and
   data-shade only ever holds a value valid for the current theme.

   RULE: the raised surface is the page lightened by one 6 % L step. That
   relationship already held in the hand-picked values to within 0.4 %, so
   it is written down rather than invented.

   Adding an option: one pair below, one selector line beneath both blocks,
   and its entry in LIGHT_SHADES / DARK_SHADES in bg-shade.js. */
:root {
  --shade-violet: var(--violet-050);  --shade-violet-s: var(--warm-000);
  --shade-azure:  var(--azure-050);   --shade-azure-s:  var(--warm-000);
}
:root[data-theme="dark"] {
  --shade-indigo: var(--indigo-800);  --shade-indigo-s: var(--indigo-700);
  --shade-noir:   var(--noir-900);    --shade-noir-s:   var(--noir-800);
}

/* Must come after both theme blocks: only one of each pair is ever defined
   at a time, so there is no override-order hazard here. */
:root[data-shade="violet"] { --surface-page: var(--shade-violet); --surface-raised: var(--shade-violet-s); }
:root[data-shade="azure"]  { --surface-page: var(--shade-azure);  --surface-raised: var(--shade-azure-s); }
:root[data-shade="indigo"] { --surface-page: var(--shade-indigo); --surface-raised: var(--shade-indigo-s); }
:root[data-shade="noir"]   { --surface-page: var(--shade-noir);   --surface-raised: var(--shade-noir-s); }

/* =====================================================================
   LAYER 3 — LEGACY BRIDGE  (temporary)
   The component sheets are untouched in this pass, so their token names
   still have to resolve. Each one points at a semantic role; nothing here
   holds a value of its own. Delete a line as its consumers migrate.
   ===================================================================== */
:root {
  --c-bg:           var(--surface-page);
  --c-surface:      var(--surface-raised);
  --c-surface-veil: var(--surface-veil);
  --c-ink:          var(--text-primary);
  --c-ink-soft:     var(--text-secondary);
  --c-line:         var(--border-subtle);
  --dot-grid:       var(--grid-dot);
  --c-accent:       var(--accent-default);
  --c-highlight:    var(--accent-highlight);

  /* --c-rule was declared but never read by any rule — dropped rather than
     bridged. It was the only fully dead token in the sheet. */

  --fs-100: var(--text-sm);       /* 14px — unchanged by the normalisation */
  --fs-200: var(--text-md);       /* 16px — unchanged */
  --fs-300: var(--text-lg);       /* 20px — unchanged */
  --fs-400: var(--text-xl);       /* 24 → 25px */
  --fs-500: var(--text-2xl);      /* 32–48 → 31.25–48.83px */
  --fs-600: var(--text-display);  /* unchanged */

  --sp-1: var(--space-2);         /*  8px — unchanged */
  --sp-2: var(--space-4);         /* 16px — unchanged */
  --sp-3: var(--space-6);         /* 24px — unchanged */
  --sp-4: var(--space-8);         /* 32px — unchanged */
  --sp-5: var(--space-12);        /* 48px — unchanged */
  --sp-6: var(--space-20);        /* 80px — unchanged */

  --radius-card: var(--radius-lg);    /* 22 → 24px */
  --radius-pill: var(--radius-full);  /* unchanged */

  /* Four names, three levels. xs and sm are NOT collapsed into one: the
     footer pills use xs at rest and sm on hover, and folding both into
     elev-1 would delete that hover. */
  --shadow-xs: var(--elev-1);
  --shadow-sm: var(--elev-2);
  --shadow-md: var(--elev-2);
  --shadow-lg: var(--elev-3);
}

/* --- layout — unchanged, no scale to normalise --- */
:root {
  --container: 72rem;
  --gutter: var(--space-6);
  /* Vertical band the fixed nav dock covers at the bottom of the viewport
     (its own height plus the gap beneath it). Page content that runs to the
     very bottom has to clear this or the dock sits on top of it. */
  --dock-space: 5rem;

  /* --- global stacking order (fixed/page-level layers only) ---
     Local, self-contained stacking (e.g. .nav__pill vs .nav__link inside the
     isolated .nav) is NOT part of this scale — those are small integers
     scoped to their own component and don't compete with anything else.
     This scale exists so cross-component overlap (the exact thing that broke
     once already — fidget tiles hiding under the navbar) has one documented
     source of truth instead of scattered magic numbers. */
  --z-dot-grid:      -1;   /* bottom-most background layer */
  --z-content:        1;   /* normal page content (sections, main) */
  --z-about-dropdown: 90;  /* About panel in its DESKTOP dropdown form */
  --z-nav:          100;   /* nav dock */
  --z-hero-fidget:  102;   /* fidget tool tiles — must win over --z-nav */
  --z-hero-controls: 103;  /* sound / shade / theme, above the tiles */
  --z-about-panel:  110;   /* About modal (+ backdrop) — above everything */
}

/* Mobile Safari renders a soft shadow this large as a visibly banded ring
   instead of a smooth gradient — especially over the dark, textured dot-grid
   the nav dock and Hero controls sit on. Trimming the raised level here
   fixes all of them at once. An override of an existing level, not a new
   step on the scale. */
@media (max-width: 768px) {
  :root { --elev-2: 0 1px 2px rgba(var(--shadow-tint), .04), 0 4px 14px rgba(var(--shadow-tint), .07); }
  :root[data-theme="dark"] { --elev-2: 0 1px 2px rgba(0, 0, 0, .05), 0 4px 14px rgba(0, 0, 0, .08); }
}

/* The real translucent veil, where the browser can mix it — see the token's
   declaration in the semantic layer for why the base value stays solid. */
@supports (background: color-mix(in srgb, red 50%, transparent)) {
  :root { --surface-veil: color-mix(in srgb, var(--surface-raised) 82%, transparent); }
}

/* =====================================================================
   ICONOGRAPHY CASE — scoped tokens (prefix --ico-)
   =====================================================================
   The Line Icons case study (work/iconography/) is a DARK-ONLY page with a
   palette of its own, taken from the Figma frame `Iconography — Dark - final`
   (file WcR5jHxWvkB0Cvvtmdet7B, node 2038:3563). It deliberately does not
   read the site's semantic roles: those flip with [data-theme], and this page
   has exactly one theme. Prefixing keeps the two systems from colliding —
   see ICONOGRAPHY_CASE.md section 4.

   Sizes live here too, not in the component sheet: rule 5 of that spec is
   "no hard-coded values in the component". Every number below is measured off
   the 1440-wide source frame. */
:root {
  /* --- colour: ROLES, not a palette ------------------------------------
     Every line below is a mapping, not a value: it says which part the case
     casts an existing semantic role in. There is no eleventh colour here and
     no --ico- primitive anywhere — the theme moves these, the case does not.

     Two of the frame's distinctions did not survive, and both were checked
     before they were dropped rather than after:

     • FOUR GROUNDS became TWO. The frame stacks page #0E0E0D, section
       #131312, plaque #1C1C1A and artboard/card #232321-#252522. The system
       has `page` and `raised`, and the pair rule (§5) makes `raised` the page
       lightened by one step — there is no third level, and in the light theme
       there is nowhere for one to go, since `raised` is already white. It
       costs nothing: page and section were never both visible (the sections
       tile the whole page), and the plaque, the artboard and the cards never
       touch each other, so one raised surface serves all three.

     • THE ICONS lost their own ink. In the frame they sit at L 90.4 against
       text at L 95.8 — a deliberate step back, because a 160px line icon
       carries far more ink than a letter and reads heavier at equal
       lightness. The ladder has no stop at 90, and a role that resolves to
       exactly `--text-primary` in both themes is not a role. So they take
       the primary ink, which is the nearest level the scale actually has. */
  --ico-bg-page:      var(--surface-page);
  --ico-bg-section:   var(--surface-page);
  --ico-bg-panel:     var(--surface-raised);
  --ico-bg-artboard:  var(--surface-raised);
  --ico-bg-card:      var(--surface-raised);
  /* Both of these are hairlines, 1.2 L apart in the frame — a difference
     nobody can see and no rule depends on. `--border-subtle` is an ALPHA in
     both themes, which is what lets the same line sit correctly on the page,
     on a plaque and on a screenshot. */
  --ico-line-grid:    var(--border-subtle);
  --ico-line-divider: var(--border-subtle);
  /* The dimension rules and the dashed optical box. Not a border: they are a
     measuring instrument and have to read as drawn, so they take the
     secondary ink rather than a hairline. */
  --ico-line-dim:     var(--text-secondary);
  --ico-text:         var(--text-primary);
  --ico-text-muted:   var(--text-secondary);
  --ico-icon:         var(--text-primary);

  /* --- type: the site's scale ------------------------------------------
     The frame's 64 and 44 are not on it. Per the scale's own rule the answer
     is the nearest LEVEL, not a fifth size: 64 and 44 both sit closest to
     --text-2xl, which would have collapsed the two headings into one, so the
     H1 takes the level above it. Both are clamps, so the case stops carrying
     its own heading sizes at each breakpoint — that is §6's rule, one
     clamp in the token rather than media queries at the consumer.

     11 → --text-xs (12.8) and 10 → --text-2xs (10.24), the step added to the
     bottom of the ladder for exactly this kind of dense labelling. */
  --ico-font:         var(--font-ui);
  --ico-fs-h1:        var(--text-display);  /* frame 64 → 44-88 */
  --ico-fs-h2:        var(--text-2xl);      /* frame 44 → 31-49 */
  --ico-fs-value:     var(--text-md);       /* frame 16, exact */
  --ico-fs-label:     var(--text-xs);       /* frame 11 → 12.8  */
  --ico-fs-micro:     var(--text-2xs);      /* frame 10 → 10.24 */
  --ico-fw-regular:   var(--weight-regular);
  --ico-fw-medium:    var(--weight-medium);
  --ico-fw-semibold:  var(--weight-semibold);
  --ico-ls-caps:      var(--tracking-caps);
  --ico-ls-micro:     var(--tracking-micro);
  --ico-lh-tight:     var(--leading-tight);

  /* --- page grid (spec 5) --------------------------------------------- */
  /* The frame is a 1440 canvas with a 1200 column. The PAGE no longer uses
     either: since the integration (step 9) the case sits in the site's own
     `.container`, which caps at 72rem and leaves 1104 of content. These two
     stay as a record of what the transfer was measured against — several
     comments below are only readable next to them — and as the numbers the
     re-fitted values are derived FROM. Nothing reads them. */
  --ico-canvas:       1440px; /* the source frame width */
  --ico-col:          1200px; /* the frame's content column */

  /* --- vertical rhythm (spec 5) ---------------------------------------
     Both were off the spacing scale and both are multiples of 4, which by §7
     makes them missing steps rather than exceptions — --space-24 and
     --space-10 were added for them. */
  --ico-sp-section:   var(--space-24);  /* 96 — section top padding */
  --ico-sp-title:     var(--space-10);  /* 40 — heading → content   */

  /* --- radii (spec 5) --------------------------------------------------
     The frame's 12 on the optical cards is not a step and does not get one:
     the scale is deliberately four steps (§7), and at 113px square the
     difference between 8 and 12 is a corner nobody measures. Nearest level,
     same rule as the type. */
  --ico-radius:       var(--radius-md);    /* 16 — panels      */
  --ico-radius-sm:    var(--radius-sm);    /* frame 12 → 8     */
  --ico-radius-img:   var(--radius-sm);    /*  8 — screenshots */
  --ico-radius-pill:  var(--radius-full);  /* the count pill   */

  /* --- section heights (spec 6) ---------------------------------------
     They sum to 4472, which is the source frame's height exactly. That is
     the check for step 1: the skeleton has to land on the same total. It was
     4817 until the Categories frame was redrawn; that section lost 345. */
  --ico-h-hero:       847px;
  --ico-h-system:     837px;
  --ico-h-categories:  834px;
  --ico-h-grid:       1059px;
  --ico-h-inuse:      895px;

  /* --- section 05, the screenshots (spec 6.05) --------------------------
     The frame renders these at 576 tall with 44.31 between them, which is the
     spec's 520 and 40 scaled by 1.1077 — the designer enlarged the whole row
     so it fills the column properly (1181.5 of 1200 rather than 1066.6). The
     scaled numbers are the ones used here, because that is the design as it
     stands; the ratio is recorded so the pair stays in step if either moves.

     Radius and stroke are NOT taken from the render. The frame reports 8.86
     and 1.108, which are the same 1.1077 riding along on a scaled group —
     residue, not intent, and a 1.108px hairline cannot land on a pixel. They
     go back to the round 8 and 1 the spec asks for. */
     REFITTED AT STEP 9. The pair at 576 needs 1181.5 and the site container
     gives 1104, so the height comes down until the row fits. The two shots
     are 3036×3116 and 3117×3117, so at height h the row is
     h × (3036/3116) + h + gap = 1.97433h + 44.31; solving for 1104 gives
     h ≤ 536.7. 536 is what ships.

     Going down rather than sideways is the point: the ALTERNATIVE was to let
     the shots share the column by width, and that is what breaks the two
     captions off one line — different aspect ratios put them 2.6% apart. The
     height model is the only one that keeps them level, so it is kept for as
     long as it fits, and abandoned only below the container's cap. */
  --ico-shot-h:          536px;   /* 576 refitted to the 1104 container */
  --ico-shot-gap:      44.31px;   /*  40 × 1.1077 */
  --ico-shot-caption:     15px;   /* image → caption */

  /* --- section 01, the Hero (spec 6.01 / 9) -----------------------------
     The three big icons sit at centres 208 / 600 / 992 in the 1200 column.
     That is not a flex distribution on its own, but it falls straight out of
     a 128 inset plus space-between: 1200 − 256 = 944 of content, minus three
     160s, leaves 464 split into two 232 gaps → 128 / 520 / 912, which is
     exactly where the frame has them.

     The ribbon step is 40 + 32 = 72 on BOTH axes: horizontally between icons,
     vertically between the three rows. Three 40-tall rows with 32 between
     them come to 184, centred in the 360 panel → rows at 88 / 160 / 232,
     again what the frame has. */
  --ico-hero-inset:      128px;  /* column edge → first big icon */
  --ico-ribbon-icon:      40px;
  --ico-ribbon-gap:       32px;  /* → 72 step, horizontally and vertically */
  --ico-mask-edge:        14%;   /* where the edge fade reaches full opacity */

  /* --- Hero motion (spec 9) --------------------------------------------
     The three ribbon durations are slow and deliberately not multiples of
     one another: multiples periodically line the icons up into vertical
     columns, which reads instantly as a loop. Change one and change all
     three in proportion, keeping them non-commensurate. */
  --ico-ribbon-dur-1:    100s;   /* left  */
  --ico-ribbon-dur-2:     76s;   /* right */
  --ico-ribbon-dur-3:    132s;   /* left  */

  /* The morph is the motion layer's `emphasis` role: something that TRAVELS,
     with an overshoot. That role — --dur plus --ease-overshoot — had no
     consumer on the site until now, and DESIGN_SYSTEM.md §8 says a role is
     the value a new consumer starts from. This is the new consumer.

     It is a real change, not a rename. The prototype was tuned to 230ms on a
     1.56 spring; the role is 320ms on a 1.4 one, so the flick becomes a
     turn and the overshoot is gentler. Kept because the alternative is two
     bespoke motion values on one page, which is the thing the layer exists to
     prevent — and because 1.56 against 1.4 was already borderline visible
     (§14.4). The morph's own cycle absorbs it: 320 of transition inside the
     1330 period leaves 1010 of hold instead of 1100. */
  --ico-morph-dur:       var(--dur);
  --ico-morph-ease:      var(--ease-overshoot);
  /* The "spin" transition from the icon-transition prototype
     (Notion → UI/UX → «Переходы между иконками — прототип»).
     The two transforms are MIRRORED on purpose: that note is the whole point
     of the prototype's directional variants — give the outgoing icon the same
     transform the incoming one starts from and it leaves back through the
     door the new one just came in, which reads as a stutter rather than a
     turn. Incoming enters from -75°, outgoing continues out to +75°. */
  --ico-morph-enter-from: rotate(-75deg) scale(.7);
  --ico-morph-leave-to:   rotate(75deg) scale(.7);

  /* --- section 02, the anatomy block (spec 6.02) ------------------------
     Measured against node 2038:3662 and everything in the spec holds. Two
     offsets are worth knowing because they are NOT set by hand: the artboard
     lands at y=60 and the spec column at y=98 inside the 520 block purely
     from centring both (520−400)/2 and (520−324)/2 — which is exactly where
     Figma has them. The 120 inset is what leaves room for the left callout
     and its label, which sit outside the artboard box. */
  --ico-anatomy-h:       520px;
  --ico-anatomy-inset:   120px;  /* block edge → artboard */
  --ico-anatomy-gap:  var(--space-24); /* 96 — artboard → spec column */
  --ico-artboard:        400px;
  --ico-spec-w:          528px;
  /* The artboard is centred in the 520 block — 60 falls out of (520−400)/2 —
     but the spec column is NOT: the frame places it at 98, which was the
     centred position back when it was 324 tall and stayed put when the
     divider gaps grew it to 360. Placed, not derived. */
  --ico-spec-top:         98px;

  /* the 24×24 grid drawn over the icon. Figma draws these 0.5 wide; 1 is the
     spec's number and the practical floor on the web. */
  --ico-grid-line:         1px;
  --ico-dash:              6 5;   /* the optical box's dash / gap */

  /* dimension callouts, both outside the artboard */
  --ico-dim-left:     var(--space-10); /* 40 — artboard → vertical line */
  --ico-dim-below:    var(--space-6);  /* 24 — artboard → horizontal line */
  --ico-dim-tick:         13px;
  --ico-dim-label-gap:    10px;  /* vertical callout → its label   */
  --ico-dim-label-gap-h:   7px;  /* horizontal callout → its label */

  /* the 2×2 parameter grid */
  --ico-param-col-gap: var(--space-8);
  --ico-param-row-gap:    28px;
  --ico-param-gap:     var(--space-1); /* 4 — label → value */

  --ico-divider-gap:      50px;  /* above and below the rule */

  /* the three optical-compensation cards */
  --ico-card-gap:      var(--space-4);
  --ico-card-pad:      var(--space-4);
  --ico-card-icon:        40px;
  --ico-card-inner-gap: var(--space-2);

  /* --- section 03, the category clusters (spec 6.03) --------------------
     The frame was redrawn (node 2038:3756): the clusters are no longer one
     column of five equal plaques but TWO ROWS of unequal ones — 725 + 463,
     then 332 + 332 + 509 — so the wide categories get wide plaques and every
     plaque in a row ends level. Order changed too: Finance moved to 02.

     Two things about the horizontal fit are worth writing down.

     The plaque widths are the frame's, unscaled, because they are what makes
     the icons wrap the way the frame wraps them (Navigation fits exactly 12
     across, Product & People exactly 8). But the frame's own block is 1204
     wide in row 1 and 1205 in row 2 — 4-5px past the 1200 column, and its two
     rows do not even agree with each other. Rather than carry that overhang,
     the row justifies the plaques across the column, which puts the gutter at
     12 / 13.5 instead of the frame's 16. A gutter nobody can measure by eye
     is a cheaper divergence than a right edge that misses the column.

     The COLUMN gap between icons is 27 everywhere except Navigation, where it
     is 25. That is not noise: at 27 only 11 icons fit its 661 of content and
     the plaque would grow a fourth row. */
  --ico-cat-rows-gap:  var(--space-4);  /* between the two rows of plaques, and
                                  between the plaques within a row           */
  --ico-cat-pad:       var(--space-8);  /* 32 — inside a plaque    */
  --ico-cat-head-h:    var(--space-4);  /* header row — the count pill is the tallest thing in it */
  --ico-cat-head-gap:  var(--space-3);  /* 12 — index → name → count */
  --ico-cat-body-gap:  var(--space-5);  /* 20 — header → icons     */
  --ico-cat-icon:       32px;
  /* Only the inline half: the pill's vertical size is its HEIGHT, which is
     the header row's, so the frame's 2px — a value on no scale — stops
     existing rather than being rounded to something that changes the row. */
  --ico-pill-pad-x:    var(--space-2);

  --ico-cat-row-gap:   var(--space-4);  /* 16 — rows of icons in a plaque */

  /* Plaque widths as RATIOS, not pixels — they are `flex-grow` values.
     The frame's own numbers (725 + 463 and 332 + 332 + 509) add up to 1188
     and needed the 1200 column; the site container gives 1104. Keeping them
     as pixels would overflow, and scaling them to a new set of pixels would
     have to be redone the next time the container moves. As grow ratios the
     proportions are the frame's exactly, at any width. */
  --ico-cat-w-navigation:  725;
  --ico-cat-w-finance:     463;
  --ico-cat-w-wellbeing:   332;
  --ico-cat-w-status:      332;
  --ico-cat-w-product:     509;
  /* Floor for a plaque once the row is allowed to wrap, below the container's
     cap. Not a size: the ratios above still set the proportions. */
  --ico-cat-basis:        260px;

  /* One gap now, not two. The frame had 25 for Navigation and 27 elsewhere,
     and the 25 existed for exactly one reason: at 27 only 11 icons fitted its
     fixed 661 of content and the plaque grew a fourth row. With the plaques
     fluid that constraint is gone, and a distinction nobody can see and
     nothing depends on is worse kept than dropped. */
  --ico-cat-col-gap:        27px;

  /* Heading → clusters. Every other section on the page uses the spec's 40
     (section 05 measures 40.2); this one alone has the block placed at y=212,
     which is 63 below the H2. Following the frame, not the rhythm — but it is
     one token if that placement turns out to be a stray nudge. */
  --ico-cat-title-gap:  63px;

  /* --- section 04, the full-set grid (spec 6.04) ------------------------
     The column model is confirmed against the source frame: panel 1200 with
     40 padding leaves 1120 of content, and 1120 / 15 = 74.67 per column —
     Figma's measured step between icons is 74.47. The row gap is confirmed
     too: measured row pitch 89.96 minus the 27.47 icon it scaled to = 62.5.

     What does NOT reconcile is the section height. See the note beside
     --ico-h-grid and the step-2 entry in the journal. */
  --ico-grid-cols:     15;
  --ico-grid-icon:     28px;
  --ico-grid-row-gap:  62px;
  --ico-panel-pad:     var(--space-10);

  /* --- Hero internals (spec 6.01) --------------------------------------
     96 + 76.8 + 114 + 160 + 40 + 360 = 846.8 → 847. The panel sits flush to
     the bottom of the section, so there is no trailing padding to add. */
  --ico-hero-gap-title: 114px; /* H1 → the row of three big icons */
  --ico-hero-icon:      160px; /* the three big icons             */
  --ico-hero-gap-row:  var(--space-10); /* 40 — row → ribbon panel */
  --ico-hero-panel-h:   360px; /* the ribbon panel                */
}

/* ===== Line Icons — breakpoints (spec 10, step 8) =========================
   Everything below is token overrides only: the case's stylesheet keeps the
   structure and reads the same names at every width. Two things that are
   genuinely structural — section 03's plaques losing their fixed widths, and
   section 05's shots stacking — live in iconography.css, because no token
   can express them.

   Desktop-first, matching the sheet: the frame is a 1440 transfer and these
   are what it degrades to, not the other way round. */

/* --- 768-1199 ------------------------------------------------------------ */
/* The headings are NOT here any more. They are clamps now (--text-display and
   --text-2xl), which is §6's rule: a responsive size is one clamp in the
   token, not three values at the consumer. */
@media (max-width: 71.99em) {
  :root {
    --ico-sp-section:       72px;
    --ico-sp-title: var(--space-8);   /* 32 */
    --ico-cat-title-gap: var(--space-12);   /* 48 */

    /* Hero. Icon size is the spec's; the inset has to come down with it or
       the three slots collide at the narrow end of this range. */
    --ico-hero-icon:       120px;
    --ico-hero-inset: var(--space-12);   /* 48 */
    --ico-hero-gap-title: var(--space-20);   /* 80 */
    --ico-hero-gap-row: var(--space-8);   /* 32 */
    --ico-hero-panel-h:    300px;
    --ico-ribbon-icon:      32px;   /* spec 10: ribbons 32, step 56 */
    --ico-ribbon-gap:       24px;

    /* Section 02. The artboard is the one fixed-size thing in the block and
       everything else is measured off it, so it is what shrinks. */
    --ico-artboard:        320px;
    --ico-anatomy-inset:    72px;
    --ico-anatomy-gap: var(--space-12);   /* 48 */
    --ico-dim-left: var(--space-8);   /* 32 */
    --ico-divider-gap:      36px;
    --ico-param-row-gap: var(--space-5);   /* 20 */

    /* Section 03 */
    --ico-cat-icon:         28px;   /* spec 10 */
    --ico-cat-pad: var(--space-6);   /* 24 */
    --ico-cat-col-gap:      20px;

    /* Section 04. The column COUNT is not here — it has its own ladder at the
       foot of this block, keyed to where 15 columns actually get cramped
       rather than to where the container stops growing. */
    --ico-grid-row-gap:     44px;
    --ico-panel-pad: var(--space-6);   /* 24 */

    /* Section 05 */
    --ico-shot-gap:         32px;
    --ico-shot-caption: var(--space-3);   /* 12 */

    /* Sections stop carrying the frame's heights: below the design width the
       content is shorter than the frame in some sections and taller in
       others, and a min-height would either strand whitespace or do nothing. */
    --ico-h-hero:         auto;
    --ico-h-system:       auto;
    --ico-h-categories:   auto;
    --ico-h-grid:         auto;
    --ico-h-inuse:        auto;
  }
}

/* --- below 768 ----------------------------------------------------------- */
@media (max-width: 767px) {
  :root {
    --ico-sp-section:       56px;
    --ico-sp-title: var(--space-6);   /* 24 */
    --ico-cat-title-gap: var(--space-8);   /* 32 */

    --ico-hero-icon:        96px;   /* spec 10 */
    --ico-hero-inset: var(--space-2);   /* 8 */
    --ico-hero-gap-title:   56px;
    --ico-hero-gap-row:     28px;
    --ico-hero-panel-h:    240px;   /* spec 10 */
    --ico-ribbon-icon:      28px;   /* spec 10: ribbons 28, step 48 */
    --ico-ribbon-gap:       20px;

    --ico-artboard:        240px;
    --ico-anatomy-inset:    52px;
    --ico-anatomy-gap: var(--space-8);   /* 32 */
    --ico-dim-left:         28px;
    --ico-dim-label-gap:     6px;

    --ico-cat-pad: var(--space-5);   /* 20 */
    --ico-cat-col-gap:      16px;

    --ico-grid-row-gap:     28px;
    --ico-panel-pad:         0px;   /* the column inset already holds it off
                                       the edge; a second one wastes width */

    --ico-shot-gap:         28px;
    --ico-card-pad: var(--space-3);   /* 12 */
    --ico-card-icon:        32px;
  }
}

/* --- section 04's column ladder, on its own breakpoints -------------------
   Deliberately NOT tied to the container's cap. The full set is the one block
   whose density is set by the column count rather than by the space around
   it, and 15 columns are still comfortable well below the cap: at a 852
   container the pitch is 53.6 against a 28px icon. Switching at the cap would
   jump straight from a 68 pitch to a 117 one — a dense grid turning into a
   sparse dot field for no reason anyone could see on screen. */
@media (max-width: 900px) { :root { --ico-grid-cols: 9; } }
@media (max-width: 767px) { :root { --ico-grid-cols: 5; } }

/* Why 9 and 5 columns rather than the spec's 10 and 6.
   The set drawn in section 04 is 135, and that number was CHOSEN — at step 2
   the grid was 136 and left one icon alone on a tenth row, which is exactly
   what dropping one fixed. 135 factors as 27 x 5 = 15 x 9, so 15, 9 and 5
   columns all come out as full rectangles; 10 leaves a last row of 5 and 6
   leaves one of 3. Taking the spec's numbers would put the orphan back at
   every width except the desktop one, undoing a decision already made. */


/* =====================================================================
   SMALL ILLUSTRATIONS CASE — scoped tokens (prefix --illos-)
   =====================================================================
   The Small Illustrations case study (work/small-illustrations/) is a
   transfer of the Figma frame `Small Illustrations — Landing` (file
   OyawfANUPtP3EYv7TeNyrl, node 117:14315, 1440 x 5933).

   It is built the way the Iconography case ENDED UP, not the way it
   started: every colour, size, weight and duration below is an ALIAS to a
   role or a step the system already has, and the case follows the site
   theme rather than pinning one. Only two things are literals, and both are
   named as exceptions with a reason — see --illos-canvas and the section
   heights.

   Rule 3 of ILLOS_PORTFOLIO_BRIEF.md is why this block exists at step 1
   rather than at the end: the case declares its vocabulary as aliases from
   the first commit, so there is never a parallel set to migrate later.
   ===================================================================== */
:root {
  /* --- colour ---------------------------------------------------------
     The page chrome takes the site's roles and flips with the theme, exactly
     as the Iconography case does since its design-system pass.

     THE ONE EXCEPTION IS THE CANVAS, and it is an exception on purpose.
     The illustrations are MULTICOLOUR ARTWORK that cannot be recoloured
     (brief §3): they were drawn on YuLife's dark purple, and several of them
     carry white and near-white parts — the glass on Elixir, the ball, the
     envelope. On a light ground those parts stop existing. So the surface
     the artwork sits on is a property OF THE ARTWORK, not of the page, and
     it stays dark in both themes. Measured off the frame's own Canvas fill
     (#241A3D at 1200x1280, node 118:16921).

     It is deliberately NOT --indigo-800: that token is a neutral tint at
     C .030, and this is a brand purple at roughly three times the chroma.
     Rounding it to the ladder would be rounding away the thing that makes
     the artwork sit correctly. Registered in DESIGN_SYSTEM.md §11. */
  --illos-canvas:      #241a3d;
  /* One step down from the canvas, same hue — the fallback under the work
     card's cover image, so a cover that has not loaded still reads as this
     case rather than as a hole. Derived from the canvas, never independent. */
  --illos-canvas-deep: #1a1230;

  --illos-text:        var(--text-primary);
  --illos-text-muted:  var(--text-secondary);
  --illos-line:        var(--border-subtle);
  --illos-surface:     var(--surface-raised);
  /* The construction guides of section 04. A measuring instrument, so it
     reads as drawn rather than as a hairline — the same call the Iconography
     case made for its dimension rules. */
  --illos-guide:       var(--text-secondary);

  /* --- type -----------------------------------------------------------
     Same three decisions the Iconography case landed on, and for the same
     reasons: the frame's 64 and 44 are both nearest to --text-2xl and would
     have collapsed into one size, so the H1 takes the level above. Both are
     clamps, so the case carries no heading sizes at any breakpoint. */
  --illos-font:        var(--font-ui);
  --illos-fs-h1:       var(--text-display);  /* frame 64 → 44-88 */
  --illos-fs-h2:       var(--text-2xl);      /* frame 44 → 31-49 */
  --illos-fs-body:     var(--text-md);       /* frame 16, exact  */
  --illos-fs-label:    var(--text-xs);       /* frame 11 → 12.8  */
  --illos-fs-micro:    var(--text-2xs);      /* frame 10 → 10.24 */
  --illos-fw-regular:  var(--weight-regular);
  --illos-fw-medium:   var(--weight-medium);
  --illos-fw-semibold: var(--weight-semibold);
  --illos-ls-caps:     var(--tracking-caps);
  --illos-ls-micro:    var(--tracking-micro);
  --illos-lh-tight:    var(--leading-tight);

  /* --- rhythm ---------------------------------------------------------
     The frame puts every section heading 96 from the section top and every
     content block 40 below its heading — the same two numbers the
     Iconography frame used, which is why they are the same two steps. */
  --illos-sp-section:  var(--space-24);  /* 96 — section top padding */
  --illos-sp-title:    var(--space-10);  /* 40 — heading → content   */
  --illos-sp-tail:     var(--space-24);  /* 96 — last section → footer */

  --illos-radius:      var(--radius-md);   /* 16 — panels     */
  --illos-radius-sm:   var(--radius-sm);   /*  8 — small tiles */

  /* --- section heights, step 1 only ------------------------------------
     LITERALS, and temporary. These are the measured heights of the CONTENT
     BLOCKS in the source frame, and at step 1 they are the only thing
     holding the seven empty sections open so the skeleton can be scrolled
     and its rhythm read. Each one is retired by the step that fills its
     section — the content then sets the height, as it does on every other
     page of the site.

     They are min-heights, not heights: when real content overflows one it
     must push the page rather than be clipped.

     Measured against the frame's 1200 column; the page renders in the
     site's .container, which caps at 1104. They are a scaffold, not a
     target, so they are not refitted. */
  --illos-h-hero:          584px;  /* frame 584.54 — the six morph slots  */
  --illos-h-palette:       263px;  /* node 125:18434                      */
  --illos-h-construction:  385px;  /* node 128:20075                      */
  --illos-h-categories:    760px;  /* node 123:18248                      */
  --illos-h-inapp:         420px;  /* node 120:18469                      */

  /* --- section 06, the full set ----------------------------------------
     The frame draws the set on a dark panel INSIDE the section: canvas 1200
     wide with 42.5 of side padding and 40 of top, ten columns of 80 at a 115
     pitch, rows at a 112 pitch. The 80 and the 32 row gap are the brief's
     numbers and are kept exactly.

     The horizontal gap is NOT kept, and could not be: the frame's 35 needs
     the 1115 the frame had, and the site container gives 1104. The columns
     are `1fr` tracks with the artwork centred in each, so the gap falls out
     of whatever width the page has — which is also what makes the section
     survive a narrow screen without a second set of numbers. Measured at the
     container's cap it comes to ~25.

     The panel padding drops from the frame's 42.5/40 to the scale's 40 on
     both axes: half a pixel of asymmetry is not a design decision. */
  --illos-grid-cols:        10;
  --illos-grid-item:       80px;
  --illos-grid-row-gap: var(--space-8);   /* 32 — the frame's row pitch minus the item */
  --illos-canvas-pad:  var(--space-10);   /* 40 */

  /* --- section 05, the four group players -------------------------------
     The frame's block is 584 x 364 on a 32 gutter, and its internals add up
     exactly: 32 padding + 13 label + 24 + 200 stage + 24 + 39 counter + 32
     padding = 364. Every one of those gaps is already a step on the scale,
     so the block is described by the scale rather than by its own numbers —
     only the 200 stage is geometry, because that is the size of a drawing.

     Widths are not carried over: two 1fr columns give the frame's own
     proportion at any container width, which is what the Iconography case
     had to convert its plaques to after the fact. */
  --illos-cat-cols:         2;
  --illos-cat-gap:     var(--space-8);   /* 32 — between blocks, both axes */
  --illos-cat-pad:     var(--space-8);   /* 32 — inside a block           */
  --illos-cat-stage:      200px;         /* the drawing                   */
  --illos-cat-label-gap:  var(--space-6); /* 24 — label → stage           */
  --illos-cat-count-gap:  var(--space-6); /* 24 — stage → counter         */
  --illos-cat-count-x:    var(--space-2); /* 6 in the frame → 8           */
  /* The frame sets the running number at 32 and the total at 14. 14 is
     --text-sm exactly. 32 is not on the scale and its neighbours are 25 and
     a clamp that reaches 48.8 at this width — far too big for a counter — so
     it takes the nearest fixed level, the same "nearest level" rule the
     Iconography case applied to its headings. */
  --illos-cat-num:     var(--text-xl);   /* frame 32 → 25    */
  --illos-cat-total:   var(--text-sm);   /* frame 14, exact  */

  /* The cross-fade between two stacked layers. Paths are never morphed —
     these objects carry dozens of differently-coloured contours and a morph
     would be mush (brief §4.05). */
  --illos-cat-fade:    var(--motion-enter);

  /* How long each block holds an illustration, and where each one starts.
     Four periodic timers WILL eventually line up; the only thing that can be
     chosen is how far away the first time is. These four were picked by
     computing it rather than by calling them "non-multiples": no four-way
     coincidence inside twenty minutes, and the first three-way is at 57.8s.
     Change one and RECOMPUTE — the Iconography case learned this the hard
     way when 1700 and 2200 turned out to share a factor of 100. */
  --illos-cat-cycle-1:  1990ms;
  --illos-cat-cycle-2:  2390ms;
  --illos-cat-cycle-3:  2810ms;
  --illos-cat-cycle-4:  3290ms;
  --illos-cat-phase-1:     0ms;
  --illos-cat-phase-2:   530ms;
  --illos-cat-phase-3:  1150ms;
  --illos-cat-phase-4:  1810ms;

  /* --- the project intro (frame 74:65666) --------------------------------
     Prose, so it gets prose settings: the case root sets 1.2 leading for a
     layout built on 1.2 text boxes, and a five-line paragraph at 1.2 is a
     wall. This one takes the site's relaxed leading back, and a measure in
     `ch` so the line length follows the type rather than a pixel width — the
     frame runs 841 of 1400, which is about 90 characters and too wide to
     read comfortably. */
  --illos-intro-lead: var(--leading-relaxed);
  --illos-intro-row:  var(--space-1);    /* between the four meta rows     */
  /* Prose and credits sit side by side. The prose column is the wider of
     the two and still lands near a readable measure — roughly 60 characters
     at the container's cap — which is what the ratio is chosen for, not the
     other way round. */
  --illos-intro-cols:      3fr 2fr;
  --illos-intro-col-gap: var(--space-12);

  /* --- section 03, the palette ------------------------------------------
     One strip of fifteen columns, the base over its shadow at equal heights,
     the hex under each column and the caption under the strip. The frame's
     263 is 40 + 136 + 12 + 10 + 12 + 13 + 40, and every gap in that sum is
     already a step — nothing here had to be rounded.

     The radius belongs to the STRIP, not to each swatch: the frame rounds the
     four outer corners and butts the columns against each other. */
  --illos-pal-cols:         15;
  --illos-pal-row:          68px;         /* one half of a pair; 2 x 68 = the frame's 136 */

  /* --- section 01, the six animation slots ------------------------------
     The frame lays the six out 3 x 2 already — the brief describes five and
     says the sixth is to be added, but `Morph slots` (117:14318) has Elixir
     in it, so the layout question answers itself: three across, two down.

     Slot pitch in the frame is 428 on a 1200 column with 72 of inset either
     side. Those are not carried over — three equal columns with the slot
     centred in each hold the same reading at any width, and the frame's
     numbers only ever described 1200.

     H1 to the first slot is 114: the frame's 40 heading gap plus 74 of
     padding inside the slots block. It is kept as one number because it is
     composition, not rhythm — and it is worth noting that the Iconography
     frame puts its own hero row at exactly 114 too, which makes it look like
     a house measure rather than a nudge. */
  --illos-hero-gap-title:  114px;
  --illos-hero-cols:           3;
  --illos-hero-slot:       200px;         /* the drawing */
  --illos-hero-row-gap: var(--space-24);  /* frame 98.8 → 96 */

  /* --- the approved animations (brief §5) --------------------------------
     EVERY NUMBER BELOW IS QUOTED, NOT CHOSEN. They come from §5 of the brief
     and from the tested prototypes behind it (Notion, "Портфолио — код" →
     "Анимации иконок"), where they were settled in a sandbox. Nothing here
     was tuned by eye and nothing should be: if one of these reads wrong,
     the place to change it is the prototype, then here.

     They live in tokens rather than in illos.js because rule 2 of the brief
     says timings are CSS variables — and because it puts the approved values
     somewhere they can be read without opening the code that consumes them. */

  /* Compass — Sway. Only the needle turns; the cap over its pivot holds
     still, because it reads as light rather than as metal. The origin is
     §5's 50.8% / 49.4% of the NEEDLE's own box restated against the whole
     illustration, which is the box the layer actually spans: the pivot is
     100.1 / 121.5 of the 200 frame, so 50.05% / 60.75%. */
  --illos-compass-amp:      19;      /* degrees */
  --illos-compass-period: 1.4s;
  --illos-compass-origin-x: 50.05%;
  --illos-compass-origin-y: 60.75%;

  /* Talking — Scale. The bubbles never move and opacity never changes; the
     pause is what reads as typing. Active phase under half the cycle is the
     point: at 100% it stops being typing and becomes a wave. */
  --illos-talking-cycle:  1080ms;
  --illos-talking-stagger: 130ms;
  --illos-talking-active:  0.49;     /* share of the cycle a dot is moving */
  --illos-talking-pulse:   0.24;     /* how much it grows */

  /* Elixir — two stars fading over a bottle that rocks. The white glint on
     the glass is NOT animated: it is light, it does not blink. Both layers
     take the same transform so the stars travel with the glass. */
  --illos-elixir-cycle:   2650ms;
  --illos-elixir-fade:     340ms;    /* both ends, smoothstep */
  --illos-elixir-s1-start: 0.31;
  --illos-elixir-s1-len:   0.68;
  --illos-elixir-s2-start: 0;
  --illos-elixir-s2-len:   0.875;
  --illos-elixir-rock:     2.5;      /* degrees */
  --illos-elixir-rock-per: 3.4s;
  --illos-elixir-bob:      2px;
  --illos-elixir-origin-x: 56.4%;
  --illos-elixir-origin-y: 94%;

  /* Envelope — one envelope, three signs in turn. The letter starts HIDDEN
     inside and rises out of the mouth; it does not appear on top. The five
     durations add to §5's cycle exactly: 200 + 3 x (620 + 1240 + 730 + 820)
     = 10 430.

     The hold is long on purpose — under about 900ms Success and Fail blur
     into a flicker — and the gap exists so the envelope is visibly empty
     between signs, which is the whole "arrived, then gone" reading.

     The geometry below is §5's 72-unit frame restated as fractions of the
     box, which is scale-free: the depth of 42 is 58.333% of 72, the mouth
     line 26.7534 is 37.157%, its apex (36.147, 47.379) is 50.204% / 65.804%. */
  --illos-env-delay:      200ms;
  --illos-env-rise:       620ms;
  --illos-env-hold:      1240ms;
  --illos-env-fall:       730ms;
  --illos-env-gap:        820ms;
  --illos-env-depth:   58.333%;   /* frame 42 of 72 */
  --illos-env-over:        1.6;   /* Out Back overshoot on the way up */
  --illos-env-tilt:         -3;   /* degrees, while hidden; 0 on landing */
  --illos-env-badge-from:  0.5;
  --illos-env-badge-lead: 320ms;  /* the pop starts before the letter lands */
  --illos-env-badge-dur:  480ms;
  --illos-env-recoil:     0.03;
  --illos-env-recoil-dur: 240ms;
  --illos-env-mouth-y:  37.157%;  /* frame 26.7534 */
  --illos-env-apex-x:   50.204%;  /* frame 36.147  */
  --illos-env-apex-y:   65.804%;  /* frame 47.379  */
  --illos-env-letter-origin-y: 39.583%;  /* frame 28.5 */
  --illos-env-badge-origin-y:  37.015%;  /* frame 26.6508 */
  --illos-env-recoil-origin-y: 99.306%;  /* frame 71.5 */

  /* Sun — approved after the brief was written; §7 still lists it as open,
     but the prototype carries a full Sun block and these are its settings.

     The corona MORPHS rather than rotates, and three of these numbers are the
     reason it reads as matter instead of as a spun picture: depth past 1 so
     the two extremes are not two static frames, four waves so no single lean
     runs round the shape, and a phase taken from each point's angle so the
     outline's seam closes. Breathing and flicker are deliberately absent —
     the morph is lively enough, and scale and opacity on top make mush. */
  --illos-sun-morph-period:  2.6s;
  --illos-sun-morph-depth:   1.25;
  --illos-sun-waves:            4;
  --illos-sun-lava-amp:        12;    /* degrees, sway — not rotation */
  --illos-sun-lava-period:   6.4s;
  --illos-sun-breathe:      0.012;    /* the disc only */
  --illos-sun-breathe-period: 4.6s;
  --illos-sun-speed:         1.05;

  /* Monitor — seven of the ten screens. The chassis never moves: the monitor
     is the frame here, not the subject.

     Three of these carry an argument. The disc's flow gets 1170 against an
     ordinary change's 760 because a hole closing has to be seen — at the
     shorter time it reads as a colour change. The sign waits until the hole
     is 48% closed, so it does not appear over a shape that is still open.
     And the stagger is applied to LINEAR progress: eased first, the leading
     bar runs to 1/0.58 = 1.72 and grows out of the screen. */
  --illos-mon-delay:       200ms;
  --illos-mon-trans:       760ms;   /* an ordinary change  */
  --illos-mon-morph:      1170ms;   /* the disc flowing    */
  --illos-mon-hold:       1100ms;
  --illos-mon-speed:        1.05;   /* cycle lands on 14 450 */
  --illos-mon-over:          1.5;   /* Out Back on the way in */
  --illos-mon-overlap:      0.22;   /* out and in cross over  */
  --illos-mon-stagger:      0.42;   /* between bars, and between form rows */
  --illos-mon-spin:         -155;   /* deg/s, the donut arc, anticlockwise */
  --illos-mon-glyph-pop:    0.45;
  --illos-mon-glyph-delay:  0.48;
  --illos-mon-slide:          14;   /* form rows enter from the left */
  --illos-mon-members-from: 0.72;
  --illos-mon-bar-from:     0.06;
}
