/* ============================================================================
   CHAMPION DIGITAL PTY LTD — cd-signature.css  ·  v1.0.0  ·  18 August 2026

   THE SIGNATURE. One file. Every page of Trade Handy, GeoBidder and
   GeoMundra — public, member, team, owner — links THIS and nothing else
   for foundation. Declared law by the owner, 18 August 2026.

   Full rationale, citations and the rules that bind this file:
        _DESIGN-LAW.md   (repo root, both repos)

   ── THE SEVEN RULES ──────────────────────────────────────────────────────
     1. Light is the default. Dark is a preference the user sets, never the
        brand. `[data-theme="dark"]` on <html>, remembered per user.
     2. Application code touches SEMANTIC tokens only. A raw hex in a page
        file is a build error — see _patch/_design_compliance.py.
     3. Hierarchy is built in NEUTRALS. Colour is reserved to carry meaning.
     4. Line-height in px ALWAYS lands on a multiple of 4.
     5. One product = one accent. The skin re-points the accent and NOTHING
        else. Same skeleton, different ramp (the Microsoft 365 model).
     6. Green is a STATUS colour only — paid, verified, passed. It is never
        a brand accent on any Champion Digital product.
     7. The peak moment is rationed. Five per platform, no more.
   =========================================================================

   ── NAMING — WHY --text AND NOT --ink ────────────────────────────────────
   th-core.css (linked by 122 of 130 pages) has defined `--ink` as the PAGE
   BED since long before this file existed. Here `--ink` would have meant the
   text colour — the exact opposite. Loading both would have inverted every
   page on the platform.

   The new file adapts, not the 122 old ones. Text tokens are --text,
   --text-body, --text-muted, --text-inverse. Do not reintroduce --ink.
   Checked 18 Aug 2026: the only other shared names (--line, --ok, --warn,
   --font-display) carry the SAME meaning in both files and are safe.
   ───────────────────────────────────────────────────────────────────────── */

/* ─────────────────────────────────────────────────────────────────────────
   1 · NEUTRAL LADDER — navy-tinted, saturation under 8%. Above ~10% a grey
   stops being neutral and starts fighting the accent.
   ───────────────────────────────────────────────────────────────────────── */
:root{
  /* ── 25 Aug 2026, LAW 74 — these are not greys. Every step is lifted from
     the owner's own Poseidon #123955 shade sheet, so the neutral ladder and
     the brand are the same colour at different lightness. Three brand colours
     on a cool grey read as an accent on a template; on their own darkest
     colour they read as one brand. Ratios measured against the real canvas
     #F4F9FD, not against white, by _patch/_colourproof.py. */
  --n-0:   #FFFFFF;
  /* THE WARM PAPER WING — the owner, 3 Sep 2026: "i did tell you im not a fan
     of this light blue" then "mix of a and c please" — white sheets (A) on a
     bronze-warmed paper ground (C), Poseidon-alpha hairlines. His word is the
     Law 74 amendment admitting these three warm neutrals, derived from Bronze
     Mist at near-white. The blue washes n-50/n-100 stay ONLY as values other
     roles read (dark-theme inks); no light GROUND reads them any more. */
  /* 3 Sep 2026, the owner on the cream: "THIS COLOUR IS OUTDATED." The warm
     tint is withdrawn. The bed is WHITE — Option A exactly — and a sunk well
     is Poseidon at 4%, never a tint of anything. */
  --w-25:  #FFFFFF;
  --w-50:  #FFFFFF;   /* THE LIGHT CANVAS — white                            */
  --w-100: rgba(18,57,85,.04);
  --n-25:  #FAFCFE;
  --n-50:  #F4F9FD;   /* the canvas                                          */
  --n-100: #EAF4FB;
  --n-200: #D0E5F5;
  --n-300: #ABD1ED;
  --n-400: #6CAFE0;
  --n-500: #2574AE;   /* 4.73:1 on canvas, 5.01:1 on surface — muted text    */   /* 5.5:1 on white, 5.2:1 on the page, 4.9:1 on the teal  */
                      /* tint. The old #6B7C88 carried a comment claiming 4.6:1 */
                      /* on white; it was 4.32:1, and 3.83:1 on the tint — so    */
                      /* every piece of muted text on the platform failed AA in  */
                      /* LIGHT mode, the default. Dark already passed. Measured   */
                      /* in a browser against real composited backgrounds.        */
  --n-600: #1B557E;   /* 7.60:1 on the warm canvas — body text             */
  --n-700: #164769;
  --n-800: #133C59;
  --n-900: #123955;   /* POSEIDON itself. 11.55:1 on the warm canvas       */

/* ─────────────────────────────────────────────────────────────────────────
   2 · SEMANTIC SURFACES — light is the default state of the platform.
   ───────────────────────────────────────────────────────────────────────── */
  /* RE-GROUNDED 3 Sep 2026 — his ruling "mix of a and c please": the ground
     is warm paper, the sheets are white, and the hairlines are Poseidon at
     low alpha instead of the pale blue steps. Ratios re-measured. */
  --canvas:      var(--w-50);
  --surface:     var(--n-0);
  --surface-sunk:var(--w-100);
  --surface-alt: var(--w-25);
  --text:         var(--n-900);
  --text-body:    var(--n-600);
  --text-muted:   #5C7590;
  --text-inverse: var(--n-0);
  --line:        #E3E8EE;
  --line-strong: #CBD5DF;

/* ─────────────────────────────────────────────────────────────────────────
   3 · SIGNAL COLOURS — fixed across every skin, deliberately distinct from
   every brand accent so meaning never depends on which product you are in.

   🔴 DANGER IS MUTED ON PURPOSE. Fire-engine red measurably suppresses
   conversion and raises anxiety in B2B software. Brick reads as "stop and
   read" without reading as "you have broken something".
   ───────────────────────────────────────────────────────────────────────── */
  /* 25 Aug 2026, LAW 79. Signals gain an explicit FILL and an explicit ink.
     The ink is what may be type; the fill is what a lamp or badge is drawn in
     and it always carries Poseidon. Splitting them is what stopped the true
     yellow being used as text, which measures 1.53:1 on its own wash. */
  --ok:        #1F6B4A;   --ok-wash:     #E7F3ED;   --ok-line:   #A8E8CF;
  --ok-fill:   #13C692;   --ok-on:       #123955;
  /* ── 23 Aug 2026: THESE ARE NOW ALIASES, kept only so existing consumers
     keep working. The metal moved to --money-* in the :root block above,
     with a corrected value: #DEA221 sat at chroma 0.147 and read as yellow
     paint rather than metal, and the owner rejected it on sight.
     THREE FILES still read these names — deploy/index.html, th-desk.css and
     th-core.css. Aliasing rather than deleting means they pick up the
     corrected bronze automatically and nothing breaks in the meantime.
     Repoint them to --money-* and then delete this block. */
  --accent-gold:    var(--money);
  --accent-gold-on: var(--money-on);
  --accent-glint:   var(--money-light);
  --accent-shadow:  var(--money-shadow);
  --accent-metal:   var(--money-metal);
  /* --warn was #8A6D22 until 24 Aug 2026, which measured 4.42:1 on its own
     --warn-wash — under the 4.5:1 AA floor, and warn text sits on warn wash by
     definition. #856819 is the smallest darkening that clears every ground it
     can land on: wash 4.75, surface 5.26, canvas 4.94. Measured through the
     CSSOM with the real six stylesheets loaded, not computed from the file. */
  --warn:      #7A5D18;   --warn-wash:   #F6F0DF;   --warn-line: #EDD98C;
  /* the glow COLOUR, as distinct from --warn-glow the whole shadow. Added
     28 Aug 2026: 60 sites hard-coded rgba(245,185,69,…) — the retired amber
     #F5B945 struck out by law 79 — and two of them were glows, which cannot
     take a full box-shadow token because they sit inside `0 0 12px <colour>`. */
  --warn-glow-c:  rgba(242,196,27,.55);
  --warn-fill: #F2C41B;   --warn-on:     #123955;   /* TRUE YELLOW, law 43 */
  --danger:    #7A2E2E;   --danger-wash: #F6E8E8;   --danger-line:#E3BDB6;
  --danger-fill:#8C3A2E;  --danger-on:   #FFFFFF;   /* 7.61:1              */
  --info:      #2B5C8A;
  --fg-on-signal: #FFFFFF;
  /* Scrims stay dark in BOTH themes — a light scrim behind a modal reads as
     a rendering fault, not as depth. */
  --scrim:     rgba(6,19,32,.62);
  --scrim-solid: #061320;
  /* ...and therefore so does the text on one. Added 22 Aug 2026: --scrim-solid
     was theme-stable and had no matching foreground, so pages inverting a band
     reached for --text-inverse, which correctly FLIPS — and flipped to dark in
     dark mode, putting dark text on a permanently dark band. A token that never
     changes needs a partner that never changes. */
  --on-scrim:  #FFFFFF;
  /* A scrim needs a SECONDARY foreground too, or everything on it has to be
     pure white. Added 22 Aug 2026: 42 pages draw a permanently dark nav bar
     (rgba(12,36,56,.85)) and coloured its links with --mut, which flips with
     the theme — so in LIGHT mode the nav read mid-grey on dark navy at 2.3:1
     and was effectively unreadable. Theme-stable ground, theme-stable text.
     5.80:1 on the translucent bar, 9.09:1 on solid navy, 10.74:1 on #061320. */
  --on-scrim-muted: #B9C6D0;
}

/* ─────────────────────────────────────────────────────────────────────────
   4 · THE THREE SKINS — set `data-skin` on <html>. The skin re-points the
   accent and nothing else. Neutrals, type, space, radius and signals are
   identical across all three: that is what makes them one company.

   Navy + a warm accent is the highest-trust pairing measured in B2B — 34%
   more trustworthy than the alternatives tested. Plain blue is table stakes
   (≈70% of SaaS already uses it), so it is the NEUTRAL here, not the accent.

   Every accent below carries >=4.5:1 against --surface, and white text on
   the solid fill also carries >=4.5:1. Verified, not assumed.
   ───────────────────────────────────────────────────────────────────────── */
