/* ============================================================================
   tokens.css — single source of truth for the player hub.
   Nothing in base.css / app.css / any page may hardcode a colour, size,
   duration or shadow. If a value is missing here, add it here first.

   Two colour families, deliberately named so they cannot be confused:

     --text-*    INFORMATION. Every one of these clears WCAG AA 4.5:1 against
                 --ink. Use for body copy, labels, table cells, form text,
                 the availability grid — anything the reader must actually read.

     --chrome-*  DECORATION. Hairlines, dividers, inactive glyphs, watermark
                 numerals, panel edges. These are BELOW 4.5:1 on purpose.
                 Never put a word a player needs on a --chrome-* colour.

   Derived from the Starmap astrogation HUD (ink, glass panels, cyan accent,
   one heavy edge per panel). The map's CSS is deliberately motionless because
   a live WebGL galaxy turns behind it. This site has no such layer, so the
   stillness it inherited had nothing to counterweight. What replaces it is not
   decoration: it is a stated lighting model (below) and a motion vocabulary
   (below) that let the interface say what just happened.
   ========================================================================= */

:root {
  /* ---- Surface: the elevation ladder ----------------------------------
     ONE light source: above the page and slightly to the LEFT, roughly 20°
     off vertical. Everything follows from that:
       · shadows offset DOWN and to the RIGHT, x:y = 1:3, always
       · the TOP edge of a raised surface is lit, the BOTTOM edge is not
       · the LEFT edge carries a trace of the same light at high elevation
       · a recessed surface inverts all of it — dark at the top, lit at the
         bottom — which is the only way a well can read as a hole and not a
         darker rectangle.

     Surface lightness is a monotonic ladder with a STATED step of ~+4 L*
     per level. Measured (CIE L*, D65, on the sRGB values below):

       --ink            #030405   L*  1.1   the page floor
       --surface-sunk   #050a11   L*  2.9   a well, the grid bed
       --panel-fill     #0a121c   L*  5.3   a content panel          (level 2)
       --panel-fill-hi  #0e1a28   L*  8.9   the page's ONE lead panel (level 3)

     The old floor→panel step was 2.4 L*, which is why every capture of this
     site rendered two background colours in the whole document. It is now
     4.2 L*, and the shadow ladder and the sheen do the rest.               */
  --ink:              #030405;  /* page floor, same as the galaxy map */
  --surface-sunk:     #050a11;  /* RECESSED: wells, the availability grid bed */
  --panel-fill:       #0a121c;  /* STATIC content panels, elevation 2. Opaque:
                                   a blurred long scrolling region costs a
                                   compositor layer per panel and, over a
                                   near-flat floor, shows almost nothing for
                                   it. DESIGN.md §4. */
  --panel-fill-hi:    #0e1a28;  /* elevation 3 — the ONE lead panel per page */
  --ink-raised:       #080f18;  /* sticky strips that must occlude the grid
                                   behind them: the hour header, the brush bar */
  --panel:            rgba(6, 10, 17, 0.42);  /* OVERLAY surfaces only: nav,
                                                 tab bar, sheet, toast, gate.
                                                 These sit over content, so the
                                                 blur behind them is real.    */
  --panel-solid:      #070c14;  /* prefers-reduced-transparency fallback */
  --panel-inset:      rgba(0, 0, 0, 0.32);   /* tint for wells over any panel */
  --panel-hover:      rgba(0, 229, 255, 0.07);
  --panel-active:     rgba(0, 229, 255, 0.13);
  --blur-panel:       blur(16px);
  --blur-overlay:     blur(24px);

  /* ---- Text: INFORMATION. All ≥4.5:1 on --ink. ------------------------ */
  --text-strong:      #ffffff;  /* 20.4:1 — headings, the number in a cell   */
  --text-primary:     #e2ebf3;  /* 17.0:1 — body copy, table cells           */
  --text-secondary:   #a8bccb;  /*  9.5:1 — field labels, captions, meta     */
  --text-muted:       #8398a8;  /*  6.2:1 — the quietest text still allowed  */
  --text-accent:      #00e5ff;  /* 12.7:1 — links, active nav, key figures   */
  --text-on-accent:   #030405;  /* text sitting ON a cyan/mint/amber fill    */
  /* ONE red on this site. This used to be a second, lighter red than the busy
     fill, because the busy fill was only 5.7:1 on --ink and error copy needed
     more. The status family now sits in a single lightness band (below) and
     busy measures 8.28:1, so the second red bought nothing and cost a colour
     a reader had to learn twice. Error copy and a busy square are the same
     refusal; they are now the same red.                                     */
  --text-danger:      var(--status-busy-fill);   /* 8.28:1 on --ink          */

  /* ---- Chrome: DECORATION ONLY. Below 4.5:1. --------------------------
     Structure on this site is DRAWN, not shaded — the Mothership half of the
     visual world. Three opacities inside a 17-point band all read as "faint
     grey line", so the ladder now spends a real range AND a second channel:
     border-STYLE is taxonomy. solid = a fact you set. dashed = inherited or
     provisional. double = a printed rule. That channel is free at zero
     contrast cost and survives greyscale, which opacity alone does not.    */
  --chrome-line:      rgba(255, 255, 255, 0.06);  /* the hairline            */
  --chrome-line-firm: rgba(255, 255, 255, 0.13);  /* input + table borders   */
  --chrome-rule:      rgba(255, 255, 255, 0.34);  /* Mothership heavy rule —
                                                     22% read as a thick
                                                     hairline, not as print  */
  --chrome-dim:       #5a6b78;  /* 3.2:1 — glyphs, dividers, disabled marks  */
  --chrome-faint:     #34404b;  /* watermark numerals, skeleton bones        */
  --chrome-accent:    rgba(0, 229, 255, 0.28);    /* tinted edges            */

  /* ---- Accent ----------------------------------------------------------
     One hue. The map's answer to "the palette feels thin" was an alpha ladder
     on a single hue, not a second hue, and this is that ladder.            */
  --accent:           #00e5ff;
  --accent-press:     #66f0ff;
  --accent-wash:      rgba(0, 229, 255, 0.10);

  /* ---- Glow: a STATE, never a resting property -------------------------
     The galaxy map has nine glow recipes and every one of them is reserved
     for a state: :hover, .selected, a critical scan, a discovery. Zero
     resting glow in its chrome. The rule is "glow is a state", and reading
     that as "no glow" cost this site its entire way of saying SOMETHING JUST
     HAPPENED — which, on a page whose whole job is recording an answer, is
     the one sentence it most needed.

     There is ONE glow, and it has a negative spread — the map's trick for a
     halo with no visible rim: the shadow is pulled inside the element's own
     edge, so the surface lights up without gaining an outline.

     There used to be three. `--glow-accent` and `--glow-none` rendered to zero
     elements across all four pages while DESIGN.md described --glow-accent as
     one of the two shipping glows: a contract for a value the code never
     spent. A glow says SOMETHING JUST HAPPENED, and exactly one thing on this
     site happens that the player did not already watch happen — the save
     reaching the server. One event, one glow.

     If you find this on an element at rest, that is a bug.                  */
  --glow-confirm:  0 0 4px -1px var(--status-free-edge),
                   0 0 16px -4px var(--status-free-edge);
  --ring-focus:    0 0 0 4px rgba(0, 229, 255, 0.28); /* keyboard focus halo */
  --scrim-fill:    rgba(0, 0, 0, 0.7);   /* behind a sheet, over content     */
  --chrome-wash:   rgba(255, 255, 255, 0.03);  /* the hover tint on a control
                                                  that carries no accent     */
  --sweep-sheen:   rgba(255, 255, 255, 0.07);  /* the light that crosses a
                                                  skeleton bone             */

  /* ---- Availability status --------------------------------------------
     Hue is never the only signal. Every status ships as a triple:
       colour + glyph + surface pattern.
     A deuteranope reads free/busy apart by the check vs the cross and by
     solid vs cross-hatch, with the colour as reinforcement only.
     --status-*-fill is the AA-safe text/glyph colour on --ink.
     --status-*-bg   is the tinted cell surface.
     --status-*-mark is the glyph the markup MUST render.

     ONE LIGHTNESS BAND. Hue tells you which answer it is; lightness must not
     also be telling you which answer matters, and it was: free #00ffaa sat at
     oklch L 0.882, maybe #ffaa00 at 0.802 and busy #ff3366 at 0.656 — a 0.23
     spread, so a grid of "free" shouted and the same grid of "busy" whispered
     for a reason that has nothing to do with meaning. All three are now placed
     at L 0.740 at the maximum chroma sRGB allows on their own hue, which lands
     the chromas at 0.167 / 0.158 / 0.161 as well: matched on both axes, so no
     status outranks another optically. Busy reads a shade pinker than it did.
     That is the cost, and it is paid by the ×, the cross-hatch and the word,
     which are the channels a red-green reader was using anyway.

     The tints follow the same rule at the other end: all three at L ~0.215,
     so a hatched cell and a flat one sit on the same floor.

     --status-*-bg is OPAQUE, and that is a correctness property, not a style
     choice. As translucent tints these composited against whatever surface
     happened to be underneath, so lifting a panel's fill silently moved eight
     measured contrast pairs. Baked opaque, every ratio below is a fixed
     property of the token pair and cannot be broken by a change to a surface
     three levels up. Measured, rendered, worst pixel (hatch included):

       free  #00ca86 on #001f1a   8.09:1   (was 11.40)
       maybe #e69800 on #1f1909   5.13:1   (was  5.56)  <- still the narrowest
       busy  #ff7b8d on #25131e   7.11:1   (was  5.16)
       unset #8398a8 on #0c1118   6.33:1   (unchanged)
       shut  #a8bccb on #0a0f16   9.81:1   (unchanged)                      */
  --status-free-fill: #00ca86;   /* oklch L 0.740 C 0.167 — 9.57:1 on --ink  */
  --status-free-bg:   #001f1a;
  --status-free-edge: var(--status-free-fill);
  --status-free-mark: "\2713";   /* ✓  solid fill, no pattern            */

  --status-maybe-fill:#e69800;   /* oklch L 0.740 C 0.158 — 8.65:1 on --ink  */
  --status-maybe-bg:  #1f1909;
  --status-maybe-edge:var(--status-maybe-fill);
  --status-maybe-mark:"\007E";   /* ~  single 45° hatch                  */

  --status-busy-fill: #ff7b8d;   /* oklch L 0.740 C 0.161 — 8.28:1 on --ink  */
  --status-busy-press:#ff939f;   /* the destructive button's hover fill      */
  --status-busy-bg:   #25131e;
  --status-busy-edge: var(--status-busy-fill);
  --status-busy-mark: "\00D7";   /* ×  cross-hatch                       */

  /* "Not yet answered" is one of the four states the grid is read for, so it is
     INFORMATION and must live in the --text-* family. --text-muted is the floor
     of that family and still the quietest of the four. */
  --status-unset-fill: var(--text-muted);
  --status-unset-bg:  #0c1118;
  --status-unset-edge: transparent;
  --status-unset-mark: "\2013"; /* –  empty, no pattern                  */

  /* SHUT BY THE WARDEN. Not a fifth answer — nobody chose it, and no player can
     change it. So it is deliberately outside the free/maybe/busy hue set: no
     tint, no hatch, a hairline box and the ▦ glyph, which reads as "this is not
     yours to fill in". It is still INFORMATION, so the glyph sits on
     --text-secondary. It must never be confused with busy: busy is the
     player's own red cross-hatch. */
  --status-shut-fill: #a8bccb;   /*  = --text-secondary                    */
  --status-shut-bg:   #0a0f16;
  --status-shut-edge: rgba(255, 255, 255, 0.13);  /* = --chrome-line-firm   */
  --status-shut-mark: "\25A6";   /* ▦  flat, no pattern                    */

  /* Ink-centring nudges for the marks above. A glyph is drawn from its
     BASELINE, not from the middle of its em box, so ✓ ~ × ▦ – do not share an
     optical centre even at one size in one font: measured in the shipped build
     the × and the – sat well BELOW the centre of a 44px square while the ✓
     happened to land true, and the same error repeated in every pill and
     calendar mark on the site. These are the measured corrections, in em of
     the mark's own size, applied by app.css. They are per GLYPH and not per
     status, because it is the drawing that is off centre, not the meaning.

     Measured raw offsets at 17px, ink centre below box centre:
       ✓  0.0px      ▦  0.0px      –  1.0px      ×  1.5px      ~  2.0px

     Re-measure with verify/runs/20-css/probe.mjs if --font-mono changes. Do
     not eyeball these; the whole point is that the eye cannot see 1px on one
     square and cannot ignore it on three hundred.                           */
  --mark-lift-dash:  -0.059em;   /* –  en dash, sits on the maths axis       */
  --mark-lift-cross: -0.088em;   /* ×  multiplication sign, same axis        */
  --mark-lift-tilde: -0.118em;   /* ~  tilde, drawn highest of the three     */

  /* Hatch patterns. Applied as a background-image layer over the opaque tint
     so the status survives greyscale printing and colour blindness alike. */
  --hatch-maybe: repeating-linear-gradient(45deg,
      rgba(230, 152, 0, 0.20) 0 2px, transparent 2px 7px);
  /* The busy hatch draws DARK, not lighter red. Two red-on-red layers crossing
     lifted the cell surface to #6a1f3a, where the × glyph measured 2.73:1 — the
     pattern was eating its own glyph. Dark lines keep the texture and put the
     worst pixel under the glyph well clear of the floor. */
  --hatch-busy: repeating-linear-gradient(45deg,
      rgba(0, 0, 0, 0.45) 0 2px, transparent 2px 6px),
    repeating-linear-gradient(-45deg,
      rgba(0, 0, 0, 0.45) 0 2px, transparent 2px 6px);

  /* ---- Type ------------------------------------------------------------
     Inter for everything human, a mono stack for anything that must line up
     in a column (dates, times, counts, dice, IDs).
     Sizes are LARGER than the galaxy map's HUD. The map is read leaning into
     a monitor; this is read one-handed on a phone on a sofa. 11px labels are
     borrowed atmosphere, not borrowed ergonomics. Body floor is 16px.

     UPPERCASE IS A LABEL TREATMENT AND NOTHING ELSE. It belongs on controls
     and column heads — things you aim at, not things you read. It came off
     h2 and .prose h3, where it was turning every section of prose into a
     shouted line, because mixed case is measurably faster to read and this
     site's whole argument is that it should be fast to read.               */
  --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  /* Pinned rather than left to ui-monospace roulette: the glyph set
     (✓ ~ × ◈ ▦ ≡ ▶ –) has to have the same metrics on Windows, macOS and
     Android or the availability grid's optical centring is per-platform. */
  --font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, 'Roboto Mono',
               'DejaVu Sans Mono', monospace;

  /* 13px is the floor for anything a player has to read. Nothing on this site
     is set below it — the one token that was (--size-micro, 11px, reserved
     for the boxed index plate) is gone along with the plate. See DESIGN.md §1. */
  --size-meta:   0.8125rem;  /* 13px — the floor: timestamps, helper text  */
  --size-label:  0.875rem;   /* 14px — uppercase labels, chips, nav, tabs,
                                       button text: every control a thumb
                                       aims at. Uppercase + tracking reads a
                                       step smaller than it measures, so this
                                       is the real 13px floor.             */
  --size-body:   1rem;       /* 16px — body copy, table cells, inputs      */
  --size-lead:   1.125rem;   /* 18px — intro paragraph, sheet body         */
  --size-h3:     1.25rem;    /* 20px */
  --size-h2:     1.5rem;     /* 24px */
  --size-h1:     2rem;       /* 32px */
  --size-display:2.75rem;    /* 44px — page title on wide screens only     */
  --size-mark:   1.0625rem;  /* 17px — the availability square's glyph. It
                                is sized against the SQUARE, not against the
                                type ladder, which is why it has its own
                                token rather than borrowing --size-lead.   */

  /* --weight-light (300) rendered to zero elements on all four pages and is
     deleted along with the 300 cut in the webfont request. Nothing on a
     near-black surface should be set lighter than 400 anyway — thin strokes
     on dark lose more contrast than the ratio predicts. */
  --weight-body:   400;
  --weight-medium: 600;
  --weight-bold:   700;
  --weight-black:  800;

  --leading-tight: 1.15;
  --leading-snug:  1.35;
  --leading-body:  1.6;

  /* Letter-spacing ladder, straight from the HUD: the smaller and more
     structural the text, the wider it is set — and, the correction the HUD
     does not need because it has no display type, the LARGER the text the
     tighter the same gesture has to be set to look like the same gesture.
     0.2em at 44px is 8.8px of air between letters; at 14px it is 2.8px. They
     are not the same treatment and cannot share a token. */
  --track-data:    0.06em;   /* ~1px at 16px — inline data                 */
  --track-label:   0.12em;   /* ~1.5px at 13px — labels                    */
  --track-header:  0.2em;    /* ~2-3px at 14px — controls, nav, plates     */
  --track-display: 0.16em;   /* 32-44px caps — the page title, optically
                                matched to --track-header at label size    */
  --track-tight:  -0.02em;   /* headings set in mixed case                 */

  --measure: 68ch;           /* body copy line length ceiling              */
  --measure-lead: 56ch;      /* an intro paragraph is read once, fast, and
                                wants a shorter line than an article does  */

  /* ---- Space (4px base) ------------------------------------------------ */
  --space-0:  0;
  --space-1:  0.25rem;   /*  4 */
  --space-2:  0.5rem;    /*  8 */
  --space-3:  0.75rem;   /* 12 */
  --space-4:  1rem;      /* 16 */
  --space-5:  1.25rem;   /* 20 */
  --space-6:  1.5rem;    /* 24 */
  --space-8:  2rem;      /* 32 */
  --space-10: 2.5rem;    /* 40 */
  --space-12: 3rem;      /* 48 */
  --space-16: 4rem;      /* 64 */
  --space-20: 5rem;      /* 80 */

  --gutter: var(--space-4);          /* page edge padding, mobile          */
  --gutter-wide: var(--space-8);     /* page edge padding, ≥720px          */
  --measure-page: 1120px;            /* max content width                  */

  /* ---- Touch ----------------------------------------------------------- */
  --tap-min: 44px;        /* absolute floor for anything tappable          */
  --tap-comfort: 48px;    /* default for primary controls                  */
  --tabbar-h: 60px;       /* + safe-area inset, see app.css                */

  /* ---- Shape ----------------------------------------------------------- */
  --radius-sm: 2px;   /* buttons, pills, cells                             */
  --radius:    3px;   /* inputs, table wrappers                            */
  --radius-lg: 4px;   /* panels, sheets — the ceiling. Nothing is rounder. */
  --edge:      3px;   /* the ONE accent edge a panel is allowed            */
  --rule:      2px;   /* nav underlines, tab markers                       */
  --rule-print:3px;   /* the Mothership rule under a panel heading, and
                         only there. At 2px/22% it read as a thick
                         hairline; the device is supposed to read as ink. */

  /* ---- Depth -----------------------------------------------------------
     Four levels and two inversions. Every one obeys the light stated at the
     top of this file: offsets go down and RIGHT at x:y = 1:3, the top edge is
     lit, the bottom edge is not, and each level stacks more layers at wider
     blur and lower opacity so the falloff is smooth rather than a single
     blurry border.

     Shadow colour is #00030a, not black: hue-matched to the blue the panels
     are lit in, so the falloff reads as shade rather than as a grey smudge
     laid over a near-black floor.

       --elev-0        on the floor. Structural groupings, rows, rules.
       --elev-1        a chip, a button, a row that has lifted under a pointer
       --elev-2        a content panel. The default.
       --elev-3        the ONE lead panel on a page. Spend it once.
       --elev-sunk     a well or the grid bed — the inverse: dark above, lit
                       below, because a hole is lit from the same place the
                       raised things are.
       --elev-press    a control taking load. It LOSES height: the drop goes
                       inside. Paired with the 90ms press and nothing else.
       --elev-overlay  nav, toast, gate — surfaces genuinely over content.
       --elev-sheet    a sheet rising from the bottom edge, so the light it
                       casts is the only one on this site that points UP.   */
  --elev-0: none;
  --elev-1:
    inset 0 1px 0 rgba(255, 255, 255, 0.04),
    0 1px 2px rgba(0, 3, 10, 0.55),
    1px 3px 6px rgba(0, 3, 10, 0.40);
  --elev-2:
    inset 0 1px 0 rgba(255, 255, 255, 0.065),
    inset 1px 0 0 rgba(255, 255, 255, 0.02),
    inset 0 -1px 0 rgba(0, 0, 0, 0.45),
    0 1px 2px rgba(0, 3, 10, 0.55),
    1px 4px 9px rgba(0, 3, 10, 0.42),
    3px 10px 24px rgba(0, 3, 10, 0.34);
  --elev-3:
    inset 0 1px 0 rgba(255, 255, 255, 0.10),
    inset 1px 0 0 rgba(255, 255, 255, 0.03),
    inset 0 -1px 0 rgba(0, 0, 0, 0.50),
    0 1px 2px rgba(0, 3, 10, 0.60),
    2px 6px 14px rgba(0, 3, 10, 0.46),
    5px 16px 38px rgba(0, 3, 10, 0.38),
    9px 28px 64px rgba(0, 3, 10, 0.30);
  --elev-sunk:
    inset 0 1px 0 rgba(0, 0, 0, 0.70),
    inset 0 2px 6px rgba(0, 0, 0, 0.55),
    inset 0 -1px 0 rgba(255, 255, 255, 0.035);
  --elev-press:
    inset 0 1px 3px rgba(0, 3, 10, 0.65),
    0 1px 1px rgba(0, 3, 10, 0.50);
  --elev-overlay:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 1px 2px rgba(0, 3, 10, 0.60),
    3px 10px 26px rgba(0, 3, 10, 0.50),
    8px 26px 70px rgba(0, 3, 10, 0.55);
  --elev-sheet:
    inset 0 1px 0 rgba(255, 255, 255, 0.09),
    0 -2px 6px rgba(0, 3, 10, 0.50),
    0 -12px 40px rgba(0, 3, 10, 0.60),
    0 -28px 90px rgba(0, 3, 10, 0.50);

  /* The vignette is a fixed full-viewport layer with one job: bend the floor
     so panels sit ON something. The map's `inset 0 0 250px` is calibrated for
     a full-bleed canvas; over a 1120px centred column it darkened margins that
     were already black and did nothing else. This is proportional instead, so
     it shapes the page at 320 and at 1440 alike, and it is offset UP-LEFT to
     agree with the light. */
  --vignette: radial-gradient(125% 95% at 42% 26%,
      rgba(0, 0, 0, 0) 0%,
      rgba(0, 0, 0, 0.35) 58%,
      rgba(0, 0, 0, 0.82) 100%);

  /* The floor's key light, agreeing with the elevation ladder's stated source:
     above and to the left. It is a surface value like any other and lived as
     three raw rgbas in base.css, which is exactly what §2 forbids. */
  --floor-lights:
    radial-gradient(70% 46% at 26% 4%, rgba(0, 132, 186, 0.155), transparent 72%),
    radial-gradient(38% 30% at 22% 18%, rgba(0, 110, 160, 0.13), transparent 70%),
    radial-gradient(34% 34% at 82% 72%, rgba(0, 80, 130, 0.115), transparent 70%);

  /* ---- Layers ---------------------------------------------------------- */
  --z-ambient:  -1;
  --z-base:      0;
  --z-sticky:   10;
  --z-nav:     100;
  --z-scrim:   200;
  --z-sheet:   210;
  --z-toast:   300;
  --z-gate:    400;

  /* ---- Motion ----------------------------------------------------------
     A vocabulary, not a house style. One curve doing every job is why five
     things of five different weights — a nav link, a table row, a 44px
     availability square, a session row, a submit button — all answered in
     the same 150ms on the same curve, and nothing felt heavier than anything
     else because nothing WAS, temporally.

     FIVE CURVES. Each one can be defended in a sentence:

       --ease         the house curve. POINTER FEEDBACK: a thing
                      acknowledging that the cursor or the finger is on it.
                      Symmetrical enough to run in both directions.
       --ease-out     expo-out. ARRIVAL: something that was not here is here.
                      Almost all of the distance is covered in the first
                      third, so it lands rather than glides in.
       --ease-exit    accelerate away. DEPARTURE: nothing needs to watch a
                      thing leave, so it leaves fast and never eases out.
       --ease-snap    the only curve with overshoot. DIRECT MANIPULATION and
                      nothing else — something the finger is touching right
                      now. Overshoot on anything the finger is NOT on reads
                      as the interface being bouncy rather than physical.
       --ease-settle  a soft spring. A thing that MOVED TO A NEW PLACE and
                      has to look like it travelled there: a promoted row, a
                      re-ordered panel.

     EIGHT DURATIONS, frequency-scaled — the more often a thing happens, the
     shorter it gets, and the asymmetry between entering and leaving is
     deliberate (an exit is ~0.55× its entrance).

     The 90ms press is the best-tuned number on this site and NOTHING may
     lengthen it. Direct manipulation is instant; only the REACTION may take
     time, and only after the event it is reacting to has already resolved. */
  --ease:        cubic-bezier(0.20, 0.80, 0.20, 1.00);
  --ease-out:    cubic-bezier(0.16, 1.00, 0.30, 1.00);
  --ease-exit:   cubic-bezier(0.40, 0.00, 1.00, 1.00);
  --ease-snap:   cubic-bezier(0.34, 1.32, 0.64, 1.00);
  --ease-settle: cubic-bezier(0.22, 1.15, 0.36, 1.00);

  --dur-press:  90ms;   /* :active — must feel instant. NEVER lengthen.    */
  --dur-hover: 150ms;   /* hover / focus: acknowledging the pointer        */
  --dur-exit:  180ms;   /* anything leaving                                */
  --dur-tick:  220ms;   /* a thing CHANGED MEANING: a save confirmed, a
                           verdict flipping, a count settling. Fires after
                           the event, never on the input path.             */
  --dur-toast: 250ms;   /* the system reporting something                  */
  --dur-enter: 320ms;   /* content arriving in place                       */
  --dur-panel: 400ms;   /* a surface that travels a screen distance        */
  --dur-sweep: 1400ms;  /* the light crossing a skeleton bone or a working
                           button's bar. Not feedback and not on the input
                           path — it is the one duration allowed to be slow,
                           because a fast sweep on six rows reads as six
                           things blinking. Both sweeps share it; they are
                           the same gesture saying the same thing.         */
  --dur-drift: 150s;    /* ambient background                              */
}
