/* ===== මග තොට — Maga Toṭa =====

   The colour is a tonal system, not a list of hexes. Twelve numbers at the top
   produce every colour in the app, in CSS, with no JavaScript anywhere in the
   colour path. Two things are layered over them:

     PUSH      contrast, brightness and vibrancy, applied to the TONES rather
               than to the roles — a tone is its role plus a measured offset,
               and a constant added after a multiplication moves every tone the
               same distance, which is brightness with the wrong name.

     POLARITY  the ground decides which way the ink runs. Push its value past
               the middle and every tone that is READ AGAINST IT mirrors about
               that middle: text turns dark, surfaces reverse their order,
               tints deepen. That is the whole of what a light theme is here,
               from one number, and it costs one multiplication. There is no
               second palette.

   Nothing below was chosen. Every offset was measured off the hand-picked
   palette this replaced and checked to round-trip to the same byte; the
   measurement and the check live in `theme/derive.py`, and the dark theme is
   still the exact palette the app shipped with.
   ========================================================================= */

:root{
  color-scheme: dark light;

  /* ---- The seed --------------------------------------------------------
     The one colour that is chosen rather than derived: the gold in the title,
     and the field colour of every warning sign in the deck. */
  --p-h: 90.37;
  --p-s: 60.48;      /* 0-100; x0.0025 is the OKLCH chroma, so 100 is about as
                        saturated as sRGB goes */
  --p-v: 88.12;      /* 0-100 */

  /* ---- The three relationships, measured off the palette this replaced ---
     Hue turn in degrees, saturation as a percentage of the seed's, value as
     a step in the same 0-100 the seed uses. */
  /* Right. */
  --s-dh: 68.57;
  --s-xs: 78.78;
  --s-dv: -31.82;
  /* Wrong. */
  --t-dh: -62.20;
  --t-xs: 120.62;
  --t-dv: -32.46;
  /* The ground. Its value is the theme switch; see POLARITY. */
  --g-dh: -5.37;
  --g-xs: 3.64;
  --g-dv-dark: -70.27;
  --g-dv-light: 9.38;

  /* ---- Push ------------------------------------------------------------
     Three multipliers over the finished palette. 1 is the palette as measured. */
  --k-contrast: 1;
  --k-bright: 1;
  --k-vib: 1;

  /* ======================================================================
     Machinery. The numbers above are the whole interface.
     ====================================================================== */
  --p-c: calc(var(--p-s) * 0.0025);
  --p-l: calc(var(--p-v) * 0.01);

  --s-h: calc(var(--p-h) + var(--s-dh));
  --s-c: calc(var(--p-c) * var(--s-xs) * 0.01);
  --s-l: calc(var(--p-l) + var(--s-dv) * 0.01);

  --t-h: calc(var(--p-h) + var(--t-dh));
  --t-c: calc(var(--p-c) * var(--t-xs) * 0.01);
  --t-l: calc(var(--p-l) + var(--t-dv) * 0.01);

  --g-h: calc(var(--p-h) + var(--g-dh));
  --g-c: calc(var(--p-c) * var(--g-xs) * 0.01);
  --g-l: calc(var(--p-l) + var(--g-dv) * 0.01);

  /* ---- Polarity --------------------------------------------------------
     +1 while the ground is dark, so every tone is the lightness it was
     measured at. -1 once the ground crosses the middle, and every tone that
     is read against the ground mirrors about it.

     clamp() is doing the job of an if. The multiplier is deliberately
     enormous so this is a step and not a ramp: halfway between the two
     polarities the ink is the same lightness as the ground, and every value
     in that ramp is one nobody can read.

     THE PIVOT IS NOT 0.5. Ink here sits at 0.937, and mirrored about 0.5 that
     lands at 0.063: pure black on white, which halates and is worse to read at
     length. Mirrored about 0.56 it lands at 0.183, which is ink. On the dark
     side the pivot costs nothing, because at polarity +1 and contrast 1 the
     whole expression is the identity whatever the pivot is.

     A SECOND POLARITY, because a palette has two kinds of colour in it.
     Everything that is READ AGAINST THE GROUND flips: the ink, the surfaces,
     the tints behind an answer. A PIGMENT does not, because it is not read
     against the ground — it is the thing being read. The gold is the same gold
     on a road sign at noon and at midnight, and the text on a gold chip is
     read against the gold, not against the page. Flipping those would not
     produce a light theme, it would produce a different app. */
  --ink-pivot: 0.56;
  --ink-flip: clamp(-1, (var(--ink-pivot) - var(--g-l)) * 1000, 1);
  --pigment-flip: 1;
}

/* The theme switch, and it is one number. Auto follows the system; the two
   explicit settings pin the ground to one side of the pivot or the other. */
:root{ --g-dv: var(--g-dv-dark); }
@media (prefers-color-scheme: light){
  :root:not([data-theme="dark"]){ --g-dv: var(--g-dv-light); color-scheme: light; }
}
:root[data-theme="light"]{ --g-dv: var(--g-dv-light); color-scheme: light; }
:root[data-theme="dark"]{ --g-dv: var(--g-dv-dark); color-scheme: dark; }