:root{
  /* ── MONEY · champagne bronze · added 23 Aug 2026 ───────────────────────
     Bronze is NOT a product colour. It is the money colour, and it belongs
     to every skin: pay, bid, win, deposit, upgrade, the tier badge. Fixing
     that meaning once is what lets a colour carry meaning at all.

     WHY THIS EXACT VALUE. The owner rejected two predecessors and was right
     both times. #DEA221 sat at chroma 0.147 and read as yellow paint;
     #9A6B10 sat at L56% and read as mud. The luxury research names the band
     as #C69B3C to #D4AF37, "warm and slightly desaturated", and warns that
     bright yellow-golds "read neon and cheap". The number that decides it is
     CHROMA, not lightness. Held near 0.095 inside the band, it reads as
     metal.

     GOLD IS NEVER TEXT. The deep gold that used to serve that role was
     rejected as brown, and the fix was to delete the role rather than soften
     it. Metal is a fill and a signal; type is navy or teal.

     Metal reads as metal through a RANGE, never one flat value. The dark
     stop is not decoration — without it the gradient reads as yellow. */
  /* ── 25 Aug 2026, LAWS 73 + 76. BRONZE MIST #9C7E41 is the metal, and the
     ramp STOPS at it — every step below the anchor is the brown the owner
     rejected, and where a darker warm tone seems wanted the answer is
     Poseidon. #CEA565 (the champagne that used to sit here) was never a
     colour anyone chose; #9C7E41 was in zero files before today. */
  --money:        #9C7E41;   /* the anchor, and the floor of the ramp       */
  --money-on:     #123955;   /* Poseidon is what goes ON the metal          */
  --money-light:  #D4C098;   /* highlight / gradient crown                  */
  --money-shadow: #B4914B;   /* gradient foot — NOT below the anchor        */
  --money-wash:   #F6F1E8;
  --money-metal:  linear-gradient(145deg,#D4C098 0%,#B4914B 52%,#9C7E41 100%);
  --money-glow:   0 5px 18px -5px rgba(156,126,65,.55);
  --money-glow-c: rgba(156,126,65,.55);   /* the colour alone, for inline glows */

  /* ═══ TIER TWO · FUNCTION — RETIRED FROM THIS FILE 30 Aug 2026 ═══════════
     Six colours (--fn-1..6 and --fn-series) lived here and are gone, on Sean's
     GO. THE PLATFORM HAD TWO TIER TWO SETS AND THEY WERE MUTUALLY ILLEGAL.

     The survivor is th-tier2.css's --t2-1..8, and _lookcheck.py already names
     that file as the single Tier Two definition — so the six here were never
     registered as law in the first place.

     BOTH WERE MACHINE-SOLVED, so this was not a taste call. The six scored
     BETTER on their own stated numbers: min ΔE00 16.9, deuteranopia 14.0,
     protanopia 14.5, against the eight's worst pair of 13.9. They were retired
     anyway, for three reasons that outrank the headline figure:

       · THE SIX SOLVE ONE GROUND. Every contrast figure recorded for them was
         "on the dark ground" (worst 4.11:1). The eight are solved across the
         LIGHT set and the DARK set simultaneously, carry a third Sunlight set,
         and hold >=3.15:1 on every ground. This platform ships three modes and
         Law 80 requires the third to work outdoors.
       · THE TWO SETS FORBID EACH OTHER. th-tier2.css:56-64 records that hues
         64-142 hold no legal Tier Two colour at all — and --fn-3 #89A253 and
         --fn-4 #54815D are exactly that band. Each set's own method declares
         the other's colours out of bounds. Both could not stand.
       · NOTHING CONSUMED THE SIX. Counted before removing: --fn-1..6 and
         --fn-series, zero uses across every .html, .js and .css in deploy/.
         The eight are used 12 to 18 times each.

     The measurements above are preserved in this comment rather than lost with
     the values, because the method is the part worth keeping: hue, L* and C*
     solved TOGETHER by annealing, never spaced evenly round the wheel — an
     evenly-spaced ramp at constant lightness was tried first and scored 8.1
     under deuteranopia, because the hues collapse. Lightness varies on purpose
     so a dichromat separates by luminance where hue fails. th-tier2.css reaches
     the same conclusion independently and says so in its own header.

     EXTENDING TIER TWO: edit th-tier2.css, re-run its solver, and never add a
     colour by eye. Do not re-add a second set here. ─────────────────────────── */
  /* --money-stroke — THE GOLD THAT MAY BE A LINE. Added 24 Aug 2026 (D16).
     --money is a FILL and only a fill on light: it measures 2.14:1 on --canvas
     against a 3:1 floor for non-text graphics. A chart line, a rule, a small
     mark all sit under that floor.
     #A97C36 holds --money's hue (36.6deg) and saturation and measures 3.51:1 on
     --canvas, 3.73:1 on --surface — a margin, not a boundary.
     NOT --money-shadow #B08A48. That reads 3.19:1 on --surface, which is why it
     was used, but 2.9982:1 on --canvas, which is where charts actually sit. It
     fails by 0.0018. Measuring a token against the wrong ground is how a
     contrast fix ships short and reads as done.
     Gold only becomes legal TEXT at #926B2E, which is brown — so "gold is never
     type" is a measurement, not a style position. */
  /* 25 Aug 2026, LAW 76: #A97C36 was derived and sits in no step of the
     owner's ramp. The ANCHOR itself clears the non-text floor — 3.62:1 on
     --canvas, 3.84:1 on --surface — so the line comes from the ramp too and
     the palette carries no invented values at all. */
  --money-stroke: #9C7E41;

  /* THE SHELL BAR. th-shell.js:397 hardcoded rgba(255,255,255,.86), so #thTop
     stayed white in dark mode across all 81 shell pages while the sidebar, the
     canvas and every panel flipped correctly around it — the account menu has
     offered a Dark button since it was built. Same value as before; now a token
     with a dark counterpart. */
  --shell-bar: rgba(255,255,255,.86);
}

:root,
[data-skin="th"]{                       /* TRADE HANDY — teal              */
  /* ── 25 Aug 2026, LAWS 73 + 75. CYAN SKY #00B8BB is the accent, and it is a
     FILL — 2.31:1 on canvas, so it can never be text or a hairline. The
     button is cyan carrying POSEIDON lettering at 4.92:1, which is forced by
     the measurement rather than chosen. --accent-text is the readable cyan
     from the owner's own ink sheet and is the ONLY cyan that may be type.
     The retired #0E7C7E was in 7 files; #00B8BB was already in 32. */
  /* ══ LAW 97, 1 Sep 2026 — THE ELECTRIC ACCENT. The owner: "those electric
     colours I like more than the teal accent — site wide." The reference
     look now wears the BRAND'S OWN HUE: the owner amended Law 97 within the
     hour — "put the teal back but give it the electric look" — so the accent
     is ELECTRIC TEAL (#10D9DE, the Cyan Sky hue at the reference's voltage) as
     the WORKING accent on every surface. The lockup's teal E is brand and is
     untouched; Poseidon stays the ink and every neutral; bronze stays money;
     teal remains one of the twelve personal choices, no longer the default.
     Measured before shipping: Poseidon #123955 on #38BDF8 = 5.8:1; the dark
     canvas #071520 on it = 8.8:1; --accent-text #1D4ED8 on white = 7.0:1. ══ */
  /* THE SERIOUS SKIN — the owner's yes, 3 Sep 2026 (Laws 73/75/97 re-worded):
     "the electric teal is baby toy like… i need a professional and serious
     look." One working accent at a serious depth — Cyan Sky's hue at L40 —
     carrying WHITE lettering (5.2:1). No gradient, no glow, no violet. */
  --accent:        #0B7A7D;   /* DEEP TEAL — white on it = 5.2:1            */
  --accent-2:      #096466;   /* the partner is the accent's own deeper step */
  --accent-grad:   linear-gradient(135deg,#0B7A7D,#0B7A7D);
  /* LAW 75 consequence: with a DARK ink the accent must BRIGHTEN on hover,
     not darken. Poseidon clears #00B8BB at 4.92:1 but only 3.90:1 on the old
     #00A3A6 — so the hover state was the thing that broke, not the base.
     Both are steps of the owner's own #00B8BB ramp. 5.87:1 and 6.61:1. */
  --accent-hover:  #096466;   /* darkens on hover — the ink on it is white  */
  --accent-press:  #064F51;
  --accent-wash:   rgba(11,122,125,.09);
  --accent-line:   rgba(11,122,125,.32);
  --accent-on:     #FFFFFF;   /* what goes ON the accent: white, 5.2:1     */
  --accent-text:   #0B7A7D;   /* links — 5.2:1 on white                    */
}

  /* ── THE ALPHA LADDER ─────────────────────────────────────────────────────
   NOT INSIDE A BLOCK UNTIL 24 Aug 2026, AND THEREFORE DEAD. The ladder was
   written after the :root above had already closed, so all eight declarations
   sat at the top level of the stylesheet. CSS discards a declaration outside a
   rule, and the parser then loses its place: measured in the browser, neither
   --accent-a70 nor --accent-a90 existed in the CSSOM, and the whole
   [data-skin="gb"] block that follows was dropped with them — so GeoBidder's
   LIGHT skin had never applied either. Wrapped in :root, which is where it
   always meant to be.

   Plain :root, not :root,[data-skin="th"]: color-mix resolves var(--accent) at
   use time, so the ladder follows whichever skin and theme the element is under
   without being declared per-skin.

   The original note, kept because the reasoning still stands: 94 pages
   hand-wrote rgba(0,184,187,a) because no token existed — artwork ink used as
   interface paint, the single largest source of drift on the platform. The
   steps were chosen by measuring all 435 real uses, not by picking round
   numbers: .40 alone is 27% of them and .50 is 14.5%. */
:root{
  --accent-a08: color-mix(in srgb, var(--accent)  8%, transparent);
  --accent-a14: color-mix(in srgb, var(--accent) 14%, transparent);
  --accent-a22: color-mix(in srgb, var(--accent) 22%, transparent);
  --accent-a32: color-mix(in srgb, var(--accent) 32%, transparent);
  --accent-a40: color-mix(in srgb, var(--accent) 40%, transparent);
  --accent-a50: color-mix(in srgb, var(--accent) 50%, transparent);
  --accent-a70: color-mix(in srgb, var(--accent) 70%, transparent);
  --accent-a90: color-mix(in srgb, var(--accent) 90%, transparent);
}


[data-skin="gb"]{                       /* GEOBIDDER — its own teal        */
  /* ── 23 Aug 2026 · THE SKIN NOW MATCHES THE MARK ────────────────────────
     This block used to be bronze (--accent #9A6B10). That was the single
     largest visual defect on the platform, and the reason GeoBidder read as
     brown: the MARK is a market stall in a teal gradient — rgb(0,112,112)
     to rgb(74,196,185), measured out of the owner's own artwork — while the
     interface around it was ochre. The brand said teal and the buttons said
     bronze, and the bronze lost.

     Measured 23 Aug, and this is why all three skins now share one accent:
       GeoBidder mark, deep stop  #007070  L49.4%  C0.084  H194.8deg
       GeoMundra mark             #007E80  L53.8%  C0.092  H196.7deg
       Trade Handy accent         #0E7C7E  L53.3%  C0.088  H196.9deg
     A 4.4-point lightness spread across three products. That is not three
     colours, it is one — the family was always one hue and only the SKINS
     had wandered. GeoMundra's had too: its accent was #175C8E, a blue, while
     its own globe is teal.

     Owner's ruling, 23 Aug: "all three sides can have the same colors...
     they shouldn't differ too far from each other." So they do not differ at
     all. Identity comes from the MARK, which is where a brand belongs — not
     from repainting the furniture.

     The skin hooks stay, because a future product may genuinely need its own
     accent. Today they all point at the same value, deliberately. */
  --accent:        #0B7A7D;   /* the serious skin, 3 Sep 2026 — every skin */
  --accent-hover:  #096466;
  --accent-press:  #064F51;
  --accent-wash:   rgba(11,122,125,.09);
  --accent-line:   rgba(11,122,125,.32);
  --accent-on:     #FFFFFF;
  --accent-text:   #0B7A7D;

  /* The orange warn override is GONE. It existed only because bronze sat
     where amber does, so "warning" and "GeoBidder" were the same colour.
     With the skin on teal, amber is free to mean warning again — and one
     warning colour across all three products is worth more than a per-skin
     workaround. */

  /* ── 24 Aug 2026 · HOISTED OUT OF THREE PAGES, UNCHANGED ────────────────
     geobidder.html, geobidder-desk.html and geobidder-listing.html each
     declared these three identically in their own :root — 30 uses on
     geobidder.html alone. Triplication is a maintenance defect: change one,
     forget two. The VALUES ARE NOT TOUCHED; they simply live where the skin
     lives now, and all three pages already carry data-skin="gb".
     Left theme-independent, exactly as the pages had them — giving them a
     dark variant would be a design change, not a move. */
  --ink-2:  #1B557E;
  --line-2: #CBD5DF;
  --urgent: #c8461f;
}

[data-skin="gm"]{                       /* GEOMUNDRA — its own teal        */
  /* Was #175C8E, a blue. The GeoMundra mark is a teal globe — rgb(0,126,128)
     measured from deploy/gm-symbol.svg — so the skin had wandered from its
     own artwork exactly as GeoBidder's had. Same correction, same reason. */
  --accent:        #0B7A7D;   /* the serious skin, 3 Sep 2026 — every skin */
  --accent-hover:  #096466;
  --accent-press:  #064F51;
  --accent-wash:   rgba(11,122,125,.09);
  --accent-line:   rgba(11,122,125,.32);
  --accent-on:     #FFFFFF;
  --accent-text:   #0B7A7D;
}

/* ─────────────────────────────────────────────────────────────────────────
   5 · DARK — a PREFERENCE, never the brand. Set `data-theme="dark"` on
   <html> and persist it per user. Owner's ruling, 18 Aug 2026: "for the
   desk a user can set the theme to dark if they really want to."

   Dark is not light inverted. Three things change: never pure black
   (halation, and no room to elevate below it); accents LIFT because a
   4.5:1 accent on white is unreadable on near-black; shadows deepen and
   the hairline flips to white.
   ───────────────────────────────────────────────────────────────────────── */
/* 28 Aug 2026 — data-ground="wash" is the DARK GROUND FLAG, and it sat in the same
   declaration block as [data-theme="dark"], so it applied whatever the theme said.
   129 of 133 pages carry it on <html> and cd-theme.js never clears it, which made
   the light-mode switch INERT on all 129: measured on login.html, setting
   data-theme="light" left --canvas at #071520 and --surface at #0C273A.
   Sean's ruling is dark by default AND switchable, so the flag now yields to an
   explicit light choice. Dark is untouched — :not([data-theme="light"]) is true
   whenever the attribute is absent or says dark. */
[data-theme="dark"],
[data-ground="wash"]:not([data-theme="light"]){
  /* Money lifts in dark: a colour tuned against white goes muddy on
     near-black. Same hue, same chroma, higher lightness. */
  --money:        #D4C098;   /* 8.01:1 on --surface — law 77             */
  --money-light:  #EAE1CE;
  --money-shadow: #B4914B;   /* still never below the anchor              */
  --money-wash:   rgba(212,192,152,.13);
  --money-metal:  linear-gradient(145deg,#EAE1CE 0%,#D4C098 50%,#B4914B 100%);
  --money-glow-c: rgba(212,192,152,.42);   /* law 77 — lighter on the dark ground */
  /* In dark, --money reads 9.23:1 on the dark canvas (re-measured after
     the 2 Sep 2026 retone — it holds) and needs no
     substitute. */
  --money-stroke: var(--money);
  /* RETONED 2 Sep 2026 — the owner, on the near-black desk: "The background
     is black which i dont like, using to much black and gray … add more
     colour tone, a little more glow on the background only." Same darkness
     family, chroma pushed toward Poseidon — blue, never black. Every ratio
     beside a token below was re-measured against the new grounds. */
  --canvas:       #08203A;   /* Poseidon 960 — blue, never #000          */
  --surface:      #0E2C48;   /* Poseidon 925                             */
  --surface-sunk: #051423;
  --surface-alt:  #133C59;
  --text:          #EAF4FB;
  --text-body:     #D0E5F5;  /* 12.68:1 on canvas                        */
  --text-muted:    #7FA6C4;  /* 6.38:1 on canvas                         */
  --text-inverse:  #071520;
  --line:         rgba(255,255,255,.10);
  --line-strong:  rgba(255,255,255,.17);

  --ok:     #13C692;  --ok-wash:     rgba(19,198,146,.13);  --ok-line:     rgba(19,198,146,.32);
  --ok-fill:#13C692;  --ok-on:       #071520;
  --warn:   #F2C41B;  --warn-wash:   rgba(242,196,27,.13);  --warn-line:   rgba(242,196,27,.32);
  --warn-glow-c:  rgba(242,196,27,.42);
  --warn-fill:#F2C41B; --warn-on:    #071520;

  /* --surface at the bar's own weight, so the chrome sits with the
     canvas instead of glowing white above it. */
  /* GLASS 3 Sep 2026 — his ruling: the chrome carries the background effect.
     Translucent over the aurora; #thTop already blurs behind it. */
  --shell-bar: rgba(14,44,72,.58);
  /* LAW 79 — the light brick #8C3A2E is 2.02:1 here and vanishes. This is
     the dark partner the old law never had. */
  --danger: #D2695B;  --danger-wash: rgba(210,105,91,.13); --danger-line: rgba(210,105,91,.32);
  --danger-fill:#D2695B; --danger-on: #071520;
  --fg-on-signal: #071520;
}

[data-theme="dark"],
[data-ground="wash"]:not([data-theme="light"]),
[data-ground="wash"][data-skin="th"]:not([data-theme="light"]),
[data-theme="dark"][data-skin="th"]{
  --accent: #2DB5B8;  --accent-hover: #48C6C9;  --accent-press: #6AD3D5;
  --accent-wash: rgba(45,181,184,.16);  --accent-line: rgba(45,181,184,.35);
  --accent-on: #062A2B;  --accent-text: #2DB5B8;   /* 6.6:1 on the dark canvas */
}
[data-theme="dark"][data-skin="gb"],
[data-ground="wash"][data-skin="gb"]:not([data-theme="light"]),
[data-ground="wash"][data-skin="gm"]:not([data-theme="light"]),
[data-theme="dark"][data-skin="gm"]{
  /* 23 Aug 2026: both now inherit the shared teal, so there is nothing skin-
     specific left to say in dark either. Kept as explicit selectors rather
     than deleted, so the next person can see the convergence was deliberate
     and not an omission. */
  --accent: #2DB5B8;  --accent-hover: #48C6C9;  --accent-press: #6AD3D5;
  --accent-wash: rgba(45,181,184,.16);  --accent-line: rgba(45,181,184,.35);
  --accent-on: #062A2B;  --accent-text: #2DB5B8;   /* 6.6:1 on the dark canvas */
}

/* 24 Aug 2026: a SECOND [data-theme="dark"][data-skin="gm"] block sat here and
   set --accent back to #6BB4E8, the old blue. Identical selector, later in the
   file, so it WON — and GeoMundra's dark accent was blue on live, three lines
   under a comment explaining that the teal convergence was deliberate. It set
   the same six properties and nothing else, so removing it changes only the
   colour it was undoing. Found by the artifact sweep; the duplicate-selector
   shape is why it survived a visual check. */

/* ─────────────────────────────────────────────────────────────────────────
   6 · TYPE — two faces, and a rule about which sets what.

   THE FACES CHANGED 2 Sep 2026 — the owner's ruling ("yes thats it … make
   sure its consistent across the whole platform including the owner team
   side"): Instrument Sans replaces Inter, Clash Display replaces Space
   Grotesk, both self-hosted in th-fonts.css (Law 85, Law 59 retirement).

   Instrument Sans is a HUMANIST-leaning grotesk (OFL). It sets everything a
   customer reads before they decide to pay you — the same trust job Inter
   held, with more warmth at text sizes.

   Clash Display is a DISPLAY grotesk (ITF, Fontshare licence). Display faces
   are striking at display size and measurably harder to read in paragraphs.
   It therefore sets headings, numerals and the wordmark — and NEVER body
   copy. This is enforced.

   Values below are hand-tuned integers carried verbatim from th-tokens.css.
   Nobody world-class ships a modular ratio.
   ───────────────────────────────────────────────────────────────────────── */
:root{
  --font-sans: "Instrument Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-display: "Clash Display",var(--font-sans);
  /* 23 Aug 2026 — the WORDMARK face, and nothing else. Owner's choice after
     seeing four set side by side. Never body, never headings: those stay
     Instrument Sans and Clash Display. A third face earns its place by doing exactly one job. */
  --font-mark: "Manrope",var(--font-display);
  --font-mono: "JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;

  --fs-11: 11px;  --lh-11: 16px;  --ls-11:  .05em;
  --fs-12: 12px;  --lh-12: 16px;  --ls-12:  0;
  --fs-13: 13px;  --lh-13: 20px;  --ls-13: -.01em;
  --fs-14: 14px;  --lh-14: 20px;  --ls-14: -.011em;   /* DEFAULT body      */
  --fs-15: 15px;  --lh-15: 24px;  --ls-15: -.011em;
  --fs-16: 16px;  --lh-16: 24px;  --ls-16: -.011em;   /* MOBILE INPUT MIN  */
  /* 18 and 28 added 28 Aug 2026 on the owner's ruling. 28 pages were already
     written against --fs-18 and --fs-28 — a bold money figure and a card
     heading — and neither existed, so both declarations were invalid and the
     elements fell back to whatever else applied. The scale is hand-tuned
     integers, not a modular ratio, so two more hand-tuned integers belong in it.
     Line-heights interpolate the neighbours and land on a multiple of 4 as the
     law requires: 16→24 and 20→28 give 18→24 (tight, and these are display
     numerals); 24→32 and 32→40 give 28→36. Letter-spacing interpolates too. */
  --fs-18: 18px;  --lh-18: 24px;  --ls-18: -.012em;
  --fs-20: 20px;  --lh-20: 28px;  --ls-20: -.012em;
  --fs-24: 24px;  --lh-24: 32px;  --ls-24: -.014em;
  --fs-28: 28px;  --lh-28: 36px;  --ls-28: -.018em;
  --fs-32: 32px;  --lh-32: 40px;  --ls-32: -.022em;
  --fs-48: 48px;  --lh-48: 52px;  --ls-48: -.022em;
  --fs-64: 64px;  --lh-64: 64px;  --ls-64: -.022em;

  --fw-regular: 400;  --fw-medium: 510;  --fw-semibold: 590;
  --fs-input-mobile: var(--fs-16);   /* below 16px iOS Safari zooms on focus */

  /* SPACE — 4px base */
  --sp-0:0; --sp-025:2px; --sp-05:4px; --sp-075:6px; --sp-1:8px; --sp-15:12px;
  --sp-2:16px; --sp-25:20px; --sp-3:24px; --sp-4:32px; --sp-5:40px; --sp-6:48px;
  --sp-8:64px; --sp-10:80px; --sp-12:96px; --sp-16:128px;

  /* RADIUS by ELEMENT. 12px on a 32px button reads as a mistake. */
  --rad-xs:2px; --rad-sm:4px; --rad-md:6px; --rad-lg:8px;
  --rad-xl:12px; --rad-2xl:16px; --rad-full:9999px;
  --r-focus-md:8px; --r-focus-lg:10px;

  /* CONTROLS. --tap-field exists because a gloved thumb needs more than 44. */
  --ctl-sm:32px; --ctl-md:36px; --ctl-lg:44px;
  --row-compact:32px; --row-default:40px; --row-comfortable:48px;
  --icon-sm:12px; --icon-md:16px; --icon-lg:20px; --icon-xl:24px;
  --tap-min:44px; --tap-field:56px;

  /* MOTION */
  --ease-out:cubic-bezier(.25,.46,.45,.94);
  --ease-in-out:cubic-bezier(.4,0,.2,1);
  --dur-fast:120ms; --dur:160ms; --dur-slow:240ms; --dur-peak:400ms;
}

/* ─────────────────────────────────────────────────────────────────────────
   7 · ELEVATION — the hairline lives INSIDE the shadow token. Multi-layer,
   hue-tinted, alpha 4-9%. `0 2px 4px rgba(0,0,0,.2)` is the most
   recognisable cheap shadow in existence. Never ship it.
   ───────────────────────────────────────────────────────────────────────── */
:root{
  --ring:  0 0 0 1px rgba(18,57,85,.08);
  /* A COLOUR-ONLY shadow tint. --sh-xs..xl are complete shadows; 61 shadows
     across 41 files needed their own geometry and so hardcoded the colour —
     32 of them as rgba(16,32,44,.10), which is the LIGHT tint. In dark that is
     a 10% navy on a near-black ground, i.e. invisible. This is the tint on its
     own so a bespoke geometry can still flip with the theme. Law: both themes
     are designed, not inverted. */
  --sh-tint: rgba(18,57,85,.10);
  --sh-xs: 0 1px 2px rgba(18,57,85,.05);
  --sh-sm: 0 1px 3px rgba(18,57,85,.06), 0 1px 2px rgba(18,57,85,.04);
  --sh-md: 0 4px 8px -2px rgba(18,57,85,.06), 0 2px 4px -2px rgba(18,57,85,.04);
  --sh-lg: 0 12px 16px -4px rgba(18,57,85,.06), 0 4px 6px -2px rgba(18,57,85,.03);
  --sh-xl: 0 24px 32px -8px rgba(18,57,85,.08), 0 8px 16px -4px rgba(18,57,85,.04);
  --sh-menu:  var(--ring), var(--sh-lg);
  --sh-modal: var(--ring), var(--sh-xl);
  --btn-edge:      inset 0 0 0 1px rgba(0,0,0,.10);
  --btn-highlight: inset 0 .5px 0 1.5px rgba(255,255,255,.55);
}
[data-theme="dark"],
[data-ground="wash"]:not([data-theme="light"]){
  --ring:  0 0 0 1px rgba(255,255,255,.10);
  --sh-tint: rgba(0,0,0,.44);
  --sh-xs: 0 1px 2px rgba(0,0,0,.30);
  --sh-sm: 0 1px 3px rgba(0,0,0,.36), 0 1px 2px rgba(0,0,0,.24);
  --sh-md: 0 4px 8px -2px rgba(0,0,0,.40), 0 2px 4px -2px rgba(0,0,0,.28);
  --sh-lg: 0 12px 16px -4px rgba(0,0,0,.44), 0 4px 6px -2px rgba(0,0,0,.28);
  --sh-xl: 0 24px 32px -8px rgba(0,0,0,.52), 0 8px 16px -4px rgba(0,0,0,.32);
  --btn-edge:      inset 0 0 0 1px rgba(255,255,255,.12);
  --btn-highlight: inset 0 .5px 0 1.5px rgba(255,255,255,.14);
}

/* ─────────────────────────────────────────────────────────────────────────
   THE PRIMITIVES th-components.css WAS WRITTEN AGAINST.  Added 24 Aug 2026.

   th-components.css is linked by 55 pages and reaches for --surface-1,
   --hairline, --brand-solid, --brand-fg and --fg-on-solid. Those are declared
   in th-tokens.css, which NO PAGE LINKS and NO STYLESHEET @imports. A var()
   that resolves to nothing and carries no fallback makes the browser drop the
   WHOLE DECLARATION, so 52 declarations were being discarded: 30 background,
   8 color, 6 border, 3 border-bottom, 2 box-shadow, and one each of
   border-color, stroke and border-top.

   Measured in a browser, not inferred. With th-core.css and th-components.css
   loaded, .btn-primary, .btn-secondary and .input all computed to
   background rgba(0, 0, 0, 0) — transparent — while a control element set to
   background:var(--accent) computed to rgb(14, 124, 126). The mechanism was
   fine; these particular tokens simply did not exist.

   WHY THE VALUES ARE TRANSPOSED, NOT COPIED.  th-tokens.css is DARK-FIRST: its
   :root carries the dark values and .theme-light,[data-theme="light"] carries
   the light ones. This file is LIGHT-FIRST. Linking th-tokens.css into the
   cascade would therefore let its dark :root override this file's light :root
   for the 88 tokens they share, and the light theme would break on every page
   that got it. So the primitives are declared here instead, the right way up.
   D22 records the remaining question: retire th-tokens.css, or make it real.
   ───────────────────────────────────────────────────────────────────────── */
:root{
  /* surfaces — light */
  --surface-0: var(--surface);
  --surface-1: #F7F9FA;
  --surface-2: #EFF3F5;
  --surface-3: #E5EAEE;
  --hairline:   rgba(16,32,44,.11);
  --hairline-2: rgba(16,32,44,.06);
  /* the brand ramp is one hue at fixed lightnesses — it does not flip */
  --brand-10:#001A1B; --brand-20:#002527; --brand-30:#00373A; --brand-40:#004A4D;
  --brand-50:#005E61; --brand-60:#007376; --brand-70:#00898C;
  --brand-80: var(--accent); --brand-90: var(--accent);
  --brand-100:#45D2D4; --brand-110:#6BDCDD; --brand-120:#90E5E6; --brand-130:#B0EDEE;
  --brand-140:#CBF3F4; --brand-150:#E0F8F9; --brand-160:#EFFCFC;
  /* semantic, and they follow the tokens above rather than repeating them */
  --brand-solid: var(--brand-80);
  /* th-tokens.css says var(--n-900), and that is right IN ITS OWN dark-first
     world: near-navy on the bright dark accent #3ED9CE is 9:1. Transposed to
     light it is navy on the darker #0E7C7E, which measures 3.17:1 — AA for
     large text only, and a button label is not large text. var(--surface)
     is correct in BOTH directions because the surface and the accent move
     opposite ways: white on #0E7C7E is 5.00:1, dark surface on #3ED9CE is
     10:1. It is also the pairing this file already documents beside --accent,
     "white on it 4.6:1". The one place the primitives are NOT copied verbatim. */
  --brand-fg:    var(--surface);
  --brand-tint:  var(--accent-a14);
  /* INK ON A SOLID SIGNAL FILL. Every use of this token in th-components.css
     — all four of them — sits on --sig-danger or --sig-safe. Those are AS 1319
     standard colours and they deliberately DO NOT flip between themes, so the
     ink must not flip either. var(--surface) followed the theme and therefore
     put near-black on red in dark: measured 3.29:1 on danger and 3.03:1 on
     safe, which is large-text only and a button label is not large text.
     White is 4.96:1 on danger and 5.39:1 on safe — AA in both themes at once.
     THE EXCEPTION, if this is ever reached for again: white on --sig-warn
     (#F5A623 amber) is 2.03:1 and fails. Amber has its own ink already,
     --sig-warn-ink #8A5A00, for exactly that reason. */
  --fg-on-solid: #FFFFFF;
  --r-xs: 2px;
}
[data-theme="dark"],
[data-ground="wash"]:not([data-theme="light"]){
  /* Retoned 2 Sep 2026 with the ground — same ladder, chroma toward blue. */
  --surface-0: #071B30;
  --surface-1: #0C2740;
  --surface-2: #123351;
  --surface-3: #173E60;
  --hairline:   rgba(255,255,255,.09);
  --hairline-2: rgba(255,255,255,.055);
}

/* THE AURORA — 2 Sep 2026, the owner's ruling on the platform ground:
   "a little more glow on the background only". Three whisper-alpha radials,
   every hue an anchor of THE LOOK — Cyan Sky, the ramp's azure, Bronze
   Mist — over a Poseidon sweep that ends on the canvas token. Background
   only: no ink, no surface, no chip changes colour because of this rule.
   [data-x] body outweighs th-core's plain `body{background:var(--ink)}`. */
[data-theme="dark"] body,
[data-ground="wash"]:not([data-theme="light"]) body{
  /* REBALANCED 3 Sep 2026 — the owner: "some places there is way too much
     Navy… i want it balanced, symmetric, colour evenly allocated and sharp
     and vibrant, aligned and not looking flat." Four corners, one glow per
     corner, alphas lifted so the colour reads through the glass chrome. */
  background:
    radial-gradient(1000px 640px at 88% -6%, rgba(0,184,187,.08), transparent 60%),
    radial-gradient(900px 620px at -6% 2%, rgba(108,175,224,.07), transparent 62%),
    radial-gradient(900px 620px at 106% 98%, rgba(212,192,152,.06), transparent 60%),
    radial-gradient(1000px 640px at -4% 102%, rgba(0,184,187,.05), transparent 62%),
    linear-gradient(178deg, #0B2A47 0%, var(--canvas) 48%, #0C2740 100%);
  background-attachment: fixed;
}

/* ─────────────────────────────────────────────────────────────────────────
   8 · BASE — every page inherits this. No page re-declares body type.
   ───────────────────────────────────────────────────────────────────────── */
html{ color-scheme: light; -webkit-text-size-adjust:100%; }
[data-theme="dark"],
[data-ground="wash"]:not([data-theme="light"]){ color-scheme: dark; }

body{
  margin:0;
  background:var(--canvas);
  color:var(--text-body);
  font-family:var(--font-sans);
  font-size:var(--fs-14);
  line-height:var(--lh-14);
  letter-spacing:var(--ls-14);
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
}

h1,h2,h3,h4,.t-display{
  font-family:var(--font-display);
  color:var(--text);
  font-weight:var(--fw-semibold);
  margin:0 0 var(--sp-2);
}
h1{font-size:var(--fs-32);line-height:var(--lh-32);letter-spacing:var(--ls-32)}
h2{font-size:var(--fs-24);line-height:var(--lh-24);letter-spacing:var(--ls-24)}
h3{font-size:var(--fs-20);line-height:var(--lh-20);letter-spacing:var(--ls-20)}
h4{font-size:var(--fs-16);line-height:var(--lh-16);letter-spacing:var(--ls-16)}

a{color:var(--accent-text);text-underline-offset:2px}
a:hover{color:var(--accent-hover)}
hr{border:0;border-top:1px solid var(--line);margin:var(--sp-4) 0}

/* ─────────────────────────────────────────────────────────────────────────
   9 · TYPE UTILITIES + the tabular-numerals rule.
   🔴 .tnum ON EVERY NUMBER. Money, quantities, timestamps, IDs, distances,
   counts, bid amounts. Numbers that change width as they update is an
   instant amateur signal — and these are invoicing and bidding products.
   ───────────────────────────────────────────────────────────────────────── */
.t-11{font-size:var(--fs-11);line-height:var(--lh-11);letter-spacing:var(--ls-11);text-transform:uppercase;font-weight:var(--fw-medium)}
.t-12{font-size:var(--fs-12);line-height:var(--lh-12)}
.t-13{font-size:var(--fs-13);line-height:var(--lh-13);letter-spacing:var(--ls-13)}
.t-14{font-size:var(--fs-14);line-height:var(--lh-14);letter-spacing:var(--ls-14)}
.t-15{font-size:var(--fs-15);line-height:var(--lh-15);letter-spacing:var(--ls-15)}
.t-16{font-size:var(--fs-16);line-height:var(--lh-16);letter-spacing:var(--ls-16)}
.t-muted{color:var(--text-muted)}
.t-with-icon{font-weight:var(--fw-medium)}
.measure{max-width:68ch}
.tnum{font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}

/* .tnum is opt-in, and measured 24 Aug 2026 it was used ZERO times — the
   mechanism was built and never applied, so the note above was aspiration
   rather than description. D10 asks for tabular numerals "everywhere money
   appears". There is no shared money class to hang that on: 279 money render
   sites wrap their output in page-local names — .r, .amt, .num, .v, .n, .fig —
   and most cannot be targeted safely. Measured which can:

     td / th   46 of the 279 money renders sit in a table cell, and a table is
               where alignment is the whole point. Digits only; text unaffected.
     .amt      15 uses, 15 of them money — 100%.
     .price    12 uses, 10 of them money — 83%.

   REJECTED, on measurement rather than taste: .n is 101 uses and only 3% money
   (it means "name"); .num is 38%, .r is 30%, .v is 25%. Applying this to them
   would have set tabular figures on names and labels across the estate.
   The remaining ~206 sites need a shared money class first — raised as D24. */
td, th, .amt, .price{font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}

/* EVERY BUTTON ANSWERS THE PRESS. Measured 24 Aug 2026: of 890 buttons in
   deploy/, only 348 carry a shared .btn class and therefore pick up
   .btn-primary:active{transform:scale(.97)} from th-components.css. The other
   542 - 171 with no class at all, the rest on page-local names like .on, .chip,
   .rm, .bell, .del - answered a press with nothing.

   Specificity 0-1-1, so this is a FLOOR: .btn-primary:active (0-2-0) and any
   page's own :active rule still win. Reduced motion is already handled - the
   global block sets transition-duration:.001ms !important, so this lands as an
   instant state change rather than an animation. */
button:active{transform:scale(.97)}

/* ─────────────────────────────────────────────────────────────────────────
   10 · SURFACES + CONTROLS — the shared primitives. A page that needs a
   card, a button or a badge uses these. It does not roll its own.
   ───────────────────────────────────────────────────────────────────────── */
.cd-card{
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--rad-xl);
  box-shadow:var(--sh-sm);
  padding:var(--sp-3);
}

.cd-btn{
  display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-075);
  min-height:var(--ctl-md);padding:0 var(--sp-2);
  font-family:var(--font-sans);font-size:var(--fs-14);font-weight:var(--fw-medium);
  letter-spacing:var(--ls-14);
  border-radius:var(--rad-md);border:1px solid transparent;
  cursor:pointer;
  transition:background var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}
.cd-btn--primary{
  /* LAW 75: the accent carries POSEIDON, never white. */
  background:var(--accent);color:var(--accent-on);
  box-shadow:var(--btn-edge),var(--btn-highlight),var(--sh-xs);
}
.cd-btn--primary:hover{background:var(--accent-hover)}
.cd-btn--primary:active{background:var(--accent-press)}
.cd-btn--secondary{
  background:var(--surface);color:var(--text);border-color:var(--line-strong);
  box-shadow:var(--sh-xs);
}
.cd-btn--secondary:hover{background:var(--surface-sunk)}
.cd-btn--ghost{background:transparent;color:var(--accent-text)}
.cd-btn--ghost:hover{background:var(--accent-wash)}
.cd-btn--danger{background:var(--danger);color:var(--fg-on-signal)}
.cd-btn[disabled]{opacity:.5;cursor:not-allowed}

/* 🔴 On any page a tradie may use on site, primary actions take the FIELD
   target. Work gloves cut effective touch precision to 20-25mm; 44px is
   not enough. Add class="cd-field" to <body> or the form. */
.cd-field .cd-btn--primary,
.cd-field input,.cd-field select{min-height:var(--tap-field)}

/* Status badges — meaning carried by colour AND word, never colour alone.
   ~8% of men have a colour vision deficiency; a green dot is not a status. */
.cd-badge{
  display:inline-flex;align-items:center;gap:var(--sp-05);
  padding:2px var(--sp-1);border-radius:var(--rad-sm);
  font-size:var(--fs-12);line-height:var(--lh-12);font-weight:var(--fw-medium);
  border:1px solid transparent;
}
.cd-badge--ok{background:var(--ok-wash);color:var(--ok);border-color:var(--ok-line)}
.cd-badge--warn{background:var(--warn-wash);color:var(--warn);border-color:var(--warn-line)}
.cd-badge--danger{background:var(--danger-wash);color:var(--danger);border-color:var(--danger-line)}
.cd-badge--info{background:var(--accent-wash);color:var(--accent-text);border-color:var(--accent-line)}

/* ─────────────────────────────────────────────────────────────────────────
   11 · FOCUS — one treatment, platform-wide. `outline`, NOT box-shadow:
   box-shadow rings vanish in Windows forced-colors mode. WCAG 2.2 SC 2.4.11
   requires >=2px and >=3:1.
   ───────────────────────────────────────────────────────────────────────── */
:where(a,button,input,select,textarea,[tabindex]):focus-visible{
  outline:2px solid var(--accent);
  outline-offset:2px;
  border-radius:var(--r-focus-md);
}

/* ─────────────────────────────────────────────────────────────────────────
   12 · THE PEAK MOMENT — the unboxing.

   Kahneman's peak-end rule: people judge an experience by its most intense
   moment and its ending, not its average. The peak is built by delivering
   MORE than expected at the moment of highest emotional stake.

   🔴 RATIONED TO FIVE PER PLATFORM. The approved five:
        1. identity verified          2. first quote accepted
        3. first payment landed       4. onboarding complete
        5. first listing won (GB) / first route saved (GM)
   Anything else gets --dur, not --dur-peak. If it appears on every card it
   is decoration, and decoration is the opposite of premium.

   Add `.is-peak` when the event fires. Never on page load.
   ───────────────────────────────────────────────────────────────────────── */
.cd-peak{
  position:relative;overflow:hidden;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--rad-xl);
  padding:var(--sp-4);
  box-shadow:var(--sh-sm);
  transition:box-shadow var(--dur-peak) var(--ease-out),
             border-color var(--dur-peak) var(--ease-out);
}
.cd-peak > *{
  opacity:0;transform:translateY(6px);
  transition:opacity var(--dur-peak) var(--ease-out),
             transform var(--dur-peak) var(--ease-out);
}
.cd-peak.is-peak{ border-color:var(--accent-line); box-shadow:var(--sh-lg); }
.cd-peak.is-peak > *{ opacity:1;transform:none; }
.cd-peak.is-peak > :nth-child(1){transition-delay:0ms}
.cd-peak.is-peak > :nth-child(2){transition-delay:90ms}
.cd-peak.is-peak > :nth-child(3){transition-delay:180ms}
.cd-peak.is-peak > :nth-child(4){transition-delay:270ms}

/* ─────────────────────────────────────────────────────────────────────────
   13 · THE FIVE LAMPS — the status vocabulary, fixed once for the platform.

   A tradesperson learns this once and it holds everywhere: on a tool tile, on
   a button, on a quote step, on a road closure in GeoMundra. That is the whole
   value. A lamp that means "fine" in one place and "on" in another is worse
   than no lamp, because it teaches people to stop reading them.

   🔴 GREY IS NOT RED, AND THIS IS THE ONE PEOPLE GET WRONG. A tool nobody has
   switched on is not broken. A feature not on your plan is not a fault. Show
   either of them in the failure colour and within a week every red lamp on the
   platform is background noise — and then a real failure goes unread. Grey is
   an invitation; red is a defect that was actually attempted.

   🔴 A LIT STATE IS A CLAIM. Nothing lights until there is a fact behind it.
   The owner's example, 23 Aug 2026: a quote panel must not show three lamps on
   before the client has chosen anything — it should be dark, and light as the
   work actually happens. The darkness is the information, and it is also where
   the sense of progress comes from.

   MEANING IS NEVER CARRIED BY COLOUR ALONE (law 45 / WCAG 1.4.1): every lamp
   ships beside a word. The colour makes it scannable; the word makes it
   readable to someone who cannot separate the two.
   ───────────────────────────────────────────────────────────────────────── */
.cd-lamp{
  width:9px;height:9px;border-radius:50%;flex:none;display:inline-block;
  background:var(--line-strong);vertical-align:middle;
  transition:background var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}
.cd-lamp.is-ok   { background:var(--ok);     box-shadow:0 0 0 3px var(--ok-wash),     0 0 10px var(--ok); }
.cd-lamp.is-warn { background:var(--warn);   box-shadow:0 0 0 3px var(--warn-wash),   0 0 10px var(--warn); }
.cd-lamp.is-bad  { background:var(--danger); box-shadow:0 0 0 3px var(--danger-wash), 0 0 10px var(--danger); }
.cd-lamp.is-money{ background:var(--money);  box-shadow:0 0 0 3px var(--money-wash),  0 0 10px var(--money); }
/* idle is the DEFAULT, and it is stated explicitly so a page cannot reach for
   .is-bad to mean "off". */
.cd-lamp.is-idle { background:var(--line-strong); box-shadow:none; }

/* A lamp may breathe ONLY where something is genuinely live and continuous —
   a running job, an open auction, a truck moving. Never on a settled state:
   a pulse on something finished reads as an unresolved problem. */
.cd-lamp.is-live{ animation:cd-breathe 2.4s var(--ease-in-out) infinite; }
@keyframes cd-breathe{ 0%,100%{opacity:1} 50%{opacity:.45} }

/* ─────────────────────────────────────────────────────────────────────────
   14 · GLASS — the floating layer, and ONLY the floating layer.

   Apple shipped Liquid Glass across iOS 26 and macOS 26 and refined it in June
   2026 specifically for readability. It is current, and it is what makes an
   interface feel like this year rather than 2019. It is NOT the 2020 blur: it
   refracts, it picks up highlights, it adapts to what passes beneath.

   🔴 WHERE IT BELONGS: the top bar, dialogs, sheets, popovers, search, a
   summary tile sitting over a photograph. Things that float ABOVE content.

   🔴 WHERE IT MUST NEVER GO: tables, forms, invoice lines, anything a
   tradesperson reads on site in sunlight. Law 45 already says field conditions
   outrank visual fashion, and Apple's own June revision was an admission that
   glass over content costs legibility. Both point the same way.

   ALWAYS PAINT A SOLID FALLBACK FIRST. Roughly one browser in twenty cannot do
   backdrop-filter, and a translucent panel with nothing behind it is unreadable
   rather than merely plainer. The @supports block is the enhancement, not the
   baseline.
   ───────────────────────────────────────────────────────────────────────── */
.cd-glass{
  background:var(--surface);                    /* the fallback, painted first */
  border:1px solid var(--line);
}
@supports ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .cd-glass{
    background:color-mix(in srgb, var(--surface) 72%, transparent);
    backdrop-filter:blur(18px) saturate(1.5);
    -webkit-backdrop-filter:blur(18px) saturate(1.5);
    border-color:color-mix(in srgb, var(--text) 12%, transparent);
  }
}
/* On the navy bar the glass sits over a dark ground, so the tint flips. */
.cd-glass-dark{ background:rgba(18,57,85,.92); border:1px solid rgba(255,255,255,.14); }
@supports ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .cd-glass-dark{
    background:rgba(18,57,85,.72);
    backdrop-filter:blur(18px) saturate(1.5);
    -webkit-backdrop-filter:blur(18px) saturate(1.5);
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   15 · INSTRUMENTS — meters, bars and figures that report their own state.

   The rule from every design system that does this well: ANIMATE TO REVEAL,
   NOT TO DECORATE. A bar draws so the ranking is legible before a single label
   is read. A figure counts so you notice it changed. Then both stop.

   🔴 NOTHING HERE LOOPS. A number that keeps counting is not information, it
   is a distraction on a screen somebody is trying to work from.

   🔴 EVERY FIGURE IS TABULAR. Money that shifts sideways as it updates reads
   as unreliable, and a column that does not line up on the decimal is the
   fastest way to look unserious about money.

   Fill widths and counter values are set by the page from real data; this file
   only supplies the movement, so a wrong number can never come from the CSS.
   ───────────────────────────────────────────────────────────────────────── */
.cd-meter{ height:7px;background:var(--surface-sunk);border-radius:5px;overflow:hidden; }
.cd-meter > i{
  display:block;height:100%;width:0;border-radius:5px;background:var(--accent);
  transition:width var(--dur-slow) var(--ease-out);
}
/* Usage approaching or at its limit is a status, so it speaks the lamp
   vocabulary rather than inventing a second one. */
.cd-meter > i.is-warn{ background:var(--warn); }
.cd-meter > i.is-full{ background:var(--danger); }

.cd-bar{ height:10px;background:var(--surface-sunk);border-radius:6px;overflow:hidden; }
.cd-bar > i{
  display:block;height:100%;width:0;border-radius:6px;background:var(--accent);
  transition:width var(--dur-slow) var(--ease-out);
}

/* Figures. font-variant-numeric is the whole point — see section 9. */
.cd-figure{
  font-family:var(--font-display);font-weight:590;
  font-variant-numeric:tabular-nums;color:var(--text);
}

/* Skeletons — a spinner says "wait", a skeleton says "it is nearly here, and
   this is its shape". It also stops the page jumping when the data lands,
   which is the part people feel without being able to name. */
.cd-skel{
  background:linear-gradient(90deg,
    var(--surface-sunk) 25%, var(--surface-alt) 37%, var(--surface-sunk) 63%);
  background-size:400% 100%;
  animation:cd-shimmer 1.4s var(--ease-in-out) infinite;
  border-radius:6px;
}
@keyframes cd-shimmer{ 0%{background-position:100% 0} 100%{background-position:0 0} }

/* ─────────────────────────────────────────────────────────────────────────
   16 · THE LOCKUP — the icon is a FILE, the wordmark is TEXT.

   Before this, the shell loaded th-logo.svg: a baked image with navy fused into
   it. That is why login.html needed a second white copy of the same mark, and
   why the lockup could never follow a theme. Splitting them fixes all of it at
   once — and fixes it by TOUCHING NOTHING.

   THE ICON is one of the four *-symbol.svg files. Each is already a mechanical
   extract of the #logo-logo group from an official pack, produced by
   _extract_symbol.py with the text group removed. Not one path changes here.
   Law 44 is satisfied by leaving the artwork alone.

   THE WORDMARK is live text in --font-mark. Which means it takes its colour
   from a token, scales without blurring, and is read aloud by a screen reader —
   three things a PNG of the same word cannot do.

   🔴 THE CHIP, AND WHY IT IS ON EVERY MARK RATHER THAN THE TWO THAT NEED IT.
   Two of the four contain navy: Trade Handy rgb(11,31,58) and GeoMundra
   rgb(12,36,56). Both are within a few points of the navy ground behind them,
   so on the dark bar they render perfectly and are invisible. GeoBidder (teal
   gradient) and Champion Digital (cyan) do not have the problem.
   The chip gives every mark the light ground it was drawn for. Applying it to
   all four rather than only the two that need it is the owner's call, 23 Aug:
   three product headers that differ slightly read as careless, where four that
   match read as a decision.

   USE .cd-lock--dark WHEREVER THE GROUND IS DARK, not where the THEME is dark.
   The top bar is navy in both themes, so the chip belongs there always. Tying
   it to [data-theme] would break it in light mode, which is the mistake waiting
   to be made here.
   ───────────────────────────────────────────────────────────────────────── */
.cd-lock{ display:inline-flex; align-items:center; gap:12px; text-decoration:none; }
.cd-lock img{ height:34px; width:auto; display:block; flex:none; }
.cd-lock .cd-wm{
  font-family:var(--font-mark);
  font-weight:590;
  font-size:25px;
  line-height:1;
  letter-spacing:-.035em;   /* a heavy geometric face reads loose without this */
  color:var(--text);
  white-space:nowrap;
}
/* On a dark ground the word turns white. The ICON is unresolved: two of the four
   marks contain navy and vanish on the dark bar. A white chip behind the icon was
   tried and rejected by the owner on 23 Aug — he is supplying new artwork instead.
   Until it lands, do not paint anything behind a mark. */
.cd-lock--dark .cd-wm{ color:#FFFFFF; }
/* Two sizes, because a bar and a hero are not the same thing. */
.cd-lock--sm img{ height:24px; }
.cd-lock--sm .cd-wm{ font-size:17px; }
.cd-lock--lg img{ height:52px; }
.cd-lock--lg .cd-wm{ font-size:38px; }

/* Reduced motion is not optional. The peak still lands — instantly. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important;animation-iteration-count:1 !important;
    transition-duration:.001ms !important;scroll-behavior:auto !important;
  }
  .cd-peak > *{opacity:1;transform:none}
  /* 23 Aug 2026 — the instruments land at their final value instantly rather
     than not at all. A meter that never fills is a broken meter, not a calm
     one; the information still has to arrive. */
  .cd-lamp.is-live{animation:none}
  .cd-skel{animation:none;background:var(--surface-sunk)}
}


/* ─────────────────────────────────────────────────────────────────────────
   PRINT — 24 August 2026 (D10).

   WHY THIS EXISTS, measured rather than assumed. A quote, an invoice and a
   contract are printed and posted, and are read by the MEMBER'S OWN CUSTOMER.
   Before this block, 15 of 131 pages had any @media print at all, no shared
   sheet had a single print rule, and the two that matter disagreed: quote.html
   forces a light ground and dark ink, invoice.html does not, and contract.html
   had no print rules whatsoever.

   THE FAILURE IS NOT "IT PRINTS DARK". Browsers drop background colours when
   printing unless a page opts into print-color-adjust, and nothing here does.
   So the paper comes out white — but TEXT COLOUR ALWAYS PRINTS, and in dark
   theme --text is #E8EEF2. Measured on invoice.html with its own print rules
   applied and data-theme="dark": the sheet computed to rgb(180,194,204) text.
   On white paper that is near-invisible. A member on dark mode was sending
   their customer a blank-looking invoice.

   THE FIX IS TO RE-ASSERT THE LIGHT DEFINITIONS, NOT TO INVENT VALUES. The
   dark theme overrides only the SEMANTIC tokens and never touches the --n-*
   neutral ramp, so the light expressions below are exactly the ones :root
   already uses. Nothing new is introduced.
   ───────────────────────────────────────────────────────────────────────── */
@media print{
  :root,
  [data-theme="dark"],
  [data-theme="light"]{
    --canvas:      var(--n-0);
    --surface:     var(--n-0);
    --text:        var(--n-900);
    --text-muted:  var(--n-500);
    --line:        var(--n-200);
    --line-strong: var(--n-300);
    /* ink on paper does not need lifting off a dark ground */
    --ring: none;
    --sh-xs: none; --sh-sm: none; --sh-md: none; --sh-lg: none; --sh-xl: none;
    --sh-menu: none; --sh-modal: none; --sh-tint: transparent;
  }

  html, body{
    background: var(--n-0) !important;
    color: var(--n-900) !important;
  }

  /* The shell is not part of the document. These ids are injected by
     th-shell.js on every page that loads it, so hiding them here saves each
     page from remembering to. Page-specific chrome stays page-specific. */
  #thTop, #thSide, #thScrim, #thToasts, #thPal, #thUserMenu, #thBurger,
  #thSearch, #thdbBanner{ display: none !important; }

  /* A document should not be cut across a row, a card or a signature line. */
  tr, .cd-card, .card, .sheet{ break-inside: avoid; page-break-inside: avoid; }
  h1, h2, h3{ break-after: avoid; page-break-after: avoid; }

  /* Shadows are a screen affordance and cost toner. */
  *{ box-shadow: none !important; text-shadow: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE TOUCH FLOOR — 25 Aug 2026, on the owner's ruling. Design law §4.5.
   ---------------------------------------------------------------------------
   THE LAW: "Mobile inputs are never below 16px. Below that, iOS Safari zooms
   the viewport on focus and the user loses their place. This is not aesthetic."

   WHAT WAS ACTUALLY THERE: 117 rules across 69 files set an input, select or
   textarea below 16px, and THREE of them are in this shared layer —
   th-components.css (.input/.select/.textarea at --fs-14), th-core.css (14.5px)
   and th-fluent.css (--fs-14) — so the baseline broke the law and every page
   inherited it before adding its own. On index.html at 375px, five inputs on
   the front-page quote form rendered at 15px: the top of the funnel, zooming on
   the first tap.

   WHY NOT EDIT THE 117: 14px in a dense tool table on a desktop is a deliberate
   choice, and lifting all of them would fatten every form on the platform to fix
   something that only happens on a phone. This changes nothing on a mouse-driven
   screen and lifts the floor only where the zoom occurs.

   `!important` is deliberate and is the narrowest tool available: those 117 rules
   are page-level and would otherwise win on specificity. It is scoped to coarse
   pointers, to form controls, and to one property. Checkboxes, radios, ranges and
   colour swatches are excluded — font-size does not size them, and forcing 16px
   would only disturb their layout.

   Verified in a browser at 375px with a coarse pointer: inputs below 16px went
   from 5 to 0 on index.html, with no horizontal overflow introduced and the form
   visually unchanged.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (hover: none) and (pointer: coarse) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
  select,
  textarea { font-size: 16px !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE TAP FLOOR — 25 Aug 2026. Design law §: "Targets: 44px minimum."
   ---------------------------------------------------------------------------
   MEASURED, not assumed. A sweep of the 54 public pages at 390px found 165
   controls under 44px across 34 of them — and only SIXTEEN distinct classes,
   with the top ten covering 158 of the 165. It is not 34 separate jobs:

        button.btn / a.btn   108      a.back        6
        a.pick                17      button.chip   5
        bare <button>          9      button.tp-x   3
        <select>               6      the rest      1–2 each

   min-height rather than padding, because 106 of the 108 .btn offenders are
   ALREADY display:flex or inline-flex — the box grows and the label stays
   centred without touching the layout mode. All 17 .pick are display:block, so
   those get flex centring too, which keeps them full width exactly as block did.

   SCOPED TO COARSE POINTERS, the same precedent as the input touch floor above:
   36px is a considered size under a mouse, and the law's rationale — a thumb,
   often gloved — is a touch rationale. Desktop is untouched.

   DELIBERATELY NOT INCLUDED: .leaflet-control-zoom-in / -out. Those are Leaflet's
   own map chrome, sized and positioned by the library, and growing them breaks
   the control stack. Recorded rather than fixed.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (hover: none) and (pointer: coarse) {
  .btn, .pick, .chip, .back, .tab, .tp-x,
  /* 165 -> 19 with the classes above. The remaining tail, added after measuring
     rather than guessed at up front: nine BARE <button> elements with no class at
     all, the .brand logo link, and four one-offs (.btn-sell, .allcats, .seeall,
     .on). A bare button is still a tap target, and the law does not care whether
     somebody gave it a class. Leaflet's zoom controls are <a>, not <button>, so
     naming button here cannot reach them. */
  button:not(.leaflet-control-zoom-in):not(.leaflet-control-zoom-out),
  .brand, .btn-sell, .allcats, .seeall,
  select:not([multiple]):not([size]) { min-height: var(--tap-min, 44px); }
  .pick, .back { display: flex; align-items: center; }
  .chip, .tab, .tp-x { display: inline-flex; align-items: center; justify-content: center; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   LAW 80 · GLASS IS A WORKING MATERIAL — 25 Aug 2026
   ---------------------------------------------------------------------------
   The law was set on 25 Aug and never built. Measured before writing this:
   --wash was declared 3 times and used as a background ZERO times, and .card
   — 267 uses across 71 pages — resolved to a flat var(--panel) box. Blur was
   scattered over 55 pages with no ground under any of it, which by the law's
   own first obligation makes every one of those panes a grey box. That is the
   whole of "everything is stale flat": not taste, a law that never shipped.

   THE THREE OBLIGATIONS, each built rather than restated:

   ONE · GLASS NEEDS A GROUND. A pane over flat canvas is a grey box, so the
   material is scoped to [data-ground="wash"] and CANNOT appear anywhere else.
   A page opts in on <html>; the ground and the material arrive together or
   not at all. Public document pages keep the light canvas and solid cards.

   TWO · THE BORDER AND THE INNER HIGHLIGHT DO THE LEGIBILITY WORK the blur
   takes away — hence the 1px cyan hairline, the inset top highlight, and the
   light-line across the pane's top edge.

   THREE · GLASS IS DEFEATED IN SUNLIGHT. [data-mode="sun"] takes blur to
   zero, panes to solid and hairlines to full-strength cyan. A blurred pane at
   62% opacity has lower effective contrast than a solid one and every trade
   on this platform works outdoors. prefers-reduced-transparency defeats it
   the same way, for the same reason.

   WHY IT IS SCOPED AND NOT WRITTEN INTO .card DIRECTLY: 70 of the 71 pages
   define .card in their own <style>, which loads after this file. A bare
   .card rule here would lose the cascade and change nothing on screen — the
   exact shape of a fix that ships and does not land. [data-ground="wash"]
   .card is (0,2,0) against a local (0,1,0) and wins on specificity from the
   shared sheet, so no page's style block is touched.

   The wash ground also re-points every dark token (see the six selectors
   extended above), so a page that opts in gets Poseidon ink for free.
   ═══════════════════════════════════════════════════════════════════════════ */

/* THE FIELD BED — 25 Aug 2026.
   th-core.css:224 hardcoded background:#fbfcfe on every text input while the
   ink stayed var(--tx). In dark that is #EAF4FB on #FBFCFE — 1.05:1, and the
   157 inputs across 125 pages have been unreadable in dark mode since dark
   mode existed. #fbfcfe is also off the Poseidon ramp by one channel, so it
   broke law 74 as well. One token fixes both: the light value moves onto the
   ramp at --n-25 and the dark value is the sunk surface. */
:root{ --field-bg: var(--n-25); }
[data-theme="dark"],
/* ── THE GUARD THAT WAS ON THE TOKENS AND NOT ON THE PAINT ──────────────────
   30 Aug 2026. Every block below paints the DARK ground. The blocks that flip
   the TOKENS to dark are all written `[data-ground="wash"]:not([data-theme="light"])`
   — lines 392, 432, 440, 555, 632, 646 — and these were not.

   So the moment anyone is on Light (a member choosing it, or the OS reporting
   light while the preference is `system`), on all 159 pages carrying
   data-ground="wash":

       · the tokens fall back to LIGHT   → --text becomes #123955
       · this paint STILL MATCHES        → the #123955 → #0C273A gradient is drawn
       · every heading resolves #123955 on #123955                    ≈ 1.0 : 1

   Sean found it by looking at the screen. No contrast checker can: the
   background COLOUR is light (#F4F9FD, 11.37:1) and the dark is a background
   IMAGE painted over it. Measured in his browser on index.html, 30 Aug.

   Adding the same :not() the token blocks already carry. Nothing else changes:
   on dark and on `system`-resolving-dark these selectors match exactly as
   before. ────────────────────────────────────────────────────────────────── */
[data-ground="wash"]:not([data-theme="light"]){ --field-bg: var(--surface-sunk); }

[data-ground="wash"]:not([data-theme="light"]){
  /* THE GROUND. Poseidon at two lightnesses — law 74, no generic grey. */
  --wash:       linear-gradient(135deg, #123955 0%, #0C273A 100%);
  /* The light source. A wash without one is a flat fill wearing a gradient. */
  --wash-lift:  radial-gradient(900px 520px at 12% -8%, rgba(84,208,216,.16), transparent 62%);
  --wash-lift2: radial-gradient(700px 460px at 92% 8%, rgba(212,192,152,.07), transparent 60%);

  /* THE PANE. Lifted verbatim from _lab/control-desk.html, which the owner
     approved on 25 Aug — "control desk is law ... love it". Not re-derived. */
  --glass-fill: linear-gradient(158deg, rgba(12,39,58,.86), rgba(7,21,32,.92));
  --glass-line: rgba(84,208,216,.18);
  --glass-hi:   rgba(140,232,238,.14);   /* the inner highlight — obligation 2 */
  --glass-edge: rgba(84,208,216,.72);    /* the top light-line                 */
  --glass-blur: blur(10px);
  --glass-sh:   0 0 0 1px rgba(7,21,32,.55), 0 20px 46px -22px rgba(0,0,0,.9);
  --glass-tick: rgba(84,208,216,.5);
}

/* ── OBLIGATION THREE · SUNLIGHT ────────────────────────────────────────────
   Owner ruling, 25 Aug 2026. The first build of this kept the Poseidon ground
   and merely brightened the hairline to cyan. That satisfied "glass is
   defeated" and broke the rest of the same sentence: law 80 says Sunlight
   takes "hairlines to full-strength POSEIDON", and #123955 on a dark ground
   is very nearly invisible — the law only coheres on a LIGHT ground. D36 had
   already proposed exactly that and was waiting on the owner's word.

   It is also the physically correct answer. In high ambient light a dark UI
   emits little and reflected glare dominates, so it washes out; a bright
   ground with dark ink keeps its contrast because the screen is competing
   with the sun rather than hiding from it.

   MEASURED, every value, on the white ground — not carried over from a dark
   palette and hoped for:
     ink      #071520  16.66:1     body   #123955  12.05:1
     muted    #1B557E   7.93:1     cyan   #007779   6.33:1  (and white ON it)
     hairline #123955  12.05:1     ok     #0E5B43   8.09:1
     danger   #8C3A2E   7.61:1     warn ink on #F2C41B 7.28:1
   Cyan drops from #00B8BB to #007779 because the brand cyan is a FILL — it
   measures 2.31:1 and can never be type or a hairline (law 75). Outdoors the
   accent has to survive as a line, so it steps down the same ramp.

   This block must restate the whole token set, not a few of them: the ground
   selector re-points every dark token, so Sunlight has to out-specify all of
   it. [data-ground][data-mode] is (0,2,0) against the ground's (0,1,0). */
[data-ground="wash"][data-mode="sun"]{
  /* the ground — flat white, no light source, no lift */
  --wash:       linear-gradient(#FFFFFF, #FFFFFF);
  --wash-lift:  none;
  --wash-lift2: none;

  /* surfaces */
  --canvas:       #FFFFFF;
  --surface:      #FFFFFF;
  --surface-sunk: #F4F9FD;
  --surface-alt:  #EAF4FB;
  --field-bg:     #FFFFFF;
  --shell-bar:    #FFFFFF;

  /* ink */
  --text:         #071520;
  --text-body:    #123955;
  --text-muted:   #1B557E;
  --text-inverse: #FFFFFF;

  /* hairlines to full strength — the words of the law */
  --line:         #123955;
  --line-strong:  #071520;

  /* the accent steps down so it can be a LINE outdoors */
  --accent:       #0B7A7D;
  --accent-hover: #123955;
  --accent-press: #123955;
  --accent-wash:  #ECFFFF;
  --accent-line:  #007779;
  --accent-on:    #FFFFFF;   /* 5.36:1 on #007779. Poseidon on it is 2.25:1 and fails. */
  --accent-text:  #007779;

  /* signals — the light-ground set, law 79 */
  --ok:     #0E5B43; --ok-wash:     #E8FAF3; --ok-line:     #0E5B43; --ok-on:     #FFFFFF;
  /* 28 Aug 2026 — THE FILL AND THE INK WERE SWAPPED HERE, and only here.
     --ok and --danger on the lines either side both hold their INK; this line
     held the FILL, so `color: var(--warn)` rendered true yellow on cream at
     1.53:1 — in the one mode built for reading a screen in direct sunlight.
     The fill is untouched: this block declares no --warn-fill, so it inherits
     #F2C41B from :root and true yellow still carries Poseidon at 7.28:1. */
  --warn:   #6E5400; --warn-wash:   #FDF6DC; --warn-line:   #6E5400; --warn-on:   #123955;
  --danger: #8C3A2E; --danger-wash: #FAE6E2; --danger-line: #8C3A2E; --danger-on: #FFFFFF;
  --fg-on-signal: #123955;

  /* money stays a fill; the stroke is the only gold that may be a line */
  --money: #9C7E41; --money-on: #123955; --money-stroke: #9C7E41;
  --money-wash: #F6F1E8;

  /* THE GLASS IS DEFEATED — panes solid, blur zero, edge a real Poseidon line */
  --glass-fill: linear-gradient(#FFFFFF, #FFFFFF);
  --glass-line: #123955;
  --glass-hi:   transparent;      /* no inner highlight: nothing to compensate */
  --glass-edge: #123955;
  --glass-blur: blur(0px);
  --glass-sh:   0 1px 2px rgba(7,21,32,.14);
  --glass-tick: #123955;

  /* type one step heavier — D36. Only the system's own weight tokens; pages
     that hardcode a weight keep it, and that is recorded rather than chased. */
  --fw-regular: 500; --fw-medium: 590; --fw-semibold: 650;

  color-scheme: light;
}

/* The mark goes back to Poseidon on the light ground — the wash rule below
   swaps it to the white lockup, and Sunlight must undo that or the wordmark
   disappears into white. Higher specificity, same mechanism. */
[data-ground="wash"][data-mode="sun"] img[src$="th-logo.svg"],
[data-ground="wash"][data-mode="sun"] .logo-img,
[data-ground="wash"][data-mode="sun"] #thH .lg{
  content: url("th-logo.svg");
}
@media (prefers-reduced-transparency: reduce){
  [data-ground="wash"]{
    --glass-fill: linear-gradient(158deg, #0C273A, #0C273A);
    --glass-line: #54D0D8;
    --glass-blur: blur(0px);
  }
}

/* THE GROUND ITSELF. Three fixed layers on body rather than a positioned
   pseudo-element: a fixed ::before at z-index 0 sits over any unpositioned
   content, and this platform has 132 pages of it. */
[data-ground="wash"]:not([data-theme="light"]) body{
  /* An opaque floor UNDER the gradients. Without it background-color stays
     transparent and the ground is carried entirely by background-image — the
     viewer's own canvas shows through anywhere a layer does not paint. */
  background-color: var(--canvas);
  background-image: var(--wash-lift), var(--wash-lift2), var(--wash);
  background-attachment: fixed, fixed, fixed;
  background-repeat: no-repeat, no-repeat, no-repeat;
  color: var(--text);
}

/* THE MATERIAL. The top light-line is a 1px background layer, not ::before —
   pages already use ::before on cards and a collision is silent. */
[data-ground="wash"]:not([data-theme="light"]) .card,
[data-ground="wash"]:not([data-theme="light"]) .cd-card,
[data-ground="wash"]:not([data-theme="light"]) .panel,
[data-ground="wash"]:not([data-theme="light"]) .tile,
[data-ground="wash"]:not([data-theme="light"]) .pane,
[data-ground="wash"]:not([data-theme="light"]) .stat,
[data-ground="wash"]:not([data-theme="light"]) .kpi{
  background-color: transparent;
  background-image:
    linear-gradient(90deg, transparent, var(--glass-edge), transparent),
    var(--glass-fill);
  background-size: 100% 1px, auto;
  background-repeat: no-repeat, no-repeat;
  background-position: 0 0, 0 0;
  border: 1px solid var(--glass-line);
  box-shadow: var(--glass-sh), inset 0 1px 0 var(--glass-hi);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
}

/* The floating layer — rails, bars and overlays. Law 80 names these. */
[data-ground="wash"]:not([data-theme="light"]) .topbar,
[data-ground="wash"]:not([data-theme="light"]) .sidebar,
[data-ground="wash"]:not([data-theme="light"]) .rail,
[data-ground="wash"]:not([data-theme="light"]) #thTop{
  background: linear-gradient(180deg, rgba(7,21,32,.52), rgba(7,21,32,.24));
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-color: var(--glass-line);
}

/* Instrument corner ticks — opt-in, never automatic. .tick + .tl/.tr/.bl/.br */
[data-ground="wash"]:not([data-theme="light"]) .tick{
  position:absolute; width:11px; height:11px; border-color:var(--glass-tick);
}
[data-ground="wash"]:not([data-theme="light"]) .tick.tl{ top:8px; left:8px;  border-top:1px solid; border-left:1px solid }
[data-ground="wash"]:not([data-theme="light"]) .tick.tr{ top:8px; right:8px; border-top:1px solid; border-right:1px solid }
[data-ground="wash"]:not([data-theme="light"]) .tick.bl{ bottom:8px; left:8px;  border-bottom:1px solid; border-left:1px solid }
[data-ground="wash"]:not([data-theme="light"]) .tick.br{ bottom:8px; right:8px; border-bottom:1px solid; border-right:1px solid }

/* THE MARK ON THE DARK GROUND. th-logo.svg is Poseidon #123955 lettering with
   the teal E — correct on canvas, invisible on the wash. th-logo-white-teal.svg
   is the same lockup with the 12 wordmark fills at #FFFFFF and the E still
   #00B8BB, and it already existed. content: url() on a replaced element swaps
   the raster without a second <img> in every header, and the URL resolves
   against THIS stylesheet, which sits beside the asset.
   NOT filter:invert() — that would take the teal E to #FF4744. */
[data-ground="wash"]:not([data-theme="light"]) img[src$="th-logo.svg"],
[data-ground="wash"]:not([data-theme="light"]) .logo-img,
[data-ground="wash"]:not([data-theme="light"]) #thH .lg{
  content: url("th-logo-white-teal.svg");
}

/* Decoration is damped in Sunlight. A soft accent glow behind a form is
   pleasant in a site office and is pure noise on a roof at midday — the mode
   exists for legibility, so anything that is not carrying information gets
   out of the way. Not removed: at .18 it still reads as the same page. */
[data-ground="wash"][data-mode="sun"] .blob,
[data-ground="wash"][data-mode="sun"] .glow,
[data-ground="wash"][data-mode="sun"] .orb{ opacity:.18; filter:saturate(.5); }

/* ═══════════════════════════════════════════════════════════════════════════
   THE TWELVE — the member's own accent, 28 August 2026

   ONE GROUND, TWELVE ACCENTS. Law 74 fixes every neutral as a step of Poseidon,
   so a palette does NOT touch the ramp — it re-points the ACCENT, which is exactly
   what design law 4 already says a skin does. The ground is #08203A dark (retoned 2 Sep 2026) and
   #F4F9FD light in all twelve. There is no black one and no grey one.

   POSEIDON IS THE LAW AND IS QUOTED. Its seven values below are byte-for-byte the
   ones already in this file — laws 73 and 75 — so a member on the default palette
   sees no change at all. The other eleven are theirs to choose.

   EVERY ACCENT TAKES POSEIDON #123955 AS ITS LETTERING. That is law 75's pattern
   for cyan, and solving the other eleven against the pixel the browser actually
   paints made it true for all twelve: 4.76-4.92:1 in light, 5.7-6.6:1 on dark.

   The eight --accent-aNN steps are color-mix off --accent and follow on their own.
   The MARK never moves: a mark is never recoloured to match a skin. Money and the
   safety signals never move either — laws 76 and 79.

   Solved and measured by _reports/_accents-solve.py. Re-run it to add a thirteenth;
   never hand-pick one.
   ═══════════════════════════════════════════════════════════════════════════ */
:root[data-palette="poseidon"]{   /* Cyan Sky */
  --accent:#0B7A7D; --accent-hover:#096466; --accent-press:#064F51;
  --accent-wash:rgba(11,122,125,.09); --accent-line:rgba(11,122,125,.32);
  --accent-on:#FFFFFF; --accent-text:#0B7A7D;
}
:root[data-theme="dark"][data-palette="poseidon"]{
  --accent:#2DB5B8; --accent-hover:#48C6C9; --accent-press:#6AD3D5;
  --accent-wash:rgba(45,181,184,.16); --accent-line:rgba(45,181,184,.35);
  --accent-on:#062A2B; --accent-text:#2DB5B8;
}
:root[data-mode="sun"][data-palette="poseidon"]{
  --accent:#0E7490; --accent-hover:#0E7490; --accent-press:#0E7490;
  --accent-on:#FFFFFF; --accent-text:#0E7490; --accent-line:#0E7490;
}
:root[data-palette="sky"]{   /* Sky */
  --accent:#19AFE9; --accent-hover:#31BCF6; --accent-press:#4BC7FF;
  --accent-wash:#E7F6FF; --accent-line:#BBE7FF;
  --accent-on:#123955; --accent-text:#075F81;   /* 28 Aug 2026: was #002536 at ~16:1, which reads as black. Same hue and chroma, lightness lifted to ~7:1 — AAA, and visibly the accent. */
}
:root[data-theme="dark"][data-palette="sky"]{
  --accent:#55C8FF; --accent-hover:#82D5FF; --accent-press:#A4DFFF;
  --accent-wash:rgba(85,200,255,.13); --accent-line:rgba(85,200,255,.32);
  --accent-on:#123955; --accent-text:#68C7F6;
}
:root[data-mode="sun"][data-palette="sky"]{
  --accent:#002536; --accent-hover:#002536; --accent-press:#002536;
  --accent-on:#FFFFFF; --accent-text:#002536; --accent-line:#002536;
}
:root[data-palette="cobalt"]{   /* Cobalt */
  --accent:#69A6FF; --accent-hover:#80B3FF; --accent-press:#93BFFF;
  --accent-wash:#EDF4FF; --accent-line:#CDE1FF;
  --accent-on:#123955; --accent-text:#1B57A9;   /* 28 Aug 2026: was #001F4D at ~16:1, which reads as black. Same hue and chroma, lightness lifted to ~7:1 — AAA, and visibly the accent. */
}
:root[data-theme="dark"][data-palette="cobalt"]{
  --accent:#8FBCFF; --accent-hover:#A8CBFF; --accent-press:#BDD8FF;
  --accent-wash:rgba(143,188,255,.13); --accent-line:rgba(143,188,255,.32);
  --accent-on:#123955; --accent-text:#94BFFF;
}
:root[data-mode="sun"][data-palette="cobalt"]{
  --accent:#001F4D; --accent-hover:#001F4D; --accent-press:#001F4D;
  --accent-on:#FFFFFF; --accent-text:#001F4D; --accent-line:#001F4D;
}
:root[data-palette="indigo"]{   /* Indigo */
  --accent:#949DFF; --accent-hover:#A3ACFF; --accent-press:#B0B9FF;
  --accent-wash:#F0F3FF; --accent-line:#D8DDFF;
  --accent-on:#123955; --accent-text:#4C4FA9;   /* 28 Aug 2026: was #1A0167 at ~16:1, which reads as black. Same hue and chroma, lightness lifted to ~7:1 — AAA, and visibly the accent. */
}
:root[data-theme="dark"][data-palette="indigo"]{
  --accent:#AAB4FF; --accent-hover:#BCC4FF; --accent-press:#CCD3FF;
  --accent-wash:rgba(170,180,255,.13); --accent-line:rgba(170,180,255,.32);
  --accent-on:#123955; --accent-text:#ADB7FF;
}
:root[data-mode="sun"][data-palette="indigo"]{
  --accent:#1A0167; --accent-hover:#1A0167; --accent-press:#1A0167;
  --accent-on:#FFFFFF; --accent-text:#1A0167; --accent-line:#1A0167;
}
:root[data-palette="violet"]{   /* Violet */
  --accent:#B88DFF; --accent-hover:#C29FFF; --accent-press:#CBAEFF;
  --accent-wash:#F5F1FF; --accent-line:#E5D8FF;
  --accent-on:#123955; --accent-text:#6D40AB;   /* 28 Aug 2026: was #2E0056 at ~16:1, which reads as black. Same hue and chroma, lightness lifted to ~7:1 — AAA, and visibly the accent. */
}
:root[data-theme="dark"][data-palette="violet"]{
  --accent:#C7A8FF; --accent-hover:#D2BBFF; --accent-press:#DDCCFF;
  --accent-wash:rgba(199,168,255,.13); --accent-line:rgba(199,168,255,.32);
  --accent-on:#123955; --accent-text:#C9ABFF;
}
:root[data-mode="sun"][data-palette="violet"]{
  --accent:#2E0056; --accent-hover:#2E0056; --accent-press:#2E0056;
  --accent-on:#FFFFFF; --accent-text:#2E0056; --accent-line:#2E0056;
}
:root[data-palette="magenta"]{   /* Magenta */
  --accent:#EC7CCA; --accent-hover:#FA89D7; --accent-press:#FF9ADF;
  --accent-wash:#FFEEF8; --accent-line:#FFCFEE;
  --accent-on:#123955; --accent-text:#972D7B;   /* 28 Aug 2026: was #400031 at ~16:1, which reads as black. Same hue and chroma, lightness lifted to ~7:1 — AAA, and visibly the accent. */
}
:root[data-theme="dark"][data-palette="magenta"]{
  --accent:#FA92D9; --accent-hover:#FFA9E2; --accent-press:#FFBFE9;
  --accent-wash:rgba(250,146,217,.13); --accent-line:rgba(250,146,217,.32);
  --accent-on:#123955; --accent-text:#F59AD8;
}
:root[data-mode="sun"][data-palette="magenta"]{
  --accent:#400031; --accent-hover:#400031; --accent-press:#400031;
  --accent-on:#FFFFFF; --accent-text:#400031; --accent-line:#400031;
}
:root[data-palette="rose"]{   /* Rose */
  --accent:#F97E85; --accent-hover:#FF9095; --accent-press:#FFA4A6;
  --accent-wash:#FFEFEF; --accent-line:#FFD3D3;
  --accent-on:#123955; --accent-text:#A12E3C;   /* 28 Aug 2026: was #46000E at ~16:1, which reads as black. Same hue and chroma, lightness lifted to ~7:1 — AAA, and visibly the accent. */
}
:root[data-theme="dark"][data-palette="rose"]{
  --accent:#FF999D; --accent-hover:#FFB1B2; --accent-press:#FFC5C5;
  --accent-wash:rgba(255,153,157,.13); --accent-line:rgba(255,153,157,.32);
  --accent-on:#123955; --accent-text:#FF9B9E;
}
:root[data-mode="sun"][data-palette="rose"]{
  --accent:#46000E; --accent-hover:#46000E; --accent-press:#46000E;
  --accent-on:#FFFFFF; --accent-text:#46000E; --accent-line:#46000E;
}
:root[data-palette="coral"]{   /* Coral */
  --accent:#F28569; --accent-hover:#FF9277; --accent-press:#FFA48D;
  --accent-wash:#FFF0EC; --accent-line:#FFD5CA;
  --accent-on:#123955; --accent-text:#9C371D;   /* 28 Aug 2026: was #420A00 at ~16:1, which reads as black. Same hue and chroma, lightness lifted to ~7:1 — AAA, and visibly the accent. */
}
:root[data-theme="dark"][data-palette="coral"]{
  --accent:#FF9D84; --accent-hover:#FFB4A0; --accent-press:#FFC7B8;
  --accent-wash:rgba(255,157,132,.13); --accent-line:rgba(255,157,132,.32);
  --accent-on:#123955; --accent-text:#F9A089;
}
:root[data-mode="sun"][data-palette="coral"]{
  --accent:#420A00; --accent-hover:#420A00; --accent-press:#420A00;
  --accent-on:#FFFFFF; --accent-text:#420A00; --accent-line:#420A00;
}
:root[data-palette="amber"]{   /* Amber */
  --accent:#D2992E; --accent-hover:#E0A53E; --accent-press:#EBB04B;
  --accent-wash:#FFF2DD; --accent-line:#FBDAA8;
  --accent-on:#123955; --accent-text:#76520A;   /* 28 Aug 2026: was #2F1E00 at ~16:1, which reads as black. Same hue and chroma, lightness lifted to ~7:1 — AAA, and visibly the accent. */
}
:root[data-theme="dark"][data-palette="amber"]{
  --accent:#E6B056; --accent-hover:#F5BF65; --accent-press:#FFCD7D;
  --accent-wash:rgba(230,176,86,.13); --accent-line:rgba(230,176,86,.32);
  --accent-on:#123955; --accent-text:#E0B166;
}
:root[data-mode="sun"][data-palette="amber"]{
  --accent:#2F1E00; --accent-hover:#2F1E00; --accent-press:#2F1E00;
  --accent-on:#FFFFFF; --accent-text:#2F1E00; --accent-line:#2F1E00;
}
:root[data-palette="lime"]{   /* Lime */
  --accent:#7BB247; --accent-hover:#87BF54; --accent-press:#92CA5F;
  --accent-wash:#E8FADB; --accent-line:#CCECB3;
  --accent-on:#123955; --accent-text:#3A6302;   /* 28 Aug 2026: was #142800 at ~16:1, which reads as black. Same hue and chroma, lightness lifted to ~7:1 — AAA, and visibly the accent. */
}
:root[data-theme="dark"][data-palette="lime"]{
  --accent:#99CD6C; --accent-hover:#A7DC7B; --accent-press:#B4E987;
  --accent-wash:rgba(153,205,108,.13); --accent-line:rgba(153,205,108,.32);
  --accent-on:#123955; --accent-text:#9DCA78;
}
:root[data-mode="sun"][data-palette="lime"]{
  --accent:#142800; --accent-hover:#142800; --accent-press:#142800;
  --accent-on:#FFFFFF; --accent-text:#142800; --accent-line:#142800;
}
:root[data-palette="emerald"]{   /* Emerald */
  --accent:#2BB985; --accent-hover:#3EC691; --accent-press:#4CD19C;
  --accent-wash:#DBFDEB; --accent-line:#B1F0D1;
  --accent-on:#123955; --accent-text:#036646;   /* 28 Aug 2026: was #00291A at ~16:1, which reads as black. Same hue and chroma, lightness lifted to ~7:1 — AAA, and visibly the accent. */
}
:root[data-theme="dark"][data-palette="emerald"]{
  --accent:#60D4A3; --accent-hover:#6FE3B1; --accent-press:#7DF1BD;
  --accent-wash:rgba(96,212,163,.13); --accent-line:rgba(96,212,163,.32);
  --accent-on:#123955; --accent-text:#6FD0A4;
}
:root[data-mode="sun"][data-palette="emerald"]{
  --accent:#00291A; --accent-hover:#00291A; --accent-press:#00291A;
  --accent-on:#FFFFFF; --accent-text:#00291A; --accent-line:#00291A;
}
:root[data-palette="teal"]{   /* Teal */
  --accent:#00B8A1; --accent-hover:#1DC5AE; --accent-press:#33D0B9;
  --accent-wash:#D8FDF4; --accent-line:#ABF0E1;
  --accent-on:#123955; --accent-text:#106457;   /* 28 Aug 2026: was #002922 at ~16:1, which reads as black. Same hue and chroma, lightness lifted to ~7:1 — AAA, and visibly the accent. */
}
:root[data-theme="dark"][data-palette="teal"]{
  --accent:#4FD3BD; --accent-hover:#60E2CC; --accent-press:#6FF0D9;
  --accent-wash:rgba(79,211,189,.13); --accent-line:rgba(79,211,189,.32);
  --accent-on:#123955; --accent-text:#64D0BC;
}
:root[data-mode="sun"][data-palette="teal"]{
  --accent:#002922; --accent-hover:#002922; --accent-press:#002922;
  --accent-on:#FFFFFF; --accent-text:#002922; --accent-line:#002922;
}

/* ── THE FOCUS RING ──────────────────────────────────────────────────────────
   th-components.css sets, on .input/.select/.textarea:focus-visible:
       outline: 2px solid var(--focus);  border-color: transparent;
   Where --focus is undefined the OUTLINE is dropped as invalid but the
   transparent border still applies, so tabbing into a text field REMOVES its
   border and draws nothing — an indicator worse than none.

   Measured 28 Aug 2026: th-components.css is linked by 83 pages, and only 9 of
   them also linked th-desk-retint.css, the single file that defined --focus.
   Seventy-four pages of forms had no visible focus on any text input.

   It resolves to --accent-text and NOT --accent. A focus ring is non-text UI and
   must clear 3:1 against what it sits on; Cyan Sky #00B8BB measures 2.30:1 on the
   light canvas and 2.45:1 on light surface, so the retired shim's own
   --focus: var(--accent) was failing on the eighteen pages that did carry it.
   --accent-text is 5.04:1 / 5.36:1 in light and 10.02:1 in dark, and it follows
   the skin and the theme, so this is one line rather than a table of exceptions.
   No new colour: this is Tier One, already ruled.                             */
:root{ --focus: var(--accent-text); }


/* ══════════════════════════════════════════════════════════════════════════
   THE GRID — one tile rhythm for the whole platform
   Added 31 Aug 2026. Measured before it existed: 95 different
   grid-template-columns declarations across 412 uses, 27 different tile
   widths from 84px to 480px, and 29 different gap values. Nothing lined up
   between two pages because there was nothing to line up TO — this sheet is
   linked by 168 pages and had no grid system at all, so every page invented
   one.

   auto-FILL, never auto-fit. auto-fit collapses the empty tracks and
   stretches whatever is left to fill the row, so five cards in a four-wide
   grid render as four normal tiles and one enormous one underneath. That is
   the single reason the tiles looked out of shape, and it was on 92 of the
   106 responsive grids.
   ══════════════════════════════════════════════════════════════════════════ */
:root{
  --tile-sm:132px;   /* meters, chips, small counters      */
  --tile:232px;      /* the standard card                  */
  --tile-lg:300px;   /* wide cards that carry a chart      */
  --gap:16px;        /* the one gap. 4px scale: 4 8 12 16 20 24 32 40 */
}
.th-grid{display:grid;gap:var(--gap);
         grid-template-columns:repeat(auto-fill,minmax(var(--tile),1fr))}
.th-grid--sm{--tile:var(--tile-sm)}
.th-grid--lg{--tile:var(--tile-lg)}
.th-grid--tight{--gap:8px}
.th-grid--roomy{--gap:24px}
/* fixed counts, for when the number of things is known and meaningful */
.th-grid--2{grid-template-columns:repeat(2,minmax(0,1fr))}
.th-grid--3{grid-template-columns:repeat(3,minmax(0,1fr))}
.th-grid--4{grid-template-columns:repeat(4,minmax(0,1fr))}
/* minmax(0,1fr) not 1fr: a long unbroken word in one cell otherwise widens
   that column and every row on the page stops aligning. */
@media (max-width:900px){
  .th-grid--3,.th-grid--4{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:560px){
  .th-grid,.th-grid--2,.th-grid--3,.th-grid--4{grid-template-columns:1fr}
}
