/* ═══════ Lotamyst flow layer ═══════
   Loaded by every page (flow.css + flow.js). Makes moving around the site feel
   fluid: cross-page transitions, a soft page fade-in, tactile button presses,
   and smooth in-page scrolling. Everything here is progressive enhancement and
   zero-specificity (:where), so page styles always win a conflict.
   Bump the ?v= query in the pages when this file changes (SW caches by URL). */

/* cross-document view transitions (Chromium): pages crossfade instead of
   flashing white. Ignored everywhere else. */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation-duration: 0.14s; }
::view-transition-new(root) { animation-duration: 0.14s; }

/* the root canvas, seen in the overscroll bounce and behind any page whose body
   is shorter than the viewport. It was #0e0f11 - a near-black that read as a
   grey seam against the true-black body once the palette changed. */
/* ⚠⚠ color-scheme was never declared, so every UA-painted surface resolved
   LIGHT: the OPEN dropdown list is a white popup in a black app, and scrollbars
   come out light. The selects themselves are readable only because they set
   background-color explicitly — the overlay had the same omission and its bare
   selects rendered white-on-white at 1.08:1.
   Safe to force dark: every site theme is dark (checked all thirteen). */
html { scroll-behavior: smooth; background: #000; color-scheme: dark; }

/* PPPPPPP EMOJI GO MONOCHROME PPPPPPP
   The site uses ~1,900 emoji as icons. On pure black, colour emoji are the
   loudest thing on every screen and the one place the palette breaks.
   font-variant-emoji:text requests the platform's TEXT (monochrome) glyph. It
   inherits, so it reaches emoji written by JS as well as emoji in the markup -
   which matters here, because 1,072 of them live inside <script> and only 82
   sit in a tag a class could target.
   � NOT filter:grayscale(). Measured: filter makes an element a containing
   block, and position:fixed descendants stop being viewport-fixed - it would
   break the nav, the modals and the cursor ring.
   � U+FE0F overrides this property, so those selectors were stripped from the
   markup (149 of them). Do not paste emoji back in with one attached.
   Browsers without support simply keep colour emoji - no layout change. */
body { font-variant-emoji: text; }
/* the escape hatch: anything whose MEANING is its colour */
.lm-colour, .lm-colour * { font-variant-emoji: emoji; }

/* ═══════════════ THE BUTTON ═══════════════
   604 button rules across 111 files, and 108 separate base definitions of
   .btn / .cta / button. The headline problem was not variety, it was
   CONTRADICTION: `.btn` meant `background: var(--panel2)` (a quiet grey fill)
   on 44 pages and `background: var(--red)` (the accent fill) on 44 others. The
   same class, opposite species, decided by whichever page you happened to be
   on. That is the thing that reads as "randomly mashed together".

   WHY THIS WINS WITHOUT !important OR EDITING 95 FILES
   137 of 145 pages link flow.css AFTER their own <style> block, so an equal
   specificity rule here already beats theirs on source order. The `html`
   prefix takes it to 0-1-1, which beats a page's `.btn` (0-1-0) even on the
   eight pages that carry a late <style>. It is deliberately NOT wrapped in
   :where() - that would drop it to 0-0-0 and lose to every page on the site.
   (This codebase has been bitten by :where() twice; see the notes further down.)

   TWO WEIGHTS, AND ONLY TWO
     .btn            hairline outline - the default, because most pages showing
                     a .btn are showing several and a wall of filled buttons is
                     just noise in a different key
     .btn.primary    solid white, black ink - the one action that matters
     .cta/.get/.addbot are marketing calls to action and take the filled weight

   Padding, font-size and font-weight are deliberately NOT set here. They vary
   for real reasons (a chip is not a hero button), pages have measured wrap
   points that depend on them, and colour+shape is what was actually broken. */
html .btn, html .cta, html a.get, html .addbot, html .auth {
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  text-decoration: none;
  box-shadow: none;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease, transform .12s ease;
}
html .btn:hover, html .cta:hover, html a.get:hover, html .addbot:hover {
  border-color: var(--edge);
  background: rgba(255,255,255,.06);
  color: var(--text);
  box-shadow: none;
}
html .btn:active, html .cta:active, html a.get:active { transform: translateY(1px); }
html .btn:focus-visible, html .cta:focus-visible, html a.get:focus-visible, html .addbot:focus-visible {
  outline: 2px solid var(--text); outline-offset: 3px;
}
/* ⚠ DELIBERATE: a disabled .btn.primary keeps its loud fill (the later .primary
   rule wins the background at equal specificity) and only dims — that is the
   standard treatment for a main action that isn't ready yet (Connections'
   Submit until four tiles are picked, a drift test until a pad connects).
   The transparent background below is for the QUIET default only; do not
   "fix" the source order to make it win over .primary. */
html .btn:disabled, html .btn[disabled], html .btn[aria-disabled="true"] {
  opacity: .45; cursor: not-allowed; background: transparent; border-color: var(--border);
}
/* the loud weight - one per screen is the intent */
/* .auth (sign in / the account chip) joins the family: it is the primary action
   for a signed-out visitor and was the last control still wearing an 8px corner */
html .btn.primary, html .cta, html a.get, html .addbot, html .auth {
  background: var(--text); color: var(--bg); border-color: var(--text);
}
html .btn.primary:hover, html .cta:hover, html a.get:hover, html .addbot:hover {
  background: var(--text2); border-color: var(--text2); color: var(--bg);
}
/* semantic buttons keep their meaning - a delete is still a delete. These are
   the ONLY coloured buttons the system allows, and they are state, not brand. */
html .btn.danger { border-color: var(--danger); color: var(--danger); }
html .btn.danger:hover { background: var(--danger); color: var(--bg); border-color: var(--danger); }
/* ── the PRESSED state, for toggle groups ──
   Pages used to signal "which mode is active" by giving the active button the
   loud fill and the rest the quiet one — a distinction the two-weight collapse
   erased, which silently blanked their selected states (pomodoro's mode row,
   crosshair-codes' filter, /app/'s two segment pairs). Selection is STATE, not
   a call to action, so it gets its own look instead of borrowing the primary's:
   a soft fill + strong edge reads as "down" beside the quiet default without
   competing with the page's one loud button. Wire it with aria-pressed — the
   attribute a screen reader needs anyway to know a toggle is a toggle.
   ⚠ At 0-2-1 this outranks the page-local .btn[aria-pressed] tints on the
   bench pages; under the mono tokens those were already this same white-tint
   family, and their box-shadow glows still apply on top. Checked, not assumed. */
html .btn[aria-pressed="true"] {
  background: rgba(255,255,255,.10);
  border-color: var(--text);
  color: var(--text);
}
html .btn[aria-pressed="true"]:hover {
  background: rgba(255,255,255,.14);
  border-color: var(--text);
  color: var(--text);
}

/* ═══════════════ THE CARD ═══════════════
   1,127 card-shaped rules across 139 files. Measured, these were already far
   healthier than the buttons: 956 of them (85%) declare the SAME edge,
   `1px solid var(--border)`, and 910 use one of three surface tokens. Colour
   was never the card problem.

   SHAPE was. Seventeen distinct corner radii: 10px (428 uses), 8px (269),
   12px (165), then 6, 5, 7, 9, 11, 14, 13… A 9px card beside a 10px card is
   not something anyone consciously notices, and that is exactly why it reads
   as sloppy rather than as a choice.

   So this normalises the RADIUS ONLY and leaves surfaces alone. The three
   background tokens are a real depth hierarchy (panel over panel2 over a
   recessed well), and flattening them to one surface would cost information
   that borders alone would have to carry back.

   ⚠ .btn is excluded - it is a pill, and it owns its shape above.
   ⚠ .sw / .swatch are excluded - those ARE colour samples (Minecraft and FiveM
     colour codes); rounding them is fine but they must never be repainted. */
html .panel, html .card, html .callout, html .result, html .results,
html .searchbox, html .stat, html .verdict, html .errbox, html .warnbox,
html .lm-modal, html .cnt, html .cbtn, html .fs-hint,
html .tool-panel, html .hublink, html .more-card, html .pop-card, html .svc {
  border-radius: 12px;
}
/* the pill family: anything that reads as a token rather than a container */
/* ⚠⚠ `.cat` is INLINE-ONLY here, and that qualifier is the whole point. The class
   is overloaded across the site: a <span> chip on /dnd/equipment/ and
   /dnd/magic-items/, but a full <div> CONTAINER on
   /discord/permissions-calculator/ — 715×962 in one case. A 999px radius on a box
   that tall curves into a huge arc across the content, which is exactly what it
   looked like. The page even asked for 12px; `html .cat` (0,1,1) simply out-ranked
   its own `.cat` (0,1,0).
   So this now matches only what the comment above always claimed to mean: tokens.
   A container keeps whatever radius its own page asked for. */
html .chip, html .tag, html .badge, html .pick,
html span.cat, html a.cat {
  border-radius: 999px;
}

/* ── the modifiers, folded onto the two weights ──
   A page modifier like `.btn.ghost` scores 0-2-0 and would out-rank the base
   rule above (0-1-1), so each one has to be named here or it keeps its old
   paint. Counted from the markup, not guessed: .ghost 190 uses / 38 files,
   .gold 16, .go 14, .primary 5, .btn-red 4, .btn-prem 2.

   .ghost is the interesting one. It meant "the quiet button" back when the
   default was a filled one - and quiet is now simply what a button IS, so it
   collapses into the base instead of becoming a third thing. */
html .btn.ghost, html .btn.solid, html .btn.steel, html .btn.mini,
html .btn.sm, html .btn.lower, html .btn.file {
  background: transparent; color: var(--text); border-color: var(--border); box-shadow: none;
}
html .btn.ghost:hover, html .btn.solid:hover, html .btn.steel:hover, html .btn.mini:hover,
html .btn.sm:hover, html .btn.lower:hover, html .btn.file:hover {
  background: rgba(255,255,255,.06); border-color: var(--edge); color: var(--text);
}
/* gold retired with the rest of the second brand colour: a premium CTA is an
   important action, so it takes the loud weight rather than a colour of its own.
   The 💎 glyph and the copy carry "premium" now. */
html .btn.gold, html .btn-gold, html .btn.prem, html .btn-prem, html .btn.go,
html .btn.primary, html .btn-red, html .btn.on, html .btn.od-on {
  background: var(--text); color: var(--bg); border-color: var(--text); box-shadow: none;
}
html .btn.gold:hover, html .btn-gold:hover, html .btn.prem:hover, html .btn-prem:hover,
html .btn.go:hover, html .btn.primary:hover, html .btn-red:hover,
html .btn.on:hover, html .btn.od-on:hover {
  background: var(--text2); border-color: var(--text2); color: var(--bg);
}

/* ── `.ghost` + a LOUD modifier on the same button: the contradiction, resolved ──
   ⚠ THIS FIXES A REPORTED BUG: the homepage's "⚒ Server Forge" button went
   unreadable on hover — measured black ink (rgb(0,0,0)) on a rgb(15,15,15)
   background, contrast 1.10.

   The markup says `class="btn ghost gold"`, which claims BOTH weights at once:
   .ghost means the quiet outline, .gold means the loud fill. Two separate rules
   then each won a different half of the hover state:
     · the PAGE's `.btn.ghost.gold:hover` (0-4-0) won `background` — a barely-there
       rgba(255,255,255,.06) — but never declared `color`;
     · so `color` fell through to `html .btn.gold:hover` (0-2-1) above, which is
       written for a FILLED button and therefore sets `color: var(--bg)` = black.
   Black ink on a transparent fill over a black page. Nothing threw, and it only
   appears while the pointer is down on it, which is why it shipped.

   ⚠ The general lesson, twice paid for now: a rule that wins the BACKGROUND of a
   state must also state the INK of that state. Never split the two across rules
   that different selectors can win.

   `.ghost` is the deciding class — it explicitly means "not filled" — so this
   restates the COMPLETE ghost state for the combination. At 0-3-1 (rest) and
   0-4-1 (hover) it outranks both contributors, so the seven buttons carrying both
   classes (homepage ⚒ Server Forge, coin-flip ×4, dice-roller ×2) render as one
   consistent thing instead of three page-specific accidents. */
html .btn.ghost.gold, html .btn.ghost.prem, html .btn.ghost.go,
html .btn.ghost.primary, html .btn.ghost.on {
  background: transparent; color: var(--text); border-color: var(--border); box-shadow: none;
}
html .btn.ghost.gold:hover, html .btn.ghost.prem:hover, html .btn.ghost.go:hover,
html .btn.ghost.primary:hover, html .btn.ghost.on:hover {
  background: rgba(255,255,255,.06); border-color: var(--edge); color: var(--text);
}

/* ═══════ THEME TOKENS (v16) ═══════
   The site's accent lives in five custom properties, and every page now spends
   them instead of hard-coding the brand red:
     --red      the accent          --red-mid  its pressed/filled shade
     --red-dim  its hairline shade  --glow     "r,g,b" of --red, for rgba() glows
     --glow-dim "r,g,b" of --red-mid
   Setting them on <html> retints the whole site, so a theme is DECLARATIVE:
   nothing has to run for a page to arrive already wearing it. That is the whole
   point of the pre-paint snippet each page carries in <head> — it stamps
   data-theme on <html> from localStorage BEFORE the first paint, these blocks
   colour it, and flow.js only has to agree afterwards. No flash of red.

   ⚠ SPECIFICITY IS THE MECHANISM, not a happy accident. Every page defines its
   own defaults in `:root` (0-1-0); `html[data-theme="x"]` is 0-1-1 and beats it
   regardless of source order, so this works even on the pages whose <style>
   block is parsed after flow.css. Do NOT wrap these in :where().

   ⚠⚠ THIS PARAGRAPH USED TO SAY "do NOT add a `carbon` block — carbon IS each
   page's own :root, which is why choosing it simply REMOVES the attribute."
   That stopped being true at the mono rebrand and the block four lines below
   contradicts it. It is corrected rather than deleted because it caused a real
   bug: /app/ was still written against it and removed data-theme for carbon,
   so picking Carbon there landed on MONO while lmTheme='carbon' was saved and
   turned every OTHER page red. MONO owns the no-attribute slot. Carbon is a
   named theme like any other.

   ⚠ INLINE STYLE BEATS ALL OF THIS. Nothing may write --red/--glow to
   element.style on <html> — that is exactly what /app/ used to do, and it made
   the app the one page the shared system could not reach. It sets data-theme
   now, like everywhere else. */
/* == CARBON: the site's original red, now an explicit CHOICE ==
   It used to be the absence of an attribute. Mono owns that slot now, so carbon
   needs somewhere to live or picking it would land on the monochrome default.
   These are the values the site actually shipped before 2026-07-27. */
html[data-theme="carbon"] {
  --bg: #0b0c0e; --bg-1: #0b0c0e; --bg-2: #141519;
  --panel: #101114; --panel2: #141519; --well: #0b0c0e; --card: #101114;
  --text: #e8e9ec; --text-dim: #bcc2cc; --text-mute: #949aa4;
  --muted: #8b8f96; --faint: #6f747c;
  --border: #26282c; --line: #26282c; --edge: #3a3d43;
  --red: #e5484d; --red-mid: #cf3e43; --red-dim: #7d2f33;
  --glow: 229,72,77; --glow-dim: 207,62,67; --on-red: #fff;
  --danger: #e5484d; --ok: #3dd68c; --warn: #e8a33d;
  --ok-rgb: 61,214,140; --danger-rgb: 229,72,77;
  --gold: #e5c07b; --gold-dim: #8a7346;
  --text2: #c9cbd1; --bg-deep: #0b0c0e; --border-hi: #34373d;
}
html[data-theme="cobalt"]    { --red: #4c8dff; --red-mid: #3c78e0; --red-dim: #28406b; --glow: 76,141,255;  --glow-dim: 60,120,224; }
html[data-theme="viridian"]  { --red: #3dd68c; --red-mid: #2fb875; --red-dim: #1f5c42; --glow: 61,214,140;  --glow-dim: 47,184,117; --on-red: #0e0f11; }
html[data-theme="violet"]    { --red: #9d6bff; --red-mid: #8657e0; --red-dim: #463366; --glow: 157,107,255; --glow-dim: 134,87,224; }
html[data-theme="amber"]     { --red: #e8a33d; --red-mid: #cf8e2e; --red-dim: #66491f; --glow: 232,163,61;  --glow-dim: 207,142,46; --on-red: #0e0f11; }
/* == MONO IS NOW THE DEFAULT ==
   :not([data-theme]) scores 0-1-1 and every page's own :root scores 0-1-0, so
   this wins on all 145 pages with no page edit and no flash  it is ordinary
   declarative CSS, applied at parse time. Picking any other theme swaps the
   attribute in and this simply stops matching.
   � Do NOT wrap this in :where() (it would drop to 0-0-0 and lose to every
   page) and do NOT let anything write these to <html>.style. */
/* ═══════ THE BASE SURFACE — every theme except carbon ═══════
   ⚠⚠ THIS BLOCK EXISTS TO FIX A SILENT, MEASURED BUG. Do not fold it back
   into the mono block below, and do not delete a page's own :root "because
   flow.css covers it" until you have re-read this.

   The ten accent themes (cobalt/viridian/violet/amber above; ember/aurum/
   glacier/synthwave/riptide/comet below) declare ONLY --red/--glow/--on-red.
   They were always designed to ride on each page's own :root for the
   SURFACES — and that worked, until the 2026-07-27 token sweep rewrote those
   page-level declarations into `--border: rgba(255,255,255,.12)`: a definition
   pointing at itself. A self-referential custom property is guaranteed-
   invalid, so under every accent theme --border/--gold/--warn resolved to
   NOTHING, and the three new token names (--text2/--bg-deep/--border-hi)
   were never declared for those themes at all.

   MEASURED in a browser, before and after: on cobalt, `border: 1px solid
   var(--border)` went invalid-at-computed-value-time and border-color fell
   back to its initial value — currentColor. Near-white hairlines, on 138 of
   the 146 pages, across 10 of the 12 themes. Nothing threw. mono and carbon
   (the two anyone actually tests in) were perfect, which is exactly why it
   shipped. Four of the ten are free themes; four more are supporter
   exclusives people paid for.

   ⚠ Specificity IS the mechanism: this selector scores 0-1-1 and beats a
   page's own `:root` at 0-1-0, so it repairs all 138 pages with ZERO page
   edits. Do not wrap it in :where(), and do not "simplify" it to plain
   `html` (0-0-1) — that loses to every page's :root and the bug returns.
   ⚠ It must declare NO accent token (--red/--red-mid/--red-dim/--glow/
   --glow-dim/--on-red). It sits AFTER the first four accent blocks, so at
   equal specificity it would win on source order and paint every accent
   theme white. Surface lives here; accent lives in the theme one-liners and
   in the mono block below. Keep that split. */
html:not([data-theme="carbon"]) {
  /* -- surfaces: black, and depth comes from borders rather than greys -- */
  --bg: #000000; --bg-1: #000000; --bg-2: #0a0a0a;
  --panel: #0a0a0a; --panel2: #101012; --well: #060607; --card: #0a0a0a;
  /* -- ink: ONE colour at five opacities. This is the whole trick. -- */
  --text: #ffffff;
  --text-dim: rgba(255,255,255,.84);
  --text-mute: rgba(255,255,255,.56);
  --muted: rgba(255,255,255,.56);
  /* .42 is moonshot's value and measures 3.95:1 on black - under the 4.5:1
     floor for normal text. .46 buys 4.52:1 and is indistinguishable by eye.
     This site has been bitten by exactly this before (--text-mute shipped at
     3.56:1 while carrying every card subtitle). */
  --faint: rgba(255,255,255,.46);
  /* -- the three greys the site had copy-pasted 745 times -- */
  --text2: rgba(255,255,255,.72);      /* between --text and --muted */
  --bg-deep: #050506;                  /* an inset well; on pure black, depth is a border's job */
  --border-hi: rgba(255,255,255,.28);  /* the hover brighten */
  /* -- edges -- */
  --border: rgba(255,255,255,.12);
  --line: rgba(255,255,255,.12);
  --edge: rgba(255,255,255,.42);
  /* -- status keeps colour, desaturated so it informs instead of decorating.
        Measured on #000: danger 6.6:1, ok 10.4:1, warn 9.9:1 -- */
  --danger: #e06c6c; --ok: #6ec28a; --warn: #d7a44a;
  /* -- the same two as RAW TRIPLETS, for rgba() tints.
        Needed because you cannot write rgba(var(--ok), .1) with a hex token. The five
        daily games each tint a win/lose result card, and before these existed the LOSE
        card spent rgba(var(--glow), …) — the ACCENT glow — so a loss rendered white on
        mono and, worse, GREEN on viridian: the exact same hue as the win card beside it. -- */
  --ok-rgb: 110,194,138; --danger-rgb: 224,108,108;
  /* -- gold retires: premium is marked by the diamond glyph and a brighter
        edge now, not by a second brand colour -- */
  --gold: rgba(255,255,255,.92); --gold-dim: rgba(255,255,255,.38);
}
/* Mono's SURFACE comes from the base block above. What is left here is only
   the ACCENT — which is the single thing that distinguishes mono from the
   ten accent themes. */
html:not([data-theme]), html[data-theme="mono"] {
  /* -- the accent is now simply white; --on-red is the ink that sits ON it -- */
  --red: #ffffff; --red-mid: rgba(255,255,255,.86); --red-dim: rgba(255,255,255,.24);
  --glow: 255,255,255; --glow-dim: 255,255,255; --on-red: #000000;
}
/* 💎 supporter exclusives */
html[data-theme="ember"]     { --red: #ff6a3d; --red-mid: #e5552e; --red-dim: #6b3421; --glow: 255,106,61;  --glow-dim: 229,85,46; }
html[data-theme="aurum"]     { --red: #f0c419; --red-mid: #d4ab12; --red-dim: #66540f; --glow: 240,196,25;  --glow-dim: 212,171,18; --on-red: #0e0f11; }
html[data-theme="glacier"]   { --red: #5fd4f5; --red-mid: #3fb9de; --red-dim: #1f5866; --glow: 95,212,245;  --glow-dim: 63,185,222; --on-red: #0e0f11; }
html[data-theme="synthwave"] { --red: #ff5fa2; --red-mid: #e0448a; --red-dim: #66284a; --glow: 255,95,162;  --glow-dim: 224,68,138; }
html[data-theme="riptide"]   { --red: #2ee6a8; --red-mid: #1fc98f; --red-dim: #166149; --glow: 46,230,168;  --glow-dim: 31,201,143; --on-red: #0e0f11; }
/* ☄ referral reward */
html[data-theme="comet"]     { --red: #8f7bff; --red-mid: #7a63e6; --red-dim: #3c3373; --glow: 143,123,255; --glow-dim: 122,99,230; }

/* ═══════ --on-red: the ink that goes ON an accent fill ═══════
   Six of the twelve accents are LIGHT. White text on them is not "a bit low
   contrast", it is gone: measured against #fff, mono is 1.44:1, aurum 1.66,
   riptide 1.61, glacier 1.72, viridian 1.88, amber 2.16 — where carbon, the
   baseline this site has always shipped, is 3.91. Those six therefore declare
   an ink of their own above; the other six inherit the #fff default, because
   flipping a 3.9:1 white label to dark ink would be a restyle, not a fix.
   The line is drawn at 2.6:1 — below carbon's baseline by enough that nobody
   would defend it. Dark ink on those six measures 8.9:1 (amber) to 14.6:1 (mono).

   ⚠ WHAT THIS CAN AND CANNOT REACH. Only the surfaces THIS FILE paints: the
   injected nav's CTA and the helper's send button. It is offered as a token so
   a page can opt in, but nothing is retro-fitted onto the pages, and that is
   the same call the note below has always made:

   ⚠ NOT DONE HERE, deliberately, and still not: any blanket "fix the contrast
   of filled buttons under the light themes" rule. `.btn` is
   `background: var(--panel2)` on 45 pages and `background: var(--red)` on 41 —
   a blanket `color: #0e0f11` would fix the second set and make the first set's
   label invisible, and CSS cannot tell them apart from here.

   The per-page sweep covered FLAT `background: var(--red)` fills: those now
   spend `var(--on-red, #fff)` at their own call site. That is the whole of what
   it covered — do not read it as "the contrast work is finished", because this
   token has two structural limits and a review found live failures at both:

     ⚠ IT IS THE INK FOR THE ACCENT, NOT FOR EVERY FILL. Spending it on a
       non-accent background inverts it. flow.js's premium-gate CTA paired it
       with a --gold pill and rendered white-on-white at 1.19:1 on five themes —
       worse than the defect this token exists to fix, in the shared layer, on
       the one dialog a visitor meets while switching themes. Gold is light on
       all twelve, so that CTA now carries a dark LITERAL. If another non-accent
       fill needs ink, give it its own token; do not reach for this one.

     ⚠ IT CANNOT SERVE A TWO-STOP GRADIENT whose ends invert across themes.
       A fill running to --red-dim (or to a hardcoded carbon-era literal like
       #8e3134) is dark at one end on every theme, so one ink is wrong at one
       end by construction — measured at ~1.9-2.6:1. The four sites that did
       this (the app's chat bubble and send button, the profile and embed-builder
       avatars) are now FLAT fills for exactly this reason. Flatten, or give the
       element an ink that matches its darkest stop; there is no third option.

   Two deliberate non-conversions remain, and neither is a miss — re-"fixing"
   either one is the regression:
     · offline.html — no <link> at all by design (it is the page for when the
       network is gone), so it carries its own permanent carbon `--red:#e5484d`.
       No stylesheet means no --on-red to resolve; white on it is the 3.91:1
       carbon baseline and correct. Dark ink there would break it.
     · the five DARK-accent themes that declare no --on-red — see the audit
       above; they inherit #fff from the base default on purpose.

   ⚠ ACCEPTED DEBT, recorded so it is a decision and not a silence: white on the
   six DARK accents does not clear AA for body text — ember 2.85, synthwave 2.83,
   cobalt 3.20, comet 3.26, violet 3.51, carbon 3.91. The first two miss even the
   3:1 large-text floor. This is the "line drawn at 2.6:1" above; if any of it is
   ever fixed, ember and synthwave are the two with no defence. */
html { --on-red: #fff; }

/* ── --danger: the red that must NEVER follow the accent ──
   Making the site themeable made SEMANTIC reds themeable too: an error, a wrong
   guess, a dead pixel, a failed check. Under Viridian a validation error read
   green; under Riptide a wrong answer looked like a correct one. 57 such rules
   across 52 pages now spend var(--danger,#e5484d) instead of the accent, so the
   meaning survives every theme. The inline fallback keeps them red even if this
   file never loads. Warnings keep their own --warn; this is failure only. */
html { --danger: #e5484d; }

/* ── --premium: the ONE surviving gold, and the only place it is allowed ──
   Gold retired site-wide: `--gold` now resolves to white, and premium is marked by
   the 💎 glyph and a brighter edge. That is right everywhere except one component —
   the free-vs-premium slider on /premium/, where the gold IS the demonstration. The
   free half is greyscale and dashed, the premium half is "lit"; take the colour out
   and the two halves stop differing, which is the entire point of the widget.

   ⚠ The page had been left HALF-retired and it showed: `var(--gold)` inside the
   widget resolved to white while ~34 raw rgba(229,192,123,…) literals stayed gold,
   so the premium side rendered as gold washes under white type. This token exists so
   the surviving gold is NAMED and can never drift from the retired one again.
   ⚠ Declared on `html` (0-0-1) rather than in the theme blocks ON PURPOSE — same
   reasoning as --danger above. "Premium" is a product fact, not a colour scheme, so
   it must not follow the accent: under Aurum it would vanish into a gold accent,
   under Mono it would disappear entirely.
   ⚠ Use it ONLY inside the .ba-* slider. Everywhere else on that page — seams, glows,
   pricing-card blooms, lock chips — is monochrome like the rest of the site. */
html { --premium: #e5c07b; --premium-dim: #8a7346; --premium-rgb: 229,192,123; }

/* ── --myst: the brand's own blue, and the ink that means "this is a reading" ──
   The law, already shipped in the overlay and the browser (lotamyst-browser/ui/
   tokens.css declares this same four): Myst marks a quantity being MEASURED —
   white ink draws the scale, Myst draws the reading, and if there is no number
   behind it, it does not get the colour. A state is not a measurement, pointer
   feedback is never Myst, and there is no glow, bloom or shadow spread anywhere
   near it: this is ink, not light.

   ⚠ Declared on `html` (0-0-1) and never inside a theme block — the same call as
   --danger and --premium above, for a stronger reason. Myst is the site
   identifying itself, not a colour scheme, so it must mean one thing under all
   twelve themes; scoped into a theme block it would simply be missing on the
   other eleven.
   ⚠ Not the glacier accent (#5fd4f5). Near neighbours, different jobs: glacier is
   a theme someone chose, Myst is the brand. Collapsing them loses the brand on
   the eleven themes that are not glacier.
   ⚠ …and on glacier ALONE they are not tellable apart — measured dE76 14.6, where
   the next-closest theme is 26.4 and the rest run 54-99. In the crown strip the
   Myst segments sit three lines above chips drawn in var(--red), so a glacier user
   sees one colour doing both jobs. Accepted rather than fixed: the law still holds
   everywhere else, and nudging the accent to protect one theme costs more than it
   buys. Do not write "visibly distinct" here — on glacier that is not true.
   ⚠ Do NOT move these up into `html:not([data-theme="carbon"])`. That block is
   0-1-1 and sits after the accent one-liners, and the warning on it explains what
   a token declared there does to every theme at once. Plain `html` carries --myst
   only because nothing at higher specificity declares it — which is NOT true of
   --danger above, whose floor the base block legitimately shadows. Keep it so.
   ⚠ A BACKGROUND spending this token passes the literal fallback,
   var(--myst,#9fdcee); a COLOUR deliberately does not, and the difference is not
   taste. The token lives in THIS file alone, so a client holding a stale cached
   flow.css resolves a bare var(--myst) to nothing and the declaration goes invalid
   at computed-value time. Measured both ways with the stylesheet detached: `color`
   then falls back to INHERITED ink, so the thing simply reads monochrome (the nav
   brand mark in index.html is built on exactly that, and adding a fallback there
   is the regression, not the fix) — while `background` falls back to TRANSPARENT
   and the painted thing disappears outright. Hence the crown bar in games-chain.js
   carries the literal and the wordmark does not.
   NOT retro-fitted: a surface opts in one at a time, same as --on-red. */
html { --myst: #9fdcee; --myst-mid: #7ec9de; --myst-line: rgba(159,220,238,.24); --on-myst: #001318; }

/* soft fade-in on load for browsers without view transitions — UNCONDITIONAL
   since batch 65c: gating it on prefers-reduced-motion hid it from most of the
   gaming audience (Windows' animation-effects perf toggle sets that flag), and
   without it, arriving from the homepage's ink-wipe ended in a hard flash.
   opacity only — a transform here would briefly re-anchor fixed elements */
body { animation: lmFlowIn 0.26s ease; }

/* tactile press: the `scale` property (not `transform`) so it never fights
   a page's own transform. :where() keeps specificity at zero. */
/* .lm-back is listed because the back control has two shapes — a <button> when
   it pops history, an <a> when it walks up to a named parent — and without it
   only the button half felt the press. */
:where(button, .btn, .chip, .ghost, [role="button"], input[type="submit"], .lm-back):active { scale: 0.97; }

@keyframes lmFlowIn { from { opacity: 0.3; } }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* ═══════ scroll reveals (v7) ═══════
   flow.js tags below-the-fold content blocks with .lm-rv and reveals them as
   you scroll to them. `translate` (not transform) so a page's own transforms
   are never fought; flow.js strips the classes after the reveal, so nothing
   lingers to disturb fixed/sticky descendants. Never applied without JS. */
.lm-rv { opacity: 0; translate: 0 22px; }
.lm-rv.lm-in {
  opacity: 1; translate: 0 0;
  transition: opacity 0.55s ease, translate 0.55s cubic-bezier(0.2, 0.6, 0.2, 1);
  transition-delay: var(--lmd, 0s);
}

/* ═══════ page-enter choreography (v8) ═══════
   flow.js tags the above-the-fold blocks with .lm-en on load and flips them to
   .lm-eni a frame later — the same rise the scroll reveals use, so a page's
   first paint has intent. Classes are stripped ~1.3s later. */
.lm-en { opacity: 0; translate: 0 12px; }
.lm-en.lm-eni {
  opacity: 1; translate: 0 0;
  transition: opacity 0.45s ease, translate 0.45s cubic-bezier(0.2, 0.6, 0.2, 1);
  transition-delay: var(--lmd, 0s);
}

/* ═══════ accessibility layer (v2) ═══════
   Keyboard users get a real focus indicator everywhere. :focus-visible only —
   mouse clicks stay clean. :where() keeps specificity at zero so any page that
   styles its own focus wins. */
:where(a, button, input, select, textarea, summary, [tabindex], [role="button"]):focus-visible {
  outline: 2px solid var(--red, #e5484d);
  outline-offset: 2px;
  border-radius: 4px;
}
/* selection matches the brand — the ACTIVE accent, not the literal red, or the
   one thing you look at while you drag stays carbon under every other theme */
::selection { background: rgba(var(--glow, 229, 72, 77), 0.35); color: #fff; }
/* honor Windows High Contrast / forced colors: never suppress outlines there */
@media (forced-colors: active) {
  :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible { outline: 2px solid CanvasText; }
}

/* ═══════ LAYOUT PRESETS (v16) ═══════
   flow.js sets data-layout on <html> (and the pre-paint snippet in each page's
   <head> sets it a beat earlier, so a preset is on before the first paint).
   Each preset RESTRUCTURES the page — where the nav lives, how wide content
   runs, where the floating chrome sits, the whole surface language. Classic =
   the page's own styles, so it has NO block here at all.

   ── THE THREE SHELLS ──
   Everything below has to land on three different page shapes, and a rule that
   only knows one of them is how this file ended up with two presets that did
   literally nothing on 140 of 141 pages:
     · app shell    <html data-shell="app"> — /app/ only: <aside id="rail"> +
                    <header> + #main, one scroll surface.
     · homepage     #nav (fixed) + <section>/.wrap + footer.
     · page shell   ~132 tool/hub pages: sticky <header> (whose <nav> flow.js
                    takes over → nav.lmn-took) + <main> + footer — plus the ~9
                    pages with no header at all, where flow.js injects #lm-nav.
   `data-shell="app"` is a STATIC attribute in app/index.html's <html> tag, not
   something JS stamps on, so `:not([data-shell="app"])` is true from byte one
   and no rule below can flash on the wrong shell.

   ── WHAT MOVES WHERE ──
   Structural moves that FIGHT the mobile collapse (rail↔bar, mirroring a bar
   that turns into a drawer) are desktop-only (min-width:1001px) — below that the
   site already collapses every rail to a top bar and arguing with it produces a
   worse phone than classic. Measure, type and surface language apply at every
   width, because they read the same on a phone.
   ⚠ "desktop-only" is a rule about FIGHTING the collapse, not a licence to skip
   the phone. Every preset must still be identifiable at 360px on every shell,
   and the way to check is not to read this file but to diff the computed values
   against Classic — three cells were provably identical to Classic before this
   revision (homepage/topbar, app/topbar, app/rightrail) and each is now closed
   by a rule that speaks the collapse's language instead of undoing it.

   ⚠ The floating corner chrome (#lm-lay-btn + #lm-lay-panel + .lm-back-fab, and
   the helper at #lm-ai-btn) is REPOSITIONED by three of the presets. Three rules
   for whoever edits it next:
     · the left stack and the right helper must never land in the same corner
       (rightrail moves both, in opposite directions);
     · on /app/ the layout BUTTON is docked inside the header (.lm-lay-inhead,
       position:static — offsets are ignored) and its panel is positioned from
       JS, so neither takes an offset from this file;
     · but the app shell is NOT exempt from the rest of it. #rail is fixed and
       full-height at z-index 55, so any corner the rail is standing in is a
       corner the floating chrome has to leave — which is why Right Rail moves
       .lm-back-fab left AND pushes #lm-ai-btn clear by --rail-w. */

/* ══ TOP BAR ══ horizontal emphasis: a wider measure, the nav reading as one
   full-width bar, and the floating chrome pulled up to the top edge. */

/* measure — every shell, every width (a cap can only ever be as wide as the
   viewport, so this is a no-op on a phone rather than a fight with it) */
/* ⚠ the auto margins are not redundant. The ~132 tool pages already say
   `main { margin: 0 auto }`, but the HOMEPAGE's <main> is a full-bleed wrapper
   with no margin at all — capping its width there without centring it pinned
   every section flush left while the footer's .wrap stayed centred, i.e. the
   page visibly split down the middle. */
/* ⚠ NO !important, and that is a measured decision, not an oversight. `:not()`
   carries the specificity of its ARGUMENT, so `main:not(#main)` is 1-0-1 and the
   whole selector is 1-1-2. Censused against the tree: every page in it styles
   its main as a bare `main {` (0-0-1), the single exception being /app/'s
   `main.show` (0-1-1) — which this selector excludes anyway. There is nothing
   left for an !important to win, and leaving one in means no page can ever opt
   out of a preset later without another !important. Same reasoning applies to
   the three rules below it. */
html[data-layout="topbar"] main:not(#main) { max-width: 1180px; margin-left: auto; margin-right: auto; }
/* the homepage's sections live INSIDE <main>, so the line above already widens
   them; this is for the .wrap that does not — the footer's — which would
   otherwise stay 1080 and visibly step in from the content above it. Same
   number on purpose: two containers, one edge. */
html[data-layout="topbar"] .wrap { max-width: 1180px; }

/* page shell — the header stops being a logo-and-links pair squeezed into the
   middle of the page and becomes a real bar: edge-to-edge padding, the site nav
   filling and centring the span between logo and edge.
   At every width on purpose. On a phone the measure caps above are dead letters
   (nothing is 1180px wide at 375) and the corner-stack move below is suppressed,
   so without this Top Bar would be a preset that does nothing at all on a phone
   — which is how two of these five ended up inert in the first place. The nav
   already wraps there (.lmn-took), so all this changes is where it wraps to. */
html[data-layout="topbar"]:not([data-shell="app"]) header,
html[data-layout="topbar"] #lm-nav {
  padding-left: clamp(1rem, 3.2vw, 2.4rem); padding-right: clamp(1rem, 3.2vw, 2.4rem);
}
html[data-layout="topbar"]:not([data-shell="app"]) header nav.lmn-took { flex: 1 1 auto; justify-content: center; }

/* homepage — the bar is SOLID from the first pixel instead of fading in on
   scroll, so the chrome reads as a fixture rather than an overlay.
   (1-1-1, so it beats the page's own #nav.solid at 1-1-0. It only supplies the
   surface: #nav.solid still owns the 62→54px height collapse on scroll, which
   is motion, not chrome, and Top Bar has no opinion about it.)
   ⚠ AT EVERY WIDTH, and that is a fix, not a widening for its own sake. This
   lived inside the 1001px block below, and the homepage's other three Top Bar
   rules — the two measure caps and the corner-stack move — are all dead letters
   at 360px (nothing is 1180px wide on a phone, and the corner move is
   desktop-only by design). MEASURED at 360×760: every single computed value on
   the homepage under Top Bar was byte-identical to Classic. The preset did
   NOTHING on the site's most-visited page on a phone. This is the one rule that
   reads the same at 360 as at 1400, so it is the one that comes out. */
html[data-layout="topbar"] #nav {
  background: rgba(11, 12, 14, 0.86); border-bottom-color: var(--border);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}

@media (min-width: 901px) {
  /* below 900 the injected bar's links become a dropdown with its own
     positioning — leave that alone */
  html[data-layout="topbar"] #lm-nav .lmn-links { margin-left: auto; margin-right: auto; }
}

@media (min-width: 1001px) {
  /* app shell — the rail becomes a horizontal bar across the top.
     ⚠ --rail-w is scoped to the app shell, not set on <html> globally: the
     floating chrome is anchored at `left: calc(var(--rail-w, 0px) + 14px)`, so
     a rail width declared on a page that has no rail shunts the picker sideways
     over nothing. (That is exactly what Focus's `--rail-w: 60px` did on all 140
     non-app pages — the button sat 60px in from the edge for no reason.) */
  html[data-layout="topbar"][data-shell="app"] { --rail-w: 0px; }
  html[data-layout="topbar"] #rail {
    position: sticky; top: 0; bottom: auto; width: 100%; height: auto;
    flex-direction: row; align-items: center;
    padding: 0.5rem 0.9rem; border-right: none; border-bottom: 1px solid var(--border);
  }
  html[data-layout="topbar"] #rail .logo { padding: 0 0.9rem 0 0.3rem; white-space: nowrap; }
  html[data-layout="topbar"] #rail nav { flex-direction: row; overflow-x: auto; overflow-y: hidden; gap: 2px; padding: 0; }
  html[data-layout="topbar"] #rail nav a { white-space: nowrap; padding: 0.5rem 0.7rem; }
  html[data-layout="topbar"] #rail nav a .ns { display: none; }
  html[data-layout="topbar"] #nav-ind, html[data-layout="topbar"] #rail-foot { display: none; }
  html[data-layout="topbar"][data-shell="app"] header { position: static; }

  /* homepage — the nav's own links centre in the bar (the SURFACE half of this
     moved out of the media query; see the note above it). Desktop-only: below
     1001px the homepage starts dropping links (.hide-m, then the CTA, then
     Contact), and centring a set that is losing members mid-collapse reads as
     drift, not as a bar. */
  html[data-layout="topbar"] #nav .links { margin-left: auto; margin-right: auto; }

  /* the corner stack becomes a ROW along the top edge, tucked under the bar.
     Geometry: the button is 40px at right:14px (so 14→54), the back pill is
     34px ending at right:62px — an 8px gap — and its top is nudged 3px to sit
     on the button's centre line. */
  html[data-layout="topbar"]:not([data-shell="app"]) #lm-lay-btn { top: 74px; bottom: auto; right: 14px; left: auto; }
  html[data-layout="topbar"]:not([data-shell="app"]) #lm-lay-panel { top: 122px; bottom: auto; right: 14px; left: auto; }
  html[data-layout="topbar"]:not([data-shell="app"]) .lm-back-fab { top: 77px; bottom: auto; right: 62px; left: auto; }
}

/* ══ RIGHT RAIL ══ the mirror image: every piece of page furniture — nav, back
   control, pickers, helper — swaps to the opposite side, content sits left. */

html[data-layout="rightrail"] main:not(#main) { margin-left: 0; margin-right: auto; }
html[data-layout="rightrail"] .wrap { margin-left: 0; margin-right: auto; }

@media (min-width: 1001px) {
  /* app shell — the rail crosses to the right edge */
  html[data-layout="rightrail"] #rail { left: auto; right: 0; border-right: none; border-left: 1px solid var(--border); }
  /* ⚠ [data-shell="app"] is load-bearing: unscoped, this reached the ~132 tool
     pages too, where --rail-w is undefined, so `margin-right: var(--rail-w)`
     was invalid at computed-value time and quietly reset their header's margin
     to 0 for no reason at all. */
  html[data-layout="rightrail"][data-shell="app"] header,
  html[data-layout="rightrail"][data-shell="app"] #main { margin-left: 0; margin-right: var(--rail-w); }
  /* ⚠ rgb(var(--lm-nav)), NOT var(--red). #nav-ind is the rail's sliding active
     pill and /app/ tints it per SECTION: navHue() mirrors the standing section's
     --bhue onto <html> as --lm-nav, and the rail's own rule spends it as
     `inset 2px 0 0 rgb(var(--lm-nav))`. Mirroring the inset to the other edge
     with a flat var(--red) silently threw that away — Right Rail was the one
     preset where the indicator stopped following the section and just sat there
     in the accent. Same edge swap, same colour source. */
  html[data-layout="rightrail"] #nav-ind { box-shadow: inset -2px 0 0 rgb(var(--lm-nav, var(--glow, 229, 72, 77))); }
  /* …which frees the bottom-LEFT corner there, so the app's chrome goes left */
  html[data-layout="rightrail"][data-shell="app"] #lm-lay-btn,
  html[data-layout="rightrail"][data-shell="app"] #lm-lay-panel,
  html[data-layout="rightrail"][data-shell="app"] .lm-back-fab { left: 14px; }
  /* ⚠ THE SECOND COLLISION, and the one the note further down missed. On the
     ~132 page shells the mirror is a clean SWAP because both corners hold
     floating chrome. On /app/ it is not: the rail itself has crossed to the
     right, it is 212px wide and full-height at z-index 55, and the 🤖 helper is
     still pinned at right:14px / bottom:14px at z-index 92 — so it parks ON TOP
     of the rail, over #rail-foot, and its 350px panel covers the site's
     navigation whenever it is open. MEASURED at 1400×900: button at x=1207,
     rail spanning x=1053→1265.
     The fix is not to send it left (that corner now holds .lm-back-fab, see
     above) but to keep it where it has always been RELATIVE TO THE CONTENT —
     the bottom-right of the reading column, which is exactly where it sits in
     Classic with the rail on the left. --rail-w is the app shell's own token,
     so this tracks the slim-rail width under Focus too if the two ever meet. */
  html[data-layout="rightrail"][data-shell="app"] #lm-ai-btn,
  html[data-layout="rightrail"][data-shell="app"] #lm-ai-panel { right: calc(var(--rail-w, 0px) + 14px); left: auto; }

  /* the two bars with mobile behaviour of their own — the homepage's #nav (which
     drops links at narrow widths) and the injected #lm-nav (which becomes a
     right-anchored dropdown under 900px) — mirror on desktop only. Flipping
     them on a phone would put the hamburger on the left and its menu on the
     right, which is not a mirror, just a mess. */
  html[data-layout="rightrail"] #lm-nav,
  html[data-layout="rightrail"] #nav { flex-direction: row-reverse; }
  html[data-layout="rightrail"] #lm-nav .lmn-links,
  html[data-layout="rightrail"] #nav .links { margin-left: 0; margin-right: auto; }
  html[data-layout="rightrail"] #lm-nav .lmn-links { justify-content: flex-start; }
}

/* the page-shell header mirrors at every width: it is a logo and a wrapping
   link set with no drawer to get out of step, so the flip reads correctly on a
   phone too. row-reverse walks the flex line backwards, which is why the
   nav's own `justify-content: flex-end` has to be flipped with it. */
html[data-layout="rightrail"]:not([data-shell="app"]) header { flex-direction: row-reverse; }
html[data-layout="rightrail"]:not([data-shell="app"]) header nav.lmn-took { justify-content: flex-start; }

/* ⚠ THE COLLISION. The bottom-right corner is not empty — the 🤖 helper has
   lived there since v11. So the mirror is a genuine SWAP: the layout/back stack
   takes the right corner and the helper takes the left one. Moving only the
   first would stack two floating controls on the same 44px.
   Deliberately OUTSIDE the desktop media query, unlike everything else in this
   preset: a corner is still a corner on a phone, and this is the one part of
   Right Rail that can be honoured at 375px without fighting the mobile top-bar
   the site already collapses to. Without it the preset is invisible there. */
html[data-layout="rightrail"]:not([data-shell="app"]) #lm-lay-btn,
html[data-layout="rightrail"]:not([data-shell="app"]) #lm-lay-panel,
html[data-layout="rightrail"]:not([data-shell="app"]) .lm-back-fab { left: auto; right: 14px; }
html[data-layout="rightrail"]:not([data-shell="app"]) #lm-ai-btn,
html[data-layout="rightrail"]:not([data-shell="app"]) #lm-ai-panel { right: auto; left: 14px; }

/* ══ THE APP SHELL ON A PHONE ══
   Below 1001px /app/ collapses its rail into a horizontal top bar and sets
   `header { position: static }` — and every structural rule the two free
   presets own is gated at min-width:1001px, so MEASURED at 360×760 both Top Bar
   and Right Rail came back byte-identical to Classic on /app/. Two of five
   presets doing nothing on the tool library's phone view is the same failure
   this whole section was written to close, one shell further in.
   Neither of these fights the collapse — they speak its language:
     · Top Bar's promise is an EDGE-TO-EDGE bar, and at this width the rail
       already IS the bar, so it takes the same edge padding the other two
       shells get from the rule near the top of this section. Nothing moves;
       the bar just stops being inset. (1-2-1 and 0-3-1, over the app's own
       `#rail` at 1-0-0 and `header` at 0-0-1.)
     · Right Rail's promise is MIRRORED FURNITURE, and a horizontal bar mirrors
       perfectly well: row-reverse puts the wordmark on the right and the
       section nav on the left, matching what the ~132 page shells already do at
       this width. #rail nav keeps its own overflow-x scroll — reversing a flex
       PARENT does not reverse a child's internal direction. */
@media (max-width: 1000px) {
  html[data-layout="topbar"][data-shell="app"] #rail,
  html[data-layout="topbar"][data-shell="app"] header {
    padding-left: clamp(1rem, 3.2vw, 2.4rem); padding-right: clamp(1rem, 3.2vw, 2.4rem);
  }
  html[data-layout="rightrail"][data-shell="app"] #rail,
  html[data-layout="rightrail"][data-shell="app"] header { flex-direction: row-reverse; }
}

/* ══ FOCUS ══ a reading mode: narrow measure, generous leading, decoration
   switched off and the secondary chrome stood down until you reach for it. */

/* ⚠ `width`, NOT `max-width`, and this is the whole reason Focus was wrong on
   nine pages. `max-width: 640px` REPLACES the page's own cap, so it does not
   narrow a page — it RESIZES it, in whichever direction 640 happens to lie. Nine
   pages in the tree (the daily-puzzle and arcade set: /wordle/, /blockle/,
   /connections/, /shotdle/, /steamdle/ + /answers/, /games/2048/, /games/pixle/,
   /games/snake/) ship `main { max-width: 560px }` on purpose — a deliberately
   narrow board column — and Focus, the READING mode, was making all nine 80px
   WIDER than Classic. Measured on /games/snake/: 560 → 640.
   Setting `width` instead leaves the page's own max-width standing, and a
   max-width always clamps a width regardless of which rule is more specific.
   So the cap becomes min(the page's own, 640) with no cascade games:
     main 900 → 640 · main 760 → 640 · main 560 → 560 · no cap at all → 640.
   box-sizing is restated because `width` counts padding where `max-width` did
   too only because all 141 pages already say `* { box-sizing: border-box }`
   (censused) — spelling it out means this rule does not silently depend on
   that staying true for the 142nd. */
html[data-layout="focus"] main:not(#main) {
  box-sizing: border-box; width: min(640px, 100%);
  margin-left: auto; margin-right: auto;
}
html[data-layout="focus"] .wrap { max-width: 640px; }   /* the footer's wrap — see Top Bar */
html[data-layout="focus"] main:not(#main) h1 { font-size: clamp(1.7rem, 4vw, 2.3rem) !important; }
html[data-layout="focus"] body { line-height: 1.78; }
html[data-layout="focus"] main:not(#main) :is(p, li, .lead, .intro, .faq) { line-height: 1.9; }
/* the page's own texture goes: nearly every page paints a dot grid or a scan
   pattern onto <body>, which is the loudest thing left once the measure narrows.
   Counted: 130 of the 141 do it as `body { background-image: … }`. */
html[data-layout="focus"] body { background-image: none !important; }
/* …and the HOMEPAGE does not. It paints its 44px grid on `body::before` — a
   fixed, masked, pointer-events:none overlay — so the rule above missed it
   entirely and Focus left the site's busiest page fully textured while every
   tool page went clean. It is the only body::before in the tree (checked), and
   it is decoration by construction. 0-1-3 over the page's 0-0-2, so no
   !important is needed to win it.
   ⚠ NOT extended to /app/'s #bg canvas, which looks like the same kind of
   scenery and is not: that one is painted by app-owned rAF that this file has
   no handle on, so hiding it would leave a loop compositing where nobody can
   see it — the exact waste flow.js's focusSync refcount exists to avoid. */
html[data-layout="focus"] body::before { display: none; }
/* nothing follows you down the page. ⚠ `relative`, NOT `static`: these headers
   carry an absolutely-positioned ::after hairline, and unsticking them with
   static would hand that pseudo-element to the initial containing block and
   fling the red rule across the viewport. */
html[data-layout="focus"]:not([data-shell="app"]) header { position: relative; }
html[data-layout="focus"] #lm-nav { opacity: 0.62; transition: opacity 0.18s; }
html[data-layout="focus"] #lm-nav:is(:hover, :focus-within) { opacity: 1; }
/* the floating chrome recedes to a hint and comes back the moment it is aimed
   at — including via the keyboard, which is why :focus-within is on the list */
html[data-layout="focus"] :is(#lm-lay-btn, #lm-ai-btn) { opacity: 0.3; transition: opacity 0.18s; }
html[data-layout="focus"] :is(#lm-lay-btn, #lm-ai-btn):is(:hover, :focus-visible, :focus-within) { opacity: 1; }
/* ⚠ the back control is split OUT of that pair, because it is the one of the
   three that is NAVIGATION. This file's own contract for it, forty lines down,
   is that it is "never suppressed" and "must stay legible" — and 0.3 on a
   graphite pill over a graphite page is not legible, it is a rumour. Somebody
   who narrowed the measure did not ask to lose the way back. 0.55 still reads
   as secondary beside full-strength body text, so it still recedes; it just
   never drops below readable. The two that ARE decoration keep the deeper 0.3.
   ⚠ a separate RULE, not another id in the list above: `:is()` takes the
   specificity of its most specific argument, so that selector weighs 1-1-1 on
   the strength of the two ids in it. A plain `html[…] .lm-back-fab` override is
   0-2-1 and would have been silently inert — the pill would have stayed at 0.3
   and this comment would have been a lie. */
html[data-layout="focus"] .lm-back-fab { opacity: 0.55; transition: opacity 0.18s; }
html[data-layout="focus"] .lm-back-fab:is(:hover, :focus-visible, :focus-within) { opacity: 1; }
html[data-layout="focus"] #lm-lay-panel.open { opacity: 1; }

@media (min-width: 1001px) {
  /* app shell — a slim icon-only rail, maximising the content area
     (--rail-w scoped to the app shell — see the note under Top Bar) */
  html[data-layout="focus"][data-shell="app"] { --rail-w: 60px; }
  html[data-layout="focus"] #rail .logo { font-size: 0; padding: 0 0 0.9rem; text-align: center; }
  html[data-layout="focus"] #rail .logo::before { content: "L."; font-size: 1.1rem; color: var(--red); font-weight: 800; }
  html[data-layout="focus"] #rail nav a { justify-content: center; font-size: 0; padding: 0.5rem 0; gap: 0; }
  html[data-layout="focus"] #rail nav a .ric { font-size: 1rem; }
  html[data-layout="focus"] #rail nav a .ns, html[data-layout="focus"] #rail-foot, html[data-layout="focus"] #nav-ind { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  html[data-layout="focus"] :is(#lm-nav, #lm-lay-btn, .lm-back-fab, #lm-ai-btn) { transition: none; }
}

/* ══ TERMINAL ══ a total aesthetic transform on every surface: monospace, sharp
   corners, uppercase headings and nav, accent hairlines. Extended here to the
   surfaces the 2026 redesigns added — the hub cards (.card + a family prefix:
   .m-card/.g-card/.t-card/.d-card), The Bench's checklist (.b-*), /app/'s
   catalogue chrome (.cat-*), the taken-over nav (.lmn-took) and the injected
   bar (#lm-nav) — because a preset that squares 2023's components and leaves
   2026's rounded is worse than one that squares nothing. */
html[data-layout="terminal"] body { font-family: 'Cascadia Mono', 'Cascadia Code', Consolas, 'Courier New', monospace !important; }
html[data-layout="terminal"] :is(.tool-panel,.card,.panel,.hublink,.searchbox,.btn,button,input,select,textarea,.chip,.rcard,.mcard,.acard,.now,.day,.stat,.nstat,.menu-item,.hp-pillar,.result,.results,.related a,.toolgrid a,.tool,#rail,#lm-lay-btn,#lm-lay-panel,.lm-back-fab) { border-radius: 0 !important; }
html[data-layout="terminal"] :is(.b-chk,.b-node,.b-go,.b-bucket,.b-odo,.g-card,.g-node,.m-card,.d-card,.d-chip,.t-card,.t-bucket,.cat-chip,.cat-search,.cat-searchrow input,.lm-th,#lm-ai-btn,#lm-ai-panel,#lm-ai-panel .aiq,#lm-ai-panel .ai-u,#lm-ai-panel .ai-a,#lm-nav .lmn-links,#lm-nav .lmn-links a,#lm-nav .lmn-burg,nav.lmn-took a.lmn-auth) { border-radius: 0 !important; }
html[data-layout="terminal"] :is(.tool-panel,.card,.panel,.hublink,.menu-item,.b-chk,.cat-chip) { border-color: var(--red-dim, #6b2f32) !important; }
html[data-layout="terminal"] :is(h1,h2,h3,.mi-title,.section-title,.logo) { text-transform: uppercase; letter-spacing: 0.04em; }
/* the nav is the one run of text on every single page, so it is where the mode
   announces itself. 0-3-2 with the html[] prefix, which is what it takes to
   beat the pages' own `header nav a` at 0-0-3. */
html[data-layout="terminal"] nav.lmn-took a,
html[data-layout="terminal"] #lm-nav .lmn-links a,
html[data-layout="terminal"] #nav .links a { text-transform: uppercase; letter-spacing: 0.06em; }
html[data-layout="terminal"] .crumbs { text-transform: uppercase; letter-spacing: 0.05em; }
html[data-layout="terminal"] #lm-nav .lmn-logo { text-transform: uppercase; letter-spacing: 0.08em; }
/* hairlines take the accent, so the mode is themed rather than always-red */
/* ⚠ `border-color`, not `border-right-color`: the rail does not always have a
   right border to tint. data-layout holds one value, so Terminal never composes
   with Top Bar or Right Rail — but it does compose with the WIDTH, and below
   1001px /app/'s own media query turns the rail into a horizontal bar with
   `border-right: none` and a 1px border-BOTTOM. Naming one physical side meant
   Terminal tinted a zero-width edge there and left the only hairline anyone can
   actually see in plain --border, on a phone, while every other surface on the
   page went accented. MEASURED at 360 before/after: bottom edge rgb(38,40,45)
   → rgb(107,47,50). The shorthand tints all four; the three with zero width
   paint nothing, so it costs exactly as much. */
html[data-layout="terminal"] #rail { border-color: var(--red-dim, #6b2f32); }
html[data-layout="terminal"] :is(header, #lm-nav, footer) { border-color: var(--red-dim, #6b2f32); }

/* ── the APPEARANCE picker (injected by flow.js on every page) ──
   One button, one panel, two groups: the 12 themes as a swatch grid and the 5
   layouts as rows. They used to be two unrelated controls — the themes lived
   only in /app/'s own modal and the layouts only in this floating panel — which
   meant "change how the site looks" had two different answers depending on
   which page you happened to be standing on. Now it has one, everywhere.
   (/app/ keeps its own richer theme modal on #theme-btn, and flow.js drops the
   theme group from this panel there rather than shipping two theme pickers
   6 pixels apart.) */
/* in-header variant: when the page has a theme button (the /app/ header), the
   layout button docks beside it and matches its styling */
#lm-lay-btn.lm-lay-inhead {
  position: static; width: auto; height: auto; border-radius: 8px;
  background: var(--panel2, #1c1d21); border: 1px solid var(--border, #26282d);
  color: var(--text, #e7e8ea); font-size: 0.8rem; padding: 0.3rem 0.5rem;
  display: inline-flex; align-items: center; justify-content: center;
}
#lm-lay-btn.lm-lay-inhead:hover { border-color: var(--red, #e5484d); }
#lm-lay-btn.lm-lay-inhead svg { width: 14px; height: 14px; display: block; }
#lm-lay-btn {
  position: fixed; left: calc(var(--rail-w, 0px) + 14px); bottom: 14px; z-index: 90; width: 40px; height: 40px; border-radius: 8px;
  background: var(--panel, #16171a); border: 1px solid var(--border, #26282d); color: var(--muted, #8b8f96);
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35); transition: color .15s, border-color .15s, transform .15s;
}
#lm-lay-btn:hover { color: var(--text, #e7e8ea); border-color: var(--red, #e5484d); transform: translateY(-1px); }
#lm-lay-btn svg { width: 19px; height: 19px; }
#lm-lay-panel {
  position: fixed; left: calc(var(--rail-w, 0px) + 14px); bottom: 62px; z-index: 91; width: 252px;
  background: var(--panel, #16171a); border: 1px solid var(--border, #26282d); border-radius: 12px;
  padding: 0.5rem; box-shadow: 0 16px 44px rgba(0, 0, 0, 0.55); display: none;
  font-family: 'Segoe UI', system-ui, sans-serif;
}
#lm-lay-panel.open { display: block; }
#lm-lay-panel .lh { font-size: 0.6rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted, #8b8f96); padding: 0.3rem 0.5rem 0.5rem; }
.lm-lay-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.5rem; border-radius: 8px; cursor: pointer; transition: background .12s; }
.lm-lay-row:hover { background: var(--panel2, #1c1d21); }
.lm-lay-row .n { font-size: 0.9rem; font-weight: 600; color: var(--text, #e7e8ea); }
.lm-lay-row .t { font-size: 0.7rem; color: var(--muted, #8b8f96); }
.lm-lay-row .rt { margin-left: auto; font-size: 0.8rem; color: var(--red, #e5484d); }
.lm-lay-row .rt.lock { filter: grayscale(1); opacity: 0.7; }
/* keyboard parity with the mouse: the rows are focusable, so they need the same
   affordance on focus that they get on hover */
.lm-lay-row:focus-visible { background: var(--panel2, #1c1d21); outline: 2px solid var(--red, #e5484d); outline-offset: -2px; }

/* ── the theme half of the panel ──
   Twelve accents in two rows of six. Colour is the whole content of the
   control, so each swatch carries its name in title + aria-label and the strip
   under the grid prints the current one — a swatch grid with no words is a
   memory test for anyone who cannot separate violet from comet at 20px. */
.lm-th-grid {
  display: grid; grid-template-columns: repeat(6, 1fr); gap: 0.3rem;
  padding: 0 0.5rem 0.15rem;
}
.lm-th {
  position: relative; width: 100%; aspect-ratio: 1 / 1; padding: 0; cursor: pointer;
  border-radius: 8px; border: 1px solid var(--border, #26282d);
  background: var(--panel2, #1c1d21); transition: transform .12s, border-color .12s;
}
/* the accent itself is an inner block, not the button's background: it keeps a
   1px graphite margin so two adjacent swatches never bleed into each other */
.lm-th i { position: absolute; inset: 3px; border-radius: 4px; background: var(--sw, #e5484d); display: block; }
.lm-th:hover { transform: translateY(-1px); border-color: var(--sw, #e5484d); }
.lm-th[aria-checked="true"] { border-color: var(--sw, #e5484d); box-shadow: 0 0 0 2px var(--sw, #e5484d); }
.lm-th[aria-checked="true"] i { inset: 4px; }
/* locked, not hidden: you can see exactly what you'd be unlocking, but the
   swatch is dimmed AND badged. Dimming alone reads as "broken", not "locked". */
.lm-th.lock i { opacity: 0.3; }
.lm-th.lock::after {
  content: "🔒"; position: absolute; inset: 0; font-size: 0.7rem; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.75);
}
.lm-th-now {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.5rem 0.5rem 0.15rem; font-size: 0.7rem; color: var(--muted, #8b8f96);
}
.lm-th-now b { color: var(--text, #e7e8ea); font-weight: 600; font-size: 0.8rem; }
/* the two groups are one control, not two — a hairline says "same panel,
   next thing", where a gap would say "two panels that happen to touch" */
#lm-lay-panel .lh.lm-lay-h { margin-top: 0.3rem; border-top: 1px solid var(--border, #26282d); padding-top: 0.5rem; }
@media (prefers-reduced-motion: reduce) {
  .lm-th, .lm-lay-row { transition: none; }
  .lm-th:hover { transform: none; }
}

/* ═══════ the back control (v1) — injected by flow.js ═══════
   Two shapes, one control: inline at the head of a page's breadcrumb line where
   there is one, otherwise a floating pill in the bottom-left corner.
   This is NAVIGATION, not decoration — it is never suppressed by ?fx=0 or by
   reduced motion (only its transition is), and it must stay legible if a page's
   own CSS wins everything else.
   ⚠ the one place in this file that deliberately does NOT use :where(): the
   pages style `.crumbs a` muted at 0-1-1, so a zero-specificity rule here would
   lose and the control would paint as just another crumb. `.crumbs .lm-back`
   (0-2-0) wins on class count regardless of source order. */
/* ═══════ THE COPY-PASTED FOUR (v19) ═══════
   These are the components the site had duplicated into every page instead of
   owning once. Counted across the tree, excluding the generated answer pages:
     .faq details   134 declarations / 3 variants — more files than declare .card
     .faq summary   134 / 3
     .faq p         125 / 2
     .hint           90 / 8
     .sub            11 / 4
   The variants are drift, not intent: the FAQ row differs only in horizontal
   padding (14 pages) or radius (3), and .hint only in margin-top. Values below
   are the plurality in every case.

   ⚠⚠ TWO SPECIFICITIES HERE, AND THE SPLIT IS MEASURED, NOT AESTHETIC.
   The migration ran in two steps: land the rules (must change nothing), then
   delete the page copies (must change only the known variants). The first cut
   wrote ALL of these as :where() so step one would be a no-op — and step two
   then moved 210 declarations instead of the 44 that were meant to.
   THE REASON: :where() is 0-0-0, and `.hint` and `.faq p` are usually a <p>.
   With the page copy deleted, that page's own plain `p { }` rule — 0-0-1 —
   outranked the shared rule and took the paragraph over. .hint lost its size and
   ink on 72 pages; .faq p lost its margin on 122. Invisible in the source and
   obvious in a computed-style diff.
   So the four that can collide with an element selector carry their ORIGINAL
   specificity (0-1-0 / 0-1-1) — exactly what the pages had, so nothing is left
   for a bare `p` to win.
   ⚠ .faq details STAYS :where(), and that is also deliberate: /forge/, /overlay/
   and /premium/ — all three product pages and nothing else — use a 12px radius
   rather than 8px. That is a consistent choice, not drift, and zero specificity
   is what lets those three keep it.
   ⚠ Deleting a page copy is safe. Deleting a page's EXTRA rules is not: 9 pages
   add a second `.faq summary` inside a media query for touch padding, and 3 add
   `.faq p { color: var(--text2) }`. Those are separate rules and stay. */
:where(.faq details) {
  border: 1px solid var(--border, rgba(255,255,255,.12));
  border-radius: 8px;
  background: var(--panel, #0a0a0a);
  padding: 0.9rem 1.1rem;
  margin-bottom: 0.5rem;
}
.faq summary { cursor: pointer; font-weight: 600; font-size: 0.9rem; color: var(--text, #e7e8ea); }
.faq p { margin: 0.5rem 0 0; }
.hint { font-size: 0.8rem; color: var(--muted, #8b8f96); margin-top: 0.7rem; }
.sub { text-align: center; color: var(--muted, #8b8f96); font-size: 0.8rem; margin: 0.3rem 0 1.3rem; }

/* ── the eyebrow: NAMED, but deliberately NOT migrated ──
   The small uppercase label above a section heading is the site's most common
   typographic gesture after body copy — 281 rules — and it has never had a name.
   At 0.7rem it ships with six different trackings: 0.06em x126, 0.05em x30,
   0.04em x23, 0.08em x9, 0.12em x6, 0.14em x6.
   ⚠ THIS IS PUBLISHED FOR NEW CODE AND THE 281 EXISTING USES ARE LEFT ALONE, on
   purpose. Migrating them means editing MARKUP on ~100 files to swap a local
   class for this one — a large, risky diff whose entire payoff is a 0.02em
   tracking change nobody can see. Naming it stops the drift growing; sweeping it
   would be motion mistaken for progress. 0.06em is the plurality and matches the
   terminal-layout rule already in this file. */
:where(.eyebrow) {
  font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted, #8b8f96); font-weight: 600;
}

/* ── base body type ──
   Set 201 times across the tree and never once here. 126 pages say 1rem/1.65,
   17 say 1rem/1.6, and 11 say nothing at all — those eleven are the reason this
   exists.
   ⚠⚠ :where(body), NOT bare `body`. A bare selector scores 0-0-1 — the SAME as
   every page's own `body {}` — and flow.css is linked AFTER their <style>, so it
   won every tie on source order and quietly pushed 17 pages from 1.6 line-height
   to 1.65. That was measured across 156 pages and is the only reason it was
   caught; nothing about it is visible on the page you happen to have open.
   Zero specificity is what makes this a floor instead of a change. */
:where(body) { font-size: 1rem; line-height: 1.65; }

/* ═══════ the breadcrumb trail, as a DEFAULT (v19) ═══════
   130 pages define `.crumbs` in their own <style> — 298 declarations across four
   near-identical variants — and flow.css never had a base for it. That was fine
   until 19 pages turned out to DESCRIBE a BreadcrumbList in their JSON-LD and
   render nothing at all: all six daily games, all six answer archives, /premium/,
   /steam/next-sale/ and four /games/ pages. Their trail is drawn now, generated
   from that same schema so the two cannot disagree — and they carry no local
   .crumbs CSS, so this is what styles it.

   ⚠⚠ :where(), AND THAT IS DELIBERATE — the opposite call to `.crumbs .lm-back`
   twelve lines below, which spells itself out at 0-2-0 precisely to WIN. This one
   must LOSE. A base rule that outranked the 130 page copies would silently
   restyle every breadcrumb on the site (44 of them use .78rem/1.6rem, not
   0.8rem/1.3rem) to fix 19 pages that have no rule at all. Zero specificity means
   it applies only where nothing else does, which is exactly the 19.
   Values are the 238-declaration majority. */
:where(.crumbs) { font-size: 0.8rem; color: var(--muted, #8b8f96); margin-bottom: 1.3rem; }
:where(.crumbs a) { color: var(--muted, #8b8f96); text-decoration: none; }
:where(.crumbs a:hover) { color: var(--text, #e7e8ea); }

.lm-back {
  font: inherit; line-height: inherit; letter-spacing: inherit;
  cursor: pointer; text-decoration: none;
  /* the <button> shape must not carry any UA control chrome into a crumb line */
  appearance: none; -webkit-appearance: none;
  white-space: nowrap; /* "← The Bench" broken across two lines is not a label */
}
/* inline variant: rides the crumb line at its own font size, so inserting it
   cannot change that line's height — only its own width, claimed before the
   first paint (flow.js is deferred and builds this synchronously). */
.crumbs .lm-back {
  display: inline-block; vertical-align: baseline;
  background: none; border: 0; border-right: 1px solid var(--border, #26282d);
  color: var(--text, #e7e8ea); font-weight: 600;
  padding: 0 0.5rem 0 0; margin: 0 0.5rem 0 0;
  transition: color 0.15s;
}
.crumbs .lm-back:hover { color: var(--red, #e5484d); }

/* floating variant: sits directly above the layout picker (#lm-lay-btn, 40px
   tall at bottom 14px) and shares its language so the corner reads as one set.
   z-index stays UNDER the layout panel (91) on purpose — that panel opens from
   bottom: 62px, the same edge this pill sits on, and letting the opaque panel
   cover it beats having the two fight for the same 34px. */
.lm-back-fab {
  position: fixed; left: calc(var(--rail-w, 0px) + 14px); bottom: 62px; z-index: 89;
  display: inline-flex; align-items: center; height: 34px; padding: 0 0.9rem;
  border-radius: 8px; font-family: 'Segoe UI', system-ui, sans-serif; font-size: 0.8rem; font-weight: 600;
  background: var(--panel, #16171a); border: 1px solid var(--border, #26282d); color: var(--muted, #8b8f96);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
  transition: color 0.15s, border-color 0.15s, transform 0.15s;
}
.lm-back-fab:hover { color: var(--text, #e7e8ea); border-color: var(--red, #e5484d); transform: translateY(-1px); }
/* /app/ docks the layout button into its own header beside #theme-btn, so the
   bottom-left corner is empty there and the pill was floating 48px up over
   nothing.
   ⚠ this was `html:has(#lm-lay-btn.lm-lay-inhead)`, which was true only AFTER
   flow.js mounted the button — the pill visibly hopped 48px on every /app/
   load, and browsers without :has() never got the fix at all. The shell is
   declared in app/index.html's <html> tag now, so it is true at first paint and
   everywhere. */
html[data-shell="app"] .lm-back-fab { bottom: 14px; }

.lm-back:focus-visible { outline: 2px solid var(--red, #e5484d); outline-offset: 2px; }
/* the ring is rounded ONLY on the inline variant, which is a bare text run with
   no radius of its own. ⚠ a blanket `border-radius: 4px` here is 0-2-0 and beat
   .lm-back-fab's 0-1-0 — the pill visibly un-rounded itself the moment it took
   keyboard focus, i.e. exactly when someone was looking at it. */
.crumbs .lm-back:focus-visible { border-radius: 4px; }

/* standing down for full-screen test patterns (see flow.js): visibility, not
   display — the crumb line keeps its width, so nothing reflows either way. */
.lm-back.lm-back-off { visibility: hidden; }

/* the control is NAVIGATION, so reduced motion only strips its motion — it is
   never hidden. ⚠ `.crumbs .lm-back` is spelled out because the inline variant's
   own transition is declared at 0-2-0 above; a lone `.lm-back` here (0-1-0)
   lost to it and left that variant animating. */
@media (prefers-reduced-motion: reduce) {
  .lm-back, .crumbs .lm-back { transition: none; }
  .lm-back-fab:hover { transform: none; }
  :where(.lm-back):active { scale: 1; }
}

/* ═══════ 🤖 the Lotamyst helper (v11) — injected by flow.js ═══════ */
#lm-ai-btn {
  position: fixed; right: 14px; bottom: 14px; z-index: 92; width: 44px; height: 44px;
  border-radius: 50%; border: 1px solid var(--border, #26282d); background: var(--panel, #16171a);
  font-size: 1.2rem; cursor: pointer; line-height: 1;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
}
#lm-ai-btn:hover { transform: translateY(-2px); border-color: var(--red, #e5484d); box-shadow: 0 8px 26px rgba(var(--glow, 229, 72, 77), 0.25); }
#lm-ai-panel {
  position: fixed; right: 14px; bottom: 66px; z-index: 93; width: min(350px, calc(100vw - 28px));
  display: none; flex-direction: column; overflow: hidden;
  background: var(--panel, #16171a); border: 1px solid var(--border, #26282d); border-radius: 12px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55); font-family: 'Segoe UI', system-ui, sans-serif;
}
#lm-ai-panel.open { display: flex; }
#lm-ai-panel .aih {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.5rem 0.9rem; border-bottom: 1px solid var(--border, #26282d);
  font-size: 0.8rem; font-weight: 700; color: var(--text, #e7e8ea);
}
#lm-ai-panel .aix { background: none; border: none; color: var(--muted, #8b8f96); font-size: 0.9rem; cursor: pointer; padding: 0.1rem 0.3rem; }
#lm-ai-panel .aix:hover { color: var(--text, #e7e8ea); }
#lm-ai-panel .aim { max-height: 320px; min-height: 140px; overflow-y: auto; padding: 0.7rem 0.9rem; display: flex; flex-direction: column; gap: 0.5rem; }
#lm-ai-panel .aiw { color: var(--muted, #8b8f96); font-size: 0.8rem; margin-bottom: 0.3rem; }
#lm-ai-panel .aiq {
  align-self: flex-start; background: transparent; border: 1px dashed var(--border, #26282d); color: var(--text, #e7e8ea);
  border-radius: 999px; padding: 0.3rem 0.7rem; font-size: 0.7rem; cursor: pointer; font-family: inherit;
  transition: border-color 0.15s;
}
#lm-ai-panel .aiq:hover { border-color: var(--red, #e5484d); }
#lm-ai-panel .ai-u, #lm-ai-panel .ai-a {
  font-size: 0.8rem; line-height: 1.5; padding: 0.5rem 0.7rem; border-radius: 8px; max-width: 88%;
  overflow-wrap: break-word;
}
#lm-ai-panel .ai-u { align-self: flex-end; background: rgba(var(--glow, 229, 72, 77), 0.14); color: var(--text, #e7e8ea); }
#lm-ai-panel .ai-a { align-self: flex-start; background: var(--panel2, #1c1d21); color: #c9cbd1; border: 1px solid var(--border, #26282d); }
#lm-ai-panel .ai-a a { color: var(--red, #e5484d); }
#lm-ai-panel .ai-dots { color: var(--muted, #8b8f96); letter-spacing: 0.2em; }
#lm-ai-panel .aif { display: flex; gap: 0.5rem; padding: 0.5rem 0.9rem 0.3rem; }
#lm-ai-panel .aif input {
  flex: 1; background: var(--bg, #0e0f11); border: 1px solid var(--border, #26282d); border-radius: 8px;
  color: var(--text, #e7e8ea); font-family: inherit; font-size: 0.8rem; padding: 0.5rem 0.7rem; outline: none;
}
#lm-ai-panel .aif input:focus { border-color: var(--red, #e5484d); }
/* ⚠ var(--on-red), not #fff — under mono this button is a near-white fill and a
   white ➤ on it is invisible. See the --on-red block at the top of this file. */
#lm-ai-panel .aif button {
  background: var(--red, #e5484d); border: none; border-radius: 8px; color: var(--on-red, #fff);
  font-size: 0.8rem; padding: 0 0.7rem; cursor: pointer;
}
#lm-ai-panel .ain { padding: 0 0.9rem 0.5rem; font-size: 0.7rem; color: var(--muted, #8b8f96); }
#lm-ai-panel .ain a { color: var(--muted, #8b8f96); }
@media (max-width: 500px) { #lm-ai-panel .aim { max-height: 46vh; } }

/* ═══════ taken-over page nav (v14) ═══════
   flow.js swaps the four contextual links in a page's own <header><nav> for the
   site set, so the same nav rides every page. The page keeps styling its own
   `header nav a`, which is the point — this only fixes the one thing that
   changes: a header built for four links now holds eight.
   ⚠ flex-wrap, not overflow scroll. These headers are `position: sticky` and a
   horizontally scrolling sticky bar is a trap on touch — you swipe to scroll the
   page and move the nav instead. Wrapping grows the header by one line at narrow
   widths, which is honest and costs nothing. */
/* ⚠ the second place in this file that deliberately does NOT use :where().
   Zero specificity loses here: the pages set `header nav { gap: 1.1rem }` (0-0-2)
   and `header nav a { font-size: 0.8rem }` (0-0-3), so a :where()-wrapped rule is
   silently inert — the nav wrapped but every tightening below did nothing. The
   class adds the weight needed to win, and ONLY on a nav flow.js took over. */
/* ⚠⚠ `display: flex` IS DECLARED HERE, NOT ASSUMED — and leaving it out broke six
   pages on every phone. This rule set flex-wrap, row-gap, column-gap and
   justify-content, all of which do NOTHING on a block box. Most pages happened to
   make the nav flex themselves; the six daily games did not, so there the eight
   links laid out inline on ONE row, each carrying the page's own
   `header nav a { margin-left: 0.9rem }`, totalling 625px inside a 374px viewport.
   The nav is also a flex ITEM of a `display:flex` header, where the default
   `min-width:auto` refuses to shrink below that content — so the whole document
   went 135px wide and every daily game scrolled sideways on a phone.
   MEASURED: /wordle/ nav display:block, 1 row, 382px wide. /tools/dice-roller/
   nav display:flex, 2 rows, 213px. Same markup, same stylesheet.
   ⭐ A rule that styles a layout must OWN the display mode it depends on.
   `min-width: 0` lets it shrink; without it a flex item is floored at its content. */
header nav.lmn-took, [role="banner"] nav.lmn-took {
  display: flex;
  min-width: 0;
  /* the site row is 62 tall wherever it is. On a one-row header this equals the
     header's own min-height and changes nothing; on /overlay/ and /forge/, whose
     header carries a second nav below, it stops the site row hugging the top
     edge (measured: Tools at y=9 there against y=20 everywhere else). */
  min-height: 61px;   /* 62 minus the header's 1px hairline, which its border-box 62 already contains; at 62 the row pushed every header to 63 */
  align-items: center;
  justify-content: flex-end;
}
/* The links now sit in a wrapper so they have a box to become a dropdown in
   (see flow.js for why a wrapper and not the <nav> itself). Above the phone
   breakpoint this reproduces exactly what the <nav> used to do on its own —
   same wrap, same gaps, same right alignment — so nothing moves on desktop. */
/* ⚠ DOUBLE SPACING ON 17 PAGES. The six daily games, their six answer archives
   and five /games/ pages write `header nav a { … margin-left: 0.9rem }` in their
   own <style>. .lmn-links below already supplies a 0.9rem column-gap, so after
   the takeover those links carry BOTH — 1.8rem between every item, which is what
   puts their bar on two rows from roughly 650px to 765px while every other page
   still fits on one.
   ⚠ ZEROED HERE, NOT DELETED FROM THE 17 PAGES. That margin is the only thing
   separating their links when JavaScript is off and the page's own contextual
   nav is what renders. Scoped to `.lmn-took` — bars the script actually took
   over — so the no-JS spelling is untouched. */
header nav.lmn-took a, [role="banner"] nav.lmn-took a { margin-left: 0; }
header nav.lmn-took .lmn-links, [role="banner"] nav.lmn-took .lmn-links {
  display: flex;
  min-width: 0;
  flex-wrap: wrap;
  align-items: center;
  row-gap: 0.3rem;
  column-gap: 1.3rem;   /* the homepage's .links gap */
  justify-content: flex-end;
  margin-left: auto;
}
/* ⭐⭐ THE TWO BARS USED TO DO OPPOSITE THINGS. This one dimmed the current item
   to opacity 0.55 — making "where you are" the FAINTEST word in the bar on 144
   pages — while #lm-nav below brightened it to full ink on the other 9. Same
   site, same attribute, inverted meaning.
   Brighten, never dim. A dimmed item reads as DISABLED, which is the one thing
   the current page is not: it is the thing you are looking at.
   ⚠ Both states are matched. flow.js now marks "page" when you ARE the
   destination and "true" when you are INSIDE it, so leaf pages finally mark
   their section — and to a visitor those two want the same paint. */
nav.lmn-took a[aria-current], #lm-nav .lmn-links a[aria-current] {
  color: var(--text, #e7e8ea); opacity: 1;
}
nav.lmn-took a.lmn-auth { font-weight: 650; color: var(--text, #e7e8ea); }
nav.lmn-took .lmn-burg { display: none; }

/* ═══════ THE BAR IS ONE COLOUR EVERYWHERE (v21) ═══════
   Measured live at 1600px before this existed:
     /          #nav          rgb(154,147,138) = --muted
     /bench/    nav.lmn-took  rgb(159,220,238) = --red
     /app/      #site-nav     rgb(159,220,238) = --red
   --red is #9fdcee site-wide (the accent was repointed to Myst), so 148 stamped
   pages and /app/ were painting NAVIGATION in the brand accent.
   ⚠⚠ THAT IS THE ONE THING MYST MAY NOT DO. The law, in all three repos:
   "Myst marks a quantity being measured. White ink draws the scale; Myst draws
   the reading. If no number is behind it, it does not get the colour." A nav
   link is a destination. No number, no colour.
   ⚠ IT WAS NEVER AUTHORED — it is a specificity accident, which is why nothing
   ever flagged it:
       html[data-lm-ctl] a  (0,1,2)  color: var(--red)    the controls layer
       header nav a         (0,0,3)  color: var(--muted)  each page's own CSS
   The controls layer wins, so it captured every bar that had no id-bearing rule
   of its own. #nav and #lm-nav both score (1,1,1) and were never affected —
   which is exactly why the homepage looked right and the other two did not.
   ⚠ THE :not() CHAIN IS LOAD-BEARING, not tidiness. This selector outranks
   `nav.lmn-took a.lmn-auth` and `nav.lmn-took a[aria-current]`, both (0,1,2).
   Without the exclusions it would paint the account chip muted grey and erase
   the you-are-here marker. They are excluded by name so this never matches them,
   rather than raising those two rules and starting a specificity race.
   ⚠ #site-nav is /app/'s own bar. flow.js empties and refills it, but the
   container keeps ITS id, so `#lm-nav .lmn-links a` never reached it. */
html nav.lmn-took .lmn-links a:not(.lmn-auth):not(.lmn-cta):not([aria-current]),
html #site-nav a:not(.lmn-auth):not(.lmn-cta):not([aria-current]) {
  color: var(--muted, #9a938a);
}
html nav.lmn-took .lmn-links a:not(.lmn-auth):not(.lmn-cta):not([aria-current]):hover,
html #site-nav a:not(.lmn-auth):not(.lmn-cta):not([aria-current]):hover {
  color: var(--text, #ffffff);
}
/* the homepage sets 0.04em on every item in its row; tracking is not colour, so
   this one applies to the whole bar, current page and account chip included. */
html nav.lmn-took .lmn-links a, html nav.lmn-took .lmn-links .lmn-search,
html #lm-nav .lmn-links a, html #lm-nav .lmn-links .lmn-search,
html #site-nav a, html #site-nav .lmn-search { letter-spacing: 0.04em; }

/* ── the phone menu (v16) ──
   Eight links do not fit a phone header, and the old answer was to shrink them
   until they did: 0.7rem type and 25x18px tap targets, wrapped over two rows on
   48 pages and THREE on 10, inside a header that is `position: sticky` on 57 of
   the 60 pages measured. That is ~80px of an 844px screen surrendered for the
   whole visit, to the smallest text and the smallest targets on the site.
   ⚠ NOT written with :where(). flow.css is zero-specificity by convention so
   pages win, but pages set `header nav a {font-size:0.8rem}` (0-0-3), and a
   :where() rule loses to that every time — the exact bug that made the v14
   tightening rules silently inert. This is the third documented exception in
   this file, after the back control and the takeover itself.
   ⚠⚠ 560 BECAME 980 WHEN THE BAR GAINED CONTACT AND THE CTA PILL. This block
   exists to stop the row wrapping — its own note above counts the two- and
   three-row headers it was written to kill — so the number has to track what is
   IN the row. Measured on /bench/ after the two items landed:
     1000: navH 39 (one row)   980: 39 (one row)   960: 77   940: 77   900: 77
   The floor is 980, so the drawer now takes over at 980 instead of leaving a
   380px-wide band (600-980) where the header stood at ~100px against the 69px
   it measures on one row. That band is a tablet held either way and a small
   laptop, not an edge case.
   ⚠ RE-DERIVE THIS IF THE ROW CHANGES AGAIN. Add or remove an item in either
   SITE array and the floor moves; the check is one line in a console at a few
   widths — nav.lmn-took's height is 39 on one row and ~77 on two. */
@media (max-width: 980px) {
  /* ⚠⚠ THE PANEL ANCHORS TO THE NAV, NOT THE HEADER — and that is not a
     preference, it is a bug I shipped into this file and caught by measuring.
     `header:has(nav.lmn-took){position:relative}` looked like the obvious way to
     give an absolutely-positioned panel something to hang off. What it actually
     did was OVERWRITE `position: sticky` on 51 of the 60 pages: the audit went
     from 57 sticky headers to 7, so the site nav silently stopped following
     anyone down a page on a phone. Nobody asked for that and nothing in the
     diff looked wrong.
     ⭐ The nav is the better anchor anyway — it has no position of its own to
     destroy, and it sits at the header's right edge, which is exactly where the
     panel should hang from. */
  header nav.lmn-took, [role="banner"] nav.lmn-took { position: relative; }
  header nav.lmn-took .lmn-burg, [role="banner"] nav.lmn-took .lmn-burg {
    display: flex; flex-direction: column; justify-content: center; gap: 4px;
    /* 44x44 is the accepted floor for a finger and this control replaces eight
       targets that were 25x18. Padding does the work so the three bars stay
       18px wide and optically centred. */
    width: 44px; height: 44px; flex: none; padding: 0 13px; margin-left: auto;
    box-sizing: border-box; cursor: pointer;
    background: none; border: 1px solid var(--border, #26282d); border-radius: 8px;
    color: var(--text, #e7e8ea);
  }
  header nav.lmn-took .lmn-burg span, [role="banner"] nav.lmn-took .lmn-burg span {
    display: block; height: 2px; width: 100%; border-radius: 2px;
    background: currentColor;
  }
  header nav.lmn-took .lmn-burg[aria-expanded="true"], [role="banner"] nav.lmn-took .lmn-burg[aria-expanded="true"] {
    border-color: var(--red, #e5484d); color: var(--red, #e5484d);
  }
  header nav.lmn-took .lmn-links, [role="banner"] nav.lmn-took .lmn-links {
    position: absolute; top: calc(100% + 8px); right: 0; left: auto;
    display: none; flex-direction: column; flex-wrap: nowrap; align-items: stretch;
    row-gap: 0.15rem; column-gap: 0; margin-left: 0;
    min-width: 210px; max-width: calc(100vw - 1.6rem);
    background: var(--panel, #16171a); border: 1px solid var(--border, #26282d);
    border-radius: 12px; padding: 0.5rem;
    box-shadow: 0 16px 44px rgba(0, 0, 0, 0.55);
    /* above the page's own sticky header z-index, which is 10 on 110 pages and
       100 at the top of the range */
    z-index: 120;
  }
  header nav.lmn-took.lmn-open .lmn-links, [role="banner"] nav.lmn-took.lmn-open .lmn-links { display: flex; }
  /* full-width rows at a readable size, instead of 11.2px crammed three across.
     ⚠ `.lmn-search` is listed alongside `a` because it is a <button>, not a
     link — without it the Search row measured 28px in a drawer where every
     other row is 44, i.e. the one control added to help people find things was
     the only one under the touch floor. */
  header nav.lmn-took .lmn-links a, [role="banner"] nav.lmn-took .lmn-links a,
  header nav.lmn-took .lmn-links .lmn-search, [role="banner"] nav.lmn-took .lmn-links .lmn-search {
    display: flex; align-items: center;
    min-height: 44px; padding: 0 0.7rem; border-radius: 8px;
    font-size: 0.9rem; white-space: nowrap; width: 100%; box-sizing: border-box;
  }
  header nav.lmn-took .lmn-links a:hover, [role="banner"] nav.lmn-took .lmn-links a:hover,
  header nav.lmn-took .lmn-links .lmn-search:hover, [role="banner"] nav.lmn-took .lmn-links .lmn-search:hover {
    background: var(--panel2, #1c1d21);
  }
  header nav.lmn-took .lmn-links a.lmn-auth, [role="banner"] nav.lmn-took .lmn-links a.lmn-auth {
    margin-top: 0.3rem; border-top: 1px solid var(--border, #26282d);
    border-radius: 0 0 8px 8px; padding-top: 0.3rem;
  }
  /* One 44px control instead of eight wrapped links. MEASURED before and after
     across the same 60 pages: nav rows 2-or-3 on 58 pages → 1 row on 51, and
     the tallest header 93px → 67. The padding is trimmed to 0.35rem so the bar
     lands at ~56px — BELOW the 59px median it had when it was two rows of 11px
     text, rather than merely below the 80px worst case. Without this the burger
     costs 61px and the median goes UP by 2px, which measuring is the only way
     to notice. */
  /* (the 0.35rem trim that lived here is superseded: ONE BAR below owns the box — padding 0, min-height 62 — so the burger state is 62 like the homepage's.) */
}

/* ⚠ OUTSIDE the media query, exactly like `nav.lmn-took .lmn-burg` above. The
   first cut only styled the burger INSIDE the 600px block and never declared a
   default, so it rendered as a bare 44px outlined button in the desktop row on
   the site's highest-traffic page. Measured at 1280: burgerHidden=false. */
#nav .lmn-burg { display: none; }
/* ⚠ Search is a DRAWER-ONLY item on the homepage BELOW 1160px, and that is a
   measured call rather than a preference. The four media queries in index.html
   tune that row down to a 0px overflow at exactly 600px; adding a Search button
   to it put the row 4px over at 601 even with the label already hidden by the
   760px rule below. Ctrl+K works on this page regardless, so the narrow row
   keeps the shape it was tuned to and the phone drawer — which has vertical
   room — gets the visible way in.
   ⚠⚠ THE SCOPE WAS THE BUG, NOT THE CALL. This was an unconditional
   `display: none`, so the constraint measured at 601px was also suppressing the
   button at 1440 and at 2560 — and the homepage was consequently the only bar on
   the site with no visible Search, while nav.lmn-took, #lm-nav and #site-nav all
   carry one. Re-measured on the live row with the button forced visible:
     1440 / 1280 / 1200 / 1140 / 1100  overflow 0
     1080  +7px      1060  +27px      1024  +61px
   So 1100 is the floor. 1160 is that plus the ~60px of slack BENCH YIELDS FIRST
   already budgets for a signed-in account chip, which is the only element in
   this row whose width is not fixed. Below it, nothing changes: the drawer rule
   in the 600px block still shows Search as the first row of the open menu. */
#nav .links .lmn-search { display: none; }
@media (min-width: 1160px) { #nav .links .lmn-search { display: inline-flex; } }

/* ═══════ TWO ROWS on the two product landing pages (v19) ═══════
   /overlay/ and /forge/ are sales pages whose own nav carries section anchors
   AND the pricing button. The old takeover emptied that nav, so both lost their
   "Get it — free" / "Get Forge" control and every in-page anchor with it — the
   link was only ever visible if flow.js had failed to run, which their own
   comments say out loud.
   gen-header.js now INSERTS the site bar above and marks the page row
   data-lm-keep so nothing touches it. This puts them on separate lines.
   ⚠ Side by side they happen to fit at 1265px (measured: 723 vs 834) and
   collide below that, so relying on the natural flex row is a bug waiting for
   a laptop. flex-basis:100% forces the break explicitly at every width.
   ⚠ :has() on the header, so the rule cannot reach a page with only one nav. */
header:has(nav[data-lm-keep]) { flex-wrap: wrap; row-gap: 0.15rem; }
header:has(nav[data-lm-keep]) nav[data-lm-keep] {
  flex: 0 0 100%; justify-content: flex-end;
  border-top: 1px solid var(--border, #26282d);
  padding-top: 0.35rem; margin-top: 0.15rem;
}
/* on a phone the site bar is a burger, so the page row is the only thing left
   in the header — let it start from the left and scroll rather than crowd. */
@media (max-width: 600px) {
  header:has(nav[data-lm-keep]) nav[data-lm-keep] {
    justify-content: flex-start; overflow-x: auto; scrollbar-width: none;
  }
  header:has(nav[data-lm-keep]) nav[data-lm-keep]::-webkit-scrollbar { display: none; }
}

/* ═══════ the homepage drawer (v19) ═══════
   The homepage owns a bespoke #nav that flow.js used to skip outright, which
   made it the only page on the site with no phone menu. MEASURED across widths,
   with all four of its own media queries already retiring Bench, Games, Contact
   and the CTA pill on the way down:
     360px overflow 126px · 375px 111 · 390px 96 · 430px 57 · 460px 28
     500px 74 (Contact returns above 460 and costs more than it saves)
     560px 16 · 600px 0 ✓
   So it is broken at EVERY width below 600 and fine at and above it. 600px is
   the breakpoint, and it is derived from this bar's own geometry.
   ⚠ NOT 560px (the takeover's number): that would leave 560-599 still clipped.
   ⚠ And this is not the thing the plan warned against — that was raising the
   TAKEOVER's burger to 780/900 to paper over a rule on 17 pages. Different bar,
   different measurement.

   ⚠ #nav is position:fixed, so it is already a containing block for the panel;
   it needs no position:relative of its own. */
@media (max-width: 600px) {
  #nav .lmn-burg {
    display: flex; flex-direction: column; justify-content: center; gap: 4px;
    width: 44px; height: 44px; flex: none; padding: 0 13px; margin-left: auto;
    box-sizing: border-box; cursor: pointer;
    background: none; border: 1px solid var(--border, #26282d); border-radius: 8px;
    color: var(--text, #e7e8ea);
  }
  #nav .lmn-burg span { display: block; height: 2px; width: 100%; border-radius: 2px; background: currentColor; }
  #nav .lmn-burg[aria-expanded="true"] { border-color: var(--red, #e5484d); color: var(--red, #e5484d); }
  #nav .links {
    position: absolute; top: calc(100% + 8px); right: clamp(1rem, 4vw, 2.6rem); left: auto;
    display: none; flex-direction: column; flex-wrap: nowrap; align-items: stretch;
    row-gap: 0.15rem; column-gap: 0; margin-left: 0;
    min-width: 210px; max-width: calc(100vw - 2rem);
    background: var(--panel, #16171a); border: 1px solid var(--border, #26282d);
    border-radius: 12px; padding: 0.5rem;
    box-shadow: 0 16px 44px rgba(0, 0, 0, 0.55);
    z-index: 120;
  }
  #nav.lmn-open .links { display: flex; }
  /* ⭐ THE DRAWER SHOWS EVERYTHING THE ROW COULD NOT AFFORD. Bench, Games,
     Contact and the CTA are hidden by four separate width rules; in a vertical
     panel there is room for all of them, so a phone visitor gets MORE of the
     site than the desktop row does, not less.
     ⚠ `html #nav.lmn-open …` is 1-2-2 and beats those rules' 1-2-1 without
     !important — they are `#nav .links a[href="/bench/"]`, an id + a class + an
     attribute selector, so a plain 1-2-1 here would tie and lose on source order
     on any page whose <style> is parsed after flow.css. */
  html #nav.lmn-open .links a,
  html #nav.lmn-open .links .lmn-search {
    display: flex; align-items: center;
    min-height: 44px; padding: 0 0.7rem; border-radius: 8px;
    font-size: 0.9rem; white-space: nowrap; width: 100%; box-sizing: border-box;
  }
  html #nav.lmn-open .links a:hover,
  html #nav.lmn-open .links .lmn-search:hover { background: var(--panel2, #1c1d21); }
  /* the two pills keep their fill but become full-width rows like everything else */
  html #nav.lmn-open .links a.cta,
  html #nav.lmn-open .links a.auth { justify-content: center; margin-top: 0.3rem; }
  /* the underline sweep is a desktop-row affordance; in a list it just flickers */
  #nav.lmn-open .links a::after { display: none; }
  /* ⚠ The label must come BACK in the drawer. `nav .lmn-search span` is hidden
     from 760px down and restored again below 561 — that second rule exists so a
     lone magnifier is not the one unreadable row in a list of words. This drawer
     opens at 600, not 560, so 561-600 would have hit exactly that. 1-1-2 beats
     the generic 0-1-2. */
  #nav .lmn-search span { display: inline; }
}

/* ═══════ site nav (v13) — injected by flow.js on nav-less pages ═══════
   Deliberately the SAME shape the site's own page headers use — fixed and
   translucent, logo left, links right, the red hairline under it — because the
   ten pages that get this are the only ones without that header, and the point
   is that they stop looking like orphans.
   ⚠ the background is a literal rgba, not var(--bg): a variable can't carry the
   0.92 the blur needs, and every page this mounts on is the site's #0e0f11. */
#lm-nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 40;
  display: flex; align-items: center; gap: 1.6rem;
  /* the homepage #nav's box, number for number */
  min-height: 62px;
  padding: 0 clamp(1rem, 4vw, 2.6rem);
  background: var(--bg, #0a0908);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  border-bottom: 1px solid var(--border, rgba(255,255,255,.12));
  font-family: inherit;
  -webkit-backdrop-filter: blur(11px); backdrop-filter: blur(11px);
}
#lm-nav::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: linear-gradient(90deg, rgba(var(--glow, 229, 72, 77), 0.55), rgba(var(--glow, 229, 72, 77), 0.08) 30%, transparent 65%);
}
#lm-nav .lmn-logo { font-weight: 700; font-size: 0.9rem; color: var(--text, #e7e8ea); text-decoration: none; white-space: nowrap; }
/* THE IDENTITY MARK, second half. index.html paints the same "myst" syllable
   var(--myst) in its own hand-written nav; this is the injected nav every OTHER
   page gets, and flow.js skips any page that already has #nav — so the two never
   both fire and the mark is carried exactly once per page, everywhere. Leaving
   this on --red was the split that made the wordmark change colour as a visitor
   moved off the homepage. No fallback literal, matching index.html: if --myst
   ever fails to resolve the syllable simply reads monochrome. */
#lm-nav .lmn-logo b { color: var(--myst); font-weight: 700; }
#lm-nav .lmn-logo i { font-style: normal; color: var(--muted, #8b8f96); font-weight: 400; font-size: 0.8rem; }

/* ── THE IDENTITY MARK, THIRD HALF — the 144 pages neither of the above reaches ──
   The comment above says the mark "is carried exactly once per page, everywhere",
   and that was only ever true of the two navs flow.js OWNS. It is not true of the
   bar on a page that keeps its own <header>: flow.js's takeover deliberately
   preserves each page's logo markup and CSS (that is what makes the bar look
   native), so 143 pages went on painting `.logo b { color: var(--red) }` from
   their own <style> block, plus app/index.html doing the same to a <span>.
   Measured: 144 of 145 headered pages. So the wordmark still changed colour the
   moment a visitor left the homepage — the exact split this file already believed
   it had fixed.

   ⚠ BOTH SHAPES OR IT MISSES THE BIGGEST PAGE. 143 pages write the syllable as
   <b>; app/index.html writes <span class> and is the most-viewed page on the site.
   A <b>-only rule looks correct on every page you would think to check and leaves
   /app/ red.
   ⚠ NOT SCOPED TO <header>, and that is not laziness. The first cut of this rule
   was `html header a.logo b, html header a.logo span` and it missed /app/ — whose
   wordmark lives in `<aside id="rail">`, not in the banner. Measured, after
   writing the warning above about missing /app/ and then missing it anyway. The
   mark is the mark wherever it is placed; scoping it to a container just encodes
   today's layout. Only three logo shapes exist site-wide and all three ARE the
   wordmark, so there is nothing else for this to hit.
   ⚠ 0-1-3 here beats the pages' own `.logo b` (0-1-1) without !important, the same
   mechanism the button and card blocks use. The homepage's `#nav .logo b` (1-1-1)
   still outranks it and already says var(--myst) — same answer either way.
   ⚠ NO FALLBACK LITERAL on a colour — see the --myst block above. A stale cached
   flow.css must let this fall back to INHERITED INK (the mark reads monochrome),
   not to transparent, which is what a background would do.
   ⚠ This assumes the <b>/<span> wraps the SECOND SYLLABLE only. forge/build was
   the one page wrapping the whole word and its markup was corrected to match
   rather than excluded here — an exception list is a thing that rots. */
html a.logo b, html a.logo span {
  color: var(--myst);
}

#lm-nav .lmn-links { margin-left: auto; display: flex; align-items: center; gap: 1.3rem; }
#lm-nav .lmn-links a {
  color: var(--muted, #8b8f96); text-decoration: none; font-size: 0.8rem;
  white-space: nowrap; transition: color 0.15s, border-color 0.15s;
}
#lm-nav .lmn-links a:hover { color: var(--text, #e7e8ea); }
/* ⚠ two classes on purpose. `#lm-nav .lmn-links a` above is 1-1-1; a lone
   `.lmn-cta` (0-1-0) would lose to it and the CTA would paint as plain muted
   text. Spelling out the descendant chain wins on count without an !important. */
/* ═══════ THE PILL PAIR, ONE SHAPE ON EVERY BAR (v21) ═══════
   Taken from the homepage, measured rather than eyeballed:
     a.cta   fill #ffffff (--text)  ink rgb(10,9,8) (--bg)  999px  0.5rem 0.9rem  600
     a.auth  same fill and ink                              999px  0.3rem 0.9rem  650
     a.auth.acct  transparent, 1px var(--border), ink var(--text), 20px avatar
   ⚠⚠ THE FILL IS --text, NOT --red, AND THAT RETIRES A KNOWN AA PROBLEM. The
   injected bar used to fill this pill with var(--red) and ink it var(--on-red),
   under a note recording that a white label on the accent measures 1.4:1 to
   2.2:1 across the six light themes — reachable by any visitor, since mono is a
   free theme. That was a consequence of filling with the ACCENT. --text on --bg
   is the pair the homepage has always used: the two invert together on every
   theme, so the contrast cannot come apart the way an accent fill can.
   ⚠ --red is #9fdcee site-wide now. Filling a control with it would also have
   spent the brand accent on a button, which is the thing the Myst law forbids:
   the accent marks a reading, never a control. */
nav.lmn-took .lmn-links a.lmn-cta, #lm-nav .lmn-links a.lmn-cta, #site-nav a.lmn-cta {
  background: var(--text, #ffffff); color: var(--bg, #0a0908); font-weight: 600;
  border: 1px solid var(--text, #ffffff); border-radius: 999px;
  padding: 0.5rem 0.9rem; white-space: nowrap;
}
nav.lmn-took .lmn-links a.lmn-cta:hover, #lm-nav .lmn-links a.lmn-cta:hover, #site-nav a.lmn-cta:hover {
  background: var(--text, #ffffff); color: var(--bg, #0a0908);
}
nav.lmn-took .lmn-links a.lmn-auth, #lm-nav .lmn-links a.lmn-auth, #site-nav a.lmn-auth {
  background: var(--text, #ffffff); color: var(--bg, #0a0908); font-weight: 650;
  border: 1px solid var(--text, #ffffff); border-radius: 999px;
  padding: 0.3rem 0.9rem; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 0.5rem;
}
/* signed in: the chip goes quiet and shows the avatar — the homepage's own
   .auth.acct treatment, to the pixel. */
nav.lmn-took .lmn-links a.lmn-auth.acct, #lm-nav .lmn-links a.lmn-auth.acct, #site-nav a.lmn-auth.acct {
  background: transparent; border: 1px solid var(--border, rgba(255,255,255,.12));
  color: var(--text, #ffffff);
}
nav.lmn-took .lmn-links a.lmn-auth img, #lm-nav .lmn-links a.lmn-auth img, #site-nav a.lmn-auth img {
  width: 20px; height: 20px; border-radius: 50%; display: block;
}

/* ⚠ The bars are three <span>s, not the ☰ character it shipped with. A glyph
   is at the mercy of whatever font resolves it — and Terminal swaps <body> to
   Cascadia Mono while #lm-nav says `font-family: inherit`, so its size and
   weight moved with the preset. Spans inherit currentColor and stay 18px wide
   under every theme and layout.
   ⚠ Measured 32x34px before this — the menu button itself was one of the
   under-44 targets it exists to replace. */
#lm-nav .lmn-burg {
  display: none; margin-left: auto; cursor: pointer;
  flex-direction: column; justify-content: center; gap: 4px;
  width: 44px; height: 44px; flex: none; padding: 0 13px; box-sizing: border-box;
  background: var(--panel, #16171a); border: 1px solid var(--border, #26282d);
  color: var(--text, #e7e8ea); border-radius: 8px;
}
#lm-nav .lmn-burg span { display: block; height: 2px; width: 100%; border-radius: 2px; background: currentColor; }
#lm-nav .lmn-burg:hover { border-color: var(--red, #e5484d); }
#lm-nav .lmn-burg[aria-expanded="true"] { border-color: var(--red, #e5484d); color: var(--red, #e5484d); }

/* below 900px the row can't hold eight items, so it becomes a menu: logo +
   toggle only (≈180px at 360px wide, no horizontal overflow), with the full set
   in a dropdown clamped to the viewport.
   ⚠ 900, not the 760 this shipped with. MEASURED: the row is logo(85) + gap(14)
   + six links + CTA + auth chip + 2×20.8 padding, and the CTA is the variable —
   it names a page, so its width is data, not a constant. The widest label the
   table can produce ("Open the Permissions Calculator →", 238px) needs 789px of
   bar. At 760–810 the row therefore ran past its own right padding and the
   "Sign in" chip clipped off the edge, on the one page that can reach the long
   labels: 404.html, which is served for ANY path and so takes its CTA from the
   FAM pool (/discord/typo → "the Permissions Calculator"). 900 clears 789 with
   room for a wider font stack. Don't lower it without re-measuring the CTA. */
@media (max-width: 900px) {
  #lm-nav .lmn-burg { display: flex; }   /* flex, not block — it stacks three bars now */
  #lm-nav .lmn-links {
    position: absolute; top: calc(100% + 6px); left: auto; right: clamp(0.8rem, 3vw, 1.3rem);
    margin-left: 0; display: none; flex-direction: column; align-items: stretch; gap: 0.12rem;
    min-width: 190px; max-width: calc(100vw - 1.6rem);
    background: var(--panel, #16171a); border: 1px solid var(--border, #26282d);
    border-radius: 12px; padding: 0.5rem; box-shadow: 0 16px 44px rgba(0, 0, 0, 0.55);
  }
  #lm-nav.lmn-open .lmn-links { display: flex; }
  /* 44px rows, like the takeover's drawer — the two menus are the same menu.
     `.lmn-search` is a <button> and needs naming explicitly for the same reason. */
  #lm-nav .lmn-links a, #lm-nav .lmn-links .lmn-search {
    display: flex; align-items: center; min-height: 44px;
    font-size: 0.9rem; padding: 0 0.7rem; border-radius: 8px; white-space: normal;
    width: 100%; box-sizing: border-box;
  }
  #lm-nav .lmn-links a:hover { background: var(--panel2, #1c1d21); }
  #lm-nav .lmn-links a.lmn-cta { text-align: center; margin-top: 0.3rem; }
  #lm-nav .lmn-links a.lmn-auth { text-align: center; border-radius: 8px; }
}

/* ⚠ …and 900 IS NOT THE ONLY ANSWER, because the measurement above assumed a
   font. Terminal swaps <body> to Cascadia Mono (!important) and #lm-nav says
   `font-family: inherit`, then adds `letter-spacing: 0.06em` and UPPERCASE to
   the links — so the same eight items get materially wider and the row that
   was measured at 789px is not the row that renders. MEASURED on 404.html
   under Terminal, the page that can reach the long CTA labels: the link set
   goes 623px → 846px, and the widest label the FAM pool can produce ("Open the
   Nether Portal Calculator →", /minecraft/ typo) needs 1058px of bar against
   the Permissions Calculator's 1041 and the tool library's 957.
   So between 901 and 1058 Terminal ran the row straight past its own right
   padding and pushed the "Sign in" chip off the screen entirely — measured at
   905px wide, the chip's right edge sat at 995 in a 905px bar. Not clipped-
   looking: gone, and with it the only way to sign in from a nav-less page.
   This is the same drawer, at the width Terminal actually needs it. 1170 clears
   1058 by the same ~110px of font-stack headroom that 900 gives 789 — the two
   numbers are the same decision made twice, once per font. Re-measure BOTH if
   the link set or the CTA table changes.
   The declarations are spelled out again rather than shared: a media query
   cannot be OR-ed with a selector, and the alternative (a JS-measured class)
   would make the drawer depend on flow.js running, which the baseline above
   deliberately does not. */
@media (min-width: 901px) and (max-width: 1170px) {
  html[data-layout="terminal"] #lm-nav .lmn-burg { display: flex; }
  html[data-layout="terminal"] #lm-nav .lmn-links {
    position: absolute; top: calc(100% + 6px); left: auto; right: clamp(0.8rem, 3vw, 1.3rem);
    margin-left: 0; margin-right: 0; display: none; flex-direction: column; align-items: stretch; gap: 0.12rem;
    min-width: 190px; max-width: calc(100vw - 1.6rem);
    background: var(--panel, #16171a); border: 1px solid var(--border, #26282d);
    padding: 0.5rem; box-shadow: 0 16px 44px rgba(0, 0, 0, 0.55);
  }
  html[data-layout="terminal"] #lm-nav.lmn-open .lmn-links { display: flex; }
  html[data-layout="terminal"] #lm-nav .lmn-links a { font-size: 0.8rem; padding: 0.5rem 0.5rem; white-space: normal; }
  html[data-layout="terminal"] #lm-nav .lmn-links a:hover { background: var(--panel2, #1c1d21); }
  html[data-layout="terminal"] #lm-nav .lmn-links a.lmn-cta { text-align: center; margin-top: 0.3rem; }
  html[data-layout="terminal"] #lm-nav .lmn-links a.lmn-auth { text-align: center; }
}

/* the Terminal layout squares every surface on the site; this one joins in
   (border-radius is deliberately NOT set in the drawer block above — this rule
   is what squares the dropdown, and it applies at every width) */
html[data-layout="terminal"] :is(#lm-nav .lmn-links, #lm-nav .lmn-links a, #lm-nav .lmn-burg) { border-radius: 0 !important; }

@media (prefers-reduced-motion: reduce) {
  #lm-nav .lmn-links a, #lm-nav .lmn-burg { transition: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   🎛 FORM CONTROLS — site-wide
   Measured first: 181 of 414 visible controls across 28 pages had no theming at
   all, including 54 of the 58 checkboxes on the Discord permissions calculator.
   The reported symptom was that they blend into the text.

   ⚠⚠ WHY THIS IS SHORT. The first attempt hand-rolled everything with
   appearance:none plus :checked / :indeterminate state rules and a pseudo-element
   tick. The BASE rules demonstrably applied (background resolved to --panel), but
   the :checked rules never took effect, and five rounds of DOM introspection could
   not establish why — the parsed-rule API contradicted itself between calls.
   Rather than keep guessing at the cascade, this uses accent-color: one standard
   property, no state selectors, nothing to lose a specificity race with. The
   browser draws the control; we only say which colour it uses.
   ⚠ Opt out with .lm-native on the control.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ⚠⚠ THESE ARE DRAWN NOW, not tinted. The previous version set `accent-color`
   and let the browser paint the widget — which is exactly the complaint: a
   default Windows checkbox in a brand colour. That retreat happened because a
   first attempt's `:checked` rules "never took effect" and five rounds of DOM
   introspection could not say why.
   ⭐ THE CAUSE WAS SPECIFICITY, and this file already knew the trick: flow.css
   wins forty other cascade fights by prefixing `html`, while the failed attempt
   used plain selectors that any page rule could out-rank. `html input[…]:checked`
   is (0,2,2) and beats what a page realistically ships. Exactly the shape of the
   giant-circle bug, where `html .cat` (0,1,1) silently beat the page's `.cat`.
   ⚠ progress KEEPS accent-color: no states to lose, no pseudo-element to draw
   into, so tinting is the right tool there. */
progress:not(.lm-native) { accent-color: var(--red, #e5484d); }

html input[type=checkbox]:not(.lm-native),
html input[type=radio]:not(.lm-native) {
  -webkit-appearance: none; appearance: none;
  /* ⚠ was 1.05em — which resolves against the INPUT's own UA font (13.33px), so
     every box on the site rendered 14px and every page's local size declaration
     silently lost to this rule (measured 2026-07-31: a full census found the
     dead declarations on five pages, all computing 14px). 20px is the value two
     pages already shipped by out-ranking this rule; now the rule itself agrees.
     Do not raise past 20: a 22px box overhangs a 19.44px label line. */
  width: 20px; height: 20px; margin: 0 0.15em 0 0; flex: none;
  vertical-align: -0.16em; position: relative; cursor: pointer;
  /* ⚠⚠ MEASURED, after finally LOOKING at these (2026-07-30). The first version
     was fill `--panel` + a `--line` border — and a checkbox almost always sits on
     a `--panel` card, so the fill was invisible and a hairline at
     rgba(255,255,255,.12) was the only thing saying the control existed:
     **1.33:1** against its own card, where WCAG asks 3:1 for a control's boundary.
     The browser's own dark-mode checkbox was easier to see than ours — the
     redesign made CHECKED look much better and quietly made OFF harder to find.
     `--edge` is the existing brightest edge token and measures 4.05:1 on --panel;
     `--well` sinks the box so it reads as something you can fill in. */
  background: var(--well, #060607);
  border: 1px solid var(--edge, rgba(255,255,255,.42)); border-radius: 4px;
  transition: background .13s ease, border-color .13s ease;
}
html input[type=radio]:not(.lm-native) { border-radius: 50%; }
/* ⚠ command-generator's segmented control hides two REAL radios under its spans
   (.vopt input declares position:absolute; opacity:0 — though the block above
   overrides the position, a recorded accident). They currently occupy 1.05em of
   invisible in-flow space inside each segment button, and growing them to 20px
   would visibly widen the segments. Frozen at the old size until that page opts
   them out properly with lm-native. */
html .vopt input[type=radio]:not(.lm-native) { width: 1.05em; height: 1.05em; }
html input[type=checkbox]:not(.lm-native):hover,
html input[type=radio]:not(.lm-native):hover { border-color: var(--text-mute, #8a8a8a); }
html input[type=checkbox]:not(.lm-native):focus-visible,
html input[type=radio]:not(.lm-native):focus-visible,
html input[type=range]:not(.lm-native):focus-visible {
  outline: 2px solid var(--red, #e5484d); outline-offset: 2px;
}
html input[type=checkbox]:not(.lm-native):checked,
html input[type=radio]:not(.lm-native):checked {
  background: var(--red, #e5484d); border-color: var(--red, #e5484d);
}
/* the tick is DRAWN — it scales with the box and can never render as a
   missing-font box the way a ✓ glyph can */
html input[type=checkbox]:not(.lm-native):checked::after {
  content: ''; position: absolute; left: 30%; top: 11%;
  width: 26%; height: 56%;
  border: solid var(--on-red, #fff); border-width: 0 2px 2px 0;
  transform: rotate(42deg);
}
html input[type=radio]:not(.lm-native):checked::after {
  content: ''; position: absolute; inset: 26%; border-radius: 50%;
  background: var(--on-red, #fff);
}

html input[type=range]:not(.lm-native) {
  -webkit-appearance: none; appearance: none;
  width: 100%; max-width: 100%; height: 1.15rem;
  background: none; cursor: pointer; margin: 0;
}
/* ⚠⚠ THE FILL CAME BACK. Drawing the track by hand lost the one thing the old
   `accent-color` version got for free: Chromium paints the portion BELOW the
   thumb in the accent, and a hand-drawn track is a flat rail. Shipped, a slider
   read as an empty line with a dot on it — while the `progress` bar directly
   beside it (which kept accent-color) was filled. Same page, same theme, two
   different languages for "how far along is this".
   Chromium exposes no way to know the value in CSS, so flow.js keeps `--lm-fill`
   in sync. ⭐ It degrades to the CURRENT look, not to a broken one: with no JS
   the fill is 0% and you get exactly the flat rail that shipped. */
html input[type=range]:not(.lm-native)::-webkit-slider-runnable-track {
  height: 5px; border-radius: 999px;
  background: linear-gradient(var(--red, #e5484d) 0 0) left center / var(--lm-fill, 0%) 100% no-repeat,
              var(--well, #0d0d0d);
  border: 1px solid var(--line, #2a2a2a);
}
html input[type=range]:not(.lm-native)::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 15px; height: 15px; margin-top: -0.3rem; border-radius: 50%;
  background: var(--red, #e5484d); border: 1px solid var(--line, #2a2a2a);
  transition: transform .13s ease, box-shadow .13s ease;
}
html input[type=range]:not(.lm-native):hover::-webkit-slider-thumb { transform: scale(1.12); }
html input[type=range]:not(.lm-native):active::-webkit-slider-thumb {
  transform: scale(1.04); box-shadow: 0 0 0 5px color-mix(in srgb, var(--red, #e5484d) 28%, transparent);
}
/* ⚠⚠ FIREFOX HAD NO SLIDER AT ALL. Drawing the track replaced `accent-color`,
   which Firefox understood, with `::-webkit-` pseudo-elements, which it does not
   — so Firefox went from a brand-coloured slider to the plain OS-grey default.
   ⚠ These must be their OWN rules: a selector list containing a pseudo-element
   one engine cannot parse makes that engine drop the WHOLE rule, so grouping
   -moz- with -webkit- would have unstyled Chromium as well.
   ⭐ Firefox needs no JS here — `::-moz-range-progress` IS the filled portion. */
html input[type=range]:not(.lm-native)::-moz-range-track {
  height: 5px; border-radius: 999px;
  background: var(--well, #0d0d0d); border: 1px solid var(--line, #2a2a2a);
}
html input[type=range]:not(.lm-native)::-moz-range-progress {
  height: 5px; border-radius: 999px 0 0 999px;
  background: var(--red, #e5484d);
}
html input[type=range]:not(.lm-native)::-moz-range-thumb {
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--red, #e5484d); border: 1px solid var(--line, #2a2a2a);
}
html input[type=checkbox]:not(.lm-native):disabled,
html input[type=radio]:not(.lm-native):disabled,
html input[type=range]:not(.lm-native):disabled { opacity: .45; cursor: not-allowed; }

/* ═══════════════ THE TEXT FIELD (v19) ═══════════════
   The last component on the site with no system. This file had already taken the
   button (colour + shape), the card (radius), the chip, the focus ring, the touch
   floor, checkbox, radio, range and select — and never once described a text
   field. Its only appearances here were a focus bloom and a text cursor. On a
   site that is mostly calculators, lookups and generators, that is the control a
   visitor touches most.

   MEASURED across the 100 pages that carry one — 453 controls, before this block:
     background  5 real variants: --bg-deep x253, #000 x101, --panel x71,
                 transparent x20, --panel2 x3  (plus 5 tier swatches, see below)
     radius      8px x347, 4px x72, 0px x32, 12px x1, 999px x1
     border      rgba(255,255,255,.12) x427 — but 20 solid white and 5 near-black
     font        426 Segoe UI, 27 Cascadia Mono
   So the SHAPE and the SHADE were the divergence; 8px and --border were already
   the site's answer on the majority and this makes them the answer everywhere.

   ⚠⚠ SURFACE, SHAPE AND INK ONLY — deliberately NOT size. Height came out at 28
   distinct values and padding at 15, and that is mostly legitimate: a two-digit
   number field is not a search box, and pages have measured wrap points that
   depend on their padding. Setting height or font-size here would relayout 100
   pages of working tools for a cosmetic gain. Same call the card block made when
   it normalised radius and left the surfaces alone.

   ⚠⚠ THE SPECIFICITY IS CHOSEN, NOT INHERITED. This scores 0-2-2. Counted from
   the tree: 155 page rules are bare-element (`input[type=text]`, 0-1-1) and 75
   are class-scoped (`.tool-panel input`, 0-1-1) — this beats both. Eight rules
   are ID-scoped (`#cp-input`, `#chat-input`, `#acct-box input`) and those still
   WIN, which is correct: they are the deliberate ones.
   ⚠ It is written as a positive type list rather than a stack of :not()s on
   purpose. `input:not([type=checkbox]):not(...)` with eight exclusions scores
   0-9-2 and would bulldoze every page rule on the site including the deliberate
   ones. Enumerating what a text field IS keeps this at 0-2-2.
   ⚠ `.lm-native` opts out, matching the convention the checkbox/radio/range/
   select rules above already use. /tools/tier-list-maker/ uses it: its tier rows
   are editable textareas whose background is the tier colour and whose ink is
   deliberately dark — a swatch, not a field. Measured before writing this. */
html :is(
  input[type="text"], input[type="search"], input[type="number"], input[type="email"],
  input[type="url"], input[type="tel"], input[type="password"], input[type="date"],
  input[type="time"], input[type="datetime-local"], input[type="month"], input:not([type]),
  textarea
):not(.lm-native) {
  background-color: var(--bg-deep, #050506);
  color: var(--text, #ffffff);
  border: 1px solid var(--border, rgba(255,255,255,.12));
  border-radius: 8px;
  /* ⚠ NO font-family HERE. The first cut set `font-family: inherit` and MEASURED
     the damage: 23 controls lost their monospace — base64, the hash generator,
     the JSON formatter, the epoch converter, system-info's dump, stronghold-
     finder's paste box, the econ calculator's number fields. Every one of those
     is mono ON PURPOSE, and the baseline had already shown the split was
     deliberate rather than accidental: 426 Segoe against 27 Cascadia, not a
     scatter. What a field CONTAINS decides its face; the shared layer has no
     business overriding that. */
}

/* ── select: appearance:none is safe here — there is no checked state to lose, only
   an arrow to add, and the audit found selects rendering as raw grey widgets on 14
   pages ──
   ⚠ radius and border token brought into line with the field block above: this was
   the one control in the shared layer still drawing a 4px corner and spending
   --line where everything else spends --border. They resolve to the same colour
   today, which is exactly why it went unnoticed. */
select:not(.lm-native) {
  -webkit-appearance: none; appearance: none;
  /* ⚠ --bg-deep, matching the text-field block above. This was --panel, which left
     70 selects sitting a shade lighter than every text input beside them —
     measured. The OPEN option list below stays --panel: that is a popup
     surface, not the control. */
  background-color: var(--bg-deep, #050506);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238a8a8a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 0.7em center; background-size: 0.62em auto;
  color: var(--text, #f4f6f8);
  border: 1px solid var(--border, rgba(255,255,255,.12)); border-radius: 8px;
  padding: 0 2em 0 0.7rem; height: 2rem; cursor: pointer; max-width: 100%;
  transition: border-color .13s ease;
}
select:not(.lm-native):hover { border-color: var(--text-mute, #8a8a8a); }
select:not(.lm-native):disabled { opacity: .45; cursor: not-allowed; }
select:not(.lm-native) option { background: var(--panel, #141414); color: var(--text, #f4f6f8); }

/* ═══════════════════════════════════════════════════════════════════════════
   🖥 DESKTOP WIDTH — every page uses the screen it is given
   Measured across all 146 pages at 1920×1080 before touching anything: the median
   page used 39% of the width. 65 pages capped `main` at 760px, 23 at 820px, 13 at
   900px, 13 at 860px. On a 1920 screen 760px IS 40%, which is the whole complaint.

   ⭐ WIDER CONTAINER, UNCHANGED READING MEASURE. Simply raising the cap would make
   prose worse — the /bench/ pages carry 1,800-3,800 words and single paragraphs of
   over 200, and a 1440px line is painful. So the SHELL grows (grids, cards, tables
   and tools get the room) while paragraphs and list items keep a ~78-character
   measure. Nothing has to be re-laid-out per page for that to be an improvement.

   ⚠ `html main` scores 0,1,1 and beats a page's own `main` at 0,0,1, which is the
   only reason one rule can lift 122 pages.
   ⚠ `main.lm-narrow` OPTS OUT — 20 pages are deliberately narrow (Wordle,
   Connections, Blockle, Pixle, Shotdle, Steamdle, 2048, Snake, Minesweeper and the
   single-field tools). A game board stretched to 1440px is worse than space beside
   it, and a canvas game would distort outright.
   ⚠ `main.lm-full` ALSO opts out — for pages that were ALREADY full-bleed and have
     no cap of their own. /app/ and the homepage are the two: capping them took /app/
     from 1693px to 1440px, which is this rule causing the very problem it fixes.
   ⚠ Uses min() against a vw so it never fights the viewport on a small laptop.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 1200px) {
  html main:not(.lm-narrow):not(.lm-full) { max-width: min(1180px, 93vw); }
}
@media (min-width: 1600px) {
  html main:not(.lm-narrow):not(.lm-full) { max-width: min(1440px, 90vw); }
}
/* keep the line length readable inside the wider shell. Scoped to running prose —
   NOT to cards, table cells, list-based grids or anything inside a tool panel,
   which are exactly the things that should use the new room. */
@media (min-width: 1200px) {
  html main:not(.lm-narrow):not(.lm-full) > p,
  html main:not(.lm-narrow):not(.lm-full) > section > p,
  html main:not(.lm-narrow):not(.lm-full) > section > ul > li,
  html main:not(.lm-narrow):not(.lm-full) > section > ol > li,
  html main:not(.lm-narrow):not(.lm-full) > .faq p,
  html main:not(.lm-narrow):not(.lm-full) > section > .tagline { max-width: 78ch; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   📏 PRE-TOOL BLOCKS — collapse the setup reading, keep the takeaway
   Measured: median page has 54 words before its first control, which is fine. But
   13 pages had 200-581 words of setup instructions sitting between the heading and
   the tool, and one (/minecraft/banner-designer/) put its tool 1553px down, below
   the fold entirely.
   ⭐ The fix is NOT deletion. That prose is load-bearing — "turn pointer
   acceleration off or the reading is wrong" is the difference between a correct and
   a meaningless result, and these pages are also how the site is found. So the block
   becomes a <details> whose <summary> carries the one sentence you must know, and
   the tool moves up by the rest.
   ═══════════════════════════════════════════════════════════════════════════ */
html details.lm-pre {
  border: 1px solid var(--line, #26282d); border-radius: 12px;
  background: var(--panel, #141414);
  padding: 0.7rem 0.9rem; margin: 0 0 1.1rem;
}
html details.lm-pre > summary {
  cursor: pointer; font-size: 0.9rem; color: var(--text, #f4f6f8);
  list-style: none; display: flex; gap: 0.5rem; align-items: baseline;
}
html details.lm-pre > summary::-webkit-details-marker { display: none; }
/* the twisty is drawn rather than relying on the UA marker, which differs per OS */
html details.lm-pre > summary::before {
  content: '▸'; color: var(--text-mute, #8a8a8a); flex: none;
  transition: transform .15s ease; transform-origin: 50% 55%;
}
html details.lm-pre[open] > summary::before { transform: rotate(90deg); }
html details.lm-pre > summary:hover { color: var(--red, #e5484d); }
html details.lm-pre > .lm-pre-body { margin-top: 0.9rem; }
/* the callout inside loses its own frame — one border is enough */
html details.lm-pre .callout { border: 0; background: transparent; padding: 0; margin: 0; }
@media (prefers-reduced-motion: reduce) {
  html details.lm-pre > summary::before { transition: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   💬 THE DISCORD BAR — see the block at the end of flow.js
   ⚠ BOTTOM-CENTRE, not a full-width bar: bottom-left is the layout picker and the
   back control, bottom-right is the helper. A full bar would sit on top of all three.
   ⚠ Collapsed it becomes a small tab rather than disappearing — the owner chose
   persistent-but-collapsible over dismissible, so there is always a way back to it.
   ═══════════════════════════════════════════════════════════════════════════ */
#lm-disc {
  position: fixed; z-index: 118;
  left: 50%; transform: translateX(-50%);
  bottom: 14px; width: min(600px, calc(100vw - 168px));
  display: flex; align-items: center; gap: 0.9rem;
  padding: 0.5rem 0.7rem 0.5rem 0.9rem;
  background: var(--panel, #16171a);
  border: 1px solid var(--line, #26282d); border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.42);
  font-family: 'Segoe UI', system-ui, sans-serif; font-size: 0.8rem;
  color: var(--text, #e7e8ea);
  animation: lm-disc-in .34s cubic-bezier(.16, 1, .3, 1) both;
}
@keyframes lm-disc-in { from { opacity: 0; transform: translate(-50%, 12px); } to { opacity: 1; transform: translate(-50%, 0); } }
/* ⚠ THE BLURPLE IS DISCORD'S AND BELONGS TO DISCORD SLOTS ONLY. The bar used to
   say exactly one thing, so #5865f2 could live on the container. It now carries
   Forge, Overlay and Premium slots too, and `.ic` paints itself from
   currentColor — so left unscoped, a Server Forge icon rendered in Discord's
   brand blue on eleven of the twelve themes.
   ⚠ Myst is NOT an option here either: no number sits behind a promo icon, and
   the accent marks a quantity being measured. Ink is the default; blurple is
   the single branded exception, and mono keeps ink as it always has. */
#lm-disc .lm-disc-ic { color: var(--text, #e7e8ea); flex: none; display: flex; }
#lm-disc[data-slot^="dc_"] .lm-disc-ic { color: #5865f2; }
html[data-theme="mono"] #lm-disc[data-slot^="dc_"] .lm-disc-ic { color: var(--text, #e7e8ea); }
/* the mask icons default to 1em, which is 13px beside this 0.8rem text — the
   hand-rolled Discord SVG next to them is 17px, so match it */
#lm-disc .lm-disc-ic .ic { width: 17px; height: 17px; vertical-align: 0; }
#lm-disc .lm-disc-tx { flex: 1; min-width: 0; color: var(--text-mute, #8b8f96); line-height: 1.45; }
#lm-disc .lm-disc-tx b { color: var(--text, #e7e8ea); font-weight: 600; }
#lm-disc .lm-disc-go {
  flex: none; text-decoration: none; font-weight: 650; font-size: 0.8rem;
  padding: 0.3rem 0.9rem; border-radius: 999px;
  background: var(--red, #e5484d); color: var(--on-red, #fff);
  transition: filter .14s ease;
}
#lm-disc .lm-disc-go:hover { filter: brightness(1.1); }
#lm-disc .lm-disc-min {
  flex: none; width: 24px; height: 24px; border-radius: 4px; cursor: pointer;
  background: transparent; border: 1px solid var(--line, #26282d);
  color: var(--text-mute, #8b8f96); font: inherit; line-height: 1;
}
#lm-disc .lm-disc-min:hover { color: var(--text, #e7e8ea); border-color: var(--text-mute, #8b8f96); }

#lm-disc-tab {
  position: fixed; z-index: 118;
  left: 50%; transform: translateX(-50%); bottom: 14px;
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.3rem 0.9rem; cursor: pointer;
  background: var(--panel, #16171a); border: 1px solid var(--line, #26282d);
  border-radius: 999px; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.36);
  font-family: 'Segoe UI', system-ui, sans-serif; font-size: 0.8rem;
  color: var(--text-mute, #8b8f96);
  transition: color .14s ease, border-color .14s ease;
}
#lm-disc-tab:hover { color: var(--text, #e7e8ea); border-color: var(--text-mute, #8b8f96); }
#lm-disc-tab svg { color: #5865f2; }
html[data-theme="mono"] #lm-disc-tab svg { color: var(--text, #e7e8ea); }

/* narrow screens: the corners are crowded, so it spans and sits above them */
@media (max-width: 720px) {
  #lm-disc { width: calc(100vw - 24px); bottom: 62px; font-size: 0.8rem; flex-wrap: wrap; }
  #lm-disc .lm-disc-tx { flex-basis: 100%; order: 3; }
  #lm-disc-tab { bottom: 62px; }
}
/* it is page furniture, so it goes away with everything else in fullscreen */
html.lm-fx-off #lm-disc, html.lm-fx-off #lm-disc-tab { display: none; }
/* ── …and while an on-screen keyboard is in view (flow.js sets .lm-kb-on) ──
   Collapsing the bar to a tab took the overlap on wordle from six keys to
   three; the pill still lands on F, G and H because the keyboard is ~327px
   wide inside a 374px viewport and there is nowhere sideways to go.
   ⚠ PHONE ONLY. On a desktop viewport the two do not compete and the visual
   diff is byte-identical there — worth keeping it that way. */
@media (max-width: 560px) {
  html.lm-kb-on #lm-disc, html.lm-kb-on #lm-disc-tab { display: none; }
}
@media (prefers-reduced-motion: reduce) { #lm-disc { animation: none; } }

/* ── THE MICRO OFFER ──
   Deliberately TEXT, under the Premium button. It is the cheap rung, and it
   earns its place by being findable rather than loud — a second filled button
   here turns a refusal into a decision, and a decision at that moment is a
   bounce. Muted body, ink link, no accent: the accent belongs to the primary. */
.lm-micro {
  margin: 0.9rem 0 0;
  font-size: 0.78rem;
  color: var(--muted, #8b8f96);
  line-height: 1.5;
}
.lm-micro .lm-micro-go {
  color: var(--text, #e7e8ea);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--muted, #8b8f96);
  white-space: nowrap;
}
.lm-micro .lm-micro-go:hover { text-decoration-color: currentColor; }

/* ── THE BENCH BAND ──
   IN-CONTENT, not floating: it sits under the result and above Related tools,
   so it reads as part of the page rather than as chrome laid over it. The
   floating bar stands down on these pages (html[data-lm-band]), so the visitor
   is asked once and only once.
   ⚠ MONOCHROME ON PURPOSE. Myst marks a quantity being measured, and no number
   sits behind a promo — but more than that, these pages are FULL of real
   readouts, and an accent here would compete with the measurement the visitor
   actually came for. Ink and the site accent on the button, nothing else.
   ⚠ --on-red is the ink for the ACCENT fill and is correct here precisely
   because the fill IS --red. It must never be spent on any other fill. */
#lmBand {
  display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap;
  max-width: 720px; margin: 2.2rem auto 0;
  padding: 0.85rem 1.05rem;
  background: var(--panel, #16171a);
  border: 1px solid var(--line, #26282d); border-radius: 12px;
  font-family: 'Segoe UI', system-ui, sans-serif;
  font-size: 0.82rem; line-height: 1.5;
  color: var(--text-mute, #8b8f96);
}
#lmBand .lmBand-ic { color: var(--text, #e7e8ea); flex: none; display: flex; }
#lmBand .lmBand-ic .ic { width: 18px; height: 18px; vertical-align: 0; }
#lmBand .lmBand-tx { flex: 1; min-width: 210px; }
#lmBand .lmBand-tx b { color: var(--text, #e7e8ea); font-weight: 600; }
#lmBand .lmBand-go {
  flex: none; text-decoration: none; font-weight: 650; font-size: 0.8rem;
  padding: 0.5rem 0.9rem; border-radius: 999px;
  background: var(--red, #e5484d); color: var(--on-red, #fff);
  transition: filter .14s ease;
}
#lmBand .lmBand-go:hover { filter: brightness(1.1); }
/* same opt-out the bar honours — if effects are off, the promo goes with them */
html.lm-fx-off #lmBand { display: none; }
@media (max-width: 560px) {
  #lmBand { margin-left: 12px; margin-right: 12px; }
  #lmBand .lmBand-tx { flex-basis: 100%; order: 3; }
}

/* ═══════ touch + reading floor (v16) ═══════
   MEASURED at 390x844 across 60 pages: 2,441 of 3,266 interactive elements
   (74.7%) were under the 44px a finger needs, and 3,478 pieces of text rendered
   under 11.5px.

   ⭐ THE FINDING THAT SHAPED THIS FILE'S HALF OF THE FIX: almost none of it was
   the pages. Ranked by how many pages carry it, the offenders are the SHARED
   furniture — the nav (25x18px on 50 pages), the footer legal line (29x15 on
   50), breadcrumbs (51x17 on 40), FAQ rows (24px tall on 32), the logo, every
   .btn (35px), the Discord bar's minimise button (24x24). Seven rules here
   reach further than seven hundred page edits would.

   ⭐ AND EVERY ONE OF THEM IS A HEIGHT PROBLEM, which is why this is safe.
   Horizontal space on a phone is contested — that is exactly why the Wordle
   keyboard's keys are 28px wide and CANNOT be 44 (ten across a 374px viewport
   is 37px at the absolute best; the keys are already 50px TALL, taller than the
   iPhone keyboard's own 42). Vertical space is free. Nothing below moves a
   layout sideways or reflows a line; it grows boxes downward or, where even
   that would cost too much, enlarges the hit area without touching the paint.

   ⚠ NOT under a plain max-width query. `(pointer: coarse)` is what actually
   asks the question "is a finger doing this?", so a touchscreen laptop gets the
   bigger targets too; the width clause covers a narrow window on a mouse
   machine, which is how this gets tested.

   ⚠⚠ THE PADDING VALUES BELOW ARE DELIBERATELY OFF THE SPACING SCALE, and a
   future sweep must leave them alone. 0.8rem and 0.6rem were not chosen for
   tidiness — they were derived from the measured content height of each control
   so the finished box lands on 44px. Snapping 0.8rem to the nearest step takes
   a button to 41px and quietly undoes the touch-target work these rules exist
   for. Section 1b's own principle: a value that encodes a decision is not a
   stray. Same reason the hairline gaps (0.15rem) and the header's 0.35rem stay
   where they are. */
@media (max-width: 700px), (pointer: coarse) {
  /* ── breadcrumbs and the footer legal line ──
     ⭐ Vertical padding on an INLINE link grows the clickable box without
     changing the line box, so the trail reads exactly as before and the target
     goes from 17px to 44. This is the one place where "make it bigger" costs
     literally no layout. */
  html .crumbs a, html .lm-copy a { padding-top: 0.85rem; padding-bottom: 0.85rem; }
  /* ⚠ the 0.85rem above assumed the legal line's 0.8rem type below — but every
     page writes the footer as <div class="lm-copy" style="…font-size:0.7rem…">,
     and an inline style beats any selector, so the font rule under this one has
     NEVER applied and Terms/Privacy measured 42.2px, 1.8px short of the floor.
     Split from .crumbs a (whose padding encodes its own measured decision) and
     topped up for the 11.2px type that actually renders: 15px line box + 2×15.2
     = 45.4px. Inline-link padding, so zero layout movement — same principle as
     the rule above. */
  html .lm-copy a { padding-top: 0.95rem; padding-bottom: 0.95rem; }
  /* the legal line was the smallest type on the site at 11.2px — NOTE: inert on
     every page that inlines font-size:0.7rem (all of them today); kept for the
     day a page drops the inline style. */
  html .lm-copy { font-size: 0.8rem; }

  /* ── disclosure rows ── full width already, 24px tall, on 32 pages.
     Every <summary> is a control by definition, so this is deliberately not
     scoped to .faq: pages spell it `<details class="faq">`, `.faq details` and
     `details.lm-pre`, and a rule that only knew one of those spellings would
     have looked right and missed most of them. Costs ~19px per row in a section
     that lives below the fold, which is a fair price for a row meant to be
     tapped. */
  html details > summary { padding-top: 0.7rem; padding-bottom: 0.7rem; }

  /* ── buttons ── 35px measured, sitewide, across every .btn on the site.
     padding-block only: the horizontal padding each page chose is untouched, so
     no button gets wider and no row of them re-wraps. */
  html .btn, html .cta, html a.get, html button.btn { padding-top: 0.8rem; padding-bottom: 0.8rem; }

  /* ── the wordmark ── 24px tall. The header's content row is 44px now (the
     menu button sets it), so this padding lands INSIDE the header rather than
     making it taller. */
  html header a.logo, html [role="banner"] a.logo { padding-top: 0.5rem; padding-bottom: 0.5rem; }

  /* ── the floating corner chrome was sitting ON the on-screen keyboard ──
     ⚠⚠ FOUND BY ASKING WHO RECEIVES THE TAP, NOT BY LOOKING. elementFromPoint
     at the layout button's own corners came back `button#key-a.key` — the
     bottom-left chrome (#lm-lay-btn at bottom:14px, .lm-back-fab above it) is
     fixed, the daily games put a keyboard at the bottom of the page, and the
     two occupy the same 100px. So on Wordle, Blockle, Connections and the rest,
     scrolling to the bottom parks two floating buttons over the A and S keys
     and every tap there goes to the wrong control.
     ⚠ PRE-EXISTING, not new — the buttons were 40px before this pass and
     overlapped then too; going to 44px made it four pixels worse. It is in this
     commit because this is the pass that could see it.
     Reserving the space at the END of the page is the fix that cannot break
     anything else: the chrome stays exactly where it is, and the keyboard now
     has somewhere to be that is not underneath it. */
  html main:has(#kb) { padding-bottom: 104px; }
}

/* ═══════ the round chrome docks into the header on the daily games (v16) ═══════
   ⭐⭐ THE MEASUREMENT THAT DECIDED THIS, because the obvious fix does not work.
   Sampling nine scroll positions on wordle / blockle / connections / steamdle
   and recording every viewport row a NON-chrome control passes through:

     bands never touched by a game control … NONE on three of the four pages,
                                             and a single 40px slot on the fourth

   A `position: fixed` button therefore cannot be MOVED out of trouble — the
   whole page scrolls under it, so wherever it is parked it will sit on a
   control at some scroll position. Relocating within the page is not a
   smaller version of the fix, it is a different bug.
   ⭐ The one band that is genuinely safe is the STICKY HEADER, because content
   already passes underneath it by design; nothing is lost by putting chrome
   there. So on the daily games the two round buttons stop floating and dock
   into the header's empty middle — between the wordmark and the menu button,
   which the burger work above emptied out.

   ⚠ THE AI BUTTON WAS THE WORSE OFFENDER and I had not been looking at it:
   elementFromPoint had it covering `#key-l` AND the wide Enter/Backspace key on
   wordle, plus FAQ rows on steamdle and the footer links on all four. The
   layout button only ever caught one key.
   ⚠ `.lm-back-fab` IS DELIBERATELY LEFT WHERE IT IS — measured across all nine
   scroll positions on all four pages, it never covers a control at bottom:62px.
   Moving it down into the corner the round buttons just vacated would look
   tidier and would put it exactly where the layout button was caught. Tidiness
   is not worth reintroducing the bug.

   ⚠ SCOPED ON `#lmChain`, WHICH IS STATIC MARKUP (`<div id="lmChain">` sits in
   each game's HTML and games-chain.js fills it later). That matters: this file
   already carries a scar from `html:has(#lm-lay-btn.lm-lay-inhead)`, which was
   only true AFTER flow.js mounted, so the pill visibly HOPPED 48px on every
   load. A marker present at first paint cannot hop.
   ⚠ `:not(:has(#nav))` excludes the homepage, which has its own full nav and
   none of this layout. It carried a chain of its own until the homepage
   simplification took it out; this guard is why that removal changed nothing
   here, and it stays as the thing that keeps a page with both from qualifying. */
@media (max-width: 560px) {
  /* THREE controls, anchored from the LEFT in a row after the wordmark. Left,
     not centred: the wordmark is a bounded width (83-93px measured across the
     six games) so 124px clears it on every one of them, whereas the burger's
     position depends on each page's own header padding and is not a number
     worth hard-coding. Verified by measurement, not arithmetic — the probe
     counts the logo and the burger as controls, so any overlap with either
     would show up as a collision. */
  html body:has(#lmChain):not(:has(#nav)) #lm-lay-btn,
  html body:has(#lmChain):not(:has(#nav)) #lm-ai-btn,
  html body:has(#lmChain):not(:has(#nav)) .lm-back-fab {
    top: 6px; bottom: auto; right: auto; z-index: 110;
  }
  html body:has(#lmChain):not(:has(#nav)) .lm-back-fab { left: 124px; }
  html body:has(#lmChain):not(:has(#nav)) #lm-lay-btn { left: 172px; }
  html body:has(#lmChain):not(:has(#nav)) #lm-ai-btn { left: 220px; }
  /* ── the back control becomes an icon ──
     "← Free Games" is 113px and there is not 113px of header to give it. Safe
     to drop the words because flow.js already writes `aria-label="Back to
     <name>"` and a matching title on this control, so the destination is still
     announced and still shows on hover — the label was never the accessible
     name. font-size:0 collapses the text node without removing it from the
     accessibility tree. */
  html body:has(#lmChain):not(:has(#nav)) .lm-back-fab {
    width: 44px; height: 44px; min-height: 44px; padding: 0;
    justify-content: center; font-size: 0; border-radius: 8px;
  }
  html body:has(#lmChain):not(:has(#nav)) .lm-back-fab::before {
    content: "\2190"; font-size: 1.15rem; line-height: 1; font-weight: 400;
  }
  /* the panels follow their buttons — one opening upward from the bottom of the
     screen while its trigger sits at the top is worse than either position */
  html body:has(#lmChain):not(:has(#nav)) #lm-lay-panel,
  html body:has(#lmChain):not(:has(#nav)) #lm-ai-panel {
    top: 58px; bottom: auto; left: 50%; right: auto; transform: translateX(-50%);
    max-width: calc(100vw - 28px); max-height: calc(100vh - 76px); overflow-y: auto;
  }
  /* ── the header goes OPAQUE on the daily games ──
     Mid-scroll the keyboard passes under the sticky header like all content,
     and through the translucent glass the keys showed behind the three docked
     controls above. Measured before deciding: no tap is ever stolen — the
     header owns this strip whether the buttons exist or not — so the problem
     was purely visual, and solid ink ends the see-through clutter without
     hiding a single control. Owner's pick over hide-on-scroll, 2026-07-31. */
  html body:has(#lmChain):not(:has(#nav)) header {
    background: var(--bg, #0e0f11);
  }
}

/* ── small icon buttons in tight chrome ──
   ⭐ A 24x24 close button in a 40px-tall bar cannot BE 44px without wrecking
   the bar. An invisible ::after that overflows it can: the paint stays exactly
   where the design put it and the finger gets its 44px anyway. `inset` is
   negative on purpose. pointer-events stay on, which is the whole trick.
   ⚠ The host needs `position: relative` or the pseudo escapes to the page.

   ⚠⚠ ALL OF IT IS INSIDE THE TOUCH QUERY, and it was not on the first pass —
   that mistake is why this comment exists. Written unconditionally, these rules
   grew the floating chrome on DESKTOP too, and because the injected nav's
   height is what flow.js reserves body padding from, a taller wordmark pushed
   whole pages down: the visual-regression run went from 0.04% to 10.81% on
   /crosshair-generator, 8.78% on /cps-test and 14.72% on /status — the ten
   pages that carry that bar. A mouse does not need a 44px target and nothing
   here should cost a desktop visitor a pixel.
   ⭐ The catch was the harness, not the diff: the CSS reads perfectly. */
@media (max-width: 700px), (pointer: coarse) {
.lm-disc-min { position: relative; }
.lm-disc-min::after { content: ""; position: absolute; inset: -10px; border-radius: 8px; }
/* ⚠ THE DISCORD BAR GETS REAL SIZE, NOT THE PSEUDO TRICK — measured, because
   the trick was tried here first and DOES NOT WORK in this one place. Probing
   with elementFromPoint at the four corners of the 44px square: breadcrumbs and
   the logo answer 4/4, and these two answer 0/4. The bar is packed edge to
   edge, so an overflowing pseudo lands under the bar's own text, which paints
   later and swallows the tap. An invisible hit area that silently does nothing
   is worse than no fix, because it reads as done.
   ⭐ The general lesson: getBoundingClientRect cannot see a pseudo-element, so
   neither the audit NOR a screenshot can tell a working hit area from a dead
   one. Only asking the browser who receives the tap can. */
#lm-disc .lm-disc-go { padding-top: 0.5rem; padding-bottom: 0.5rem; }
#lm-disc .lm-disc-min { width: 32px; height: 32px; }
/* ⚠ …and the same trick for the inline back control, for a DIFFERENT reason.
   `.crumbs .lm-back` is display:inline-block, and its own comment says the
   point of that is that inserting it "cannot change that line's height" —
   vertical padding on an inline-block DOES grow the line box, so padding here
   would undo a deliberate decision. The pseudo grows only the hit area.
   ⚠ It also needs its own selector: `.crumbs .lm-back` is (0,2,0) and beat the
   `html .crumbs a` padding rule above outright, which is why the back control
   was the one crumb still measuring 21px tall after the first pass. */
.crumbs .lm-back { position: relative; }
.crumbs .lm-back::after { content: ""; position: absolute; inset: -12px -2px; }
/* the layout picker measured 40x40 — four pixels short, so just say 44 */
#lm-lay-btn { width: 44px; height: 44px; }
/* the floating back pill, 34px tall */
.lm-back-fab { min-height: 44px; display: inline-flex; align-items: center; }
/* the injected nav's wordmark, 20px tall — the takeover's got its padding from
   `header a.logo`, but #lm-nav is not a <header>, so it was missed */
#lm-nav .lmn-logo { padding-top: 0.6rem; padding-bottom: 0.6rem; }
/* ── the daily-game chain ──
   Six 36x22 links in a row on the seven daily-game pages. They are INLINE, so
   vertical padding grows the hit box and leaves the line box exactly where it
   was — the same trick the breadcrumbs use, and the reason none of this reflows
   a page.
   ⚠⚠ VERTICAL ONLY, and the horizontal padding this shipped with is why. Each
   link is a glyph plus a ▶/✓ marker, and the chips already carry
   `margin: 0 0.35rem` from an inline style; 0.15rem each side added ~29px
   across the row and tipped it past the width of a 374px phone. The row then
   wrapped IN THE MIDDLE OF A LINK — the sixth icon on one line and its own ▶
   alone on the next. Caught by looking at the deployed page, not by the
   numbers: the hit area got bigger exactly as intended and the audit was happy.
   ⭐ `nowrap` is belt and braces — a glyph and its status marker are one label
   and must never be split, whatever the row width does later. */
html #lmChain a { padding: 0.6rem 0; white-space: nowrap; }
}

/* ── the reading floor ──
   11.2px (0.7rem, the bottom rung of the type scale) is fine for a caption on a
   desktop and too small on a phone held at arm's length. Raised only where the
   text is CHROME, never where a smaller size is doing a job: Connections tiles
   use `clamp(0.6rem, 2.6vw, 0.82rem)` so a long word fits its box, and
   recipe-book's corner badges are deliberate quiet markers. Both were read
   before being left alone. */
@media (max-width: 560px) {
  html #lm-ai-panel .aiq { font-size: 0.8rem; }
  html .lm-lay-row .t { font-size: 0.8rem; }
}

/* ═══════ skip link (v16) ═══════
   Injected by flow.js. Zero of the 60 pages audited had one, and every page
   carries a sticky nav of eight links — so a keyboard user paid eight-plus Tab
   presses to reach the content, on every page, every time.
   ⚠ NOT display:none or visibility:hidden when idle — either would make it
   unfocusable and the link would not exist for the only people who need it.
   It is moved off-screen with a transform, which keeps it in the tab order. */
a.lm-skip {
  position: fixed; top: 0; left: 0; z-index: 200;
  transform: translateY(-150%);
  /* inline-flex + min-height so the link itself clears 44px — it measured 40
     on the first pass, i.e. the control added FOR accessibility was itself
     under the touch floor */
  display: inline-flex; align-items: center; min-height: 44px;
  background: var(--red, #e5484d); color: var(--on-red, #fff);
  padding: 0.7rem 1.1rem; border-radius: 0 0 10px 0;
  font-size: 0.9rem; font-weight: 650; text-decoration: none; line-height: 1.2;
  transition: transform 0.12s ease;
}
a.lm-skip:focus { transform: translateY(0); }
/* ⚠ The skip target takes tabindex="-1" so script can focus it — which makes it
   match the global [tabindex]:focus-visible rule above and draw a 2px red
   outline around the ENTIRE page body. A scroll destination is not a control;
   it should not look like one. */
html main[tabindex="-1"]:focus, html main[tabindex="-1"]:focus-visible,
html [role="main"][tabindex="-1"]:focus, html [role="main"][tabindex="-1"]:focus-visible,
html article[tabindex="-1"]:focus, html article[tabindex="-1"]:focus-visible,
html h1[tabindex="-1"]:focus, html h1[tabindex="-1"]:focus-visible { outline: none; }
@media (prefers-reduced-motion: reduce) { a.lm-skip { transition: none; } }

/* ═══════ the unglamorous states (v16) — loading · error · nothing found ═══════
   These are the screens nobody photographs, because you only reach them when
   something is slow, missing or broken. So they ship unseen, and they did:
   MEASURED on a live page, the shared status slot renders at 14.4px in
   rgba(255,255,255,0.56) with ZERO animations, and the same element carries all
   three messages. "Loading spells…", "Could not reach the D&D 5e API" and
   "Search an item above to see its stats" are, to look at, the same picture.

   ⭐ THE LEVERAGE: 21 pages already share `<div class="loading" id="loading">`
   as one status slot, and 32 share `.err`. flow.js tells the three states apart
   (structurally — see there) and stamps a class; everything below is CSS, so
   this lands on every one of those pages with no page edits at all.

   ⚠ Written WITHOUT :where(). Pages define `.loading{color:var(--muted);
   font-size:0.9rem}` at (0,1,0) and `.err{color:var(--danger)}` the same, so a
   zero-specificity rule loses every time — the trap this file has already been
   caught by twice. `html .loading.lm-st-x` is (0,2,1) and wins on count. */

/* ── shared shell for all three ── */
html .loading.lm-st-loading,
html .loading.lm-st-error,
html .loading.lm-st-hint,
html .lm-st-loading, html .lm-st-error, html .lm-st-hint {
  display: flex; align-items: flex-start; gap: 0.7rem;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--border, #26282d);
  border-left-width: 3px;
  border-radius: 8px;
  font-size: 0.9rem; line-height: 1.5;
  background: var(--panel, #16171a);
}

/* ── 1. LOADING ── the text is not the point; the MOTION is.
   A visitor cannot tell a slow page from a dead one, and 56%-grey static text
   answers neither question. An indeterminate hairline sweeping under the label
   says "still working" without pretending to know a percentage.
   ⚠ NOT gated behind prefers-reduced-motion, on purpose and against the usual
   reflex. Two reasons: this project already learned that the owner's own
   Windows asks for reduced motion (batch 65b — gating the homepage on it hid
   the site from them), and a loading indicator's movement IS its information,
   which is exactly the case the guidance exempts. It is 2px and slow. */
html .loading.lm-st-loading, html .lm-st-loading {
  position: relative; overflow: hidden;
  border-left-color: var(--red, #e5484d);
  color: var(--text, #e7e8ea);
}
html .lm-st-loading::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 2px; width: 40%;
  background: linear-gradient(90deg, transparent, var(--red, #e5484d), transparent);
  animation: lmStBar 1.15s ease-in-out infinite;
}
@keyframes lmStBar { 0% { transform: translateX(-110%); } 100% { transform: translateX(360%); } }
/* the pulsing dot that marks the row as live even in a still screenshot */
html .lm-st-loading::before {
  content: ""; flex: none; width: 8px; height: 8px; margin-top: 0.35rem;
  border-radius: 50%; background: var(--red, #e5484d);
  animation: lmStPulse 1.15s ease-in-out infinite;
}
@keyframes lmStPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }

/* ── 2. ERROR ── it was one line of small red text floating in dead space, the
   same weight as a caption. An error is the most important thing on the page
   when it happens, so it gets a panel, a danger edge and full-strength ink. */
html .loading.lm-st-error, html .lm-st-error {
  border-left-color: var(--danger, #e5484d);
  background: color-mix(in srgb, var(--danger, #e5484d) 8%, var(--panel, #16171a));
  color: var(--text, #e7e8ea);
}
/* ⚠ color-mix is recent; this is the fallback order, not a duplicate — a
   browser that cannot parse the line above keeps the plain panel and loses
   only the tint. */
@supports not (background: color-mix(in srgb, red 8%, blue)) {
  html .lm-st-error { background: var(--panel, #16171a); }
}
html .lm-st-error::before {
  content: "!"; flex: none;
  width: 20px; height: 20px; margin-top: 0.1rem;
  display: grid; place-items: center;
  border-radius: 50%; background: var(--danger, #e5484d); color: #fff;
  font-size: 0.8rem; font-weight: 700; line-height: 1;
}
/* the message inside stops being a lone red sentence and becomes the body text
   of the block it now lives in */
html .lm-st-error .err { color: var(--text, #e7e8ea); }

/* ── 3. HINT ── "Search an item above to see its stats" is not a failure and
   should not look like one. Quiet edge, muted ink, no motion: it is guidance
   for a page where nothing has happened yet. */
html .loading.lm-st-hint, html .lm-st-hint {
  border-left-color: var(--border-hi, #3a3d44);
  color: var(--muted, #8b8f96);
  background: transparent;
}

/* ── 4. NOTHING FOUND ── the empty row inside a results dropdown.
   MEASURED: 16 of these are written as
   `<div class="ritem" style="cursor:default;color:var(--muted)">No item found.</div>`
   — i.e. a RESULT ROW, so it takes the hover highlight and reads as something
   you can click. It keeps .ritem (that is where the dropdown's own padding and
   dividers come from — dropping it would need this rule to re-invent them per
   page) and adds .lm-none, which turns off the affordances and says plainly
   that the list is empty. */
html .lm-none {
  cursor: default;
  color: var(--muted, #8b8f96);
  display: flex; align-items: center; gap: 0.5rem;
  font-style: normal;
}
html .lm-none:hover, html .ritem.lm-none:hover, html .ritem.lm-none.sel { background: none; }
html .lm-none::before {
  content: ""; flex: none; width: 14px; height: 14px;
  border: 1.5px solid currentColor; border-radius: 50%; opacity: 0.55;
  /* a struck-through circle: "none", drawn rather than spelled */
  background: linear-gradient(to bottom right, transparent calc(50% - 0.9px), currentColor calc(50% - 0.9px), currentColor calc(50% + 0.9px), transparent calc(50% + 0.9px));
}

/* ── …and the contradiction giving both at once ──
   ⚠ THIS ONE IS MY OWN DOING, or at least my own amplification. These pages
   show "No item found." in the dropdown AND "Search an item above to see its
   stats." underneath, at the same time — one saying your search failed, the
   other saying you have not searched. It was always there, but as two runs of
   faint grey text it read as background noise; once both became bordered
   blocks it read as two boxes disagreeing.
   The idle hint is for a page where nothing has happened yet, so it stands
   down while a search is actively showing no results. `.show` is the universal
   open-dropdown class here — checked, all 19 of the swept pages use it and 20
   define `.results.show{display:block}`.
   ⚠ A browser without :has() simply keeps today's behaviour. */
html body:has(.show .lm-none) .lm-st-hint { display: none; }

/* ═══════ the button component (v16) ═══════
   MEASURED across the 133 files that define a bare `.btn` rule, and the result
   is not what "15 button-ish class names" suggested. They already agree almost
   everywhere:
       cursor         1 distinct value   (pointer, 95 of 95)
       font-family    1 distinct value   (inherit, 91 of 91)
       border-radius  2 distinct         (8px x91, 999px x1)
       font-weight    3 distinct         (600 x87)
       font-size      4 distinct         (0.8rem x56, 0.9rem x41 — both on scale)
   ⭐ THE ONE REAL SPLIT IS NOT DRIFT, IT IS TWO COMPONENTS SHARING A NAME:
       loud   background var(--red)     colour var(--on-red)  border none      (~45 pages)
       quiet  background var(--panel2)  colour var(--text)    border 1px solid (~45 pages)
   So `.btn` means "primary" on half the site and "secondary" on the other half.
   Picking one would restyle roughly forty-five pages' worth of buttons, which
   is a visible design decision and not mine to make quietly — it is reported
   rather than swept.

   ⚠ WRITTEN WITH :where(), DELIBERATELY, and this is the opposite of the
   `.lmn-took` case where :where() made rules silently inert. Here inert-when-a-
   page-speaks is exactly right: every one of these properties is already
   declared identically by the pages that have buttons, so this changes NOTHING
   that exists. What it buys is that the next page to add a button gets the
   component for free instead of copying a 134th definition. */
:where(.btn, .cta, a.get, .addbot, button.btn) {
  cursor: pointer;
  font-family: inherit;
  font-weight: 600;
  border-radius: 8px;
  text-decoration: none;
}

/* ═══════ site search (v16) — injected by flow.js ═══════
   The catalogue of all 169 tools used to live only inside /app/, so from any
   other page you had to know where to go before you could look for anything.
   Ctrl+K, "/", or the Search item in the nav. */
#lm-search {
  position: fixed; inset: 0; z-index: 210;
  display: flex; justify-content: center; align-items: flex-start;
  padding: clamp(0.9rem, 8vh, 5rem) 0.9rem 0.9rem;
  background: rgba(0, 0, 0, 0.62);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  font-family: 'Segoe UI', system-ui, sans-serif;
}
#lm-search[hidden] { display: none; }
/* the page behind must not scroll while a modal is over it */
html.lm-search-open, html.lm-search-open body { overflow: hidden; }
#lm-search .lm-sp {
  width: min(560px, 100%); max-height: 80vh; display: flex; flex-direction: column;
  background: var(--panel, #16171a); border: 1px solid var(--border, #26282d);
  border-radius: 12px; box-shadow: 0 24px 70px rgba(0, 0, 0, 0.6); overflow: hidden;
}
#lm-search .lm-sq {
  flex: none; width: 100%; min-height: 52px; box-sizing: border-box;
  background: transparent; border: 0; border-bottom: 1px solid var(--border, #26282d);
  color: var(--text, #e7e8ea); font-family: inherit; font-size: 1rem;
  padding: 0.9rem 1.1rem; outline: none;
}
#lm-search .lm-sq::placeholder { color: var(--muted, #8b8f96); }
/* ⚠ the input is bare by design (the panel edge IS its boundary), so the
   global focus ring would draw a box inside a box. It is the only focusable
   thing when the palette opens, and the caret is in it. */
#lm-search .lm-sq:focus-visible { outline: none; }
#lm-search .lm-sh { margin: 0; padding: 1.1rem; color: var(--muted, #8b8f96); font-size: 0.9rem; }
#lm-search .lm-sl { overflow-y: auto; -webkit-overflow-scrolling: touch; }
#lm-search .lm-sr {
  display: flex; align-items: center; gap: 0.7rem;
  min-height: 44px; padding: 0.5rem 1.1rem;
  color: var(--text, #e7e8ea); text-decoration: none; font-size: 0.9rem;
  border-left: 3px solid transparent;
}
#lm-search .lm-sr-n { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#lm-search .lm-sr-p {
  flex: none; font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--muted, #8b8f96);
}
/* one highlight for hover AND keyboard, so the two never disagree about which
   row Enter is going to open */
#lm-search .lm-sr.on, #lm-search .lm-sr:hover {
  background: var(--panel2, #1c1d21); border-left-color: var(--red, #e5484d);
}
#lm-search .lm-sr:focus-visible { outline: 2px solid var(--red, #e5484d); outline-offset: -2px; }

/* ── the Search item in the nav ── */
nav .lmn-search {
  display: inline-flex; align-items: center; gap: 0.4rem;
  background: none; border: 0; padding: 0; cursor: pointer;
  color: inherit; font: inherit; font-family: inherit;
}
nav .lmn-search i {
  flex: none; width: 13px; height: 13px; border: 1.6px solid currentColor;
  border-radius: 50%; position: relative;
}
nav .lmn-search i::after {
  content: ""; position: absolute; right: -5px; bottom: -4px;
  width: 6px; height: 1.6px; background: currentColor; transform: rotate(45deg);
}
nav .lmn-search:hover { color: var(--text, #e7e8ea); }
/* ⚠ MEASURED, because this is a NINTH item in a row the notes already warned
   about ("don't add another without re-measuring"). Signed in, every item sits
   on one row down to 700px; at 640px "Sign in" drops to a second line. Dropping
   the word between 561 and 760 buys back ~40px and puts the wrap point back
   where it was, while the icon keeps the control there.
   ⚠ The second query is not a duplicate — below 561px the links are a vertical
   drawer, where a lone magnifier with no label would be the one unreadable row
   in a list of words. */
@media (max-width: 760px) { nav .lmn-search span { display: none; } }
@media (max-width: 560px) { nav .lmn-search span { display: inline; } }

/* ═══════ first run: a box that says what it is waiting for (v16) ═══════
   Swept 24 tool pages on a FRESH profile looking for the shape a missing
   first-run state takes — a visibly bordered or filled container, big enough to
   read as a region, with nothing at all inside. Four hits, and reading each one
   mattered more than counting them: a tier row's drop zone and the colour
   picker's swatch are SUPPOSED to be empty, and were left alone. Two were real:
   the tier list's "Cards to rank" tray, and the colour-code generator's output
   box sitting under the words "Click the output below to copy" — an instruction
   pointing at a blank rectangle.
   ⭐ Written as a PRIMITIVE, not two one-off rules: any container can say what
   it is waiting for by carrying the sentence in an attribute, and the message
   disappears by itself the moment real content arrives, because `:empty` stops
   matching. No JS, no state to get out of sync.
   ⚠ `:empty` is strict — a single newline between the tags is a text node and
   the element no longer matches. Both call sites are written closed-up, and
   that is load-bearing rather than a formatting accident. */
[data-empty-hint]:empty::before {
  content: attr(data-empty-hint);
  display: block;
  padding: 0.9rem 1.1rem;
  color: var(--muted, #8b8f96);
  font-size: 0.9rem;
  line-height: 1.5;
  text-align: center;
}

/* ═══════ offline indicator (v16) — injected by flow.js ═══════
   Top-centre, not the bottom: the bottom edge already holds the Discord bar,
   the layout button and the helper, and a connection notice that has to queue
   behind three pieces of promotional furniture is not a notice. */
#lm-offline {
  position: fixed; top: 10px; left: 50%; transform: translateX(-50%);
  z-index: 200; max-width: calc(100vw - 24px);
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.5rem 0.9rem; border-radius: 999px;
  background: var(--panel, #16171a); border: 1px solid var(--danger, #e5484d);
  color: var(--text, #e7e8ea); font-family: 'Segoe UI', system-ui, sans-serif;
  font-size: 0.8rem; line-height: 1.4; text-align: center;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
#lm-offline::before {
  content: ""; flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--danger, #e5484d);
}
#lm-offline.lm-off-back { border-color: var(--ok, #46a758); }
#lm-offline.lm-off-back::before { background: var(--ok, #46a758); }
/* ⚠ the Discord bar stands down while offline — it is an invitation to join a
   service the visitor demonstrably cannot reach, sitting on top of a page they
   are trying to read. */
html.lm-is-offline #lm-disc, html.lm-is-offline #lm-disc-tab { display: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   MOTION LAYER (v17) — thirty distinct behaviours, one per thing that happens
   ═══════════════════════════════════════════════════════════════════════════
   MEASURED FIRST: the site was never short of motion — 560 transition
   declarations, 210 of them involving a transform, 88 keyframes, and 94-100%
   of every control category already animating something. What it was short of
   was VARIETY: 244 of those transitions animate `border-color` and nothing
   else, so almost every hover on the site says the same quiet thing.

   So this adds RANGE rather than volume, and every behaviour is tied to
   something the visitor did — hover, press, focus, open, arrive. Nothing here
   plays by itself while you are trying to read, because that is exactly the
   effect I was asked to delete from /tools/ once already.

   ⚠⚠ THE CONFLICT THIS IS BUILT AROUND. `transition` is a SHORTHAND: a rule
   that sets it replaces the page's own entirely, so a naive site-wide
   `transition: transform .2s` would silently delete 244 border-colour fades.
   Two techniques avoid that:
     · effects drawn on ::before / ::after, which no page styles, so they can
       never collide;
     · where the element itself must move, the transition is written as a
       SUPERSET (transform, opacity, border-color, box-shadow, background,
       color) so nothing a page was already easing is lost.
   ⚠ transform and opacity only on the moving parts — both composite on the
   GPU, so none of this can cost a frame on a phone.
   ⚠ Gated on `html:not([data-fx="off"])`, which `?fx=0` now sets (flow.js).
   ⚠ NOT gated on prefers-reduced-motion, for the reason batch 65b recorded:
   this owner's own Windows requests it, and gating on it once hid the whole
   site from them. `?fx=0` is the honest opt-out.
   ⚠ .lm-fx-off (fullscreen) stands the whole layer down — see the end.  */

/* ═══════ THE TIMING SCALE (v19) ═══════
   ⚠⚠ MOVED OUT OF THE data-fx GUARD, AND THAT IS THE POINT. These three used to
   be declared inside `html:not([data-fx="off"])`, so under ?fx=0 they did not
   exist — which meant NO PAGE COULD SAFELY SPEND THEM. A `var(--lm-quick)` in a
   page would resolve to nothing for anyone with effects off and take the whole
   transition declaration invalid with it. That is why the tokens had 30 uses
   inside this file and ZERO across the other 200 pages: they were unusable, not
   unpopular.
   The values live on plain `html` now and are always defined. The OPT-OUT is
   unaffected — every effect below is still gated on the attribute, because the
   gate belongs on the rules, not on the numbers they read.

   ⚠ --lm-quick IS 0.15s, NOT 0.16s. Measured across 136 files: 911 transition
   durations, 52 distinct values, and 0.15s/.15s accounts for 482 of them — 53%.
   The token was 0.16s, so the site's single most-used timing could never be
   spelled with the token that exists for it. Aligning the token to the corpus is
   one line; sweeping 482 declarations to match an arbitrary 0.16s would be a
   136-file diff for a one-millisecond difference nobody can perceive.
   ⚠ --lm-slow names the .34s cluster (46 uses), the third real step. */
html {
  --lm-ease: cubic-bezier(0.2, 0.7, 0.3, 1);      /* settles, never bounces */
  --lm-quick: 0.15s;
  --lm-calm: 0.28s;
  --lm-slow: 0.34s;

  /* -- the asymmetric hover law (Nightfall port, phase 1) ------------------
     Hover ENGAGES fast and LETS GO slow. One duration in both directions is
     the most common reason an interface feels cheap: the pointer leaves and
     the surface snaps after it. A hoverable states --lm-out at rest and
     overrides to --lm-in on :hover, so the asymmetry costs one extra
     declaration per rule and no script.
     Nothing below spends these yet -- the controls layer does, next phase. */
  --lm-in: 0.14s;
  --lm-out: 0.42s;

  /* two curves --lm-ease does not cover: it settles, and never travels. */
  --lm-arrive: cubic-bezier(0.2, 0.6, 0.2, 1);    /* things that settle in */
  --lm-travel: cubic-bezier(0.5, 0, 0.2, 1);      /* things that cross the screen */

  /* --lm-big names the 0.62s the sheen below already runs at, and
     --lm-press-scale the 0.978 the press below already uses -- the same move
     as aligning --lm-quick to the corpus rather than the corpus to the token.
     ⚠ --lm-press is the port's 70ms; the press rule below spends 0.06s and is
     NOT changed here. Phase 1 declares and paints nothing. */
  --lm-big: 0.62s;
  --lm-press: 70ms;
  --lm-press-scale: 0.978;
}

/* ── the disabled state, once ──
   Seven different opacities across the tree: .45 x22, .5 x10, .55 x4, then .4,
   .35, .3, .75. This file was itself part of the split — its .btn:disabled says
   .45 while its checkbox/radio/range and select rules say .5.
   .45 is the plurality and the one the button block already uses, so it becomes
   the answer for the controls that had no rule at all.
   ⚠ 0-1-1 via `html`, so a page that genuinely wants a different disabled look
   still wins — this is a floor for the controls nobody styled, not a mandate. */
html :is(button, input, select, textarea):disabled,
html [aria-disabled="true"] { opacity: .45; cursor: not-allowed; }

/* ── 1. BUTTON LIFT + 2. PRESS ───────────────────────────────────────────
   The superset transition: everything a page might already have been easing
   is named here, so adding transform costs nothing that existed. */
html:not([data-fx="off"]) :is(.btn, .cta, a.get, .addbot, button.btn) {
  transition: transform var(--lm-quick) var(--lm-ease),
              box-shadow var(--lm-quick) var(--lm-ease),
              border-color var(--lm-quick) var(--lm-ease),
              background var(--lm-quick) var(--lm-ease),
              color var(--lm-quick) var(--lm-ease),
              opacity var(--lm-quick) var(--lm-ease);
}
html:not([data-fx="off"]) :is(.btn, .cta, a.get, .addbot):hover {
  transform: translateY(-1.5px);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
html:not([data-fx="off"]) :is(.btn, .cta, a.get, .addbot):active {
  transform: translateY(0) scale(0.978);
  box-shadow: none;
  transition-duration: 0.06s;                      /* a press must feel instant */
}

/* ── 3. BUTTON SHEEN ── a light passes across the face, once, on hover.
   Entirely inside a pseudo-element, so no page's styling is touched. */
html:not([data-fx="off"]) :is(.btn, .cta, a.get) { position: relative; overflow: hidden; }
html:not([data-fx="off"]) :is(.btn, .cta, a.get)::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(105deg, transparent 35%, rgba(255,255,255,0.13) 50%, transparent 65%);
  transform: translateX(-120%);
  opacity: 0;
}
html:not([data-fx="off"]) :is(.btn, .cta, a.get):hover::after {
  animation: lmSheen 0.62s var(--lm-ease);
}
@keyframes lmSheen {
  from { transform: translateX(-120%); opacity: 1; }
  to   { transform: translateX(120%);  opacity: 1; }
}

/* ── 4. CARD LIFT + 5. CARD EDGE WARM ────────────────────────────────────
   Cards get a longer, calmer rise than buttons — a card is a place, a button
   is an action, and they should not move at the same speed. */
html:not([data-fx="off"]) :is(.card, .tool-panel, .hublink, .related a, .g-card, .m-card, .t-card, .d-card, .rcard, .mcard, .acard) {
  transition: transform var(--lm-calm) var(--lm-ease),
              box-shadow var(--lm-calm) var(--lm-ease),
              border-color var(--lm-calm) var(--lm-ease),
              background var(--lm-calm) var(--lm-ease);
}
html:not([data-fx="off"]) :is(.card, .tool-panel, .hublink, .related a, .g-card, .m-card, .t-card, .d-card, .rcard, .mcard, .acard):hover {
  transform: translateY(-3px);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.4);
}
html:not([data-fx="off"]) :is(.card, .hublink, .related a):active { transform: translateY(-1px) scale(0.995); }

/* ── 6. INLINE LINK UNDERLINE ── wipes in from the left, out to the right, so
   entering and leaving are visibly different gestures. */
html:not([data-fx="off"]) :is(main, article) p > a[href],
html:not([data-fx="off"]) :is(main, article) li > a[href] {
  position: relative; text-decoration: none;
}
html:not([data-fx="off"]) :is(main, article) p > a[href]::after,
html:not([data-fx="off"]) :is(main, article) li > a[href]::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -2px; height: 1px;
  background: currentColor; transform: scaleX(0); transform-origin: right;
  transition: transform var(--lm-quick) var(--lm-ease);
}
html:not([data-fx="off"]) :is(main, article) p > a[href]:hover::after,
html:not([data-fx="off"]) :is(main, article) li > a[href]:hover::after {
  transform: scaleX(1); transform-origin: left;
}

/* ── 7. NAV UNDERLINE ── grows from the centre, which reads as "this one",
   where the inline-link wipe reads as "follow this". */
html:not([data-fx="off"]) nav.lmn-took .lmn-links a,
html:not([data-fx="off"]) #lm-nav .lmn-links a { position: relative; }
html:not([data-fx="off"]) nav.lmn-took .lmn-links a::after,
html:not([data-fx="off"]) #lm-nav .lmn-links a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -3px; height: 1.5px;
  background: var(--red, #e5484d); border-radius: 2px;
  transform: scaleX(0); transition: transform var(--lm-quick) var(--lm-ease);
}
html:not([data-fx="off"]) nav.lmn-took .lmn-links a:hover::after,
html:not([data-fx="off"]) #lm-nav .lmn-links a:hover::after,
/* ⚠ the underline used to fire for .lmn-took only, so the two bars disagreed on
   whether the current item is underlined as well as on whether it is brighter.
   Both bars, both aria-current states, one gesture. */
html:not([data-fx="off"]) nav.lmn-took .lmn-links a[aria-current]::after,
html:not([data-fx="off"]) #lm-nav .lmn-links a[aria-current]::after { transform: scaleX(1); }

/* ── 8. CHIP POP ── small things deserve a small, fast gesture. */
html:not([data-fx="off"]) :is(.chip, .tag, .pick, .cat-chip, .badge, .lmn-links a.lmn-auth) {
  transition: transform var(--lm-quick) var(--lm-ease),
              border-color var(--lm-quick) var(--lm-ease),
              background var(--lm-quick) var(--lm-ease),
              color var(--lm-quick) var(--lm-ease);
}
html:not([data-fx="off"]) :is(.chip, .tag, .pick, .cat-chip):hover { transform: translateY(-1px) scale(1.035); }
html:not([data-fx="off"]) :is(.chip, .tag, .pick, .cat-chip):active { transform: scale(0.96); }

/* ── 9. ROW ACCENT BAR + 10. ROW TINT ── a table row grows an edge rather than
   flashing a colour, so a long list does not strobe as the pointer crosses it. */
html:not([data-fx="off"]) :is(tbody tr, .ritem, .lm-lay-row) { position: relative; }
html:not([data-fx="off"]) :is(tbody tr, .ritem, .lm-lay-row)::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--red, #e5484d); transform: scaleY(0); transform-origin: center;
  transition: transform var(--lm-quick) var(--lm-ease); pointer-events: none;
}
html:not([data-fx="off"]) :is(tbody tr, .ritem, .lm-lay-row):hover::before { transform: scaleY(1); }

/* ── 11. INPUT FOCUS BLOOM ── the ring expands rather than appearing. */
html:not([data-fx="off"]) :is(input:not([type=checkbox]):not([type=radio]):not([type=range]), textarea, select) {
  transition: border-color var(--lm-quick) var(--lm-ease),
              box-shadow var(--lm-calm) var(--lm-ease),
              background var(--lm-quick) var(--lm-ease);
}
html:not([data-fx="off"]) :is(input:not([type=checkbox]):not([type=radio]):not([type=range]), textarea):focus {
  box-shadow: 0 0 0 3px rgba(var(--glow, 229, 72, 77), 0.16);
}

/* ── 12. TICK-IN ── a checkbox that snaps shut has more authority than one
   that fades. Scale overshoot is deliberate and tiny. */
html:not([data-fx="off"]) :is(input[type=checkbox], input[type=radio]):checked {
  animation: lmTick 0.2s var(--lm-ease);
}
@keyframes lmTick { 0% { transform: scale(0.82); } 60% { transform: scale(1.08); } 100% { transform: scale(1); } }

/* ── 13. DISCLOSURE MARKER + 14. CONTENT UNFOLD ─────────────────────────── */
html:not([data-fx="off"]) details > summary { transition: color var(--lm-quick) var(--lm-ease); }
html:not([data-fx="off"]) details > summary::marker { font-size: 0.9em; }
html:not([data-fx="off"]) details[open] > summary ~ * { animation: lmUnfold var(--lm-calm) var(--lm-ease); }
@keyframes lmUnfold { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }

/* ── 15. ICON NUDGE ── the drawn icon inside a control leans toward the action. */
html:not([data-fx="off"]) :is(.btn, .cta, a.get, .chip, .ritem, .related a) i.ic {
  transition: transform var(--lm-quick) var(--lm-ease);
}
html:not([data-fx="off"]) :is(.btn, .cta, a.get, .chip):hover i.ic { transform: translateX(1.5px); }
/* ── 16. …except an arrow, which travels further, and a refresh, which turns */
html:not([data-fx="off"]) :is(.btn, .cta, a.get):hover i.ic-arrow-right { transform: translateX(4px); }
html:not([data-fx="off"]) :is(.btn, .cta, a.get):hover i.ic-refresh { transform: rotate(150deg); }

/* ── 17. BREADCRUMB SLIDE ── each crumb steps toward where it would take you. */
html:not([data-fx="off"]) .crumbs a { transition: color var(--lm-quick) var(--lm-ease), transform var(--lm-quick) var(--lm-ease); display: inline-block; }
html:not([data-fx="off"]) .crumbs a:hover { transform: translateX(2px); }
/* ── 18. …and the back control pulls the other way, because it goes back. */
html:not([data-fx="off"]) .lm-back:hover { transform: translateX(-2px); }
html:not([data-fx="off"]) .lm-back { transition: color var(--lm-quick) var(--lm-ease), transform var(--lm-quick) var(--lm-ease), border-color var(--lm-quick) var(--lm-ease); }

/* ── 19. BURGER MORPH ── the three bars become a cross when the menu is open,
   so the button states what it will do next rather than what it did. */
html:not([data-fx="off"]) .lmn-burg span {
  transition: transform var(--lm-calm) var(--lm-ease), opacity var(--lm-quick) var(--lm-ease);
}
html:not([data-fx="off"]) .lmn-burg[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
html:not([data-fx="off"]) .lmn-burg[aria-expanded="true"] span:nth-child(2) { opacity: 0; transform: scaleX(0.3); }
html:not([data-fx="off"]) .lmn-burg[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

/* ── 20. DRAWER STAGGER ── the menu rows arrive in sequence, top to bottom,
   which reads as a list unrolling rather than a block appearing. */
html:not([data-fx="off"]) nav.lmn-open .lmn-links > *,
html:not([data-fx="off"]) #lm-nav.lmn-open .lmn-links > * {
  animation: lmRowIn 0.26s var(--lm-ease) backwards;
}
html:not([data-fx="off"]) .lmn-open .lmn-links > *:nth-child(1) { animation-delay: 0.02s; }
html:not([data-fx="off"]) .lmn-open .lmn-links > *:nth-child(2) { animation-delay: 0.04s; }
html:not([data-fx="off"]) .lmn-open .lmn-links > *:nth-child(3) { animation-delay: 0.06s; }
html:not([data-fx="off"]) .lmn-open .lmn-links > *:nth-child(4) { animation-delay: 0.08s; }
html:not([data-fx="off"]) .lmn-open .lmn-links > *:nth-child(5) { animation-delay: 0.10s; }
html:not([data-fx="off"]) .lmn-open .lmn-links > *:nth-child(6) { animation-delay: 0.12s; }
html:not([data-fx="off"]) .lmn-open .lmn-links > *:nth-child(7) { animation-delay: 0.14s; }
html:not([data-fx="off"]) .lmn-open .lmn-links > *:nth-child(8) { animation-delay: 0.16s; }
html:not([data-fx="off"]) .lmn-open .lmn-links > *:nth-child(9) { animation-delay: 0.18s; }
@keyframes lmRowIn { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }

/* ── 21. PALETTE ARRIVAL ── the search panel drops in; the backdrop fades. */
html:not([data-fx="off"]) #lm-search { animation: lmFade 0.14s linear; }
html:not([data-fx="off"]) #lm-search .lm-sp { animation: lmDrop 0.24s var(--lm-ease); }
@keyframes lmFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes lmDrop { from { opacity: 0; transform: translateY(-10px) scale(0.985); } to { opacity: 1; transform: none; } }
/* ── 22. RESULT CASCADE ── each result arrives a beat after the one above. */
html:not([data-fx="off"]) #lm-search .lm-sr { animation: lmRowIn 0.2s var(--lm-ease) backwards; }
html:not([data-fx="off"]) #lm-search .lm-sr:nth-child(1) { animation-delay: 0.00s; }
html:not([data-fx="off"]) #lm-search .lm-sr:nth-child(2) { animation-delay: 0.02s; }
html:not([data-fx="off"]) #lm-search .lm-sr:nth-child(3) { animation-delay: 0.04s; }
html:not([data-fx="off"]) #lm-search .lm-sr:nth-child(4) { animation-delay: 0.06s; }
html:not([data-fx="off"]) #lm-search .lm-sr:nth-child(5) { animation-delay: 0.08s; }
html:not([data-fx="off"]) #lm-search .lm-sr:nth-child(n+6) { animation-delay: 0.10s; }

/* ── 23. FLOATING CHROME BOB ── the corner controls rise to meet the pointer. */
html:not([data-fx="off"]) :is(#lm-lay-btn, #lm-ai-btn, .lm-back-fab, #lm-disc-tab) {
  transition: transform var(--lm-quick) var(--lm-ease), border-color var(--lm-quick) var(--lm-ease),
              color var(--lm-quick) var(--lm-ease), box-shadow var(--lm-quick) var(--lm-ease);
}
html:not([data-fx="off"]) :is(#lm-lay-btn, #lm-ai-btn, .lm-back-fab, #lm-disc-tab):hover {
  transform: translateY(-2px); box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45);
}

/* ── 24. DAILY CHAIN HOP ── the six game links hop, one at a time. */
html:not([data-fx="off"]) #lmChain a { display: inline-block; transition: transform var(--lm-quick) var(--lm-ease); }
html:not([data-fx="off"]) #lmChain a:hover { transform: translateY(-3px) scale(1.06); }

/* ── 25. PANEL RISE ── the layout and helper panels come up from their button. */
html:not([data-fx="off"]) #lm-lay-panel[style*="block"], html:not([data-fx="off"]) #lm-ai-panel[style*="block"],
html:not([data-fx="off"]) #lm-lay-panel.on, html:not([data-fx="off"]) #lm-ai-panel.on {
  animation: lmRise 0.22s var(--lm-ease);
}
@keyframes lmRise { from { opacity: 0; transform: translateY(8px) scale(0.99); } to { opacity: 1; transform: none; } }

/* ── 26. ERROR ARRIVAL ── one small shake, once. A failure should be noticed;
   it should not keep asking for attention. */
html:not([data-fx="off"]) .lm-st-error { animation: lmNudge 0.3s var(--lm-ease); }
@keyframes lmNudge { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-3px); } 60% { transform: translateX(2px); } }

/* ── 27. HINT + EMPTY ARRIVAL ── the quiet states fade up instead of blinking in. */
html:not([data-fx="off"]) :is(.lm-st-hint, .lm-none) { animation: lmSoft 0.34s var(--lm-ease); }
@keyframes lmSoft { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }

/* ── 28. COPY FLASH ── flow.js adds .lm-copied when something is copied. */
html:not([data-fx="off"]) .lm-copied { animation: lmCopied 0.55s var(--lm-ease); }
@keyframes lmCopied {
  0%   { box-shadow: 0 0 0 0 rgba(70, 167, 88, 0.55); }
  35%  { box-shadow: 0 0 0 5px rgba(70, 167, 88, 0.28); }
  100% { box-shadow: 0 0 0 0 rgba(70, 167, 88, 0); }
}

/* ── 29. IMAGE FADE ── flow.js marks an image .lm-img once it has decoded, so
   pictures resolve instead of snapping in as they stream. */
html:not([data-fx="off"]) img.lm-img { animation: lmImg 0.42s var(--lm-ease); }
@keyframes lmImg { from { opacity: 0; transform: scale(1.012); } to { opacity: 1; transform: none; } }

/* ── 30. OFFLINE PILL ── slides down from the top edge it belongs to. */
html:not([data-fx="off"]) #lm-offline { animation: lmDropIn 0.3s var(--lm-ease); }
@keyframes lmDropIn { from { opacity: 0; transform: translate(-50%, -14px); } to { opacity: 1; transform: translate(-50%, 0); } }

/* ⚠ the whole layer stands down in fullscreen, with everything else. */
html.lm-fx-off :is(.btn, .cta, .card, .chip, .ritem, tbody tr) { transition: none !important; }
html.lm-fx-off :is(.btn, .cta, a.get)::after { display: none; }

/* ══ LOTAMYST ICONS — generated, do not hand-edit ══ */
/* 44 line icons replacing the emoji that were doing the icon system's job.
   Delivered as a MASK, not inline SVG: the glyph is painted with currentColor,
   so an icon inherits the colour and size of the text beside it for free and is
   defined once instead of at 701 call sites. `<i class="ic ic-save"></i>`.
   ⚠⚠ THE GENERATOR IS GONE. scratchpad/icons.js lived in a session temp
      directory and did not survive it — searched the whole tree and the temp
      roots on 2026-08-21, no copy. So "regenerate, never hand-edit" is advice
      nobody can follow today: adding a 45th icon means either rebuilding the
      generator or hand-writing one mask URI and saying so here.
   ⚠ Adding icons does NOT by itself retire the emoji still in the markup — that
      needs ~1,000 call sites edited, and it is worth less than it sounds: this
      file already renders every emoji MONOCHROME (font-variant-emoji: text,
      verified applying), so there is no colour clash left to fix. A large share
      of what a naive grep counts is CONTENT, not chrome — the Forge preview's
      "📺 Anime & Manga" is a Discord channel name, not an icon.
   ⚠ -webkit-mask is listed first and unprefixed second; Safari still needs it. */
.ic {
  display: inline-block; width: 1em; height: 1em; flex: none;
  background-color: currentColor; vertical-align: -0.135em;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}
/* a hair larger where an icon stands alone in a button rather than beside text */
.ic-lg { width: 1.25em; height: 1.25em; vertical-align: -0.24em; }
.ic-premium { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.6 21 9l-9 12.4L3 9zM3 9h18M8.4 9l3.6 12.4M15.6 9 12 21.4'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.6 21 9l-9 12.4L3 9zM3 9h18M8.4 9l3.6 12.4M15.6 9 12 21.4'/%3E%3C/svg%3E"); }
.ic-check { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 12.4 9.8 17.7 19.5 6.6'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 12.4 9.8 17.7 19.5 6.6'/%3E%3C/svg%3E"); }
.ic-close { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6.2 6.2 17.8 17.8M17.8 6.2 6.2 17.8'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6.2 6.2 17.8 17.8M17.8 6.2 6.2 17.8'/%3E%3C/svg%3E"); }
.ic-copy { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.5 9.5h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-9a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2zM5.5 15.5h-1a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.5 9.5h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-9a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2zM5.5 15.5h-1a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1'/%3E%3C/svg%3E"); }
.ic-save { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2zM17 21v-8H7v8M7 3v5h8'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2zM17 21v-8H7v8M7 3v5h8'/%3E%3C/svg%3E"); }
.ic-calendar { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 6.5a2 2 0 0 1 2-2h13a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2zM16 2.5v4M8 2.5v4M3.5 10.5h17'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 6.5a2 2 0 0 1 2-2h13a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2zM16 2.5v4M8 2.5v4M3.5 10.5h17'/%3E%3C/svg%3E"); }
.ic-games { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 12.5a4 4 0 0 1 4-4h11a4 4 0 0 1 0 8h-11a4 4 0 0 1-4-4zM6.4 12.5h3M7.9 11v3'/%3E%3Ccircle cx='15.7' cy='11.3' r='1.15' fill='%23000' stroke='none'/%3E%3Ccircle cx='18.1' cy='13.7' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 12.5a4 4 0 0 1 4-4h11a4 4 0 0 1 0 8h-11a4 4 0 0 1-4-4zM6.4 12.5h3M7.9 11v3'/%3E%3Ccircle cx='15.7' cy='11.3' r='1.15' fill='%23000' stroke='none'/%3E%3Ccircle cx='18.1' cy='13.7' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); }
.ic-pin { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21.5s6.8-6.3 6.8-11a6.8 6.8 0 1 0-13.6 0c0 4.7 6.8 11 6.8 11z'/%3E%3Ccircle cx='12' cy='10.5' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21.5s6.8-6.3 6.8-11a6.8 6.8 0 1 0-13.6 0c0 4.7 6.8 11 6.8 11z'/%3E%3Ccircle cx='12' cy='10.5' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); }
.ic-dice { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 6.5a3 3 0 0 1 3-3h11a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3h-11a3 3 0 0 1-3-3z'/%3E%3Ccircle cx='8.4' cy='8.4' r='1.15' fill='%23000' stroke='none'/%3E%3Ccircle cx='12' cy='12' r='1.15' fill='%23000' stroke='none'/%3E%3Ccircle cx='15.6' cy='15.6' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 6.5a3 3 0 0 1 3-3h11a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3h-11a3 3 0 0 1-3-3z'/%3E%3Ccircle cx='8.4' cy='8.4' r='1.15' fill='%23000' stroke='none'/%3E%3Ccircle cx='12' cy='12' r='1.15' fill='%23000' stroke='none'/%3E%3Ccircle cx='15.6' cy='15.6' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); }
.ic-streak { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21.5c3.9 0 6.8-2.6 6.8-6.4 0-3.9-2.9-5.9-4.4-9.3-.6 2.5-1.9 3.4-2.9 4.1-1.4-1-1.9-2.8-1.6-5C7.1 6.8 5.2 10.1 5.2 14.1c0 4.6 2.9 7.4 6.8 7.4z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21.5c3.9 0 6.8-2.6 6.8-6.4 0-3.9-2.9-5.9-4.4-9.3-.6 2.5-1.9 3.4-2.9 4.1-1.4-1-1.9-2.8-1.6-5C7.1 6.8 5.2 10.1 5.2 14.1c0 4.6 2.9 7.4 6.8 7.4z'/%3E%3C/svg%3E"); }
.ic-camera { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21.5 19a2 2 0 0 1-2 2h-15a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h3l1.8-2.8h5.4L16.5 7h3a2 2 0 0 1 2 2zM12 17.5a4 4 0 1 1 0-8 4 4 0 0 1 0 8z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21.5 19a2 2 0 0 1-2 2h-15a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h3l1.8-2.8h5.4L16.5 7h3a2 2 0 0 1 2 2zM12 17.5a4 4 0 1 1 0-8 4 4 0 0 1 0 8z'/%3E%3C/svg%3E"); }
.ic-bolt { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.2 2.5 4.8 13.6h6L9.9 21.5l8.4-11.1h-6z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.2 2.5 4.8 13.6h6L9.9 21.5l8.4-11.1h-6z'/%3E%3C/svg%3E"); }
.ic-folder { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 19.6 6 11.2h15l-2.4 8.4a1.2 1.2 0 0 1-1.2.9H4.7a1.2 1.2 0 0 1-1.2-.9zM3.5 18.6V5.8a1.2 1.2 0 0 1 1.2-1.2h4.6L11.5 7h5.8a1.2 1.2 0 0 1 1.2 1.2v3'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 19.6 6 11.2h15l-2.4 8.4a1.2 1.2 0 0 1-1.2.9H4.7a1.2 1.2 0 0 1-1.2-.9zM3.5 18.6V5.8a1.2 1.2 0 0 1 1.2-1.2h4.6L11.5 7h5.8a1.2 1.2 0 0 1 1.2 1.2v3'/%3E%3C/svg%3E"); }
.ic-download { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.2v11.6M7.4 10.6 12 15.2l4.6-4.6M4 20.4h16'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.2v11.6M7.4 10.6 12 15.2l4.6-4.6M4 20.4h16'/%3E%3C/svg%3E"); }
.ic-stats { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 20.5V11M12 20.5V4M19 20.5v-6'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 20.5V11M12 20.5V4M19 20.5v-6'/%3E%3C/svg%3E"); }
.ic-monitor { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 6a2 2 0 0 1 2-2h15a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-15a2 2 0 0 1-2-2zM8.5 21h7M12 17v4'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 6a2 2 0 0 1 2-2h15a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-15a2 2 0 0 1-2-2zM8.5 21h7M12 17v4'/%3E%3C/svg%3E"); }
.ic-fullscreen { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9.2V5.4a1.4 1.4 0 0 1 1.4-1.4h3.8M20 9.2V5.4A1.4 1.4 0 0 0 18.6 4h-3.8M4 14.8v3.8A1.4 1.4 0 0 0 5.4 20h3.8M20 14.8v3.8a1.4 1.4 0 0 1-1.4 1.4h-3.8'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9.2V5.4a1.4 1.4 0 0 1 1.4-1.4h3.8M20 9.2V5.4A1.4 1.4 0 0 0 18.6 4h-3.8M4 14.8v3.8A1.4 1.4 0 0 0 5.4 20h3.8M20 14.8v3.8a1.4 1.4 0 0 1-1.4 1.4h-3.8'/%3E%3C/svg%3E"); }
.ic-ai { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11.8 3 13.4 8.6 19 10.2l-5.6 1.6-1.6 5.6-1.6-5.6L4.6 10.2l5.6-1.6zM18.4 16.2l.6 2.1 2.1.6-2.1.6-.6 2.1-.6-2.1-2.1-.6 2.1-.6z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11.8 3 13.4 8.6 19 10.2l-5.6 1.6-1.6 5.6-1.6-5.6L4.6 10.2l5.6-1.6zM18.4 16.2l.6 2.1 2.1.6-2.1.6-.6 2.1-.6-2.1-2.1-.6 2.1-.6z'/%3E%3C/svg%3E"); }
.ic-confirm { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21a9 9 0 1 1 0-18 9 9 0 0 1 0 18zM8 12.4l2.9 2.9 5.4-5.9'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21a9 9 0 1 1 0-18 9 9 0 0 1 0 18zM8 12.4l2.9 2.9 5.4-5.9'/%3E%3C/svg%3E"); }
.ic-theme { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21a9 9 0 1 1 0-18c4.9 0 9 3.4 9 7.5 0 2.5-2 4.5-4.5 4.5H15a2 2 0 0 0-1.5 3.3A1.8 1.8 0 0 1 12 21z'/%3E%3Ccircle cx='7.6' cy='12' r='1.15' fill='%23000' stroke='none'/%3E%3Ccircle cx='9.9' cy='8.1' r='1.15' fill='%23000' stroke='none'/%3E%3Ccircle cx='14.4' cy='7.6' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21a9 9 0 1 1 0-18c4.9 0 9 3.4 9 7.5 0 2.5-2 4.5-4.5 4.5H15a2 2 0 0 0-1.5 3.3A1.8 1.8 0 0 1 12 21z'/%3E%3Ccircle cx='7.6' cy='12' r='1.15' fill='%23000' stroke='none'/%3E%3Ccircle cx='9.9' cy='8.1' r='1.15' fill='%23000' stroke='none'/%3E%3Ccircle cx='14.4' cy='7.6' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); }
.ic-link { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 13.6a4 4 0 0 0 5.7.3l3-3a4 4 0 0 0-5.7-5.7l-1.7 1.7M14 10.4a4 4 0 0 0-5.7-.3l-3 3a4 4 0 0 0 5.7 5.7l1.7-1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 13.6a4 4 0 0 0 5.7.3l3-3a4 4 0 0 0-5.7-5.7l-1.7 1.7M14 10.4a4 4 0 0 0-5.7-.3l-3 3a4 4 0 0 0 5.7 5.7l1.7-1.7'/%3E%3C/svg%3E"); }
.ic-minecraft { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.6 20.6 7v10L12 21.4 3.4 17V7zM3.4 7 12 11.5 20.6 7M12 11.5v9.9'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.6 20.6 7v10L12 21.4 3.4 17V7zM3.4 7 12 11.5 20.6 7M12 11.5v9.9'/%3E%3C/svg%3E"); }
.ic-trash { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 7h16M10 11.2v6.2M14 11.2v6.2M6.2 7l.9 12.4a1.5 1.5 0 0 0 1.5 1.4h6.8a1.5 1.5 0 0 0 1.5-1.4L17.8 7M9 7V4.6a1.5 1.5 0 0 1 1.5-1.4h3a1.5 1.5 0 0 1 1.5 1.4V7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 7h16M10 11.2v6.2M14 11.2v6.2M6.2 7l.9 12.4a1.5 1.5 0 0 0 1.5 1.4h6.8a1.5 1.5 0 0 0 1.5-1.4L17.8 7M9 7V4.6a1.5 1.5 0 0 1 1.5-1.4h3a1.5 1.5 0 0 1 1.5 1.4V7'/%3E%3C/svg%3E"); }
.ic-broken { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.6 6.8a4.8 4.8 0 0 0-6.8 0L12 8.6l-1.8-1.8a4.8 4.8 0 1 0-6.8 6.8L12 21.6l8.6-8a4.8 4.8 0 0 0 0-6.8zM13.4 8.2 11 12.2h3l-2 4'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.6 6.8a4.8 4.8 0 0 0-6.8 0L12 8.6l-1.8-1.8a4.8 4.8 0 1 0-6.8 6.8L12 21.6l8.6-8a4.8 4.8 0 0 0 0-6.8zM13.4 8.2 11 12.2h3l-2 4'/%3E%3C/svg%3E"); }
.ic-edit { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16.4 3.4a2.1 2.1 0 0 1 3 3L7.5 18.4 3 19.9l1.5-4.5zM14.5 5.4l4 4'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16.4 3.4a2.1 2.1 0 0 1 3 3L7.5 18.4 3 19.9l1.5-4.5zM14.5 5.4l4 4'/%3E%3C/svg%3E"); }
.ic-duel { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19.6 4.4 10 14M4.4 4.4 14 14M8.4 17.6 5.9 20.1M15.6 17.6l2.5 2.5M17 3.2h3v3M7 3.2H4v3'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19.6 4.4 10 14M4.4 4.4 14 14M8.4 17.6 5.9 20.1M15.6 17.6l2.5 2.5M17 3.2h3v3M7 3.2H4v3'/%3E%3C/svg%3E"); }
.ic-boost { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.6c3 2 4.9 5.6 4.9 9.4 0 2.2-.6 4.2-1.6 5.9H8.7A11.5 11.5 0 0 1 7.1 12c0-3.8 1.9-7.4 4.9-9.4zM8.7 17.9 6.2 20.5l.5-4.3M15.3 17.9l2.5 2.6-.5-4.3'/%3E%3Ccircle cx='12' cy='11.6' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.6c3 2 4.9 5.6 4.9 9.4 0 2.2-.6 4.2-1.6 5.9H8.7A11.5 11.5 0 0 1 7.1 12c0-3.8 1.9-7.4 4.9-9.4zM8.7 17.9 6.2 20.5l.5-4.3M15.3 17.9l2.5 2.6-.5-4.3'/%3E%3Ccircle cx='12' cy='11.6' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); }
.ic-star { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.4 14.6 9l6 .8-4.4 4.2 1.1 6-5.3-2.9L6.7 20l1.1-6L3.4 9.8l6-.8z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.4 14.6 9l6 .8-4.4 4.2 1.1 6-5.3-2.9L6.7 20l1.1-6L3.4 9.8l6-.8z'/%3E%3C/svg%3E"); }
.ic-forge { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 8.6h7.6l2.4 2.9h4.9a2.4 2.4 0 0 1-2.4 2.4h-6.6l1.3 4.3h2.6v2.3H8.3v-2.3H11l-1.3-4.3H8.3a3.8 3.8 0 0 1-3.8-3.8z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 8.6h7.6l2.4 2.9h4.9a2.4 2.4 0 0 1-2.4 2.4h-6.6l1.3 4.3h2.6v2.3H8.3v-2.3H11l-1.3-4.3H8.3a3.8 3.8 0 0 1-3.8-3.8z'/%3E%3C/svg%3E"); }
.ic-crown { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 8.2 6.8 11.4 12 4.6l5.2 6.8 3.6-3.2-1.5 10.4H4.7zM4.8 19h14.4'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 8.2 6.8 11.4 12 4.6l5.2 6.8 3.6-3.2-1.5 10.4H4.7zM4.8 19h14.4'/%3E%3C/svg%3E"); }
.ic-gift { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 9.6h17.6v11.2H3.2zM12 9.6v11.2M12 9.6C12 6.2 10.6 3.6 8.6 3.6a2 2 0 1 0 0 4H12M12 9.6c0-3.4 1.4-6 3.4-6a2 2 0 1 1 0 4H12'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 9.6h17.6v11.2H3.2zM12 9.6v11.2M12 9.6C12 6.2 10.6 3.6 8.6 3.6a2 2 0 1 0 0 4H12M12 9.6c0-3.4 1.4-6 3.4-6a2 2 0 1 1 0 4H12'/%3E%3C/svg%3E"); }
.ic-target { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 20.2a8.2 8.2 0 1 1 0-16.4 8.2 8.2 0 0 1 0 16.4zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 1.6v3.4M12 19v3.4M1.6 12H5M19 12h3.4'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 20.2a8.2 8.2 0 1 1 0-16.4 8.2 8.2 0 0 1 0 16.4zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 1.6v3.4M12 19v3.4M1.6 12H5M19 12h3.4'/%3E%3C/svg%3E"); }
.ic-image { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 6a2 2 0 0 1 2-2h13.6a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5.2a2 2 0 0 1-2-2zM8.4 11.2a1.8 1.8 0 1 1 0-3.6 1.8 1.8 0 0 1 0 3.6zM3.6 17.4 9 12l4 4 3-2.6 4.4 4.4'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 6a2 2 0 0 1 2-2h13.6a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5.2a2 2 0 0 1-2-2zM8.4 11.2a1.8 1.8 0 1 1 0-3.6 1.8 1.8 0 0 1 0 3.6zM3.6 17.4 9 12l4 4 3-2.6 4.4 4.4'/%3E%3C/svg%3E"); }
.ic-chat { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.6 11.4a7.6 7.6 0 0 1-10.9 6.8L4 20l1.9-5.5A7.6 7.6 0 1 1 20.6 11.4z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.6 11.4a7.6 7.6 0 0 1-10.9 6.8L4 20l1.9-5.5A7.6 7.6 0 1 1 20.6 11.4z'/%3E%3C/svg%3E"); }
.ic-warning { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.4 22 20.2H2zM12 10.2v4.6'/%3E%3Ccircle cx='12' cy='17.4' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.4 22 20.2H2zM12 10.2v4.6'/%3E%3Ccircle cx='12' cy='17.4' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E"); }
.ic-tools { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6.6a5 5 0 0 1-6.7 6.7L6 20.6 3.4 18l7.3-7.3A5 5 0 0 1 17.4 4l-3 3 2.6 2.6 3-3z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6.6a5 5 0 0 1-6.7 6.7L6 20.6 3.4 18l7.3-7.3A5 5 0 0 1 17.4 4l-3 3 2.6 2.6 3-3z'/%3E%3C/svg%3E"); }
.ic-search { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.6 17.2a6.6 6.6 0 1 1 0-13.2 6.6 6.6 0 0 1 0 13.2zM15.4 15.4 21 21'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.6 17.2a6.6 6.6 0 1 1 0-13.2 6.6 6.6 0 0 1 0 13.2zM15.4 15.4 21 21'/%3E%3C/svg%3E"); }
.ic-trend { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 17.2 9.6 10.6l4 4L21 7.2M15.4 7.2H21v5.6'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 17.2 9.6 10.6l4 4L21 7.2M15.4 7.2H21v5.6'/%3E%3C/svg%3E"); }
.ic-settings { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM19.2 14.6a1.5 1.5 0 0 0 .3 1.7l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.5 1.5 0 0 0-2.6 1v.3a2 2 0 1 1-4 0v-.2a1.5 1.5 0 0 0-2.6-1l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.5 1.5 0 0 0-1-2.6H3.4a2 2 0 1 1 0-4h.2a1.5 1.5 0 0 0 1-2.6l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.5 1.5 0 0 0 1.7.3h.1a1.5 1.5 0 0 0 .9-1.4V3.4a2 2 0 1 1 4 0v.2a1.5 1.5 0 0 0 2.6 1l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.5 1.5 0 0 0-.3 1.7v.1a1.5 1.5 0 0 0 1.4.9h.3a2 2 0 1 1 0 4h-.2a1.5 1.5 0 0 0-1.4.9z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM19.2 14.6a1.5 1.5 0 0 0 .3 1.7l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.5 1.5 0 0 0-2.6 1v.3a2 2 0 1 1-4 0v-.2a1.5 1.5 0 0 0-2.6-1l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.5 1.5 0 0 0-1-2.6H3.4a2 2 0 1 1 0-4h.2a1.5 1.5 0 0 0 1-2.6l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.5 1.5 0 0 0 1.7.3h.1a1.5 1.5 0 0 0 .9-1.4V3.4a2 2 0 1 1 4 0v.2a1.5 1.5 0 0 0 2.6 1l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.5 1.5 0 0 0-.3 1.7v.1a1.5 1.5 0 0 0 1.4.9h.3a2 2 0 1 1 0 4h-.2a1.5 1.5 0 0 0-1.4.9z'/%3E%3C/svg%3E"); }
.ic-trophy { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 4h10v5a5 5 0 0 1-10 0zM7 5.6H4.4V7a3.6 3.6 0 0 0 3 3.5M17 5.6h2.6V7a3.6 3.6 0 0 1-3 3.5M12 14v3.4M8.5 20.6h7l-.7-3.2H9.2z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 4h10v5a5 5 0 0 1-10 0zM7 5.6H4.4V7a3.6 3.6 0 0 0 3 3.5M17 5.6h2.6V7a3.6 3.6 0 0 1-3 3.5M12 14v3.4M8.5 20.6h7l-.7-3.2H9.2z'/%3E%3C/svg%3E"); }
.ic-volume { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11 5 6.6 9H3v6h3.6L11 19zM15.4 8.6a5 5 0 0 1 0 6.8M18.4 5.6a9 9 0 0 1 0 12.8'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11 5 6.6 9H3v6h3.6L11 19zM15.4 8.6a5 5 0 0 1 0 6.8M18.4 5.6a9 9 0 0 1 0 12.8'/%3E%3C/svg%3E"); }
.ic-record { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21a9 9 0 1 1 0-18 9 9 0 0 1 0 18zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21a9 9 0 1 1 0-18 9 9 0 0 1 0 18zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8z'/%3E%3C/svg%3E"); }
.ic-stream { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.6 8.4a2 2 0 0 1 2-2h14.8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4.6a2 2 0 0 1-2-2zM8 21h8M9.8 10.4 15 12.4l-5.2 2z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.6 8.4a2 2 0 0 1 2-2h14.8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4.6a2 2 0 0 1-2-2zM8 21h8M9.8 10.4 15 12.4l-5.2 2z'/%3E%3C/svg%3E"); }
.ic-comet { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 12.2a4.2 4.2 0 1 1 0-8.4 4.2 4.2 0 0 1 0 8.4zM12.6 10.8 4 19.4M9.4 9 5.4 12M14.4 15.2l-3 4'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 12.2a4.2 4.2 0 1 1 0-8.4 4.2 4.2 0 0 1 0 8.4zM12.6 10.8 4 19.4M9.4 9 5.4 12M14.4 15.2l-3 4'/%3E%3C/svg%3E"); }
/* ══ END LOTAMYST ICONS ══ */

/* ══════════ HIDDEN CONSOLE (v18) ══════════
   Moved out of app/index.html, where it hardcoded #e5484d and so stayed red
   through the whole rebrand — on a site whose accent is now white in Mono and a
   different colour in each of the other seven themes. Everything here paints
   from tokens instead, so the console follows the theme the way the rest of the
   site does, and the layout presets (Terminal squares every corner) reach it too.

   ⚠ The overlay is appended to <body> by flow.js on FIRST open, not shipped in
     any page's markup. Nothing below may assume it exists at load. */

#lm-console {
  position: fixed; inset: 0; z-index: 300;
  display: none; flex-direction: column;
  padding: clamp(1.1rem, 4vw, 2rem) clamp(0.9rem, 6vw, 4rem);
  background: var(--bg, #000);
  color: var(--text, #e7e8ea);
  font-family: 'Cascadia Mono', 'Cascadia Code', Consolas, 'Courier New', monospace;
  overflow: hidden;
}
#lm-console.open { display: flex; animation: lmcIn 0.3s ease; }

/* the ground: faint scanlines + a vignette, so the console reads as a screen
   rather than as another panel on the same page */
#lm-console::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background:
    repeating-linear-gradient(0deg, transparent 0 3px, rgba(var(--glow, 255, 255, 255), 0.022) 3px 4px),
    radial-gradient(ellipse at 50% 40%, transparent 55%, rgba(0, 0, 0, 0.55) 100%);
}
#lm-console > * { position: relative; z-index: 1; }

/* ── the header band ── */
.lmc-head { opacity: 0; transform: translateY(-8px); transition: opacity 0.45s, transform 0.45s; }
#lm-console.ready .lmc-head { opacity: 1; transform: none; }

.lmc-bar {
  display: flex; gap: clamp(0.7rem, 3vw, 1.6rem); flex-wrap: wrap;
  font-size: 0.7rem; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--muted, #8b8f96); margin-bottom: 1.1rem;
}
.lmc-bar b { color: var(--red, #fff); font-weight: 700; }
.lmc-bar .lmc-tag { color: var(--red, #fff); }
.lmc-bar .lmc-esc { margin-left: auto; }
.lmc-title {
  font-size: clamp(1rem, 4vw, 1.8rem); font-weight: 700; letter-spacing: 0.06em;
  color: var(--red, #fff); margin-bottom: 0.4rem;
}
.lmc-sub {
  font-size: 0.7rem; color: var(--muted, #8b8f96);
  letter-spacing: 0.15em; margin-bottom: 1.4rem;
}

/* ── the log ──
   flex:1 + min-height:0 is what keeps a long session scrolling INSIDE the log
   instead of pushing the prompt off the bottom of the viewport. */
.lmc-log {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  font-size: clamp(0.7rem, 1.8vw, 0.9rem); line-height: 2;
  color: var(--text, #e7e8ea);
  scrollbar-width: thin; scrollbar-color: var(--border, rgba(255,255,255,.12)) transparent;
}
.lmc-log:focus-visible { outline: 1px solid var(--red-dim, rgba(255,255,255,.24)); outline-offset: 2px; }
.lmc-line { animation: lmcFade 0.2s ease; white-space: pre-wrap; word-break: break-word; }
.lmc-line b { color: var(--red, #fff); font-weight: 700; }
.lmc-ok  { color: var(--ok, #46a758); }
.lmc-err { color: var(--danger, #e5484d); }
.lmc-dim { color: var(--muted, #8b8f96); }
.lmc-dirent { color: var(--red, #fff); }
.lmc-ps1 { color: var(--red, #fff); }
.lmc-pre { line-height: 1.06; font-size: 0.8rem; margin: 0.3rem 0; font-family: inherit; }

/* ── the prompt ── */
.lmc-inrow {
  display: flex; align-items: center; gap: 0.5rem;
  margin-top: 0.9rem; padding-top: 0.9rem;
  border-top: 1px solid var(--border, rgba(255,255,255,.12));
  opacity: 0.2; pointer-events: none; transform: none;
  transition: opacity 0.35s;
}
#lm-console.ready .lmc-inrow { opacity: 1; pointer-events: auto; }
.lmc-ps1 { font-size: 0.8rem; white-space: nowrap; }
#lmc-input {
  flex: 1; min-width: 0; background: transparent; border: none;
  color: var(--text, #e7e8ea); font-family: inherit; font-size: 0.8rem;
  outline: none; padding: 0.2rem 0;
}
#lmc-input::placeholder { color: var(--faint, rgba(255,255,255,.46)); }

/* ── the breach alarm, while access is being forced ── */
#lm-console.breach::after {
  content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 2;
  background:
    repeating-linear-gradient(0deg, transparent 0 6px, rgba(var(--glow, 255,255,255), 0.035) 7px 8px),
    radial-gradient(ellipse at center, transparent 52%, rgba(var(--glow, 255,255,255), 0.2) 100%);
  animation: lmcBreach 0.55s ease-in-out infinite;
}
#lm-console.closing { animation: lmcOut 0.45s cubic-bezier(0.55, 0, 0.85, 0.36) forwards; pointer-events: none; }

.lmc-matrix { position: absolute; inset: 0; z-index: 5; pointer-events: none; mix-blend-mode: screen; }

/* the page underneath must not scroll while a full-screen console is up */
html.lmc-lock, html.lmc-lock body { overflow: hidden; }

@keyframes lmcIn { from { opacity: 0; transform: scale(1.04); filter: blur(5px); } to { opacity: 1; transform: none; filter: none; } }
@keyframes lmcOut { to { opacity: 0; transform: scale(1.12); filter: blur(8px); } }
@keyframes lmcFade { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
@keyframes lmcBreach { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  #lm-console.open, #lm-console.closing, #lm-console.breach::after, .lmc-line { animation: none; }
  .lmc-head, .lmc-inrow { transition: none; }
}

/* The Terminal layout preset squares every corner on the site; the console is
   already square, but its scrollbar and the prompt border should follow suit. */
html[data-layout="terminal"] #lm-console { border-radius: 0; }

/* On a phone the header band eats a third of a 844px screen before a single
   line of output. Everything above the log tightens; the log never does. */
@media (max-width: 560px) {
  #lm-console { padding: 0.9rem 0.9rem 0.7rem; }
  .lmc-bar { font-size: 0.6rem; letter-spacing: 0.12em; gap: 0.6rem; margin-bottom: 0.7rem; }
  .lmc-bar .lmc-esc { margin-left: 0; }
  .lmc-title { margin-bottom: 0.2rem; }
  .lmc-sub { margin-bottom: 0.8rem; }
  .lmc-log { line-height: 1.75; }
}

/* ═══════ THE CANONICAL CONTROLS (v20) — Nightfall port, phase 2 ═══════
   design_handoff_site/port/lm-controls.css, adapted. Every VALUE is the
   source's; the SELECTORS are not, and this comment is why.

   ⚠⚠ THE HANDOFF'S PREMISE IS THE OPPOSITE OF THIS FILE. It says ".btn is not
   defined in flow.css" and "a page-local rule of equal specificity loads later
   and wins, so adding rules here changes nothing until phase 3 deletes them".
   Measured: THIRTY-THREE rules in this file already style .btn, and the block
   at THE BUTTON says in writing why they carry an `html` prefix — "takes it to
   0-1-1, which beats a page's .btn (0-1-0)". The handoff's grep (`^\s*\.btn\s*{`)
   cannot see `html .btn`, which is how it reached the wrong conclusion.

   So a bare `.btn` append would not have produced the redesign. It would have
   produced a MONGREL: height/padding/font-size landing (nothing else sets them)
   while background, border, radius, colour, shadow and transition all stayed
   with the old system. And `html .btn` alone is not enough either — the motion
   layer owns the hover at `html:not([data-fx="off"]) :is(.btn,…):hover`, 0-3-1.

   ⚠ WHY A GATE INSTEAD OF AN ARMS RACE. Escalating far enough to beat 0-3-1
   everywhere would silently restyle .btn on all 208 pages at once — the exact
   thing phase 3 says to avoid ("ten pages, deploy, look at them, then the next
   ten"). `html[data-lm-ctl]` puts the base at 0-2-1 and the hover at 0-3-1, so
   it TIES the motion layer and wins on source order (this block is last in the
   file) — but only on a page that has opted in. Phase 3 flips pages over in
   batches as it deletes their local blocks. When every page carries it, the
   attribute can be dropped and these selectors folded down to `html`.

   ⚠ DROPPED: the port's `.lm-rv` block. Its comment claims the name "cannot
   collide with anything in flow.css" — this file has owned `.lm-rv` since v7
   (scroll reveals), using `translate` and `.lm-in`. The port uses `transform`
   and `.in`. Both would have applied: flow.js clears `translate` on reveal and
   never adds `.in`, so every revealed block would have kept a 16px transform
   FOREVER. `.lm-rule` is kept and re-pointed at the `.lm-in` this site fires.

   ⚠ `--i` IS SET BY NOTHING — not by flow.js, not by lm-reveal.js. The port's
   stagger `calc(var(--i,0) * 46ms)` is always zero. This site's stagger variable
   is `--lmd`, a time, set by flow.js. `.lm-rule` spends that instead.

   ⚠ GOLD IS `--premium` HERE, NOT `--gold`. On eleven of the twelve themes
   `--gold` resolves to white (line ~360). `--premium` is "the ONE surviving
   gold, and the only place it is allowed", declared on plain `html` with a
   matching `--premium-rgb`. Using it also keeps site-lint check 6d at its
   floor of 15 — the port's three hard-coded gold literals would have taken it
   to 18 and hard-failed the deploy. ⚠ That check greps RAW TEXT, comments
   included, so naming the forbidden literals here would itself have failed it.

   ⚠ `--mono` was consumed by the port five times and declared nowhere, so every
   keycap, tag, chip count and stat figure would have fallen back to
   `ui-monospace` while the rest of the site renders Cascadia. It is declared
   once, below, on plain `html`, so pages can spend it too.

   GEOMETRY. 46px / 22px / 14.5px / 8px is the approved button; `data-lm-dense`
   on a <body> returns 36 / 15 / 13 / 7, which is what the bench and tool pages
   already are. Do not snap 14.5px or 22px to a grid and do not add a third
   size — they are the source's own values. */

html { --mono: 'Cascadia Mono', 'Cascadia Code', Consolas, 'Courier New', monospace; }

html[data-lm-ctl] {
  --lm-btn-h: 46px; --lm-btn-px: 22px; --lm-btn-fs: 14.5px; --lm-btn-r: 8px;
  --lm-hi: var(--border-hi, rgba(255,255,255,.26));
}
html[data-lm-ctl] [data-lm-dense] { --lm-btn-h: 36px; --lm-btn-px: 15px; --lm-btn-fs: 13px; --lm-btn-r: 7px; }

/* ── base ──
   Height, not vertical padding: a fixed height is what makes a button row line
   up with an input and a select without three separate corrections. */
html[data-lm-ctl] :is(.btn, button.btn) {
  position: relative; isolation: isolate; display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  height: var(--lm-btn-h); padding: 0 var(--lm-btn-px); border: 0; border-radius: var(--lm-btn-r);
  font-family: inherit; font-weight: 600; font-size: var(--lm-btn-fs); line-height: 1;
  letter-spacing: -.005em; white-space: nowrap;
  cursor: pointer; overflow: hidden; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent;
  background: 0; color: var(--text-dim, var(--text));
  box-shadow: inset 0 0 0 1px var(--edge, var(--border)), inset 0 1px 0 rgba(255,255,255,.05), 0 1px 2px rgba(0,0,0,.34);
  transition: transform var(--lm-out) var(--lm-ease), box-shadow var(--lm-out) var(--lm-ease),
              color var(--lm-out), opacity var(--lm-out);
}
html[data-lm-ctl] .btn.ghost, html[data-lm-ctl] .btn.g {
  box-shadow: inset 0 0 0 1px var(--edge, var(--border)), inset 0 1px 0 rgba(255,255,255,.05), 0 1px 2px rgba(0,0,0,.34);
}
/* the fill SLIDES IN from the left rather than switching on, and the edge is an
   inset shadow rather than a border, so hovering cannot shift layout by a pixel */
html[data-lm-ctl] .btn::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg, var(--panel2), var(--panel));
  transform: translateX(-101%); transition: transform var(--lm-out) var(--lm-arrive);
}
html[data-lm-ctl] :is(.btn, button.btn):hover {
  color: var(--text); transform: none; background: 0;
  box-shadow: inset 0 0 0 1px var(--lm-hi), inset 0 1px 0 rgba(255,255,255,.07), 0 4px 14px -4px rgba(0,0,0,.6);
  transition-duration: var(--lm-in);
}
html[data-lm-ctl] .btn:hover::before { transform: none; transition-duration: var(--lm-slow); }
/* the v17 sheen and this block's ::before fill are two hover lightings on one
   surface. The new one wins; the old one stands down where it is opted in. */
html[data-lm-ctl] :is(.btn, button.btn):hover::after { animation: none; }
html[data-lm-ctl] :is(.btn, button.btn):active {
  transform: scale(var(--lm-press-scale)); transition-duration: var(--lm-press);
  box-shadow: inset 0 2px 7px rgba(0,0,0,.44), inset 0 0 0 1px var(--lm-hi);
}
html[data-lm-ctl] .btn[disabled], html[data-lm-ctl] .btn[aria-disabled="true"] { opacity: .42; pointer-events: none; }
html[data-lm-ctl] .btn:focus-visible { outline: 2px solid var(--red); outline-offset: 3px; transform: none; }

/* ── primary ──
   A SOLID accent fill, not the old rgba(--glow,.12) wash — the wash read as a
   disabled button next to real content, which is why it changed. Four inset
   shadows do the physical work: a lit top edge, a shaded bottom edge, a
   hairline containing both, then the cast shadow and the accent bloom. */
html[data-lm-ctl] .btn.primary, html[data-lm-ctl] .btn.p {
  background: var(--red); color: var(--on-red); border-color: transparent;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.4), inset 0 -1px 0 rgba(0,0,0,.14), inset 0 0 0 1px rgba(255,255,255,.1),
              0 1px 2px rgba(0,0,0,.5), 0 8px 22px -8px rgba(var(--glow),.42);
}
html[data-lm-ctl] .btn.primary::before, html[data-lm-ctl] .btn.p::before { content: none; }
/* the travelling light — above the fill, below the label. --lm-mx/--lm-my are
   set by lm-reveal.js in phase 4; until then it rests dead centre. */
html[data-lm-ctl] .btn.primary::after, html[data-lm-ctl] .btn.p::after {
  content: ""; position: absolute; inset: 0; z-index: -1; opacity: 0; animation: none;
  background: radial-gradient(150px circle at var(--lm-mx,50%) var(--lm-my,50%), rgba(255,255,255,.34), rgba(255,255,255,.05) 68%);
  transform: none; transition: opacity var(--lm-out);
}
html[data-lm-ctl] .btn.primary:hover::after, html[data-lm-ctl] .btn.p:hover::after { opacity: 1; transition-duration: var(--lm-in); }
html[data-lm-ctl] .btn.primary:hover, html[data-lm-ctl] .btn.p:hover {
  color: var(--on-red); background: var(--red);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.5), inset 0 -1px 0 rgba(0,0,0,.14), inset 0 0 0 1px rgba(255,255,255,.16),
              0 2px 3px rgba(0,0,0,.5), 0 16px 38px -10px rgba(var(--glow),.66);
}
/* pressed: the lit edge inverts to a shadow — the key is now BELOW the surface */
html[data-lm-ctl] .btn.primary:active, html[data-lm-ctl] .btn.p:active {
  box-shadow: inset 0 2px 7px rgba(0,0,0,.36), inset 0 0 0 1px rgba(0,0,0,.16), 0 1px 1px rgba(0,0,0,.4);
}
html[data-lm-ctl] .btn.primary:active::after, html[data-lm-ctl] .btn.p:active::after { opacity: .5; }

html[data-lm-ctl] .btn.sm { height: 36px; padding: 0 15px; font-size: 13px; border-radius: 7px; }
html[data-lm-ctl] .btn.big { height: 54px; padding: 0 30px; font-size: 16px; border-radius: 9px; }
html[data-lm-ctl] .btn-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }

/* a keycap inside a button, lit the same way the button is */
html[data-lm-ctl] .btn .k {
  font-family: var(--mono); font-size: 11px; font-weight: 400; padding: 3px 7px; border-radius: 4px;
  background: rgba(0,0,0,.22); box-shadow: inset 0 1px 0 rgba(255,255,255,.14), inset 0 -1px 0 rgba(0,0,0,.34);
  transition: background var(--lm-out);
}
html[data-lm-ctl] .btn:hover .k { background: rgba(0,0,0,.3); transition-duration: var(--lm-in); }

/* ── press ripple ──
   Spawned by lm-reveal.js at the pointer (phase 4). z-index:-1 keeps it under
   the label so the text never smears. Inert until that file ships. */
html[data-lm-ctl] .rip {
  position: absolute; z-index: -1; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 99px; pointer-events: none;
  background: radial-gradient(circle, rgba(255,255,255,.42), transparent 70%);
  animation: lmRip .5s var(--lm-travel) forwards;
}
@keyframes lmRip { from { transform: scale(1); opacity: .75 } to { transform: scale(24); opacity: 0 } }

/* ── icon button ── */
html[data-lm-ctl] .ib {
  display: grid; place-items: center; width: 36px; height: 36px; border: 0; background: 0; border-radius: 8px; cursor: pointer;
  color: var(--muted); transition: color var(--lm-out), background var(--lm-out), transform var(--lm-out) var(--lm-ease);
}
html[data-lm-ctl] .ib:hover { color: var(--text); background: var(--panel2); transition-duration: var(--lm-in); }
html[data-lm-ctl] .ib:active { transform: scale(.93); transition-duration: var(--lm-press); }

/* ── chips ── */
html[data-lm-ctl] .chip {
  position: relative; isolation: isolate; display: inline-flex; align-items: center; height: 32px; padding: 0 14px; border: 0;
  border-radius: 99px; background: 0; font-size: 12.5px; font-weight: 600; color: var(--muted); cursor: pointer; overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--border);
  transition: color var(--lm-out), box-shadow var(--lm-out), transform var(--lm-out) var(--lm-ease);
}
html[data-lm-ctl] .chip:hover { color: var(--text); box-shadow: inset 0 0 0 1px var(--lm-hi); transition-duration: var(--lm-in); }
html[data-lm-ctl] .chip:active { transform: scale(.955); transition-duration: var(--lm-press); }
html[data-lm-ctl] .chip.on { background: var(--red); color: var(--on-red); box-shadow: none; }
html[data-lm-ctl] .chip b { margin-left: 6px; font-family: var(--mono); font-weight: 400; opacity: .6; }

/* ── tags ── */
html[data-lm-ctl] .tag {
  display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 8px; border-radius: 5px;
  font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
}
html[data-lm-ctl] .tag.acc { color: var(--red); box-shadow: inset 0 0 0 1px rgba(var(--glow),.4); background: rgba(var(--glow),.1); }
html[data-lm-ctl] .tag.gold { color: var(--premium); box-shadow: inset 0 0 0 1px rgba(var(--premium-rgb),.42); background: rgba(var(--premium-rgb),.1); }
html[data-lm-ctl] .tag.ok { color: var(--ok); box-shadow: inset 0 0 0 1px rgba(var(--ok-rgb),.4); }

/* ── ONE card physics ──
   Every card family states only its own geometry; the lift, the edge, the
   shadow and the pointer light all come from here, so a panel and a tool card
   feel like the same material. Add class names to this list rather than
   writing a second hover treatment. */
html[data-lm-ctl] :is(.panel, .card, .tool, .pill) {
  position: relative; overflow: hidden; border-radius: 14px; background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--border);
  transition: background var(--lm-out), box-shadow var(--lm-out) var(--lm-ease), transform var(--lm-out) var(--lm-ease);
}
html[data-lm-ctl] :is(.panel, .card, .tool, .pill)::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0; border-radius: inherit;
  background: radial-gradient(290px circle at var(--lm-mx,50%) var(--lm-my,50%), rgba(var(--glow),.11), transparent 66%);
  transition: opacity var(--lm-out);
}
html[data-lm-ctl] :is(.panel, .card, .tool, .pill):hover {
  background: var(--panel2);
  box-shadow: inset 0 0 0 1px var(--edge, var(--border)), 0 20px 44px rgba(0,0,0,.5);
  transform: translateY(-4px); transition-duration: var(--lm-in);
}
html[data-lm-ctl] :is(.panel, .card, .tool, .pill):hover::after { opacity: 1; transition-duration: var(--lm-in); }

/* ── stat cells: hairline-under-the-number, drawn on hover ── */
html[data-lm-ctl] .stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 1px;
  background: var(--border); border-radius: 14px; overflow: hidden;
}
html[data-lm-ctl] .stat { position: relative; padding: 22px 20px; background: var(--panel); transition: background var(--lm-out); }
html[data-lm-ctl] .stat:hover { background: var(--panel2); transition-duration: var(--lm-in); }
html[data-lm-ctl] .stat::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--red);
  transform: scaleX(0); transform-origin: left; transition: transform var(--lm-out) var(--lm-arrive);
}
html[data-lm-ctl] .stat:hover::after { transform: scaleX(1); transition-duration: var(--lm-slow); }
html[data-lm-ctl] .stat .v {
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  font-size: clamp(28px, 3.4vw, 40px); font-weight: 700; letter-spacing: -.03em; line-height: 1;
}
html[data-lm-ctl] .stat .l { margin-top: 9px; font-size: 13px; color: var(--muted); }

/* ── inputs ── */
html[data-lm-ctl] :is(input, textarea, select) { caret-color: var(--red); }
html[data-lm-ctl] .field { margin-top: 18px; }
html[data-lm-ctl] .field label {
  display: block; margin-bottom: 7px; font-family: var(--mono); font-size: 10px;
  letter-spacing: .14em; text-transform: uppercase; color: var(--faint, var(--muted));
}
html[data-lm-ctl] .field input {
  width: 100%; height: 46px; padding: 0 14px; border: 0; border-radius: 10px; background: var(--panel2);
  color: var(--text); font: inherit; box-shadow: inset 0 0 0 1px var(--edge, var(--border));
  transition: box-shadow var(--lm-out), background var(--lm-out);
}
html[data-lm-ctl] .field input:hover { box-shadow: inset 0 0 0 1px var(--lm-hi); }
html[data-lm-ctl] .field input:focus {
  outline: 0; box-shadow: inset 0 0 0 1px var(--red), 0 0 0 3px rgba(var(--glow),.14);
  transition-duration: var(--lm-in);
}

/* ── links: the underline GROWS from the left ──
   text-decoration cannot be animated; a background-size can. Left at one class
   of specificity above bare `a` so any page that styles its own links still
   wins — this is the widest-reaching rule in the block and it defers. */
html[data-lm-ctl] a {
  color: var(--red); text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor); background-repeat: no-repeat;
  background-position: 0 100%; background-size: 0 1px;
  transition: background-size var(--lm-out) var(--lm-arrive), color var(--lm-out);
}
html[data-lm-ctl] a:hover { background-size: 100% 1px; transition-duration: var(--lm-in); }
html[data-lm-ctl] :is(a.btn, a.chip, a.ib, a.mark, nav a, header a, footer a) { background-image: none; }

/* a hairline that draws itself a beat after the heading arrives.
   Pointed at .lm-in and --lmd, which is what this site actually fires. */
html[data-lm-ctl] .lm-rule {
  height: 1px; background: var(--border); transform: scaleX(0); transform-origin: left;
  transition: transform var(--lm-slow) var(--lm-arrive);
}
html[data-lm-ctl] .lm-rv.lm-in .lm-rule, html[data-lm-ctl] .lm-rule.lm-in {
  transform: scaleX(1); transition-delay: calc(var(--lmd, 0s) + 120ms);
}

/* Only the NEW decorative pseudo-elements stand down here. The site's own
   timing scale deliberately does not gate on this query — see the note at THE
   MOTION LAYER (v17) about batch 65b. */
@media (prefers-reduced-motion: reduce) {
  html[data-lm-ctl] .btn::before,
  html[data-lm-ctl] .btn.primary::after, html[data-lm-ctl] .btn.p::after,
  html[data-lm-ctl] :is(.panel, .card, .tool, .pill)::after { display: none; }
  html[data-lm-ctl] .rip { display: none; }
}

/* ═══════ THE BACKDROP AND THE COLD OPEN (v20) — Nightfall port, phase 4 ═══════
   design_handoff_site/port/lm-myst.css, adapted. Pairs with /lm-myst.js, which
   injects every element styled here and adds `lm-myst` to <html>.

   ⚠⚠ THE PORT'S STACKING RULE WOULD HAVE UN-STUCK THE NAV ON 149 PAGES, and
   this file already records that exact incident. It ships:

       body>header,body>main,body>footer,body>#lm-nav,body>.lmn-took
         { position:relative; z-index:1 }

   `body>header` is 0-0-2 and overrides the bare `header{position:sticky;top:0;
   z-index:10}` that 149 pages declare at 0-0-1. Measured: /overlay/ and
   /premium/ — two of the three pages this phase enables — both have a STICKY
   header at z-index 10. See "the audit went from 57 sticky headers to 7, so the
   site nav silently stopped following anyone down a page on a phone. Nobody
   asked for that and nothing in the diff looked wrong."
   `body>.lmn-took` is dead in any case: flow.js puts that class on a <nav>
   INSIDE <header>, never on a direct child of body.

   SO THE STACKING IS SOLVED FROM THE OTHER END. The backdrop drops to z-index
   -1 and <body> gives up its opaque fill on backdrop pages only. A negative
   fixed layer paints above the canvas background and below every in-flow
   element, so NOTHING needs lifting: header keeps whatever position it had,
   main and footer are untouched, and there is no specificity fight to lose.
   ⚠ That works only because `html` paints #000 itself (line ~23) — the canvas
   background is html's, not body's, so the page floor survives body going
   transparent. Do not "tidy" that html rule away.

   ⚠ EVERYTHING IS SCOPED TO html.lm-myst, which /lm-myst.js sets. flow.css
   loads on all 208 pages and only three run the script, so without the scope
   every rule here would be a site-wide change in service of a feature three
   pages have.

   ⚠ --lm-sans and --lm-mono were consumed by the port and declared nowhere, so
   the wordmark would have rendered in system-ui while the site renders Segoe
   UI, and the sub-label in ui-monospace while the site renders Cascadia. Both
   are pointed at what the site actually uses. */

html.lm-myst { --lm-sans: 'Segoe UI', system-ui, sans-serif; --lm-mono: var(--mono); }
html.lm-myst body { background: transparent; }

/* ⚠ width/height are NOT optional on the canvas. inset:0 does not stretch a
   replaced element, so without them it renders at its intrinsic 300x150. */
html.lm-myst #lm-veil, html.lm-myst #lm-field, html.lm-myst #lm-grain {
  position: fixed; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: -1;
}
html.lm-myst #lm-field { display: block; }
html.lm-myst #lm-veil {
  background:
    radial-gradient(112% 82% at 20% 4%, rgba(var(--glow),.085), transparent 58%),
    radial-gradient(96% 76% at 84% 94%, #241c14 0%, transparent 54%),
    linear-gradient(168deg, #0e0b09, #070605 62%, #0c0a08);
}
html.lm-myst #lm-grain { opacity: .5; mix-blend-mode: overlay; background-repeat: repeat; }

/* ── the cold open ──
   1.8s, once per session. No transitions on the wordmark: lm-myst.js writes
   transform, opacity and blur every frame from the same clock that drives the
   fog. A CSS transition here would be a second, competing curve — which is
   exactly what makes a handoff stumble. */
html.lm-myst #lm-intro {
  position: fixed; inset: 0; z-index: 60; pointer-events: none;
  transition: opacity .7s var(--lm-ease), visibility .7s;
}
html.lm-myst #lm-intro.gone { opacity: 0; visibility: hidden; }
html.lm-myst #lm-intro .word {
  position: absolute; left: 50%; top: 46%; white-space: nowrap; opacity: 0;
  transform: translate(-50%,-50%) scale(1.06); filter: blur(18px);
  font: 700 clamp(34px,7.4vw,98px)/1 var(--lm-sans); letter-spacing: -.035em; color: #fff;
  text-shadow: 0 0 60px rgba(var(--glow),.35);
  will-change: transform, opacity, filter; backface-visibility: hidden;
}
html.lm-myst #lm-intro .word i { font-style: normal; font-weight: 400; opacity: .45; }
html.lm-myst #lm-intro .sub {
  position: absolute; left: 50%; top: 57%; opacity: 0;
  transform: translate(-50%,0) translateY(12px);
  font-family: var(--lm-mono); font-size: 11px; letter-spacing: .3em;
  text-transform: uppercase; color: rgba(255,255,255,.42); will-change: transform, opacity;
}
html.lm-myst #lm-skip {
  position: fixed; right: 22px; bottom: 20px; z-index: 61; border: 0; background: 0; cursor: pointer;
  font-family: var(--lm-mono); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: rgba(255,255,255,.42); transition: color var(--lm-in);
}
html.lm-myst #lm-skip:hover { color: rgba(255,255,255,.82); }
html.lm-myst #lm-intro.gone ~ #lm-skip { display: none; }

/* the page-change scan, and the scroll progress line */
html.lm-myst #lm-scan {
  position: fixed; left: 0; top: 0; width: 100%; height: 2px; z-index: 45; pointer-events: none; opacity: 0;
  background: linear-gradient(90deg, transparent, var(--red), transparent);
  box-shadow: 0 0 18px rgba(var(--glow),.7);
}
html.lm-myst #lm-scan.go { animation: lmScan .58s var(--lm-travel); }
@keyframes lmScan { 0% { opacity: 0; transform: translateY(0) } 12% { opacity: 1 } 100% { opacity: 0; transform: translateY(100vh) } }
html.lm-myst #lm-prog {
  position: fixed; left: 0; top: 0; width: 100%; height: 2px; z-index: 46; background: var(--red);
  box-shadow: 0 0 12px rgba(var(--glow),.6); transform: scaleX(0); transform-origin: left;
}

/* ⚠ The intro is skipped under reduced motion by the script, and the fog paints
   ONE static frame instead of running a loop. These two only matter if a page
   is somehow left mid-intro. */
@media (prefers-reduced-motion: reduce) {
  html.lm-myst #lm-intro { display: none; }
  html.lm-myst #lm-scan { animation: none; }
}

/* ═══════ THE FACE (v20) — Nightfall port ═══════
   The mockup is set in Source Sans 3. The site was set in Segoe UI, and that
   one difference carries a lot of the distance between them.

   ⚠ SELF-HOSTED, BECAUSE GOOGLE FONTS CANNOT WORK HERE. _headers ships
   `font-src 'self' data:` with no fonts.gstatic.com, so a <link> to Google
   would be refused by CSP in production — and would look perfect in local
   testing, because localhost serves no _headers at all. That is the trap this
   file already warns about for cache headers, in a second costume.
   The woff2s are the latin subset @fontsource/source-sans-3 ships, copied
   from lotamyst-overlay/ui/fonts where they were already in use. The OFL
   licence travels beside them in /fonts/ and is a condition of shipping the
   face, not a courtesy.

   ⚠ THREE WEIGHTS, AND THE RAMP STILL RESOLVES. The tree declares 650 ×427,
   500 ×27, 800 ×21 and 900 ×1 alongside 400/600/700. CSS font matching sends
   650→700, 500→400, 800→700, 900→700, which is what Segoe UI already does for
   all of them — so 475 of 476 render exactly as they do today. The single 900
   is the one real change and it is worth finding rather than blocking on.

   ⚠ `html body`, not `body`. 207 of the 208 pages set their own body
   font-family in a <style> block. A bare `body` here is 0-0-1, ties them, and
   resolves on source order — which flow.css loses on the eight pages that
   carry a late <style>. The `html` prefix takes it to 0-0-2 and beats all of
   them, exactly as THE BUTTON does for .btn.

   ⚠ FACE ONLY. The mockup's body is `400 16.5px/1.6`; the size and the
   leading are deliberately NOT ported here, because they move every measured
   wrap point on 208 pages. Those get their own commit. */
@font-face { font-family: 'Source Sans 3'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('/fonts/source-sans-3-latin-400-normal.woff2') format('woff2'); }
@font-face { font-family: 'Source Sans 3'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('/fonts/source-sans-3-latin-600-normal.woff2') format('woff2'); }
@font-face { font-family: 'Source Sans 3'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('/fonts/source-sans-3-latin-700-normal.woff2') format('woff2'); }

html body { font-family: 'Source Sans 3', system-ui, -apple-system, 'Segoe UI', sans-serif;
  -webkit-font-smoothing: antialiased; }
/* the terminal layout still overrides this with !important further up — that
   is deliberate and stays. */

/* ═══════ NIGHTFALL SURFACES (v20) — the mockup's palette ═══════
   The port shipped buttons, motion, a backdrop and a hero, and the owner said it
   barely resembled the mockup. This is the largest single reason: the mockup is
   built on a WARM near-black ramp and the site is built on pure black with white
   at five opacities.

   Appended rather than edited. Every declaration below re-states a token the
   blocks above already declare, at the SAME specificity, so it wins on source
   order alone — reverting this commit restores the old palette exactly, and the
   original blocks keep their comments and their reasons intact.

   THE MAPPING, mockup -> this file. Every value is the source's:
     --s0 #0a0908  -> --bg --bg-1        the floor
     --s1 #0f0d0b  -> --bg-2 --panel --card
     --s2 #14110e  -> --panel2           raised
     --s3 #1a1613  -> (kept as --s3, unused for now)
     --edge  #272119 -> --border --line  the hairline
     --edge2 #37302a -> --border-hi      the hover brighten
     --edge3 #4a423a -> --edge           ⚠ NOT --edge2. This file's --edge is its
       STRONGEST hairline (it was rgba(255,255,255,.42)), so it maps to the
       source's third step, not its second. Mapping it to --edge2 measured 1.50:1
       on --panel against 4.06:1 today, which is the control ring on every button
       going nearly invisible; --edge3 measures 2.02:1 and is still the source's
       own value rather than a hand-mixed one.
     --tx  #fff     -> --text
     --tx2 #d2ccc4  -> --text-dim --text2
     --tx3 #9a938a  -> --text-mute --muted
     --tx4 #6d665e  -> --faint
   ⚠ The source has a FOUR-step ink ramp and this file has five. --text2 and
   --text-dim therefore land on the same value. That is the honest collapse: the
   alternative is inventing a step the design does not have.

   ⚠ --well and --bg-deep sit BELOW the floor and the source has no token for
   them. #070605 is not invented either — it is the darkest stop of the backdrop
   gradient the source paints in #lm-veil.

   ⚠ NOT TOUCHED, DELIBERATELY:
     · carbon. It is the one theme with its own surface block, and it is the
       site's original red kept as an explicit choice. The source has no
       equivalent and overwriting it would delete a theme.
     · --gold. This file retired it to white on purpose and --premium is "the ONE
       surviving gold, and the only place it is allowed". The source's gold is
       the premium one; nothing here needs a second.
     · --ok --warn --danger. Already identical to the source's --ok #6ec28a,
       --warn #d7a44a, --bad #e06c6c. Nothing to do.
     · --myst. Stays exactly where it is, on plain html, at the same value.

   ⚠⚠ AND THE ONE THING THIS CANNOT FIX. 1,150 rgba(255,255,255,alpha) literals
   are hand-written across the 208 pages — hairlines, dim ink and hover washes
   painted directly rather than through a token. No token reaches them. Until
   they are swept the site is warm underneath and cool on top, and that is
   expected rather than a mistake in this block. The sweep is the next commit. */
html:not([data-theme="carbon"]) {
  --bg: #0a0908; --bg-1: #0a0908; --bg-2: #0f0d0b;
  --panel: #0f0d0b; --panel2: #14110e; --well: #070605; --card: #0f0d0b;
  --bg-deep: #070605;
  --s3: #1a1613; --s4: #221d18;

  --text: #ffffff;
  --text-dim: #d2ccc4;
  --text2: #d2ccc4;
  --text-mute: #9a938a;
  --muted: #9a938a;
  --faint: #6d665e;

  --border: #272119;
  --line: #272119;
  --border-hi: #37302a;
  --edge: #4a423a;
}

/* ── the default accent becomes Myst ──
   The source's UNTHEMED default is --acc:var(--myst); its mono theme is white.
   This file's no-attribute slot and its mono theme are ONE RULE — mono owns the
   no-attribute slot, deliberately, and the comment above records the /app/ bug
   that came from assuming otherwise. So the two cannot be split without
   inventing a thirteenth theme and a second default, and the smaller true change
   is to give the default the source's accent.
   ⚠ THE COST, stated rather than discovered: on this theme alone, the accent and
   the measurement ink are now the same colour, so "Myst draws the reading" stops
   distinguishing anything HERE. It still holds on the other eleven. That is the
   same trade the --myst law already accepts for glacier at dE76 14.6, taken
   knowingly at dE76 0 for one theme — and taken because Myst being the brand's
   own default accent is the brand identifying itself, not a page reaching for a
   colour it did not earn. The sentence "pointer feedback is never Myst" is still
   the operative rule everywhere a page might reach for it.
   ⚠ If mono-as-white matters more than the source's default, this block is the
   one to delete: everything else in this commit stands without it. */
html:not([data-theme]), html[data-theme="mono"] {
  --red: var(--myst); --red-mid: var(--myst-mid); --red-dim: var(--myst-line);
  --glow: 159,220,238; --glow-dim: 126,201,222;
  /* the source's ink on accent. --on-myst is #001318 and stays as it is; this is
     a shade lighter because it sits on a fill, not beside one. */
  --on-red: #06222b;
}

/* the source's radii and shell width, as tokens. Declared, not yet spent —
   applying them is a layout change and belongs in its own commit. */
html { --r: 10px; --r-lg: 14px; --shell: 1180px; }

/* ── the cursor light ──
   A 620px accent radial that follows the pointer, riding --cx/--cy so the whole
   thing composites and nothing reads layout. It is the source's own size and
   opacity. Off under reduced motion: a light that chases the pointer is exactly
   the kind of movement that query is asking about.
   ⚠ z-index 0, above the backdrop's -1 and below everything in flow. */
html.lm-myst #lm-cursor { position: fixed; inset: 0; pointer-events: none; z-index: 0; }
html.lm-myst #lm-cursor::before {
  content: ""; position: absolute; left: 0; top: 0; width: 620px; height: 620px;
  margin: -310px 0 0 -310px; border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--glow),.075), transparent 62%);
  transform: translate3d(var(--cx, 50vw), var(--cy, 50vh), 0);
}
@media (prefers-reduced-motion: reduce) { html.lm-myst #lm-cursor { display: none; } }

/* ═══════ THE BAR, WARMED (v20) ═══════
   #lm-nav painted rgba(0,0,0,.92) — a cool near-black behind the one element
   that is on every page at every scroll position. The source's bar is 80% of
   the floor with an 11px blur and a --edge hairline; --s0 is --bg here.
   ⚠ The solid declaration comes first on purpose. color-mix is recent and this
   file already carries a fallback ordering for it elsewhere — an engine without
   it must keep a real background, not a transparent bar over moving text. */
/* ponytail: the fill, blur and hairline that lived here moved into the #lm-nav
   box rule above (v21) so the bar has ONE owner — this later copy at 92% was
   silently winning over the 82% that matches the homepage. Nothing left to set. */
/* the stamped bar sits inside each page's own <header>, which paints its own
   background — so this warms the hairline it shares and leaves the fill alone. */
header nav.lmn-took, [role="banner"] nav.lmn-took { border-color: var(--border); }

/* ═══════ THE BAR'S TYPE, PINNED (v21) ═══════
   Measured across all 148 stamped pages in one pass: 127 sat on the homepage's
   pixel, 17 had the wordmark 1px low and the link box 1px short. Those 17 set
   a different line-height on the page and the bar INHERITED it (25.6 vs 26.4 on
   a 16px wordmark). The stamped wordmark was also weight 700 / normal tracking
   against the homepage's 800 / 0.01em, and the injected one 0.9rem / 700.
   Nothing in the bar may inherit type from the page any more; it is pinned
   here, for all three bar kinds, to the homepage's #nav numbers. */
/* box-sizing too: the 44 dated answer pages ship no `* { box-sizing }` reset,
   so #lm-nav there was 62 + its 1px hairline = 63 while every other bar was 62. */
html:not([data-shell="app"]) body > header, #lm-nav, html[data-shell="app"] body > header { line-height: 1.65; box-sizing: border-box; }
html:not([data-shell="app"]) body > header > a.logo, #lm-nav .lmn-logo, html[data-shell="app"] header .logo {
  font-size: 1rem; font-weight: 800; letter-spacing: 0.01em; line-height: 1.65;
}
/* ponytail: the row's right edge was 1383 on pages that scroll and 1398 on the
   three that do not, because the 15px scrollbar gutter came and went. Reserve
   it always; the bar then ends on the same pixel on every page. */
html { scrollbar-gutter: stable; }

/* ═══════ THE DIALS (v20) ═══════
   The fog controls, living inside the appearance panel flow.js already builds
   rather than in a second one of their own. Values and geometry are the
   source's; the class prefix is lmf- because .seg is already spoken for on the
   bench pages. */
html.lm-myst #lm-lay-panel .lmf-wrap { margin-top: 4px; }
html.lm-myst .lmf-grp { margin-bottom: 13px; }
html.lm-myst .lmf-grp > label {
  display: block; margin-bottom: 7px; font-family: var(--mono); font-size: 10px;
  letter-spacing: .14em; text-transform: uppercase; color: var(--faint);
}
html.lm-myst .lmf-seg { display: flex; gap: 4px; }
html.lm-myst .lmf-seg button {
  flex: 1; height: 30px; padding: 0 6px; border: 0; border-radius: 8px;
  background: var(--panel2); color: var(--muted); font: inherit; font-size: 12px; cursor: pointer;
  box-shadow: inset 0 0 0 1px var(--border);
  transition: background var(--lm-out), color var(--lm-out), box-shadow var(--lm-out),
              transform var(--lm-out) var(--lm-ease);
}
html.lm-myst .lmf-seg button:hover { color: var(--text); background: var(--bg-2); transition-duration: var(--lm-in); }
html.lm-myst .lmf-seg button:active { transform: scale(.955); transition-duration: var(--lm-press); }
html.lm-myst .lmf-seg button.on { background: var(--red); color: var(--on-red); font-weight: 600; box-shadow: none; }
html.lm-myst .lmf-acts { display: grid; gap: 7px; margin-bottom: 0; }
html.lm-myst .lmf-act {
  width: 100%; height: 32px; border: 0; border-radius: 8px; cursor: pointer;
  background: 0; color: var(--text-dim); font: inherit; font-size: 12.5px;
  box-shadow: inset 0 0 0 1px var(--border);
  transition: color var(--lm-out), box-shadow var(--lm-out), background var(--lm-out);
}
html.lm-myst .lmf-act:hover { color: var(--text); background: var(--panel2); box-shadow: inset 0 0 0 1px var(--border-hi); transition-duration: var(--lm-in); }

/* ── --quant: the source's ink for a measured quantity ──
   #f0c419, declared on plain html like --premium and --myst so it means one
   thing under every theme.
   ⚠ On aurum alone this is the accent as well, hex for hex. That is the same
   collision the token commit accepted knowingly for Myst on the default theme,
   and it is accepted here on the same terms rather than refused on grounds
   already waived. Eleven themes are unaffected.
   ⚠ NOT the retired premium gold. --premium is a different colour with a
   different job, and site-lint ratchets ITS literal — which is why this note
   does not spell it out. Naming a forbidden literal while explaining it has
   failed that check twice already in this port. */
html { --quant: #f0c419; }

/* ═══════ ONE BAR, EVERYWHERE (v20) ═══════
   The homepage owns a hand-written #nav; the other 148 pages get nav.lmn-took
   stamped into their own <header>. Measured at 1440px the two disagreed on
   height (62 vs 56) and, more visibly, on surface: the stamped header paints
   rgba(0,0,0,.72), a COOL black, on a site whose floor is now #0a0908.

   This gives the stamped header the treatment the homepage's bar wears once it
   has scrolled, so the two read the same everywhere the bar is actually read.
   The homepage additionally starts transparent over its hero, which is a
   difference in one state on one page rather than two different bars.

   ⚠ min-height, not height. Three pages deliberately run a two-row bar
   (/overlay/, /forge/, /forge/build/ — gen-header's TWO_ROW set) and several
   carry a breadcrumb in the same element. A fixed height would clip them.
   ⚠ Scoped off [data-shell="app"]: /app/ owns its whole shell.
   ⚠ The solid colour is stated before the color-mix for the reason this file
   already gives elsewhere — an engine without color-mix must keep an opaque
   bar, not a transparent one over moving text. */
html:not([data-shell="app"]) body > header {
  min-height: 62px;
  /* the homepage's own inset and vertical rhythm. padding 0 + min-height, so a
     one-row bar centres at exactly 62 and the two-row pages still grow. This
     (0,1,2) outranks every page's own `header { padding: 0.9rem }`, which is
     what was stacking 7px on top of the 62 and putting the stamped bar at 69. */
  padding: 0 clamp(1rem, 4vw, 2.6rem);
  background: var(--bg, #0a0908);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  border-bottom: 1px solid var(--border);
  -webkit-backdrop-filter: blur(11px); backdrop-filter: blur(11px);
}
