/* ============================================================
   Superus design tokens
   Four layers: ramps → semantic roles → ladders → compat aliases.
   Components should only reference semantic tokens, never ramps.

   Colour is authored in OKLCH (perceptually uniform: equal L steps
   read as equal brightness steps across every hue) and every colour
   role is declared once with light-dark(light, dark), so the two
   themes are defined side by side and can never drift apart.
   ============================================================ */

:root {
  color-scheme: light dark; /* follow the OS by default */
}
:root[data-theme='light'] {
  color-scheme: light;
}
:root[data-theme='dark'] {
  color-scheme: dark;
}

:root {
  /* ============================================================
     1. RAMPS  (primitives — do not use directly in components)

     Every ramp has 12 steps and every step number means the same
     thing in every ramp and in both themes. That fixed contract is
     what makes theme switching mechanical instead of manual:

       1   app / canvas background
       2   subtle background
       3   component background            (rest)
       4   component background            (hover)
       5   component background            (active / selected)
       6   subtle border, separator
       7   border, focus ring
       8   strong border, hovered border
       9   SOLID fill — the pure colour    (rest)
       10  solid fill                      (hover)
       11  low-contrast text  (≥4.5:1 on steps 1-3)
       12  high-contrast text (≥12:1 on steps 1-3)

     Lightness runs 1→12 dark-ward in light theme and light-ward in
     dark theme; step 9 holds the same hue + chroma in both so brand
     colour never shifts identity.
     ============================================================ */

  /* ---- neutral ramp — hue 268, chroma rises with depth ----
     Steps 1-3 are spaced unevenly on purpose. 1→2 is a near-merge
     (2.5pt): chrome and canvas are one quiet ground, told apart by a
     hairline, not a fill. 2→3 is the widest step in the whole system
     (6pt): that is the ground→content jump, and it is the only place
     lightness is spent on hierarchy.                               */
  --n-1: light-dark(oklch(99.2% 0.002 268), oklch(12.5% 0.005 268));
  --n-2: light-dark(oklch(97.6% 0.004 268), oklch(15% 0.007 268));
  --n-3: light-dark(oklch(95.5% 0.006 268), oklch(21% 0.011 268));
  --n-4: light-dark(oklch(93% 0.008 268), oklch(24.5% 0.013 268));
  --n-5: light-dark(oklch(90.5% 0.01 268), oklch(28% 0.015 268));
  --n-6: light-dark(oklch(85.5% 0.012 268), oklch(32.5% 0.016 268));
  --n-7: light-dark(oklch(81.5% 0.014 268), oklch(38% 0.018 268));
  --n-8: light-dark(oklch(75% 0.018 268), oklch(47% 0.021 268));
  --n-9: light-dark(oklch(64% 0.02 268), oklch(53.5% 0.022 268));
  --n-10: light-dark(oklch(59.5% 0.021 268), oklch(59.5% 0.022 268));
  --n-11: light-dark(oklch(50% 0.021 268), oklch(76.5% 0.018 268));
  --n-12: light-dark(oklch(24% 0.02 268), oklch(95% 0.008 268));

  /* ---- accent ramp — indigo, hue 272. Step 9 is the brand. ---- */
  --a-1: light-dark(oklch(99.2% 0.006 272), oklch(19% 0.022 272));
  --a-2: light-dark(oklch(98.2% 0.014 272), oklch(22.4% 0.038 272));
  --a-3: light-dark(oklch(96% 0.03 272), oklch(27.5% 0.07 272));
  --a-4: light-dark(oklch(93.5% 0.05 272), oklch(31.5% 0.095 272));
  --a-5: light-dark(oklch(90.5% 0.07 272), oklch(35.5% 0.112 272));
  --a-6: light-dark(oklch(86.5% 0.09 272), oklch(40.5% 0.124 272));
  --a-7: light-dark(oklch(81% 0.112 272), oklch(45% 0.138 272));
  --a-8: light-dark(oklch(73% 0.146 272), oklch(50.5% 0.168 272));
  --a-9: light-dark(oklch(56% 0.2 272), oklch(56% 0.2 272));
  --a-10: light-dark(oklch(51% 0.196 272), oklch(62% 0.192 272));
  --a-11: light-dark(oklch(48% 0.188 272), oklch(80% 0.142 272));
  --a-12: light-dark(oklch(30% 0.108 272), oklch(93% 0.05 272));

  /* ---- status ramps (compressed: only the steps a status needs) ----
     3 = soft surface · 6 = soft border · 9 = solid · 10 = solid hover
     11 = text on a soft/neutral surface                             */
  --g-3: light-dark(oklch(95.6% 0.035 156), oklch(26% 0.052 156));
  --g-6: light-dark(oklch(86% 0.082 156), oklch(37.5% 0.082 156));
  --g-9: light-dark(oklch(64.5% 0.155 156), oklch(70% 0.152 156));
  --g-10: light-dark(oklch(59.5% 0.15 156), oklch(75% 0.145 156));
  --g-11: light-dark(oklch(50% 0.125 156), oklch(82% 0.13 156));

  --y-3: light-dark(oklch(97% 0.04 82), oklch(26.5% 0.048 82));
  --y-6: light-dark(oklch(89.5% 0.09 82), oklch(38.5% 0.078 82));
  --y-9: light-dark(oklch(80.5% 0.15 82), oklch(82% 0.15 82));
  --y-10: light-dark(oklch(75.5% 0.148 82), oklch(87% 0.14 82));
  --y-11: light-dark(oklch(52% 0.11 72), oklch(88% 0.12 82));

  --r-3: light-dark(oklch(96.2% 0.022 22), oklch(26.5% 0.06 22));
  --r-6: light-dark(oklch(88% 0.062 22), oklch(38.5% 0.098 22));
  --r-9: light-dark(oklch(60% 0.205 25), oklch(64.5% 0.198 25));
  --r-10: light-dark(oklch(55% 0.2 25), oklch(70% 0.185 25));
  --r-11: light-dark(oklch(52% 0.185 25), oklch(79% 0.145 22));

  --p-9: light-dark(
    oklch(64% 0.235 345),
    oklch(70% 0.225 345)
  ); /* alignment guides */

  /* ---- content tone ramps — the five card / connector colours ----
     These label *content*, not state, so they stay recognisably the
     same hue in both themes: pale wash in light, deep wash in dark.
     3 = card fill · 6 = card border · 9 = solid (stroke, swatch)
     11 = label text readable on step 3                             */
  --tone-slate-3: light-dark(oklch(95.8% 0.004 268), oklch(30.5% 0.012 268));
  --tone-slate-6: light-dark(oklch(87.5% 0.008 268), oklch(38% 0.014 268));
  --tone-slate-9: light-dark(oklch(62% 0.016 268), oklch(60% 0.02 268));
  --tone-slate-11: light-dark(oklch(45% 0.016 268), oklch(78% 0.016 268));

  --tone-blue-3: light-dark(oklch(95.2% 0.03 264), oklch(31% 0.084 264));
  --tone-blue-6: light-dark(oklch(85% 0.078 264), oklch(40% 0.11 264));
  --tone-blue-9: light-dark(oklch(58% 0.185 264), oklch(64% 0.18 264));
  --tone-blue-11: light-dark(oklch(46% 0.165 264), oklch(82% 0.11 264));

  --tone-teal-3: light-dark(oklch(95.8% 0.032 186), oklch(30.5% 0.063 186));
  --tone-teal-6: light-dark(oklch(86% 0.072 186), oklch(39% 0.08 186));
  --tone-teal-9: light-dark(oklch(62% 0.118 186), oklch(70% 0.112 186));
  --tone-teal-11: light-dark(oklch(46.5% 0.092 186), oklch(84% 0.09 186));

  --tone-amber-3: light-dark(oklch(96.8% 0.042 84), oklch(31% 0.06 84));
  --tone-amber-6: light-dark(oklch(89% 0.092 84), oklch(40% 0.082 84));
  --tone-amber-9: light-dark(oklch(78% 0.148 76), oklch(80% 0.148 82));
  --tone-amber-11: light-dark(oklch(49% 0.105 70), oklch(87% 0.115 84));

  --tone-rose-3: light-dark(oklch(95.8% 0.026 8), oklch(31% 0.074 8));
  --tone-rose-6: light-dark(oklch(87.5% 0.068 8), oklch(40% 0.1 8));
  --tone-rose-9: light-dark(oklch(63% 0.19 8), oklch(68% 0.18 8));
  --tone-rose-11: light-dark(oklch(49% 0.165 8), oklch(83% 0.11 8));

  /* ---- alpha primitives ----
     Translucent ink/light for anything that must sit on an unknown
     background: cards over the grid, hover layers over any surface,
     borders over both chrome and web pages.                        */
  --alpha-1: light-dark(oklch(20% 0.02 268 / 0.035), oklch(100% 0 0 / 0.035));
  --alpha-2: light-dark(oklch(20% 0.02 268 / 0.055), oklch(100% 0 0 / 0.055));
  --alpha-3: light-dark(oklch(20% 0.02 268 / 0.085), oklch(100% 0 0 / 0.085));
  --alpha-4: light-dark(oklch(20% 0.02 268 / 0.13), oklch(100% 0 0 / 0.13));
  --alpha-5: light-dark(oklch(20% 0.02 268 / 0.2), oklch(100% 0 0 / 0.2));

  /* ============================================================
     2. LADDER — SURFACE / ELEVATION

     One rule, identical in both themes:

         canvas  <  chrome  <  content,
         and content is the only plane that casts a shadow.

     The canvas is VOID, not a reading surface — content sits on it
     as separate objects — so it is the deepest plane in the app.
     Measured against the field: Figma (+5.8pt dark / +3.0 light),
     Excalidraw (+7.6pt dark) and tldraw (+0.6pt light) all place
     chrome above the canvas. The one counter-example, VS Code Dark
     Modern (-3.0pt), is a different case: there the canvas IS the
     content, so its chrome has to sit below it.

     Chrome takes the smallest step in the field — 2.5-3pt, half of
     Figma's — because it only has to read as a frame, not announce
     itself. Spending any more on it is spending it twice: every
     point given to the bezel is a point taken off the one jump that
     carries the hierarchy, canvas->content.

     LIGHT IS A DIFFERENT SHAPE FROM DARK. Dark has headroom above the
     canvas, so each plane can take its own step and a floating layer
     reads as floating because it is lighter. Light has none: content
     is already white, so stacking chrome between a grey canvas and a
     white card only shaves the one jump that matters. Measured, the
     old arrangement put canvas-vs-chrome at 1.08:1 and canvas-vs-card
     at 1.23:1 — three planes squeezed into the top fifth of the range,
     which is why the light theme read as washed rather than layered.
     So light takes the Figma shape instead: the frame IS the content
     plane (both white) and the canvas is a well cut into it, one step
     deeper than before. That spends the whole light range on the only
     boundary that carries meaning — board vs. object — and moves it
     from 1.23:1 to 1.33:1. Above `raised`, light has nowhere left to
     go, so elevation there is carried entirely by shadow and hairline,
     which is what --elev-* and --hairline are for.

     Below roughly 25% lightness, equal lightness steps stop buying
     contrast — the flare term in the contrast formula swamps them,
     and chrome-vs-canvas lands near 1.03:1 no matter how the steps
     are spaced. That is not a bug to tune around: structural planes
     are supposed to merge. Hierarchy inside the frame is carried by
     hairline borders (alpha, so they read at any depth) and by
     shadow; the whole lightness range is spent on ground→content,
     where a white page reaches ~20:1 against the canvas.
     ============================================================ */
  --surface-sunken: light-dark(
    var(--n-5),
    var(--n-1)
  ); /* canvas — the deepest plane */
  --surface-chrome: light-dark(
    oklch(100% 0 0),
    var(--n-2)
  ); /* topbar, sidebar — in light, the frame is the content plane */
  --surface-raised: light-dark(oklch(100% 0 0), var(--n-3)); /* cards, panels */
  --surface-overlay: light-dark(
    oklch(100% 0 0),
    var(--n-4)
  ); /* popovers, toolbars, menus */
  --surface-top: light-dark(oklch(100% 0 0), var(--n-5)); /* modals */
  /* How much of a floating layer is its own colour vs. what shows through.
     In dark the ladder separates by lightness, so a floating layer can give
     up a quarter of itself and still read as the plane above. In light every
     plane above the canvas is white and depth is carried by shadow alone —
     so there the glass is almost opaque and the blur is only a garnish.
     This one cannot use light-dark(): that function returns a COLOUR, and
     color-mix() needs a real <percentage> here. It gets the three-state
     treatment at the bottom of this file instead. */
  --overlay-opacity: 92%;
  --surface-base: var(--surface-chrome); /* alias */

  /* inverted surface — tooltips, scrims, "always opposite" chrome */
  --surface-inverted: light-dark(var(--n-12), var(--n-12));
  --text-on-inverted: light-dark(var(--n-1), var(--n-1));

  /* content surface — a rendered web page. Deliberately NOT themed:
     a page is real content, and recolouring it would be a lie.
     See --page-veil for how dark mode tames it instead.            */
  --surface-page: #ffffff;
  --text-page: #202124;

  /* fills that sit *on* a surface (inputs, chips, wells) */
  --fill-subtle: light-dark(var(--n-3), var(--n-4));
  --fill-strong: light-dark(var(--n-4), var(--n-5));

  /* ============================================================
     3. LADDER — STATE

     State is an alpha layer, never a fixed colour, so the same token
     works on chrome, on a coloured note and over a web page.
     ============================================================ */
  /* Layer opacity, not a colour swap — which is what lets one hover token
     work on chrome, on a white page and on an amber note alike, where a
     solid --surface-N painted a grey slab over the note's own colour.
     The two themes do NOT get the same alpha: equal alpha is not equal
     perceived contrast at opposite ends of the lightness range. 3.5% on
     dark chrome (rgb 10,11,14) is a nine-level move — invisible; on a
     white card the same 3.5% already reads. Measured against chrome:
       light  5.5% / 9.5%   dark  13% / 20%   →  hover lands ~1.4:1 in both */
  --state-hover: light-dark(
    oklch(20% 0.02 268 / 0.055),
    oklch(100% 0 0 / 0.13)
  );
  --state-active: light-dark(
    oklch(20% 0.02 268 / 0.095),
    oklch(100% 0 0 / 0.2)
  ); /* pressed */
  --state-selected: color-mix(in oklch, var(--a-9) 14%, transparent);
  --state-drop: color-mix(in oklch, var(--a-9) 22%, transparent);
  --state-disabled-opacity: 0.45;

  /* Hover and selection must not speak the same language. Hover is
     NEUTRAL — a brighter edge and one notch more lift, saying "you
     could act on this". Accent is reserved for states the user has
     actually caused: selected, focused, connecting, drop target.
     On a canvas the pointer sweeps over many cards, so an accent
     hover would strobe the board with false selections.            */
  --object-border: var(--border-default);
  --object-border-hover: var(--border-strong);
  --object-border-selected: var(--accent);

  /* ============================================================
     4. LADDER — TEXT EMPHASIS
     Four rungs, deliberately no fifth. Measured against
     --surface-raised, light / dark:
       primary   16.5 / 13.8 : 1   the thing to read
       secondary  6.0 /  7.7 : 1   supporting copy, inactive icons
       tertiary   4.0 /  4.0 : 1   metadata, placeholders, hints
       disabled   2.2 /  2.5 : 1   unavailable (never meaning-bearing)
     Step 9 of the accent sits at L 56% in both themes: the one
     lightness where white label text clears 4.5:1 on the solid AND
     the solid itself clears 3:1 against both a white card and a
     dark one, so one brand colour serves fills and outlines alike.
     ============================================================ */
  --text-primary: var(--n-12);
  --text-secondary: var(--n-11);
  --text-tertiary: var(--n-10);
  --text-disabled: var(--n-8);
  --text-inverse: var(--text-on-inverted);
  --text-on-accent: light-dark(oklch(100% 0 0), oklch(100% 0 0));
  --text-link: var(--a-11);

  /* ============================================================
     5. LADDER — BORDER
     Alpha-based so one token reads correctly on every surface.
     ============================================================ */
  --border-subtle: var(--alpha-1); /* inner dividers, table rules */
  --border-default: var(--alpha-3); /* component outline */
  --border-strong: var(--alpha-4); /* emphasised / hovered outline */
  --border-accent: var(--a-8);
  /* A floating layer's own outline. In dark it can be the ordinary hairline,
     because the layer is already a lighter plane than what it covers. In light
     every plane above the canvas is white, so the border is doing the work the
     lightness step used to do — and gets the firmer value. */
  --overlay-border: light-dark(var(--border-strong), var(--border-default));

  /* ============================================================
     6. LADDER — ACTION IMPORTANCE
     Exactly four rungs of emphasis, one solid per view.
       solid   → the single primary action
       tonal   → secondary, still a real choice
       outline → tertiary
       ghost   → quiet / iconographic
     ============================================================ */
  --action-solid-bg: var(--a-9);
  --action-solid-bg-hover: var(--a-10);
  --action-solid-fg: var(--text-on-accent);

  --action-tonal-bg: var(--a-3);
  --action-tonal-bg-hover: var(--a-4);
  --action-tonal-fg: var(--a-11);

  --action-outline-border: var(--border-default);
  --action-outline-fg: var(--text-primary);

  --action-ghost-fg: var(--text-secondary);
  --action-ghost-fg-hover: var(--text-primary);

  --action-danger-bg: var(--r-9);
  --action-danger-bg-hover: var(--r-10);
  --action-danger-fg: light-dark(oklch(100% 0 0), oklch(100% 0 0));

  /* ============================================================
     7. ACCENT + STATUS ROLES
     ============================================================ */
  --accent: var(--a-9);
  --accent-hover: var(--a-10);
  --accent-active: light-dark(var(--a-11), var(--a-8));
  --accent-text: var(--a-11);
  --accent-soft: var(--a-3);
  --accent-border: var(--a-6);
  --accent-ring: color-mix(in oklch, var(--a-9) 45%, transparent);

  --success: var(--g-9);
  --success-hover: var(--g-10);
  --success-text: var(--g-11);
  --success-soft: var(--g-3);
  --success-border: var(--g-6);
  --warning: var(--y-9);
  --warning-hover: var(--y-10);
  --warning-text: var(--y-11);
  --warning-soft: var(--y-3);
  --warning-border: var(--y-6);
  --danger: var(--r-9);
  --danger-hover: var(--r-10);
  --danger-text: var(--r-11);
  --danger-soft: var(--r-3);
  --danger-border: var(--r-6);
  --info: var(--accent);
  --info-text: var(--accent-text);
  --info-soft: var(--accent-soft);

  /* ============================================================
     8. LADDER — CANVAS OBJECT SALIENCE
     What the eye should reach first on the board, in order:
       selected  >  hovered  >  related  >  rest  >  attenuated
     ============================================================ */
  --canvas-bg: var(--surface-sunken);
  --canvas-guide: var(--p-9);

  /* The grid is a paper texture, not a measuring instrument — it should
     never announce itself. Dot POSITIONS are world-locked so panning
     feels physical, but dot SIZE is screen-constant and the cascade
     switches level as you zoom: two layers, one octave apart, crossfade
     so the perceived spacing never leaves ~17-34px anywhere in the
     8%-400% range (a single fixed grid swings 1.9-96px and has to be
     switched off at the bottom). Because target = unit x step, every
     power-of-two zoom stop — 25 / 50 / 100 / 200 / 400% — lands the
     fade on exactly one layer at a clean 24px.
     app.js writes --grid-sa/-sb/-aa/-ab/-x/-y onto #canvas-grid once per
     camera move — a leaf element, so these six inherit into nothing. */
  --canvas-grid-unit: 12; /* world px — base cell of the cascade */
  --canvas-grid-step: 2; /* octave between levels */
  --canvas-grid-target: 24; /* screen px — spacing we steer toward */
  --canvas-grid-dot: light-dark(
    oklch(20% 0.02 268 / 0.105),
    oklch(100% 0 0 / 0.055)
  );
  --canvas-dot-r: 0.85px; /* solid radius, screen px */
  --canvas-dot-feather: 1.8px; /* falloff: a point of light, not a pixel */
  --canvas-edge-fade: 40px; /* texture sinks under the frame */

  /* The canvas is a well cut into the bezel — chrome < canvas made
     physical, so the two planes read as one object rather than two. */
  --canvas-well: light-dark(oklch(40% 0.03 268 / 0.17), oklch(0% 0 0 / 0.42));
  --canvas-marquee-stroke: var(--a-9);
  --canvas-marquee-fill: color-mix(in oklch, var(--a-9) 10%, transparent);

  --object-ring-selected: var(--state-selected);

  --link-stroke: light-dark(var(--n-9), var(--n-9));
  --link-stroke-hover: light-dark(var(--n-11), var(--n-11));
  --link-stroke-related: var(--a-9);

  /* Depth of field, not a dimmer. On a ground this deep an unveiled
     white page is a searchlight, so peripheral cards sit back and snap
     to full fidelity the moment they are hovered or selected — the card
     you are working on is the sharp one. Focused reading surfaces
     (Side Peek, expanded card) are never veiled at all.
     The veil is the CANVAS colour, not black: at 10% it was invisible
     (20.2:1 -> 16.2:1, measured), and pure black turns a page's own
     colours to mud instead of sending it behind the board. 22% of the
     ground lands an unfocused page at ~12:1 while the focused one keeps
     the full 20:1 — which is what makes the card you are in the bright
     one, for free.                                                   */
  --page-veil: light-dark(
    oklch(93% 0.008 268 / 0),
    oklch(12.5% 0.005 268 / 0.22)
  );

  /* Frames are a region, not an object. A region is a large area, and
     large areas need far less contrast to read than a thin line does —
     so the region is drawn as a PLANE, not an outline. No border: the
     fill alone delineates it, and it still loses to a card, which adds
     a shadow and a hairline on top of its own fill.
     The corner is smaller than a card's, not larger: the bigger the
     shape, the less radius it needs before it starts reading as a
     bubble rather than a region. */
  --frame-fill: light-dark(
    oklch(20% 0.02 268 / 0.065),
    oklch(100% 0 0 / 0.065)
  );
  --frame-fill-hover: light-dark(
    oklch(20% 0.02 268 / 0.1),
    oklch(100% 0 0 / 0.1)
  );
  --frame-fill-selected: color-mix(in oklch, var(--a-9) 10%, transparent);

  /* ============================================================
     9. CONTENT TONES — semantic names for the five card colours
     ============================================================ */
  --note-dark-bg: var(--tone-slate-3);
  --note-dark-fg: var(--tone-slate-11);
  --note-dark-solid: var(--tone-slate-9);
  --note-blue-bg: var(--tone-blue-3);
  --note-blue-fg: var(--tone-blue-11);
  --note-blue-solid: var(--tone-blue-9);
  --note-teal-bg: var(--tone-teal-3);
  --note-teal-fg: var(--tone-teal-11);
  --note-teal-solid: var(--tone-teal-9);
  --note-amber-bg: var(--tone-amber-3);
  --note-amber-fg: var(--tone-amber-11);
  --note-amber-solid: var(--tone-amber-9);
  --note-rose-bg: var(--tone-rose-3);
  --note-rose-fg: var(--tone-rose-11);
  --note-rose-solid: var(--tone-rose-9);
  --note-dark-border: var(--tone-slate-6);
  --note-blue-border: var(--tone-blue-6);
  --note-teal-border: var(--tone-teal-6);
  --note-amber-border: var(--tone-amber-6);
  --note-rose-border: var(--tone-rose-6);

  /* ============================================================
     10. ELEVATION SHADOWS
     Composed from two tinted shadow colours so the whole set flips
     with the theme from one place. Light: a cool, believable cast.
     Dark: near-black plus a top hairline of light, because a shadow
     on a dark ground reads as nothing.
     ============================================================ */
  /* Three layers per level, one light source (straight down), on a
     geometric ramp — contact / direct / ambient. Each layer roughly
     doubles the one before in offset and blur while its alpha falls,
     which is what separates a believable shadow from a single blur:
     the tight layer draws the contact edge, the wide one the ambient
     occlusion. Three is the ceiling; a fourth is imperceptible.
     Light: cool-tinted, low alpha, shadow carries the depth.
     Dark:  near-black plus a top hairline, because on a dark ground
            a shadow reads as almost nothing on its own.            */
  /* Light was tuned when chrome sat between the canvas and the card and could
     help carry elevation. It no longer does — above `raised` the light theme has
     no lightness left — so the light cast is a notch heavier now. Dark is
     unchanged: there the planes still separate by lightness. */
  --shadow-contact: light-dark(
    oklch(40% 0.03 268 / 0.12),
    oklch(0% 0 0 / 0.44)
  );
  --shadow-direct: light-dark(oklch(40% 0.03 268 / 0.085), oklch(0% 0 0 / 0.3));
  --shadow-ambient: light-dark(oklch(40% 0.03 268 / 0.06), oklch(0% 0 0 / 0.2));
  --shadow-edge: light-dark(transparent, oklch(100% 0 0 / 0.055));

  --elev-1:
    0 1px 1px var(--shadow-contact), 0 2px 4px var(--shadow-direct),
    0 4px 8px var(--shadow-ambient);
  --elev-2:
    0 1px 2px var(--shadow-contact), 0 4px 8px var(--shadow-direct),
    0 10px 20px var(--shadow-ambient);
  --elev-3:
    0 2px 4px var(--shadow-contact), 0 8px 16px var(--shadow-direct),
    0 18px 36px var(--shadow-ambient);
  --elev-4:
    0 4px 8px var(--shadow-contact), 0 14px 28px var(--shadow-direct),
    0 30px 60px var(--shadow-ambient);
  --elev-5:
    0 6px 12px var(--shadow-contact), 0 20px 40px var(--shadow-direct),
    0 44px 88px var(--shadow-ambient);

  --hairline: inset 0 0 0 1px var(--border-default);
  --top-edge: inset 0 1px 0 var(--shadow-edge);

  --shadow-sm: var(--elev-1);
  --shadow-md: var(--elev-3);
  --shadow-lg: var(--elev-4);
  --shadow-card: var(--elev-2), var(--hairline), var(--top-edge);
  --shadow-drag: var(--elev-4), var(--hairline), var(--top-edge);
  --shadow-modal: var(--elev-5), var(--hairline), var(--top-edge);
  --scrim: light-dark(oklch(30% 0.02 268 / 0.32), oklch(0% 0 0 / 0.55));

  --ring-selected: 0 0 0 4px var(--state-selected);
  --ring-focus: 0 0 0 3px var(--accent-ring);

  /* ============================================================
     11. CARD CHROME

     The compat aliases that used to live here are gone: styles.css now
     names the plane it wants (sunken / chrome / raised / overlay / top),
     the state it wants (hover / active / selected) and the fill it wants
     (subtle / strong), so a component can no longer land on the wrong
     plane by picking a shade. What is left below is a real hook, not a
     bridge — a card's title bar sits ON the card's own plane, flush with
     the body below it, so the card reads as one object; a hairline is all
     that separates handle from content. The title bar used to sit a shade
     below the body, which cut a hard grey band across the top of every
     white page on the canvas.
     ============================================================ */
  --card-chrome: light-dark(oklch(100% 0 0), var(--n-3));
  /* selected barely lifts: the accent outline already says "chosen", so the
     head only needs to stay flush instead of announcing the state twice */
  --card-chrome-selected: light-dark(var(--n-2), var(--n-4));
  /* the only seam between head and body */
  --card-chrome-divider: var(--alpha-2);

  /* A WEB card's title bar belongs to the page below it, not to the app
     frame. On the app plane it happened to work in light (both white) and
     cut a hard band across the top of every page in dark. So it takes the
     page's own surface and ink, and rides under the same --page-veil as the
     body: the two dim and undim together and cannot drift apart. Un-themed
     for exactly the reason --surface-page is — the page is real content. */
  --card-chrome-page: var(--surface-page);
  --card-chrome-page-selected: color-mix(
    in oklab,
    var(--surface-page),
    var(--text-page) 5%
  );
  --card-chrome-page-divider: color-mix(
    in oklab,
    var(--text-page),
    transparent 92%
  );
  --card-chrome-page-ink: color-mix(
    in oklab,
    var(--text-page),
    var(--surface-page) 38%
  );
  --card-chrome-page-hover: color-mix(
    in oklab,
    var(--text-page),
    transparent 93%
  );
  --card-chrome-page-active: color-mix(
    in oklab,
    var(--text-page),
    transparent 88%
  );
  --grid-dot: var(--canvas-grid-dot);
  --guide: var(--canvas-guide);

  /* ============================================================
     12. COMPONENT HOOKS
     A short, closed list. Anything that needs a colour but is not
     covered by a ladder gets a named hook here rather than a literal
     in styles.css — so no component can go off-system.
     ============================================================ */

  /* ink placed ON a rendered web page (resize grips, collapse button) —
     fixed, because the page underneath is fixed */
  --on-page-ink: oklch(20% 0.02 268 / 0.4);
  --on-page-scrim: oklch(18% 0.01 268 / 0.72);
  --on-page-scrim-hover: oklch(14% 0.01 268 / 0.86);
  --on-page-fg: oklch(100% 0 0);

  /* page skeletons stand in for page content, so they follow the page */
  --skeleton-base: oklch(93.5% 0.003 268);
  --skeleton-shine: oklch(97% 0.002 268);
  --reader-bg: oklch(98.5% 0.006 96);

  /* zoomed-out (LOD) card cover — our summary, not the page: themed */
  --lod-bg: light-dark(var(--n-2), var(--n-4));
  --lod-fg: var(--text-primary);
  --lod-meta: var(--text-tertiary);

  /* canvas handles.
     Connect anchors rest NEUTRAL and only go accent under the pointer.
     Miro does the same; FigJam keeps them blue throughout. Neutral-at-rest
     is the better fit here because this system already spends accent on
     "the user caused this" — an anchor at rest is only an offer. */
  --anchor-fill: var(--surface-raised);
  --anchor-ring: light-dark(oklch(20% 0.02 268 / 0.42), oklch(100% 0 0 / 0.48));
  /* Tree indent guides. A hairline weaker than a divider: it is a hint about
     structure, not a rule that separates anything, and a list four levels deep
     draws four of them on every row. */
  --tree-guide: var(--alpha-2);

  /* Canvas scrollbars. Alpha, not a fixed colour: the track runs along the edge
     of the canvas but a thumb can end up over a white page card or a coloured
     note, and one value has to read on all three. Quiet at rest — a scrollbar
     is a position readout, not an invitation — and it firms up under the
     pointer, where it becomes a control. */
  --sb-thumb: light-dark(oklch(20% 0.02 268 / 0.22), oklch(100% 0 0 / 0.22));
  --sb-thumb-hover: light-dark(
    oklch(20% 0.02 268 / 0.36),
    oklch(100% 0 0 / 0.36)
  );
  --sb-thumb-active: light-dark(
    oklch(20% 0.02 268 / 0.5),
    oklch(100% 0 0 / 0.52)
  );

  --handle-fill: var(--surface-raised);
  --handle-ring: var(--accent);
  --handle-halo: var(--alpha-4);

  /* misc */
  --avatar-gradient: linear-gradient(135deg, var(--a-9), var(--tone-teal-9));
  --traffic-red: #ff5f57;
  --traffic-yellow: #febc2e;
  --traffic-green: #28c840;

  /* ---------- typography primitives ---------- */
  --font-ui:
    'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI',
    Roboto, Helvetica, Arial, sans-serif;
  --font-serif: 'Iowan Old Style', 'Georgia', 'Times New Roman', serif;
  --font-mono: 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;

  /* Type scale — six rungs, anchored at the 13px UI base, plus one
     display size that only the zoomed-out board uses.

     It was nine. Nine rungs one or two pixels apart is not a scale, it
     is a menu: 14 sat between the UI base and the first heading and got
     picked by whatever felt right that day, and 24 existed for a single
     heading. Measured, the app chrome was rendering 26 distinct
     size/weight/tracking combinations. Two rungs are gone and every
     site that used them moved onto a neighbour on purpose:
       14 -> 13   note body, palette rows, panel titles, form inputs.
                  (A mock web page's body text is 12.5px, so a note at
                  13 still sits just above its sources — which is the
                  relationship a research board wants.)
       24 -> 20   the empty-state heading, the only site.
     Each rung pairs with the --ls- of the same number below. */
  --fs-2xs: 10px; /* badges, eyebrow labels */
  --fs-xs: 11px; /* meta, hints, kbd */
  --fs-sm: 12px; /* secondary UI, card chrome */
  --fs-md: 13px; /* base UI, note body, list rows, inputs */
  --fs-xl: 16px; /* modal titles, reader view */
  --fs-2xl: 20px; /* section titles, empty-state heading */
  /* The LOD title is no longer a fixed size — it is derived from the card it
     sits in (see .lod-cover's --u in styles.css), so there is no display step
     on the ladder any more. --ls-34 stays: it is the tracking that size band
     wants, and the band is still centred on ~34px. */

  --lh-tight: 1.2;
  --lh-snug: 1.35;
  --lh-base: 1.5;
  --lh-relaxed: 1.65;

  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;

  /* Inter needs tracking that moves with the size: open the small sizes
     up, close the large ones in. There used to be a single --ls-tight
     used everywhere, which made it the wrong value at every size but
     one — it was a 17px value sitting on 13px body text while the
     10-11px labels ran at `normal`. It is gone; each step below pairs
     with the --fs- of the same number, and a rule that sets a font
     size sets the matching letter-spacing beside it. --ls-12 is 0 by
     design: that is where Inter's curve crosses zero, and Chrome
     reports it back as `normal`.                                    */
  --ls-10: 0.01em; /* --fs-2xs   badges, eyebrows */
  --ls-11: 0.005em; /* --fs-xs    meta, hints, kbd */
  --ls-12: 0em; /* --fs-sm    card chrome */
  --ls-13: -0.003em; /* --fs-md    base UI */
  --ls-16: -0.011em; /* --fs-xl    modal titles */
  --ls-20: -0.017em; /* --fs-2xl   section titles */
  --ls-34: -0.022em; /* LOD titles, ~26-46px */
  --ls-caps: 0.06em; /* all-caps labels keep their own, wider, value */

  /* ---------- spacing (4px grid) ---------- */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-8: 32px;
  --sp-10: 40px;

  /* ---------- radii ----------
     Concentric rule: a rounded box inside a rounded box takes
         inner radius = outer radius − the gap between them.
     Give both the same radius and the gap reads wider at the corners
     than along the edges. Three nestings exist in this app, and the
     scale is built so all three fall straight out of it:
         popover / toolbar  r-lg 9  − sp-1 4px  →  item  r-sm 5
         command palette    r-xl 12 −      5px  →  row   r-md 7
         segmented control  r-sm 5  −      2px  →  item  r-xs 3
     For a pair not on the scale, write the subtraction:
         border-radius: calc(var(--r-lg) - var(--sp-2));

     The ladder used to run 4 / 6 / 8 / 10 / 14 and it read soft — a
     560px card carrying a 10px corner looks like a widget, not like a
     document, and it fights the square content of the page inside it.
     Every rung came down roughly a quarter, to 3 / 5 / 7 / 9 / 12,
     which keeps all three nestings exact (the palette's list padding
     moved 6→5 to make its pair land) and reads engineered rather than
     bubbly. Odd numbers throughout: a corner arc lands on the pixel
     grid better at odd radii on a 1x display.

     Anything inside the zoomable world multiplies by --px as well, so
     a card's corner stays the same size on screen at every zoom.   */
  --r-xs: 3px;
  --r-sm: 5px;
  --r-md: 7px;
  --r-lg: 9px;
  --r-xl: 12px;
  --r-full: 999px;

  /* ---------- control sizes ---------- */
  --ctl-sm: 26px;
  --ctl-md: 30px;
  --ctl-lg: 36px;
  --icon-sm: 14px;
  --icon-md: 16px;
  --icon-lg: 18px;
  /* Rendered icon stroke, in screen px, identical at every icon size.
     The set is authored at stroke 3.2 on a 48 viewBox, so the same file
     drawn at 12-18px rendered 0.80-1.20px — a 50% swing in optical
     weight across one screen. `vector-effect: non-scaling-stroke` lifts
     the stroke out of the viewBox scale so this number IS the result. */
  --icon-stroke: 1.25;

  /* ---------- motion ---------- */
  --dur-instant: 80ms;
  --dur-fast: 120ms; /* hover, press, colour */
  --dur-base: 180ms; /* popovers, toolbars, small reveals */
  --dur-slow: 280ms; /* panels, camera moves */
  --dur-lazy: 400ms; /* card enter, flash */
  --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1); /* standard: things arriving */
  --ease-in: cubic-bezier(0.4, 0, 1, 1); /* things leaving */
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* camera / layout */
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1); /* playful pops */

  /* ---------- z-index scale ---------- */
  --z-canvas-ui: 10;
  --z-toolbar: 20;
  --z-overlay: 25;
  --z-chrome: 30;
  --z-popover: 60;
  --z-modal: 80;
  --z-toast: 90;
  --z-tip: 100;
}

/* Non-colour tokens cannot ride light-dark(), so the few that differ per theme
   get the same three states the rest of the system gets from that function:
   bare :root is light, the media query covers "system", the stamp wins over it. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --overlay-opacity: 76%;
  }
}
:root[data-theme='dark'] {
  --overlay-opacity: 76%;
}

/* Higher contrast: firm up the two rungs that carry the least weight. */
@media (prefers-contrast: more) {
  :root {
    --text-secondary: var(--n-12);
    --text-tertiary: var(--n-11);
    --border-subtle: var(--alpha-3);
    --border-default: var(--alpha-4);
    --border-strong: var(--alpha-5);
  }
}

/* Reduced motion: keep state changes, drop the choreography */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-instant: 0ms;
    --dur-fast: 0ms;
    --dur-base: 0ms;
    --dur-slow: 0ms;
    --dur-lazy: 0ms;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