:root{
  /* The brand, and the colour of every warning sign in the deck. */
  --gold-lr: calc(var(--p-l) - 0.0000);
  --gold-l: calc(((var(--gold-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--pigment-flip) + var(--ink-pivot)) * var(--k-bright));
  --gold-c: calc(var(--p-c) * 1.0000 * var(--k-vib));
  --gold-h: calc(var(--p-h) - 0.00);
  --gold: oklch(var(--gold-l) var(--gold-c) var(--gold-h));
  /* The gold with the light off it: meters, the bar under the chip. */
  --gold-deep-lr: calc(var(--p-l) - 0.0896);
  --gold-deep-l: calc(((var(--gold-deep-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--pigment-flip) + var(--ink-pivot)) * var(--k-bright));
  --gold-deep-c: calc(var(--p-c) * 1.0390 * var(--k-vib));
  --gold-deep-h: calc(var(--p-h) - 4.68);
  --gold-deep: oklch(var(--gold-deep-l) var(--gold-deep-c) var(--gold-deep-h));
  /* Text on a gold fill. */
  --gold-ink-lr: calc(var(--p-l) - 0.5806);
  --gold-ink-l: calc(((var(--gold-ink-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--pigment-flip) + var(--ink-pivot)) * var(--k-bright));
  --gold-ink-c: calc(var(--p-c) * 0.4061 * var(--k-vib));
  --gold-ink-h: calc(var(--p-h) - 0.59);
  --gold-ink: oklch(var(--gold-ink-l) var(--gold-ink-c) var(--gold-ink-h));
  /* Headings and anything being read. */
  --ink-lr: calc(var(--p-l) + 0.0560);
  --ink-l: calc(((var(--ink-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--ink-flip) + var(--ink-pivot)) * var(--k-bright));
  --ink-c: calc(var(--p-c) * 0.0463 * var(--k-vib));
  --ink-h: calc(var(--p-h) - 1.73);
  --ink: oklch(var(--ink-l) var(--ink-c) var(--ink-h));
  /* The second voice: labels, secondary copy. */
  --ink-2-lr: calc(var(--p-l) - 0.1694);
  --ink-2-l: calc(((var(--ink-2-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--ink-flip) + var(--ink-pivot)) * var(--k-bright));
  --ink-2-c: calc(var(--p-c) * 0.0681 * var(--k-vib));
  --ink-2-h: calc(var(--p-h) + 3.24);
  --ink-2: oklch(var(--ink-2-l) var(--ink-2-c) var(--ink-2-h));
  /* Counts, section labels, metadata. */
  --ink-3-lr: calc(var(--p-l) - 0.2912);
  --ink-3-l: calc(((var(--ink-3-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--ink-flip) + var(--ink-pivot)) * var(--k-bright));
  --ink-3-c: calc(var(--p-c) * 0.0721 * var(--k-vib));
  --ink-3-h: calc(var(--p-h) + 3.29);
  --ink-3: oklch(var(--ink-3-l) var(--ink-3-c) var(--ink-3-h));
  /* A step in from the ground: tracks, wells, the inset boxes. */
  --bg2-lr: calc(var(--p-l) - 0.6632);
  --bg2-l: calc(((var(--bg2-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--ink-flip) + var(--ink-pivot)) * var(--k-bright));
  --bg2-c: calc(var(--p-c) * 0.0430 * var(--k-vib));
  --bg2-h: calc(var(--p-h) - 5.37);
  --bg2: oklch(var(--bg2-l) var(--bg2-c) var(--bg2-h));
  /* A raised surface. */
  --card-lr: calc(var(--p-l) - 0.6498);
  --card-l: calc(((var(--card-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--ink-flip) + var(--ink-pivot)) * var(--k-bright));
  --card-c: calc(var(--p-c) * 0.0496 * var(--k-vib));
  --card-h: calc(var(--p-h) - 5.37);
  --card: oklch(var(--card-l) var(--card-c) var(--card-h));
  /* The hairline that separates one from the next. */
  --card-line-lr: calc(var(--p-l) - 0.5800);
  --card-line-l: calc(((var(--card-line-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--ink-flip) + var(--ink-pivot)) * var(--k-bright));
  --card-line-c: calc(var(--p-c) * 0.0628 * var(--k-vib));
  --card-line-h: calc(var(--p-h) - 5.37);
  --card-line: oklch(var(--card-line-l) var(--card-line-c) var(--card-line-h));
  /* Right. */
  --ok-lr: calc(var(--s-l) + 0.0000);
  --ok-l: calc(((var(--ok-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--pigment-flip) + var(--ink-pivot)) * var(--k-bright));
  --ok-c: calc(var(--s-c) * 0.9999 * var(--k-vib));
  --ok-h: calc(var(--s-h) - 0.00);
  --ok: oklch(var(--ok-l) var(--ok-c) var(--ok-h));
  /* The tile behind a right answer. */
  --ok-bg-lr: calc(var(--s-l) - 0.3046);
  --ok-bg-l: calc(((var(--ok-bg-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--ink-flip) + var(--ink-pivot)) * var(--k-bright));
  --ok-bg-c: calc(var(--s-c) * 0.2947 * var(--k-vib));
  --ok-bg-h: calc(var(--s-h) + 4.99);
  --ok-bg: oklch(var(--ok-bg-l) var(--ok-bg-c) var(--ok-bg-h));
  /* Wrong. */
  --bad-lr: calc(var(--t-l) + 0.0000);
  --bad-l: calc(((var(--bad-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--pigment-flip) + var(--ink-pivot)) * var(--k-bright));
  --bad-c: calc(var(--t-c) * 1.0001 * var(--k-vib));
  --bad-h: calc(var(--t-h) - 0.00);
  --bad: oklch(var(--bad-l) var(--bad-c) var(--bad-h));
  /* The tile behind a wrong answer. */
  --bad-bg-lr: calc(var(--t-l) - 0.3290);
  --bad-bg-l: calc(((var(--bad-bg-lr) - var(--ink-pivot)) * var(--k-contrast) * var(--ink-flip) + var(--ink-pivot)) * var(--k-bright));
  --bad-bg-c: calc(var(--t-c) * 0.1804 * var(--k-vib));
  --bad-bg-h: calc(var(--t-h) - 5.11);
  --bad-bg: oklch(var(--bad-bg-l) var(--bad-bg-c) var(--bad-bg-h));

  /* The ground itself is pushed like every tone but NOT flipped: mirroring the
     surface about the middle would just undo the slider being held. */
  --bg-l: calc(((var(--g-l) - var(--ink-pivot)) * var(--k-contrast) + var(--ink-pivot)) * var(--k-bright));
  --bg-c: calc(var(--g-c) * var(--k-vib));
  --bg: oklch(var(--bg-l) var(--bg-c) var(--g-h));
  /* The frame a desktop window shows either side of the phone column. */
  --frame: oklch(calc(var(--bg-l) + 0.03 * var(--ink-flip)) var(--bg-c) var(--g-h));

  /* ---- Material --------------------------------------------------------
     Judgement calls, each behind one number, because a value found by
     reasoning about it is a value found badly. The build sheet turns these. */
  --k-shadow: 1;
  --shadow-a: calc((0.065 + 0.305 * (1 + var(--ink-flip)) * 0.5) * var(--k-shadow));
  --shadow-a2: calc((0.075 + 0.265 * (1 + var(--ink-flip)) * 0.5) * var(--k-shadow));
  --shadow: 0 1px 2px rgb(0 0 0 / var(--shadow-a)), 0 8px 24px rgb(0 0 0 / var(--shadow-a2));
  --shadow-lift: 0 2px 6px rgb(0 0 0 / calc(var(--shadow-a) * 1.45)),
                 0 18px 44px rgb(0 0 0 / calc(var(--shadow-a2) * 1.6));

  --r:18px; --r-s:12px;
  --safe-t:env(safe-area-inset-top,0px);
  --safe-b:env(safe-area-inset-bottom,0px);
  --tap:48px;

  /* The halo behind a sign, and how much tooth it has. Both follow the
     polarity: a glow needs more of itself to read on paper than on ink. */
  --k-glow: 1;
  --glow-core: calc((0.36 - 0.06 * var(--ink-flip)) * var(--k-glow));
  --glow-mid:  calc((0.175 - 0.025 * var(--ink-flip)) * var(--k-glow));
  --glow-edge: calc((0.055 - 0.005 * var(--ink-flip)) * var(--k-glow));
  --grain-a:   calc(0.26 - 0.04 * var(--ink-flip));

  /* ---- Film grain ------------------------------------------------------
     Generated in the stylesheet, so there is no asset to load. The plate keeps
     its own still copy; the one over the whole app moves, because real grain
     is a different set of crystals every frame and a still speckle reads as a
     dirty screen. */
  --grain:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  --grain-tile:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' seed='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  --grain-size: 260px;
  --grain-opacity: calc(0.30 - 0.06 * var(--ink-flip));
  --grain-fps: 15;      /* 0 holds it still */
  --grain-drift: 1.3;   /* how far each step moves the field, as a % of screen */
}

*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0; background:var(--bg); color:var(--ink);
  font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  -webkit-font-smoothing:antialiased; -webkit-tap-highlight-color:transparent;
  overscroll-behavior:none;
  /* an app, not a page: nothing selects, nothing drags, nothing long-press menus */
  -webkit-user-select:none; user-select:none;
  -webkit-touch-callout:none;
}
input,textarea,select,[contenteditable],.code-box{
  -webkit-user-select:text; user-select:text; -webkit-touch-callout:default;
}
img,svg{-webkit-user-drag:none;user-drag:none}
/* Sinhala and Tamil need their own stack wherever they appear, which since the
   deck can be read entirely in one of them is now most of the app, not a line
   under the English. Scoped by [lang] so the whole screen picks it up. */
.si,.ta,[lang="si"],[lang="ta"],
:root[data-lang="si"] .q-prompt,:root[data-lang="si"] .opt,:root[data-lang="si"] .ans-txt .en,
:root[data-lang="si"] .bcell span,:root[data-lang="si"] .tcard-en,:root[data-lang="si"] .st-h,
:root[data-lang="ta"] .q-prompt,:root[data-lang="ta"] .opt,:root[data-lang="ta"] .ans-txt .en,
:root[data-lang="ta"] .bcell span,:root[data-lang="ta"] .tcard-en,:root[data-lang="ta"] .st-h{
  font-family:"Nirmala UI","Iskoola Pota","Noto Sans Sinhala","Noto Sans Tamil","Latha",sans-serif}
/* Both scripts carry tall ascenders and deep descenders that a Latin line
   height crops. */
:root[data-lang="si"] .opt,:root[data-lang="ta"] .opt{line-height:1.45}
button,input,select{font:inherit;color:inherit}
button{background:none;border:0;cursor:pointer;padding:0}
[hidden]{display:none!important}
img{max-width:100%;display:block}

/* ---------- splash ---------- */
.splash{
  position:fixed;inset:0;z-index:99;display:grid;place-items:center;align-content:center;gap:18px;
  background:var(--bg);
}
.splash-mark{display:block;width:min(58vw,260px);opacity:0;transform:scale(.86);will-change:transform,opacity}
.splash-mark img{width:100%}
.splash-sub{font-size:13px;color:var(--ink-3);letter-spacing:.02em;opacity:0}
.splash.go .splash-mark{animation:markIn .52s cubic-bezier(.2,1.5,.4,1) forwards}
.splash.go .splash-sub{animation:subIn .34s ease-out .30s forwards}
.splash.out{animation:splashOut .26s cubic-bezier(.5,0,.9,.4) forwards}
@keyframes markIn{from{opacity:0;transform:scale(.86)}40%{opacity:1}to{opacity:1;transform:scale(1)}}
@keyframes subIn{to{opacity:1}}
@keyframes splashOut{to{opacity:0;transform:scale(1.05)}}
@media (prefers-reduced-motion:reduce){
  .splash.go .splash-mark{animation:none;opacity:1;transform:none}
  .splash.go .splash-sub{animation:none;opacity:1}
  .splash.out{animation:fadeOut .18s linear forwards}
  @keyframes fadeOut{to{opacity:0}}
}

/* ---------- shell ---------- */
.app{height:100%;height:100dvh;display:flex;flex-direction:column;overflow:hidden;position:relative}
.screen{position:absolute;inset:0;display:flex;flex-direction:column;background:var(--bg);
  transform:translate3d(0,0,0)}
/* the leading-edge shadow a pushed screen casts on the one behind it */
.screen.nav-top{box-shadow:-8px 0 26px rgba(0,0,0,.18)}
/* vertical scrolling stays native; the horizontal axis belongs to the back gesture */
.screen.nav-swipe{touch-action:pan-y}

/* A presented screen covers the stack instead of joining it. It casts its
   shadow downward, because it came up rather than across. */
.screen.nav-modal{box-shadow:0 -10px 34px rgba(0,0,0,.30);border-radius:14px 14px 0 0}
/* and the screen it covers settles back rather than sliding aside */
.screen.nav-recede{transform:scale(.94);opacity:.6;border-radius:14px;overflow:hidden;
  transition:transform 500ms cubic-bezier(0.32,0.72,0,1),opacity 500ms cubic-bezier(0.32,0.72,0,1)}
@media (prefers-reduced-motion:reduce){.screen.nav-recede{transition:none}}

.back-btn{display:flex;align-items:center;gap:1px;min-height:var(--tap);padding:0 10px 0 4px;
  color:var(--ink-2);font-size:15px;font-weight:500;max-width:42%}
.back-btn:active{opacity:.55}
.back-btn svg{flex:0 0 auto}
.back-btn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.scroll{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;
  padding:4px 16px calc(28px + var(--safe-b));overscroll-behavior:contain}
/* A screen with no bar of its own still starts under the status bar, so it
   carries the inset itself. Short content then centres in what is left rather
   than sitting against the top with a band of nothing underneath. */
.center-col{display:flex;flex-direction:column;align-items:center;gap:12px;
  min-height:100%;justify-content:center;
  padding-top:calc(24px + var(--safe-t));padding-bottom:12px}

.bar{display:flex;align-items:center;gap:10px;padding:calc(8px + var(--safe-t)) 10px 8px;
  border-bottom:1px solid var(--card-line);background:var(--bg)}
.bar-title{flex:1;text-align:center;font-weight:650;font-size:15px}
.bar-count{min-width:56px;text-align:right;font-size:13px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.icon-btn{min-width:var(--tap);min-height:var(--tap);display:grid;place-items:center;
  border-radius:12px;color:var(--ink-2);font-size:19px;line-height:1}
.icon-btn:active{background:var(--bg2)}

/* ---------- home ---------- */
.home-head{display:flex;align-items:flex-end;justify-content:space-between;
  padding:calc(10px + var(--safe-t)) 12px 6px}
.home-mark{display:block;margin-left:6px}
.home-mark img{height:52px;width:auto}
.countdown-card{background:var(--card);border:1px solid var(--card-line);border-radius:var(--r);
  padding:18px 18px 15px;box-shadow:var(--shadow);margin:12px 0}
.cd-num{font-size:44px;font-weight:800;line-height:1;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.cd-lbl{font-size:13px;color:var(--ink-2);margin-top:4px}
/* two segments over one track: solid, then shaky, then what is left is unseen */
.cd-bar{height:8px;border-radius:99px;background:var(--bg2);margin:14px 0 9px;overflow:hidden;display:flex}
.cd-bar i{display:block;height:100%;width:0;transition:width .6s cubic-bezier(.2,.7,.3,1)}
.cd-solid{background:linear-gradient(90deg,var(--gold-deep),var(--gold))}
.cd-shaky{background:var(--gold);opacity:.30}
.cd-foot{font-size:12px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.cd-foot b{color:var(--ink);font-weight:750}
.cd-why{margin:7px 0 0;font-size:11.5px;line-height:1.55;color:var(--ink-3)}

.due-row{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:14px}
.due-pill{background:var(--card);border:1px solid var(--card-line);border-radius:var(--r-s);
  padding:11px 8px;text-align:center}
.due-pill b{display:block;font-size:21px;font-weight:750;font-variant-numeric:tabular-nums}
.due-pill span{font-size:11px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.06em}

.cta{width:100%;min-height:56px;border-radius:16px;background:var(--ink);color:var(--bg);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;
  font-weight:700;font-size:16px;box-shadow:var(--shadow);
  transition:transform .12s cubic-bezier(.3,1.4,.5,1),opacity .12s}
.cta:active{transform:scale(.975)}
.cta[disabled]{opacity:.4}
.cta-sub{font-size:12px;font-weight:500;opacity:.62}
.cta.small{min-height:50px;font-size:15px}
.cta.gold{background:var(--gold);color:var(--gold-ink)}

.mode-row{display:flex;gap:7px;margin:12px 0 16px;overflow-x:auto;padding-bottom:2px;
  scrollbar-width:none}
.mode-row::-webkit-scrollbar{display:none}
.chip{flex:0 0 auto;padding:9px 14px;border-radius:99px;background:var(--card);
  border:1px solid var(--card-line);font-size:13px;font-weight:600;color:var(--ink-2);white-space:nowrap}
.chip.is-on{background:var(--ink);color:var(--bg);border-color:var(--ink)}

.cat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:18px}
.cat-cell{background:var(--card);border:1px solid var(--card-line);border-radius:var(--r-s);
  padding:11px 12px;text-align:left;display:flex;flex-direction:column;gap:7px}
.cat-cell:active{background:var(--bg2)}
.cat-name{font-size:13px;font-weight:650}
.cat-meter{height:5px;border-radius:99px;background:var(--bg2);overflow:hidden}
.cat-meter i{display:block;height:100%;background:var(--gold-deep);border-radius:99px}
.cat-sub{font-size:11px;color:var(--ink-3);font-variant-numeric:tabular-nums}

.home-links{display:flex;flex-direction:column;gap:2px;margin-bottom:10px}
.link-btn{min-height:var(--tap);color:var(--ink-2);font-size:14px;font-weight:600;text-decoration:underline;
  text-underline-offset:3px;text-decoration-color:var(--card-line)}
.tiny-note{font-size:11px;line-height:1.5;color:var(--ink-3);margin:6px 0 0}

/* ---------- sign plate ---------- */
/* A sign floats on its own halo rather than a white card. The halo is the sign's
   field colour, so the thing you read from a distance is the first thing you see. */
.plate{--g:228,226,220;
  position:relative;isolation:isolate;background:transparent;border:0;border-radius:16px;
  display:flex;align-items:center;justify-content:center;padding:12px;overflow:hidden}
/* closest-side keeps the halo a circle sized to the short edge, so it always
   reaches zero before the box and never shows a clipped rectangle */
.plate{--halo:radial-gradient(closest-side circle at 50% 48%,
      rgba(var(--g),var(--glow-core)) 0%,
      rgba(var(--g),var(--glow-mid)) 38%,
      rgba(var(--g),var(--glow-edge)) 62%,
      rgba(var(--g),0) 82%)}
.plate::before{content:'';position:absolute;inset:0;z-index:0;background:var(--halo)}
.plate::after{content:'';position:absolute;inset:0;z-index:0;pointer-events:none;
  background-image:var(--grain);background-size:160px 160px;
  opacity:var(--grain-a);mix-blend-mode:soft-light;
  -webkit-mask-image:var(--halo);mask-image:var(--halo)}
.plate img{position:relative;z-index:1;
  max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain}
@media (prefers-reduced-transparency:reduce){.plate::after{display:none}}

/* ---------- teach deck ---------- */
.deck{flex:1;display:flex;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;
  scrollbar-width:none;-webkit-overflow-scrolling:touch;overscroll-behavior-x:contain}
.deck::-webkit-scrollbar{display:none}
.tcard{flex:0 0 100%;scroll-snap-align:center;scroll-snap-stop:always;
  display:flex;flex-direction:column;gap:12px;padding:14px 16px;overflow-y:auto}
.tcard .plate{height:min(38vh,260px);width:100%}
.tcard-code{font-size:11px;letter-spacing:.1em;color:var(--ink-3);font-weight:700}
.tcard-en{font-size:22px;font-weight:750;line-height:1.2;letter-spacing:-.01em}
.tcard-tr{font-size:15px;color:var(--ink-2);line-height:1.7}
.tcard-do{background:var(--card);border:1px solid var(--card-line);border-left:3px solid var(--gold);
  border-radius:var(--r-s);padding:12px 14px;font-size:14px;line-height:1.6}
.tcard-logic{font-size:12.5px;color:var(--ink-3);line-height:1.6}
.tcard-logic b{color:var(--ink-2)}
.deck-dots{display:flex;gap:5px;justify-content:center;padding:8px 0 2px}
.deck-dots i{width:5px;height:5px;border-radius:99px;background:var(--card-line);transition:all .2s}
.deck-dots i.on{background:var(--ink-2);width:16px}
.deck-foot{padding:8px 16px calc(16px + var(--safe-b))}

/* ---------- round ---------- */
.prog{flex:1;display:flex;align-items:center;gap:9px}
.prog-track{flex:1;height:7px;border-radius:99px;background:var(--bg2);overflow:hidden}
.prog-fill{height:100%;width:0;background:var(--ink);border-radius:99px;
  transition:width .34s cubic-bezier(.2,.7,.3,1)}
.prog-txt{font-size:12px;font-variant-numeric:tabular-nums;color:var(--ink-2);font-weight:650}
.prog-txt .dim{color:var(--ink-3);margin:0 1px}
.redrill{font-size:11px;font-weight:700;color:var(--gold-ink);background:var(--gold);
  padding:3px 8px;border-radius:99px;font-variant-numeric:tabular-nums}

.q{flex:1;display:flex;flex-direction:column;padding:10px 16px calc(14px + var(--safe-b));min-height:0}
.q-prompt{font-size:13px;color:var(--ink-3);text-align:center;font-weight:600;
  letter-spacing:.02em;padding:4px 0 10px}
.q-stage{flex:1;min-height:0;display:grid;place-items:center;padding-bottom:12px}
.q-stage .plate{height:100%;width:100%;max-height:38vh;max-width:min(80vw,360px)}
/* The stage always takes the space left over and centres what is in it. As a
   fixed-height block it left the leftover height as one dead band above the
   tiles, which reads as a layout that has given up rather than as breathing
   room. */
.q-stage.word-only{padding:16px 0 20px}
.q-stage .word{font-size:clamp(22px,6vw,32px);font-weight:750;text-align:center;line-height:1.2;
  letter-spacing:-.01em;padding:0 8px}
.q-stage .word small{display:block;font-size:13px;font-weight:500;color:var(--ink-3);margin-top:8px;letter-spacing:.08em}

/* Four squares, always. A vertical list of four sentences cannot be read in the
   fraction of a second this is training for; a two-by-two can, and the thumb
   learns where the corners are. The cap on height is what keeps the sign above
   them from being squeezed off a short screen. */
.q-options{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:auto}
.opt{aspect-ratio:1/1;max-height:min(30vh,188px);min-height:var(--tap);
  border-radius:16px;background:var(--card);border:1.5px solid var(--card-line);
  padding:10px;display:grid;place-items:center;text-align:center;
  font-size:14.5px;font-weight:650;line-height:1.3;letter-spacing:-.005em;
  overflow:hidden;
  /* Never mid-word. `hyphens:auto` with no `lang` on the document lets the
     browser guess at syllables it does not know, and a sign name chopped as
     "administra-tive" is unreadable in the fraction of a second this is
     training for. Balance the lines instead: the browser evens out a two or
     three line block so no line is left with one orphan word. */
  hyphens:manual;overflow-wrap:normal;word-break:keep-all;text-wrap:balance;
  transition:transform .1s cubic-bezier(.3,1.4,.5,1),background .14s,border-color .14s}
.opt>span{max-height:100%;overflow:hidden}
.opt.short{font-size:16px}
.opt.long{font-size:13px;line-height:1.28}
.opt:active{transform:scale(.982)}
.opt.img{padding:8px}
.opt.img .plate{width:100%;height:100%;padding:4px;border-radius:12px}
.opt.right{border-color:var(--ok);background:var(--ok-bg)}
.opt.wrong{border-color:var(--bad);background:var(--bad-bg)}
.opt.mute{opacity:.42}
.opt[disabled]{cursor:default}
@media (max-height:660px){.opt{max-height:min(26vh,150px);font-size:13.5px}.opt.short{font-size:15px}}

/* a wrong answer lands in the body before the sheet has finished sliding up */
@keyframes qshake{
  0%{transform:translate3d(0,0,0)}   13%{transform:translate3d(-8px,0,0)}
  27%{transform:translate3d(7px,0,0)} 42%{transform:translate3d(-4.5px,0,0)}
  57%{transform:translate3d(3px,0,0)} 72%{transform:translate3d(-1.8px,0,0)}
  87%{transform:translate3d(.9px,0,0)} 100%{transform:translate3d(0,0,0)}
}
.q.shake{animation:qshake .42s cubic-bezier(.36,.07,.19,.97)}
@media (prefers-reduced-motion:reduce){.q.shake{animation:none}}

/* ---------- sheets ---------- */
/* What dims behind a presented sheet. Tapping it dismisses. */
/* layers: screens 10+, a presented screen 40, scrim 50, sheets 51, toast 85 */
.scrim{position:absolute;inset:0;z-index:50;background:rgba(0,0,0,.42);opacity:0;
  -webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}
.sheet{position:absolute;left:0;right:0;bottom:0;z-index:51;background:var(--card);
  border-top:1px solid var(--card-line);border-radius:22px 22px 0 0;box-shadow:var(--shadow-lift);
  padding:0 16px calc(16px + var(--safe-b));max-height:86%;display:flex;flex-direction:column;
  will-change:transform;transform:translate3d(0,100%,0)}
/* The grabber is the one part that always belongs to the sheet, so it gets a
   finger-sized target around the 4px pill and refuses every other gesture. */
.sheet-grip{position:relative;height:26px;flex:0 0 auto;touch-action:none;cursor:grab}
.sheet-grip::before{content:'';position:absolute;left:50%;top:11px;width:38px;height:4px;
  margin-left:-19px;border-radius:99px;background:var(--card-line)}
.sheet-grip:active{cursor:grabbing}
/* the body scrolls natively; the sheet only takes the gesture over at its top */
.sheet-body{overflow-y:auto;-webkit-overflow-scrolling:touch;padding-bottom:12px;
  touch-action:pan-y;overscroll-behavior:contain}
.sheet-cta{margin-top:2px}
.verdict{display:flex;align-items:center;gap:9px;font-size:16px;font-weight:750;margin-bottom:10px}
.verdict.ok{color:var(--ok)} .verdict.no{color:var(--bad)}
.verdict .ms{margin-left:auto;font-size:12px;font-weight:600;color:var(--ink-3);font-variant-numeric:tabular-nums}
.ans-row{display:flex;gap:12px;align-items:flex-start;margin-bottom:12px}
.ans-row .plate{width:88px;height:88px;flex:0 0 88px;padding:8px}
.ans-txt .en{font-size:17px;font-weight:750;line-height:1.25}
.ans-txt .code{font-size:11px;letter-spacing:.09em;color:var(--ink-3);font-weight:700;margin-bottom:3px}
/* where the card's name is not the regulation's filing label, say so rather
   than quietly replacing it */
.gaz{font-size:11px;color:var(--ink-3);margin-top:3px}
.ans-txt .tr{font-size:13.5px;color:var(--ink-2);line-height:1.65;margin-top:5px}
/* what the regulation itself says, kept visually distinct from our own advice */
.ans-off,.tcard-off{background:transparent;border:1px solid var(--card-line);border-radius:var(--r-s);
  padding:10px 12px;font-size:13px;line-height:1.6;color:var(--ink-2);margin-bottom:10px}
.ans-off cite,.tcard-off cite{display:block;margin-top:6px;font-style:normal;font-size:10.5px;
  letter-spacing:.05em;text-transform:uppercase;color:var(--ink-3)}
.ans-do{background:var(--bg2);border-radius:var(--r-s);border-left:3px solid var(--gold);
  padding:11px 13px;font-size:14px;line-height:1.6;margin-bottom:10px}
.confuse{border:1px solid var(--card-line);border-radius:var(--r-s);padding:11px 12px;background:var(--bg)}
.confuse h4{margin:0 0 9px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--bad);font-weight:750}
.confuse-pair{display:flex;gap:10px;align-items:center}
.confuse-pair .cf{flex:1;text-align:center}
.confuse-pair .plate{height:70px;padding:6px;margin-bottom:5px}
.confuse-pair .cf span{font-size:11.5px;color:var(--ink-2);font-weight:600;line-height:1.3;display:block}
.confuse-pair .vs{font-size:11px;color:var(--ink-3);font-weight:700}

.flag{width:100%;min-height:44px;margin-top:12px;border-radius:12px;
  border:1px dashed var(--card-line);color:var(--ink-3);font-size:12.5px;font-weight:600}
.flag:active{background:var(--bg2)}
.report-box{width:100%;border-radius:12px;border:1px solid var(--card-line);background:var(--bg2);
  padding:11px 12px;font-size:15px;line-height:1.5;resize:vertical;min-height:96px}
.report-row{display:flex;gap:8px;align-items:center;margin-top:10px}

/* ---------- reports screen ---------- */
.rep{display:grid;grid-template-columns:52px 1fr auto;gap:11px;align-items:start;
  background:var(--card);border:1px solid var(--card-line);border-radius:var(--r-s);
  padding:10px 11px;margin-bottom:8px}
.rep.done{opacity:.5}
.rep-plate{height:46px;padding:4px}
.rep-head{font-size:12px;font-weight:700;color:var(--ink-2)}
.rep-text{font-size:13.5px;line-height:1.5;margin:4px 0 3px;white-space:pre-wrap}
.rep-meta{font-size:11px;color:var(--ink-3)}
.rep-tick{align-self:center;font-size:12px;padding:7px 11px}
.legend code{font-size:11px;word-break:break-all}

/* ---------- recap ---------- */
.recap-ring{width:118px;height:118px;border-radius:99px;display:grid;place-items:center;
  font-size:30px;font-weight:800;font-variant-numeric:tabular-nums;margin-top:8px}
.recap-h{font-size:20px;font-weight:750;margin:2px 0 2px}
.recap-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;width:100%;margin:8px 0 4px}
.recap-stats div{background:var(--card);border:1px solid var(--card-line);border-radius:var(--r-s);
  padding:11px 6px;text-align:center}
.recap-stats b{display:block;font-size:18px;font-weight:750;font-variant-numeric:tabular-nums}
.recap-stats b.up{color:var(--ok)} .recap-stats b.dn{color:var(--bad)} .recap-stats b.dim{color:var(--ink-3)}
.recap-stats span{font-size:10.5px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.05em}
.recap-note{margin:2px 0 0;font-size:11.5px;line-height:1.6;color:var(--ink-3);text-align:center;max-width:34em}
.recap-missed{width:100%;display:flex;gap:8px;overflow-x:auto;padding:6px 0 10px;scrollbar-width:none;
  scroll-snap-type:x proximity}
.recap-missed::-webkit-scrollbar{display:none}
.miss{flex:0 0 96px;scroll-snap-align:start}
.miss .plate{height:78px;padding:7px;margin-bottom:5px}
.miss span{font-size:10.5px;color:var(--ink-2);line-height:1.3;display:block}

/* ---------- stats ---------- */
.st-h{font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-3);
  font-weight:750;margin:22px 0 9px}
.st-h:first-child{margin-top:12px}
.panel{background:var(--card);border:1px solid var(--card-line);border-radius:var(--r);
  padding:14px;box-shadow:var(--shadow)}
.kpi{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.kpi div{background:var(--card);border:1px solid var(--card-line);border-radius:var(--r-s);padding:12px 13px}
.kpi b{display:block;font-size:22px;font-weight:780;font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.kpi span{font-size:11.5px;color:var(--ink-3)}
.kpi em{font-style:normal;font-size:11px;font-weight:700}
.kpi em.up{color:var(--ok)} .kpi em.dn{color:var(--bad)}
.bars{display:flex;flex-direction:column;gap:9px}
.barrow{display:grid;grid-template-columns:96px 1fr 42px;gap:9px;align-items:center;font-size:12px}
.barrow .bt{color:var(--ink-2);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.barrow .bb{height:8px;background:var(--bg2);border-radius:99px;overflow:hidden}
.barrow .bb i{display:block;height:100%;border-radius:99px;background:var(--gold-deep)}
.barrow .bv{text-align:right;color:var(--ink-3);font-variant-numeric:tabular-nums}
.spark{width:100%;height:96px;display:block}
.offenders{display:flex;flex-direction:column;gap:7px}
.off{display:grid;grid-template-columns:56px 1fr auto;gap:11px;align-items:center;
  background:var(--card);border:1px solid var(--card-line);border-radius:var(--r-s);padding:8px 11px 8px 8px}
.off .plate{height:48px;padding:5px}
.off .on{font-size:13px;font-weight:650;line-height:1.25}
.off .os{font-size:11px;color:var(--ink-3);margin-top:2px}
.off .ov{font-size:12px;font-weight:750;font-variant-numeric:tabular-nums;text-align:right}
.off .ov.bad{color:var(--bad)}
.ready-split{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.ready-split .rs{border-radius:var(--r-s);padding:11px 6px;text-align:center;border:1px solid var(--card-line)}
.ready-split b{display:block;font-size:21px;font-weight:780;font-variant-numeric:tabular-nums}
.ready-split span{font-size:10.5px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.05em}
.ready-split .solid{background:var(--gold);color:var(--gold-ink);border-color:var(--gold-deep)}
.ready-split .solid span{color:var(--gold-ink);opacity:.7}
.ready-split .shaky{background:var(--bg2)}
.legend{font-size:11.5px;color:var(--ink-3);line-height:1.6;margin-top:9px}
.legend b{color:var(--ink-2);font-weight:700}
.empty{color:var(--ink-3);font-size:13px;padding:14px 2px;text-align:center}

/* ---------- browse ---------- */
.browse-search{padding:10px 16px 4px}
.browse-search input{width:100%;min-height:var(--tap);border-radius:12px;padding:0 14px;
  background:var(--card);border:1px solid var(--card-line);font-size:15px}
.bgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(96px,1fr));gap:9px;padding-top:8px}
/* A <button> lays its own content out centred, so in a grid row sized by the
   tallest caption every shorter cell floated its sign and text down by half the
   leftover and left the other half as a gap underneath — the rows never lined
   up. Lay the cell out ourselves and let all the slack fall to the bottom. */
.bcell{text-align:center;display:flex;flex-direction:column;
  align-items:stretch;justify-content:flex-start}
.bcell .plate{height:84px;padding:8px;margin-bottom:5px;flex:0 0 auto}
.bcell span{font-size:10.5px;color:var(--ink-2);line-height:1.3;display:block}
.bcell em{font-style:normal;font-size:9.5px;color:var(--ink-3);letter-spacing:.06em;font-weight:700}

/* ---------- settings ---------- */
.set-row{display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:14px 0;border-bottom:1px solid var(--card-line)}
.set-row label{font-size:14px;font-weight:600}
.set-row p{margin:3px 0 0;font-size:12px;color:var(--ink-3);line-height:1.5}
.set-row input[type=date],.set-row input[type=text],.set-row select{
  min-height:42px;border-radius:10px;padding:0 11px;background:var(--bg2);
  border:1px solid var(--card-line);font-size:14px;max-width:56%}
.code-box{font:700 17px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.06em;
  background:var(--bg2);border:1px dashed var(--card-line);border-radius:12px;
  padding:13px;text-align:center;margin:10px 0;user-select:all;-webkit-user-select:all}
.danger{color:var(--bad)}

/* ---------- toast ---------- */
.toast{position:fixed;left:50%;bottom:calc(24px + var(--safe-b));transform:translateX(-50%);
  background:var(--ink);color:var(--bg);padding:11px 17px;border-radius:99px;font-size:13px;
  font-weight:600;z-index:85;box-shadow:var(--shadow-lift);animation:toastIn .22s ease-out}
@keyframes toastIn{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}

/* ---------- wider screens ----------
   The app is a phone, so on a desktop it stays a phone-width column in the
   middle of the window rather than stretching into a website. */
:root{--app-w:460px}
@media (min-width:560px){
  body{display:flex;align-items:stretch;justify-content:center;background:var(--frame)}
  .app{width:var(--app-w);max-width:100vw;flex:0 0 auto;
    border-left:1px solid var(--card-line);border-right:1px solid var(--card-line);
    box-shadow:0 0 60px rgb(0 0 0 / calc(var(--shadow-a) * 1.3))}
  .splash{left:50%;transform:translateX(-50%);width:var(--app-w);max-width:100vw}
}
@media (min-width:900px){
  :root{--app-w:480px}
}

/* ---------- film grain ----------
   Two elements, and the split is the fix. A mask is painted in the element's
   own coordinates and *then* the element is transformed, so a fade that has to
   stay still cannot live on a layer that moves. The wrapper carries the mask,
   the blend and the strength and never moves; the field inside carries the
   texture and does all the moving. The blend has to be on the wrapper, because
   a mask makes a stacking context and a child blending inside one blends
   against the wrapper's nothing.

   The field is oversized on purpose: it drifts, and the overhang is what stops
   the drift exposing an edge. */
.grain{position:fixed;inset:0;z-index:90;overflow:hidden;pointer-events:none;
  opacity:var(--grain-opacity);mix-blend-mode:overlay}
.grain::before{content:'';position:absolute;inset:-12%;
  background-image:var(--grain-tile);background-size:var(--grain-size) var(--grain-size);
  background-repeat:repeat;will-change:transform;
  animation:grain-drift calc(8s / max(var(--grain-fps), 0.0125)) steps(1,end) infinite}

/* Film grain animating at fifteen frames a second costs about fourteen
   milliseconds of a drag's worst frame, because a full-screen blend layer
   re-blends over a surface that is itself being repainted. During a drag the
   eye is on the card. Hold it still while a finger is down and start it again
   on release; there is nothing to see at either end. */
[data-busy] .grain::before{animation-play-state:paused}

/* Eight positions, held rather than tweened: the point is that each frame is a
   different field, not that the field slides. */
@keyframes grain-drift{
  0%{transform:translate3d(0,0,0)}
  12.5%{transform:translate3d(calc(-1.7% * var(--grain-drift)),calc(0.9% * var(--grain-drift)),0)}
  25%{transform:translate3d(calc(1.1% * var(--grain-drift)),calc(-1.5% * var(--grain-drift)),0)}
  37.5%{transform:translate3d(calc(-0.8% * var(--grain-drift)),calc(-1.1% * var(--grain-drift)),0)}
  50%{transform:translate3d(calc(1.5% * var(--grain-drift)),calc(1.3% * var(--grain-drift)),0)}
  62.5%{transform:translate3d(calc(-1.3% * var(--grain-drift)),calc(1.6% * var(--grain-drift)),0)}
  75%{transform:translate3d(calc(0.7% * var(--grain-drift)),calc(-0.7% * var(--grain-drift)),0)}
  87.5%{transform:translate3d(calc(-1.5% * var(--grain-drift)),calc(-1.4% * var(--grain-drift)),0)}
}
@media (prefers-reduced-motion:reduce){.grain::before{animation:none}}
@media (min-width:560px){.grain{left:50%;transform:translateX(-50%);width:var(--app-w);max-width:100vw}}

/* ---------- theme segment ---------- */
/* A segmented control never shares a line with its own explanation. Squeezed
   between a paragraph and the screen edge it runs out of room, and because a
   rounded control has to clip its corners it loses whole segments off the right
   rather than wrapping. It gets the full width under the text instead, and the
   segments divide it evenly, which is what makes it read as one control. */
.set-row.stacked{flex-direction:column;align-items:stretch;gap:10px}
.set-row.stacked > div{width:100%}
.seg{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;
  border:1px solid var(--card-line);border-radius:10px;overflow:hidden;background:var(--bg2)}
.set-row.stacked .seg{width:100%}
.seg button{padding:9px 6px;font-size:13px;font-weight:600;color:var(--ink-2);min-height:42px;
  min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.seg button + button{border-left:1px solid var(--card-line)}
.seg button.is-on{background:var(--ink);color:var(--bg)}

/* ---------- the build sheet ----------
   Chrome, not a screen: it does not belong in the navigation stack, because
   nothing in it is a place you can be. */
.build-no{display:block;margin:18px auto 4px;padding:8px 14px;border-radius:99px;
  font-size:11px;letter-spacing:.09em;text-transform:uppercase;font-weight:700;
  color:var(--ink-3);border:1px solid var(--card-line)}
.build-no:active{background:var(--bg2)}
.build-sheet{max-height:86%}
.build-intro{font-size:12px;line-height:1.6;color:var(--ink-3);margin:0 0 4px}
.knobs{display:flex;flex-direction:column;gap:2px}
.knob{padding:12px 0;border-bottom:1px solid var(--card-line)}
.knob-head{display:flex;align-items:center;gap:8px}
.knob-head label{font-size:14px;font-weight:650;flex:1}
.knob-val{font:700 12px/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--ink-2);
  font-variant-numeric:tabular-nums;background:var(--bg2);padding:5px 7px;border-radius:7px}
/* An indicator that is also the way back. It only exists once there is
   something to go back from, so it is never a decoration. */
.knob-reset{font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;font-weight:700;
  color:var(--gold-deep);padding:5px 7px;border-radius:7px;visibility:hidden}
.knob.is-changed .knob-reset{visibility:visible}
.knob.is-changed .knob-head label::after{content:'';display:inline-block;width:5px;height:5px;
  border-radius:99px;background:var(--gold-deep);margin-left:6px;vertical-align:middle}
.knob-note{margin:6px 0 0;font-size:11px;line-height:1.55;color:var(--ink-3)}
.knob input[type=range]{width:100%;margin:10px 0 0;accent-color:var(--gold-deep);height:24px}

/* The hue ring. A control by every behavioural definition and by no structural
   one, so it says so with data-grabs and the sheet's own drag leaves it be. */
.knob-ring{position:relative;width:132px;height:132px;margin:12px auto 2px;touch-action:none}
.knob-ring-dial{position:absolute;inset:0;border-radius:99px;
  background:conic-gradient(from 90deg,
    oklch(.72 .16 0),oklch(.72 .16 60),oklch(.72 .16 120),oklch(.72 .16 180),
    oklch(.72 .16 240),oklch(.72 .16 300),oklch(.72 .16 360));
  -webkit-mask:radial-gradient(closest-side circle,transparent 58%,#000 60%);
  mask:radial-gradient(closest-side circle,transparent 58%,#000 60%)}
.knob-ring-thumb{position:absolute;left:50%;top:50%;width:3px;height:50%;margin-left:-1.5px;
  transform-origin:50% 0;background:var(--ink);border-radius:99px;pointer-events:none}
.knob-ring::after{content:'';position:absolute;inset:26%;border-radius:99px;
  background:var(--gold);box-shadow:var(--shadow)}

.build-log{margin-top:16px;border:1px solid var(--card-line);border-radius:var(--r-s);padding:12px}
.build-log h4{margin:0 0 8px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3)}
.build-log p{font-size:11.5px;line-height:1.6;color:var(--ink-3);margin:10px 0 6px}
.build-diff{display:flex;align-items:center;gap:8px;font-size:12px;padding:4px 0}
.build-diff b{flex:1;font-weight:600;color:var(--ink-2)}
.build-diff span{font:700 11.5px/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--ink-3);
  font-variant-numeric:tabular-nums}
.build-diff .to{color:var(--gold-deep)}
.build-diff em{font-style:normal;color:var(--ink-3)}
.build-css{background:var(--bg2);border-radius:8px;padding:10px;overflow-x:auto;
  font:11.5px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace;margin:0;
  -webkit-user-select:text;user-select:text;white-space:pre}
.build-actions{display:flex;gap:8px;margin-top:10px}

/* ---------- the stepped slider ----------
   Built rather than borrowed from <input type=range>, because a range input
   with a step jumps the thumb away from the finger and there is no way to stop
   it. See slider.js for what it copies instead. */
.slider{padding:14px 2px 6px;touch-action:none;cursor:grab;-webkit-tap-highlight-color:transparent}
.slider.is-grabbed{cursor:grabbing}
.slider:focus-visible{outline:2px solid var(--gold);outline-offset:6px;border-radius:12px}
.slider-track{position:relative;height:28px;display:flex;align-items:center}
.slider-track::before{content:'';position:absolute;left:0;right:0;height:4px;
  border-radius:99px;background:var(--bg2);border:1px solid var(--card-line)}
.slider-fill{position:absolute;left:0;right:0;height:4px;border-radius:99px;
  background:var(--gold-deep);transform-origin:left center;transform:scaleX(0)}
.slider-thumb{position:absolute;left:0;width:28px;height:28px;border-radius:99px;
  background:var(--ink);box-shadow:var(--shadow-lift);will-change:transform}
.slider.is-grabbed .slider-thumb{transform-origin:center}
/* the ladder of values, so the steps are visible rather than only felt */
.slider-scale{display:flex;justify-content:space-between;font-size:11px;
  color:var(--ink-3);font-variant-numeric:tabular-nums;margin-top:2px}
.slider-read{display:flex;align-items:baseline;gap:8px;margin-top:8px}
.slider-read b{font-size:22px;font-weight:750;font-variant-numeric:tabular-nums;line-height:1}
.slider-read span{font-size:12.5px;color:var(--ink-2);line-height:1.45}
.slider-note{font-size:12px;color:var(--ink-3);line-height:1.5;margin-top:6px}
.slider-note.warn{color:var(--gold-deep)}

/* ---------- the round summary's charts ----------
   Right and wrong are a status pair, not two series, so they wear --ok and
   --bad and never carry the meaning alone: a wrong mark also carries a cross.
   Everything else is one hue, because everything else is magnitude. */
.rc-block{width:100%;margin:18px 0 4px;text-align:left}
.rc-block h4{margin:0 0 10px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-3);font-weight:750;display:flex;align-items:baseline;gap:8px}
.rc-block h4 small{font-size:10px;letter-spacing:.02em;text-transform:none;font-weight:600;
  color:var(--ink-3);margin-left:auto}
.rc-bars{display:flex;flex-direction:column;gap:7px}
.rc-bar{display:grid;grid-template-columns:86px 1fr 44px;align-items:center;gap:9px}
.rc-bar-k{font-size:11.5px;color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rc-bar-t{height:9px;border-radius:99px;background:var(--bg2);overflow:hidden}
.rc-bar-t i{display:block;height:100%;border-radius:99px;background:var(--gold-deep)}
.rc-bar-v{font-size:12px;font-weight:700;text-align:right;font-variant-numeric:tabular-nums}
.rc-bar-v span{color:var(--ink-3);font-weight:600}

.rc-strip{position:relative;height:calc(var(--arm) * 2 + 18px);display:flex;align-items:center;
  gap:2px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.rc-strip::-webkit-scrollbar{display:none}
/* the axis the marks diverge about */
.rc-strip::after{content:'';position:absolute;left:0;right:0;top:50%;height:1px;
  background:var(--card-line);pointer-events:none}
/* Marks grow out of the midline, so a percentage here has to be one that
   resolves against HEIGHT. `bottom`/`top` do; `margin-bottom` does not — a
   percentage margin resolves against the containing block's WIDTH, which put
   every mark 2px from the axis instead of at it. */
.rc-mark{position:relative;flex:1 1 0;min-width:4px;height:100%;padding:0;background:none}
.rc-mark i{position:absolute;left:0;right:0;bottom:50%;
  border-radius:3px 3px 0 0;background:var(--ok)}
.rc-mark.no i{bottom:auto;top:50%;border-radius:0 0 3px 3px;background:var(--bad)}
.rc-strip.wide .rc-mark{min-width:9px}
.rc-mark:active i{opacity:.55}
.rc-median{position:absolute;left:0;right:0;height:0;border-top:1px dashed var(--ink-3);
  pointer-events:none;z-index:1}
.rc-median span{position:absolute;right:0;top:-13px;font-size:10px;color:var(--ink-3);
  background:var(--card);padding:0 3px}
.rc-key{display:flex;align-items:center;gap:6px;margin-top:7px;font-size:11px;color:var(--ink-3)}
.rc-key .k{width:9px;height:9px;border-radius:3px;background:var(--ok);display:inline-block}
.rc-key .k.no{background:var(--bad);margin-left:8px}
.rc-key-r{margin-left:auto}
.rc-spark{width:100%;height:56px;display:block}
.recap-charts{width:100%}
