﻿

@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/assets/fonts/space-grotesk-var.woff2') format('woff2');
}
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/assets/fonts/hanken-grotesk-var.woff2') format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url('/assets/fonts/jetbrains-mono-var.woff2') format('woff2');
}

:root {
  
  --bg:          #0b0d12;
  --bg-1:        #0f121a;
  --bg-2:        #141823;
  --bg-3:        #1b2030;
  --bg-glass:    rgba(11,13,18,0.92);  
  --bg-card:     #12151d;              
  --bg-elevated: #1a1f2a;              

  --border:      #1e2430;
  --border-2:    #2c3444;
  --border-glow: rgba(232,163,61,0.16);
  --hairline:    var(--veil-3);

  --text:        #eef2f8;
  --text-2:      #9aa7bd;
  --text-3:      #7c8aa3;

  /* Each accent is declared TWICE: once solid, once as a bare `r,g,b` triplet. The triplet is not
     decoration -- it is the reason Colorblind-friendly mode is one block rather than a second
     stylesheet. Around seventy hand-written rgba(var(--green-rgb),.35) borders and tints are scattered
     through the page <style> blocks; re-pointing only --green would have left every one of them
     green around a blue number, which reads as a rendering fault rather than a palette. They all
     compose from the triplet now, so one override moves the solid AND every tint made from it.
     The solid stays a literal hex on purpose: charts.js reads these back through getPropertyValue
     and parses them as hex, and rgb(var(--x-rgb)) would hand it a string it cannot parse. The two
     spellings of one colour have to be kept in step by hand -- that is the price of the hex. */
  --blue-rgb:       108,140,255;
  --green-rgb:      63,185,132;
  --gold-rgb:       232,163,61;
  --red-rgb:        229,72,77;
  --red-bright-rgb: 242,109,113;
  --purple-rgb:     176,124,240;
  --region-eu-rgb:  108,140,255;
  --region-jp-rgb:  52,208,180;
  --region-na-rgb:  229,115,154;

  --blue:        #6c8cff;
  --blue-bright: #8aa2ff;
  --blue-hover:  #5570e6;
  --blue-dim:    rgba(var(--blue-rgb),0.12);
  --blue-glow:   rgba(var(--blue-rgb),0.30);

  --green:       #3fb984;
  /* The lift the confirm button takes on hover. A token beside --gold-hover and --blue-hover,
     because it was the last hardcoded green in the button set and it made .btn-success revert to
     the shipped green under the pointer while its resting state was azure. */
  --green-hover: #46c78e;
  --green-dim:   rgba(var(--green-rgb),0.12);
  --green-glow:  rgba(var(--green-rgb),0.30);
  /* The two extra stops a FILLED positive control paints its gradient from, and the near-black
     label that sits on it. Tokens rather than the #4fd196 / #2fa572 / #052318 they were, because a
     three-stop gradient whose middle follows the palette and whose ends do not renders
     green-azure-green. Deliberately not re-declared per theme: the filled buttons that use them
     paint the same gradient on paper as on the dark board, and always did. */
  --green-lift:  #4fd196;
  --green-sink:  #2fa572;
  --on-green:    #052318;

  /* Labels printed ON a filled semantic swatch -- the best/worst day tags on /statistics, the only
     two rules on the site of that shape. Near-black on the positive, white on the negative, which
     is correct right up until Colourblind-friendly mode changes how LIGHT those two fills are: the
     negative goes from #e5484d to a much lighter #ff8a4c and white on it falls to 2.34:1. So each
     colourblind block re-picks the one whose ground moved, and only that one. These values are the
     literals the two rules already carried, so Standard mode is unchanged to the byte. */
  --on-pos-tag:  #04140d;
  /* Near-black, not the white it was. White on this red measures 3.91:1 -- the .mt-tag.worst pill
     is 9px bold and was never large-text exempt. Near-black reads 4.57:1 here and 7.66:1 once the
     mode lightens the negative to orange, so one value serves both and the mode needs no override. */
  --on-neg-tag:  #2a1004;

  --gold:        #e8a33d;
  --gold-hover:  #d08b27;
  --gold-dim:    rgba(var(--gold-rgb),0.12);
  --gold-glow:   rgba(var(--gold-rgb),0.30);

  --red:         #e5484d;
  --red-bright:  #f26d71;
  --red-dim:     rgba(var(--red-rgb),0.12);
  /* Label for text sitting ON a dark red fill -- .btn-danger and nothing else, now that .badge-red
     has been corrected to --red-bright. --red is the ground there and would vanish. A token rather
     than the #fca5a5 it was, so the mode can move it with the fill instead of leaving pink text on
     an orange button. Do not reach for this on a tinted chip: the fill is the dark thing here and
     the pale thing there, and one token cannot be both. */
  --red-pale:    #fca5a5;
  /* The destructive button's fill and its hover, carried as whole rgba() values rather than
     derived. A colour-mix from --red was tried and rejected: it produces an OPAQUE colour, and
     these are translucent by design so the button darkens whatever card it sits on. Holding the
     literal keeps Standard mode byte-identical and still gives the mode one place to move. */
  --red-fill:       rgba(127,29,29,0.55);
  --red-fill-hover: rgba(153,27,27,0.8);

  --purple:      #b07cf0;
  --purple-dim:  rgba(var(--purple-rgb),0.12);
  --region-eu:   #6c8cff;
  --region-jp:   #34d0b4;
  --region-na:   #e5739a;

  /* Readable names for the two colours that carry meaning. An audit of every var(--green) and
     var(--red) on this site found not one decorative use of either -- they ARE the positive and
     the negative already, which is why Colorblind-friendly mode re-points them rather than a
     parallel set that nothing consumes. These aliases exist so new code can say what it means
     instead of naming a hue that the reader may not be seeing. */
  --positive:    var(--green);
  --negative:    var(--red);
  --warning:     var(--gold);
  --info:        var(--blue);

  /* The diverging scale the market-timing grid paints, as triplets because that grid composes its
     own alpha per cell to carry magnitude. Cheap hour against dear hour was green against red at
     matched lightness: 13.6 CIEDE2000 apart for a deuteranope, which is a whole grid reading as
     one colour at varying strength. */
  --heat-cheap-rgb: var(--green-rgb);
  --heat-dear-rgb:  var(--red-rgb);

  /* Grade colors are the single most meaning-bearing encoding in a market product, and they were
     defined three times with three disagreeing palettes (shared .gborder-*, database's g-*,
     nix's g-*). ONE source now; every grade system consumes these. Hexes are the .gborder set,
     which had the widest deployment. Same values in both themes on purpose: grades sit on item
     icon tiles whose ground does not re-pick. */
  --grade-c:   #9aa7bd;  --grade-c-dim:   rgba(154,167,189,0.14);
  --grade-b:   #2c9a63;  --grade-b-dim:   rgba(44,154,99,0.14);
  --grade-a:   #5575e4;  --grade-a-dim:   rgba(85,117,228,0.14);
  --grade-aa:  #9050d4;  --grade-aa-dim:  rgba(144,80,212,0.14);
  --grade-aaa: #ee8a3c;  --grade-aaa-dim: rgba(238,138,60,0.14);
  --grade-s:   #f0506a;  --grade-s-dim:   rgba(240,80,106,0.14);
  --grade-ss:  #f0506a;  --grade-ss-dim:  rgba(240,80,106,0.14);

  --r-xs: 6px;  --r-sm: 8px;  --r-md: 12px;
  --r-lg: 16px; --r-xl: 22px; --r-pill: 999px;
  /* Data containers (boards, toolbars, feed rows, app modals) sit on --r-surface; the rounder
     steps are marketing-surface radii. A terminal's containers are quiet. */
  --r-surface: 10px;

  --shadow-sm:  0 1px 2px var(--scrim-2);
  --shadow-md:  0 6px 20px var(--scrim-2);
  --shadow-lg:  0 14px 44px var(--scrim-2);
  --shadow-xl:  0 28px 80px var(--scrim-3);
  /* --glow-blue / --glow-gold are RETIRED (design direction R3): colored drop-glows were the
     signature ambient-dashboard tell. Elevation is hairline borders; shadows are for overlays. */

  --ease:        cubic-bezier(0.4, 0, 0.2, 1);
  --ease-out:    cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.5, 1);
  --t-fast:   0.15s;
  --t-med:    0.26s;
  --t-slow:   0.5s;

  /* ── Rail micro-interaction tokens ──────────────────────────────────────────────────────
     One duration for every glyph in the rail, so a cursor moving down it meets sixteen controls
     that all answer in the same beat. The leave is deliberately the longer of the two: arriving
     is a command and is answered at once, leaving is not, so the glyph relaxes rather than
     snapping back. --rail-mo-turn is the single exception and earns it -- the Settings gear
     travels 40 degrees against the 5-8 the rest of the rail uses, and the same 0.26s over six
     times the arc reads as a flick rather than a turn.

     --rail-u converts a screen pixel into icon user units. A transform on a <path> INSIDE the
     SVG is measured in viewBox units (24 across a 17px box), so a bare translateY(-1px) on a
     part moves 0.7px while the same value on the <svg> itself moves a full one. Writing the
     part movements as <px> * var(--rail-u) keeps both halves of the system speaking one unit.

     --rail-motion is the master switch: every transform in the rail is multiplied by it, so the
     whole system collapses to its resting geometry with one declaration for a reader who has
     asked for less movement. Colour, background and glow feedback are deliberately NOT gated on
     it -- those are what say "this row is interactive", and they must survive the preference. */
  --rail-mo:      0.26s;
  --rail-mo-out:  0.34s;
  --rail-mo-turn: 0.42s;
  /* Gesture lengths. A gesture is a motion with a shape -- a ring, a beat, a drop -- and it needs
     long enough to be read as one event rather than a twitch. --rail-g-long is for the two that
     oscillate (the bell and the question mark's double-take): the extra 180ms is what turns four
     swings into a damped one instead of a shudder. */
  --rail-g:      0.6s;
  --rail-g-long: 0.78s;
  --rail-ease:      cubic-bezier(0.22, 0.72, 0.28, 1);   /* enter: immediate, decelerating, no overshoot */
  --rail-ease-out:  cubic-bezier(0.4, 0, 0.2, 1);        /* leave */
  --rail-u:      calc(24px / 17);
  --rail-motion: 1;

  
  --maxw: 1200px;
  --maxw-data: 1400px;
  --maxw-data-wide: min(1760px, 97vw);
  
  --gutter: 22px;
  --nav-h: 64px;

  --font-display: 'Space Grotesk', system-ui, sans-serif;
  --font-body:    'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono:    'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;

  
  --space-1:4px; --space-2:8px; --space-3:12px; --space-4:16px; --space-5:24px;
  --space-6:32px; --space-7:48px; --space-8:64px; --space-9:96px; --space-10:128px;
  --measure:60ch; --measure-tight:42ch; --pad-x:22px;
  --section-gap-marketing: var(--space-9);
  --section-gap-app: var(--space-5);
  
  --fs-display: clamp(40px, 7vw, 76px);
  --fs-h1: clamp(28px, 4.5vw, 40px);
  --fs-h2: clamp(22px, 3.5vw, 30px);
  --fs-h3: 18px; --fs-h4: 16px;
  --fs-body-lg: 16px; --fs-body: 14px; --fs-small: 13px; --fs-micro: 12px;
  --fs-label: 12px; --fs-mono-xl: 30px; --fs-mono: 13px;
  /* Where the product actually lives (design direction R7/R10). Operate pages title at
     --fs-title-page, never at the display scale; tables and their metadata stop hardcoding
     31 distinct px sizes and use these. */
  --fs-title-page: 20px;
  --fs-table: 12px; --fs-table-dense: 11.5px; --fs-meta: 10.5px;

  color-scheme: dark;
}

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; overflow-x: hidden; -webkit-text-size-adjust: 100%; }

body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  /* clip, NOT hidden. Both do the same job here -- stop a stray wide child from letting the page
     scroll sideways -- but `hidden` makes the element a SCROLL CONTAINER, and body is one that never
     scrolls because the document scrolls the root. Every `position: sticky` descendant on this site
     was therefore sticking to a scrollport that never moved, and silently did nothing; that is why
     .tl-nav is `position: fixed` rather than sticky. The workaround was written instead of the fix.
     `clip` clips without establishing a scroll container, so sticky works and the guard stays.
     Removing it outright was tried first and measured: /ledger at 768 gained 146px of real sideways
     scroll, from a table that has always been that wide and was only ever hidden by this line. */
  overflow-x: clip;
  line-height: 1.5;
  letter-spacing: -0.011em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display); letter-spacing: -0.02em;
  
  text-wrap: balance;
}
p, .section-sub, li, dd, figcaption, .deals-gate-copy, .deals-gate-alt { text-wrap: pretty; }
th, td, .stat-value, .mc-value, .price-card-value, input[type="number"], .tabnum,
.profit, .pct-positive, .pct-negative, .item-id { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

.item-cell, .item-cell a, .mm-card-name { font-family: var(--font-body); letter-spacing: -0.005em; }

.empty-state, .empty-state *,
.state-box,   .state-box *,
.loading-state, .loading-state * { font-family: var(--font-body); }

/* The ambient background is SITE-WIDE by owner decision (2026-08-22, reaffirmed 2026-08-27 after
   the design waves turned it off — "the background is off when it should not be" is a regression
   report, not a preference). The 2026-08 R3 marketing-only gate is therefore retired: the static
   .lux-aurora vignette and the animated particle layers render on every page, governed ONLY by
   the settings gates (data-bg / data-perf / data-motion, further down this sheet), which are what
   made the layers acceptable in the first place. The warmer double radial wash below stays a
   marketing flourish: the documents (/, /upgrade, /guide) opt in with body.page-marketing. */
body.page-marketing::before {
  content: '';
  position: fixed; inset: 0;
  z-index: 0; pointer-events: none;
  background:
    radial-gradient(54vw 52vh at 14% -10%, rgba(232,163,61,0.06) 0%, transparent 60%),
    radial-gradient(48vw 50vh at 92% 108%, rgba(108,140,255,0.06) 0%, transparent 62%);
}

.tl-nav, .page-wrap, .page-wrap-wide, .tl-footer { position: relative; z-index: 1; }

:focus-visible { outline: 2px solid var(--blue-bright); outline-offset: 2px; border-radius: 4px; }
a:focus-visible, button:focus-visible, .btn:focus-visible { outline-offset: 3px; }

.toast-msg {
  position: fixed; bottom: 28px; left: calc(50% + var(--rail-w, 0px) / 2); transform: translateX(-50%) translateY(20px);
  background: var(--bg-elevated); border: 1px solid var(--border-2); color: var(--text);
  padding: 11px 20px; border-radius: var(--r-md); font-size: var(--fs-small); font-weight: 600;
  box-shadow: var(--shadow-lg); z-index: 2000; max-width: min(92vw, 52ch); text-align: center;
  opacity: 0; transition: opacity var(--t-med) var(--ease), transform var(--t-med) var(--ease-spring);
  pointer-events: none;
}
.toast-msg.show { opacity: 1; transform: translateX(-50%) translateY(0); }

.toast-msg.is-error { border-color: color-mix(in srgb, var(--red) 45%, var(--border-2)); }

::selection { background: rgba(108,140,255,0.30); color: #fff; }

.tl-nav {
  position: sticky; top: 0; z-index: 100;
  background: var(--bg-glass);
  border-bottom: 1px solid transparent;
  transition: border-color var(--t-med) var(--ease), background var(--t-med) var(--ease), box-shadow var(--t-med) var(--ease);
}
.tl-nav.scrolled { background: rgba(8,10,16,0.97); border-bottom-color: var(--border); box-shadow: var(--shadow-md); }
.tl-nav-inner {
  max-width: var(--maxw); margin: 0 auto;
  padding: 0 var(--gutter); height: var(--nav-h);
  display: flex; align-items: center; gap: 8px;
}
.tl-brand {
  display: flex; align-items: center; gap: 10px;
  font-weight: 700; font-size: 15px; letter-spacing: -0.02em;
  text-decoration: none; color: var(--text);
  margin-right: 12px; flex-shrink: 0;
  transition: opacity var(--t-fast) var(--ease);
}
.tl-brand:hover { opacity: 0.85; }

.tl-brand-mark {
  width: 20px; height: 20px; flex-shrink: 0;
  background: no-repeat center/100% 100% url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' role='img' aria-label='TL Tracker'%3E %3Cdefs%3E %3CclipPath id='cp'%3E%3Crect width='32' height='32' rx='7'/%3E%3C/clipPath%3E %3CradialGradient id='bg' cx='50%25' cy='14%25' r='86%25'%3E %3Cstop offset='0' stop-color='%230a1418'/%3E%3Cstop offset='1' stop-color='%2304080b'/%3E %3C/radialGradient%3E %3Cfilter id='gl' filterUnits='userSpaceOnUse' x='-5' y='-5' width='42' height='42' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='0.9'/%3E%3C/filter%3E %3Cfilter id='gs' filterUnits='userSpaceOnUse' x='-5' y='-5' width='42' height='42' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='0.45'/%3E%3C/filter%3E %3Cmask id='ol' maskUnits='userSpaceOnUse' x='0' y='0' width='32' height='32'%3E %3Crect width='32' height='32' fill='%23000'/%3E %3Cg fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round'%3E %3Cpath d='M5.20 7.92 H14.32' stroke-width='4.0'/%3E%3Cpath d='M9.84 7.92 V23.60' stroke-width='3.68'/%3E%3Cpath d='M19.76 8.40 V23.60 H27.28' stroke-width='3.68'/%3E %3C/g%3E %3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round'%3E %3Cpath d='M5.20 7.92 H14.32' stroke-width='2.30'/%3E%3Cpath d='M9.84 7.92 V23.60' stroke-width='1.98'/%3E%3Cpath d='M19.76 8.40 V23.60 H27.28' stroke-width='1.98'/%3E %3C/g%3E %3C/mask%3E %3C/defs%3E %3Cg clip-path='url(%23cp)'%3E %3Crect width='32' height='32' fill='url(%23bg)'/%3E %3Cpath d='M2.6 6.4 Q16 23.4 29.4 27.4' fill='none' stroke='%232fd4c7' stroke-width='0.5' opacity='0.42'/%3E %3Cline x1='4.4' y1='5.6' x2='4.4' y2='9.2' stroke='%232fd4c7' stroke-width='0.45' opacity='0.5'/%3E %3Crect x='3.8' y='6.4' width='1.3' height='2.0' rx='0.3' fill='%232fd4c7' opacity='0.8'/%3E %3Cline x1='10.2' y1='10.0' x2='10.2' y2='14.3' stroke='%232fd4c7' stroke-width='0.45' opacity='0.5'/%3E %3Crect x='9.4' y='11.0' width='1.6' height='2.4' rx='0.4' fill='%232fd4c7' opacity='0.8'/%3E %3Cline x1='16.0' y1='14.5' x2='16.0' y2='19.3' stroke='%232fd4c7' stroke-width='0.45' opacity='0.5'/%3E %3Crect x='15.0' y='15.5' width='2.0' height='2.8' rx='0.5' fill='%232fd4c7' opacity='0.8'/%3E %3Cline x1='21.8' y1='18.9' x2='21.8' y2='24.4' stroke='%232fd4c7' stroke-width='0.45' opacity='0.5'/%3E %3Crect x='20.7' y='20.0' width='2.3' height='3.2' rx='0.6' fill='%232fd4c7' opacity='0.8'/%3E %3Cline x1='27.6' y1='23.3' x2='27.6' y2='29.5' stroke='%232fd4c7' stroke-width='0.45' opacity='0.5'/%3E %3Crect x='26.3' y='24.6' width='2.6' height='3.6' rx='0.7' fill='%232fd4c7' opacity='0.8'/%3E %3Cg fill='none' stroke='%2304080b' stroke-linecap='round' stroke-linejoin='round'%3E %3Cpath d='M5.20 7.92 H14.32' stroke-width='4.0'/%3E%3Cpath d='M9.84 7.92 V23.60' stroke-width='3.68'/%3E%3Cpath d='M19.76 8.40 V23.60 H27.28' stroke-width='3.68'/%3E %3C/g%3E %3Cg mask='url(%23ol)' filter='url(%23gl)' opacity='0.85'%3E%3Crect width='32' height='32' fill='%2322e0f0'/%3E%3C/g%3E %3Cg mask='url(%23ol)' filter='url(%23gs)'%3E%3Crect width='32' height='32' fill='%2322e0f0'/%3E%3C/g%3E %3Cg mask='url(%23ol)'%3E%3Crect width='32' height='32' fill='%23eafcff'/%3E%3C/g%3E %3Crect x='1' y='1' width='30' height='30' rx='6.2' fill='none' stroke='%2322e0f0' stroke-width='0.7' opacity='0.45'/%3E %3C/g%3E %3C/svg%3E");
}

.tl-nav-links { display: flex; align-items: center; gap: 2px; flex: 1; }
.tl-nav-links a {
  position: relative;
  color: var(--text-2); text-decoration: none;
  padding: 8px 13px; border-radius: var(--r-sm);
  font-size: 13px; font-weight: 500;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
  white-space: nowrap;
}
.tl-nav-links a:hover { color: var(--text); background: var(--veil-2); }
.tl-nav-links a.active { color: var(--text); }
.tl-nav-links a.active::after {
  content: ''; position: absolute; left: 13px; right: 13px; bottom: 2px;
  height: 2px; border-radius: 2px;
  background: linear-gradient(90deg, var(--blue), var(--blue-bright));
  transform-origin: left; animation: nav-underline var(--t-med) var(--ease-out);
}
@keyframes nav-underline { from { transform: scaleX(0); opacity: 0; } to { transform: scaleX(1); opacity: 1; } }
/* The one Premium entry point. Flat gold fill, no gradient, no glow, no motion: gold already
   names money here (R2), so the pill needs no further volume — the breath animation and the
   hover lift went in the adversarial pass, along with the gradient. Hover states the intent
   with color alone, like every other control. */
.tl-nav-links a.nav-upgrade,
.rail-foot a.nav-upgrade {
  color: var(--gold) !important;
  background: var(--gold-dim);
  border: 1px solid rgba(232,163,61,0.28);
  font-weight: 600; margin-left: 6px;
  transition: background var(--t-med) var(--ease), border-color var(--t-med) var(--ease), color var(--t-med) var(--ease);
}
.tl-nav-links a.nav-upgrade::after,
.rail-foot a.nav-upgrade::after { display: none; }

.tl-nav-links a.nav-upgrade:hover,
.rail-foot a.nav-upgrade:hover {
  background: rgba(232,163,61,0.18) !important;
  border-color: rgba(232,163,61,0.5) !important;
  color: #fcd34d !important;
}

.tl-nav-auth { display: flex; align-items: center; gap: 9px; font-size: 13px; color: var(--text-2); flex-shrink: 0; margin-left: auto; }
.tl-nav-auth img { width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--border-2); }
.tl-nav-auth a {
  border: 1px solid var(--border); background: var(--veil-1);
  color: var(--text-2); border-radius: var(--r-sm);
  padding: 6px 12px; text-decoration: none;
  font-size: 12px; font-weight: 500; transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.tl-nav-auth a:hover { border-color: var(--border-2); color: var(--text); background: var(--veil-2); }
.tl-nav-auth .admin-btn { color: #93c5fd; border-color: rgba(108,140,255,0.3); background: rgba(108,140,255,0.08); }
.tl-nav-auth .admin-btn:hover { background: rgba(108,140,255,0.16); border-color: rgba(108,140,255,0.5); }
.auth-name { font-weight: 500; color: var(--text); }

.tl-nav-auth .auth-me { display: inline-flex; align-items: center; gap: 8px; text-decoration: none;
  border-radius: var(--r-pill); padding: 2px; transition: box-shadow var(--t-fast) var(--ease); }
.tl-nav-auth .auth-me:hover { box-shadow: 0 0 0 1px var(--border-2); }
.tl-nav-auth .auth-me:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }

.tl-hamburger {
  display: none; background: var(--veil-1);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  color: var(--text-2); padding: 8px 11px; cursor: pointer;
  font-size: 16px; line-height: 1; margin-left: auto;
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.tl-hamburger:hover { border-color: var(--border-2); color: var(--text); }
.tl-hamburger svg { width: 22px; height: 22px; display: block; }

@keyframes menu-drop { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }

.tier-badge, .tier { border-radius: var(--r-pill); padding: 3px 10px; font-size: 11px; font-weight: 600; letter-spacing: 0.02em; display: inline-block; white-space: nowrap; }
.tier-free    { background: var(--veil-3); border: 1px solid var(--border);        color: var(--text-2); }

.tier-premium { background: rgba(108,140,255,0.10); border: 1px solid rgba(108,140,255,0.32); color: var(--blue-bright); }
.tier-admin   { background: var(--blue-dim);         border: 1px solid rgba(108,140,255,0.3); color: #93c5fd; }

@media (max-width: 880px) {
    /* Rail is off-canvas here and --rail-w is NOT reset to 0, so every content-centred fixed
       layer has to come back to the viewport centre explicitly. */
    .lux-toast, .toast-msg, .perf-hint { left: 50% !important; }

  .tl-nav-links { display: none; }
  .tl-hamburger { display: block; flex-shrink: 0; }
}

@media (max-width: 768px) {
  .tl-nav-auth .auth-name, .tl-nav-auth .tier-badge, .tl-nav-auth .tier { display: none; }
  
  .tl-nav-inner .region-leds { display: none; }
  
  .tl-nav-inner .tl-nav-auth { gap: 6px; }
  .tl-nav-inner .tl-nav-auth a { padding: 5px 9px; font-size: 11px; }
}

.tl-footer {
  border-top: 1px solid var(--border);
  background: linear-gradient(to bottom, var(--bg), #04050a);
  margin-top: 96px; padding: 56px var(--gutter) 36px;
}
.tl-footer-inner { max-width: calc(var(--maxw) - 2 * var(--gutter)); margin: 0 auto; display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 40px; }
.tl-footer-brand .tl-brand { margin-bottom: 14px; }
.tl-footer-brand p { font-size: 13px; color: var(--text-3); line-height: 1.7; max-width: 300px; }
/* h2, not h4, since 2026-08-27: most pages hold only an h1, so the column titles were a
   h1 -> h4 jump in every outline (axe heading-order). Same 11px caps either way; the h4
   alternative stays in the selector for any straggler markup. */
.tl-footer-col h2, .tl-footer-col h4 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.09em; color: var(--text-3); margin: 0 0 15px; }
.tl-footer-col a { display: block; color: var(--text-2); text-decoration: none; font-size: 13px; margin-bottom: 10px; width: fit-content; transition: color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease); }
.tl-footer-col a:hover { color: var(--text); transform: translateX(3px); }
.tl-footer-bottom {
  max-width: calc(var(--maxw) - 2 * var(--gutter)); margin: 36px auto 0;
  padding-top: 22px; border-top: 1px solid var(--border);
  display: flex; justify-content: space-between; align-items: center;
  gap: 12px; flex-wrap: wrap; font-size: 12px; color: var(--text-3);
}
.tl-footer-legal {
  max-width: calc(var(--maxw) - 2 * var(--gutter)); margin: 18px auto 0; padding-top: 16px;
  border-top: 1px solid var(--hairline);
  font-size: 11px; line-height: 1.65; color: var(--text-3); text-align: left;
  
  max-width: 118ch;
}
.tl-footer-legal a { color: var(--text-2); text-decoration: none; }
.tl-footer-legal a:hover { color: var(--text); text-decoration: underline; }
@media (max-width: 768px) { .tl-footer-inner { grid-template-columns: 1fr 1fr; gap: 32px; } .tl-footer-brand { grid-column: 1 / -1; } }
/* TWO COLUMNS BELOW 480, NOT ONE. The single-column rule stacked Product (6 links), Resources (2)
   and Legal (3) one per row, and every footer link carries a 44px tap floor from the block above —
   so the footer measured 1,030-1,098px on a phone, between 13% and 40% of the whole document
   depending on the page, and on /me it was the single largest block by a factor of three. The brand
   paragraph keeps the full width; the three link columns pair up. Measured across /me, /deals and
   /rewards at 390 and 320: no link clipped, no horizontal overflow, ~250px back on every page. */
@media (max-width: 480px) {
  .tl-footer-inner { grid-template-columns: 1fr 1fr; gap: 28px; }
  .tl-footer-bottom { flex-direction: column; text-align: center; }
}

.btn {
  position: relative; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  border-radius: var(--r-md); padding: 11px 21px;
  font-size: 14px; font-weight: 600; font-family: inherit; letter-spacing: -0.01em;
  cursor: pointer; text-decoration: none; border: 1px solid transparent;
  transition: transform var(--t-med) var(--ease-spring), box-shadow var(--t-med) var(--ease), background var(--t-med) var(--ease), border-color var(--t-med) var(--ease), color var(--t-med) var(--ease);
  white-space: nowrap; isolation: isolate;
}

/* .btn pairs white-space:nowrap with the overflow:hidden that clips the shine sweep, so a label
   too long for a narrow screen is silently CUT, with no ellipsis to admit it happened. "Start free
   with Discord" and "Get alerts within seconds" both lost their last word at 320px. Wrapping is
   the right answer over truncating: the button grows a line, and nothing is hidden. */
@media (max-width: 400px) {
  .btn { white-space: normal; text-align: center; line-height: 1.3; }
}

/* FLAT, as of the 2026-08 design direction (R3). The previous recipe was the canonical AI button
   verbatim — 135deg gradient fill, colored drop-glow, a clipped shine sweep on hover, and a
   translateY lift. On a terminal a button is a control, not a jewel: one solid fill per intent,
   a 1px border, hover shifts the BACKGROUND, active gives a 1-frame press. The shine ::after and
   every hover lift are gone, not toned down. */
.btn:active { transform: scale(0.985); }
/* [aria-disabled] alongside :disabled because the disabled control is not always a <button>: the
   item page's create-alert CTA is an <a class="btn">, and :disabled does not apply to anchors, so
   that one rendered at full strength while announcing itself as disabled. The href is removed in JS
   rather than blocked here — an <a> without one is not a link, and leaving pointer events alive is
   what lets its title explain why it is off. */
.btn:disabled,
.btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; transform: none; }
.btn[aria-disabled="true"]:hover { background: inherit; }

.btn-primary { background: #4256c9; border-color: rgba(138,162,255,0.45); color: #fff; }
.btn-primary:hover { background: #4c63e0; border-color: rgba(138,162,255,0.7); }
.btn-premium { background: var(--gold); border-color: transparent; color: #1a1206; font-weight: 700; }
.btn-premium:hover { background: #f0b254; }
.btn-outline { background: var(--veil-1); border-color: var(--border-2); color: var(--text-2); }
.btn-outline:hover { border-color: var(--blue); color: var(--text); background: var(--blue-dim); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--text-2); }
.btn-ghost:hover { background: var(--veil-2); color: var(--text); }
.btn-danger { background: var(--red-fill); border-color: rgba(var(--red-rgb),0.3); color: var(--red-pale); }
.btn-danger:hover { background: var(--red-fill-hover); border-color: rgba(var(--red-rgb),0.5); }

.btn-success { background: var(--green); border-color: rgba(9,46,32,0.35); color: var(--on-green); font-weight: 700; }
/* The label follows the token, like the resting rule. It was a hardcoded #04200f, which was
   harmless while the hover fill was a light green and became 3.5:1 the moment paper made that
   fill dark -- the same shape of bug as .mt-tag.worst, in the one rule still holding a literal. */
.btn-success:hover { background: var(--green-hover); color: var(--on-green); }
.btn-success:active { transform: scale(0.985); }
.btn-success:disabled { background: rgba(var(--green-rgb),0.14); color: var(--text-3); border-color: rgba(var(--green-rgb),0.18); transform: none; }
.btn-sm  { padding: 7px 14px; font-size: 12px; border-radius: var(--r-sm); }
.btn-lg  { padding: 14px 30px; font-size: 16px; border-radius: var(--r-md); }
.btn-xl  { padding: 17px 38px; font-size: 17px; border-radius: var(--r-lg); }

.card {
  background:
    linear-gradient(var(--bg-card), var(--bg-card)) padding-box,
    linear-gradient(135deg, var(--hairline), transparent 40%) border-box;
  border: 1px solid transparent;
  border-radius: var(--r-lg); padding: 22px;
  transition: border-color var(--t-med) var(--ease), box-shadow var(--t-med) var(--ease), transform var(--t-med) var(--ease);
  position: relative; overflow: hidden;
}
.card:hover { border-color: var(--border-2); }
/* .card-glow retired with the glow tokens (R3): a hovered card brightens its border, nothing more. */
.card-glow:hover { border-color: var(--border-2); }

.card-spotlight::before {
  content: ''; position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: radial-gradient(340px circle at var(--mx, 50%) var(--my, 0%), rgba(138,162,255,0.10), transparent 60%);
  transition: opacity var(--t-med) var(--ease);
}
.card-spotlight:hover::before { opacity: 1; }

.stat-card {
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: 20px 22px;
  transition: border-color var(--t-med) var(--ease), transform var(--t-med) var(--ease), box-shadow var(--t-med) var(--ease);
  position: relative; overflow: hidden;
}
.stat-card:hover { border-color: var(--border-2); }
.stat-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-3); font-weight: 600; margin-bottom: 8px; }
.stat-value { font-size: 30px; font-weight: 700; letter-spacing: -0.03em; line-height: 1; }
.stat-sub   { font-size: 12px; color: var(--text-2); margin-top: 6px; }

.badge { display: inline-flex; align-items: center; border-radius: var(--r-pill); padding: 3px 10px; font-size: 11px; font-weight: 600; gap: 5px; }
.badge-green  { background: var(--green-dim);  border: 1px solid rgba(var(--green-rgb),0.25);  color: var(--green); }
/* --red-bright, not --red-pale. A badge is dark text on a PALE wash; --red-pale is the light label
   for a DARK fill. Pointing both at one token made this 1.62:1 on paper -- light pink on pale pink. */
.badge-red    { background: var(--red-dim);    border: 1px solid rgba(var(--red-rgb),0.25);  color: var(--red-bright); }
.badge-blue   { background: var(--blue-dim);   border: 1px solid rgba(108,140,255,0.25); color: #93c5fd; }
.badge-gold   { background: var(--gold-dim);   border: 1px solid rgba(232,163,61,0.25); color: var(--gold); }
.badge-muted  { background: var(--veil-2); border: 1px solid var(--border); color: var(--text-2); }

.input {
  background: var(--bg-1); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: 10px 14px;
  color: var(--text); font: inherit; font-size: 13px;
  outline: none; transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
  width: 100%;
}
.input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-dim); }
.input::placeholder { color: var(--text-3); }

/* Grade rings, flat (design direction R7 + the critic's double-encoding charge): one 1.5px ring
   plus a quiet dim wash, consuming the --grade-* tokens that are now the ONLY grade palette.
   The old version stacked a 2px ring, a drop shadow AND a radial halo gradient per icon — rarity
   was shouted twice per row. Color IS the encoding; the halo was decoration. */
.gborder-C   { box-shadow: 0 0 0 1.5px var(--grade-c) !important;   border-color: var(--grade-c) !important;   background-color: var(--grade-c-dim) !important;   background-image: none !important; }
.gborder-B   { box-shadow: 0 0 0 1.5px var(--grade-b) !important;   border-color: var(--grade-b) !important;   background-color: var(--grade-b-dim) !important;   background-image: none !important; }
.gborder-A,
.gborder-A2  { box-shadow: 0 0 0 1.5px var(--grade-a) !important;   border-color: var(--grade-a) !important;   background-color: var(--grade-a-dim) !important;   background-image: none !important; }
.gborder-AA,
.gborder-AA2,
.gborder-AA3 { box-shadow: 0 0 0 1.5px var(--grade-aa) !important;  border-color: var(--grade-aa) !important;  background-color: var(--grade-aa-dim) !important;  background-image: none !important; }
.gborder-AAA { box-shadow: 0 0 0 1.5px var(--grade-aaa) !important; border-color: var(--grade-aaa) !important; background-color: var(--grade-aaa-dim) !important; background-image: none !important; }
.gborder-S,
.gborder-SS  { box-shadow: 0 0 0 1.5px var(--grade-s) !important;   border-color: var(--grade-s) !important;   background-color: var(--grade-s-dim) !important;   background-image: none !important; }

.page-wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gutter); width: 100%; }
.page-wrap-wide { max-width: 1400px; margin: 0 auto; padding: 0 var(--gutter); }

/* ═══ THE SHARED OPERATE-PAGE PRIMITIVES (2026-08 design direction, R1/R4/R5/R6/R7) ═══════════════
   Each of these REPLACES page-local competitors as pages migrate onto it; a page defining its own
   variant of one of these after this date is the drift this block exists to end. Specs live in
   DESIGN_DIRECTION_2026-08.md; the rules that shaped them, in one line each:
   data above the fold (titlebar, not hero) · one gold per board (col-rank) · pills are states,
   filters are controls · tables are the product · rows not boxes for stats · one banner grammar. */

/* R1 — the operate-page header: title + status + actions in ONE row, ~56px total. */
.page-titlebar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 18px 0 12px; }
.page-titlebar h1 { font-family: var(--font-display); font-size: var(--fs-title-page); font-weight: 700; letter-spacing: -0.015em; margin: 0; line-height: 1.2; }
.page-titlebar .pt-status { display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: var(--fs-meta); color: var(--text-3); font-variant-numeric: tabular-nums; letter-spacing: .04em; }
.page-titlebar .pt-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }
/* min-width:100% is load-bearing: max-width alone CLAMPS the flex base size, so the sub rendered
   BESIDE the h1 with the actions stranded mid-bar — /potentials found this and patched it locally;
   the fix lives in the one home now (adversarial pass, 2026-08-27). */
.page-titlebar .pt-sub { flex-basis: 100%; min-width: 100%; font-size: 12.5px; line-height: 1.5; color: var(--text-3); margin: 0; max-width: 92ch; }

/* R6 — THE table. Sticky mono-caps header, right-aligned numerals, quiet hover, one gold ranked
   column per board (.col-rank), density via the existing html[data-density]. */
.data-table { width: 100%; border-collapse: collapse; font-size: var(--fs-table); }
.data-table thead th { position: sticky; top: 0; z-index: 2; background: var(--bg-2); font-family: var(--font-mono); font-size: var(--fs-meta); font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-3); text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--border-2); white-space: nowrap; }
.data-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.data-table th.col-num, .data-table td.col-num { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.data-table tbody tr { transition: background var(--t-fast) var(--ease); }
.data-table tbody tr:hover { background: var(--veil-2); }
.data-table td.col-rank { color: var(--gold); font-weight: 700; }
html[data-density="compact"] .data-table { font-size: var(--fs-table-dense); }
html[data-density="compact"] .data-table td { padding: 5px 10px; }

/* R7 — the quiet button: exactly three sizes, one background, one radius. Replaces the ~14
   page-local classes at 7 heights the audit counted. */
.btn-quiet { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 38px; padding: 0 14px; background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--r-sm); color: var(--text-2); font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; text-decoration: none; white-space: nowrap; transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease); }
.btn-quiet:hover { border-color: var(--border-2); color: var(--text); background: var(--bg-2); }
.btn-quiet.is-active { background: var(--blue-dim); border-color: var(--blue); color: var(--blue-bright); }
.btn-quiet:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
.btn-quiet--sm { min-height: 34px; padding: 0 11px; font-size: 12px; }
.btn-quiet--lg { min-height: 44px; padding: 0 18px; font-size: 13px; }

/* R7 — the view switcher (the alerts / my-alerts / potentials family): a compact segmented strip, ONE
   position (directly under the titlebar). Replaces the three stacked description cards. */
.view-strip { display: inline-flex; padding: 3px; gap: 2px; background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--r-sm); margin: 0 0 12px; max-width: 100%; overflow-x: auto; }
.view-strip a, .view-strip button { display: inline-flex; align-items: center; gap: 6px; min-height: 34px; padding: 0 14px; border-radius: 6px; border: 0; background: transparent; color: var(--text-3); font: inherit; font-size: 12.5px; font-weight: 600; text-decoration: none; cursor: pointer; white-space: nowrap; transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease); }
.view-strip a:hover, .view-strip button:hover { color: var(--text); background: var(--veil-2); }
/* aria-current="true" included: it is the correct value for an in-page jump nav (rewards uses
   one), where "page" would claim the whole document. */
.view-strip [aria-current="page"], .view-strip [aria-current="true"], .view-strip [aria-pressed="true"], .view-strip .is-active { background: var(--bg-3); color: var(--text); }
.view-strip .vs-n { font-family: var(--font-mono); font-size: var(--fs-meta); color: var(--text-3); font-variant-numeric: tabular-nums; }
.view-strip a:focus-visible, .view-strip button:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; }
@media (pointer: coarse) { .view-strip a, .view-strip button { min-height: 40px; } }
/* CROSS-PAGE, NOT CROSS-PANEL. Every other .view-strip in this estate is a role="tablist" that
   swaps a panel inside one document. The alerts family is the exception: a <nav> of real links to
   three separate pages -- /alerts, /my-alerts, /my-alerts?view=potentials -- and it was wearing the
   same compact pill geometry as the SHOW and REGION filter chips sitting directly beneath it.
   Three stacked rows of pills, so readers took the top one for a third filter row and never
   followed it. Reported as: people miss that these exist.

   So the fix is not more contrast, it is a different SHAPE. A control that changes the PAGE must
   not look like a control that changes a view OF the page. These become underlined tabs sitting on
   a rule that spans the content column -- the one grammar on this page that is not a pill -- while
   the tablist users keep the segmented strip untouched. The modifier is an explicit class rather
   than nav.view-strip, because rewards.html is also a <nav> and its children are in-page buttons.

   Inactive links move --text-3 -> --text-2 (5.3:1 -> 7.5:1 on the dark ground). That was never an
   accessibility failure; it was a hierarchy one -- they read as disabled rather than as somewhere
   you can go. */
.view-strip--pages { display: flex; padding: 0; gap: 2px; background: transparent; border: 0;
  border-bottom: 1px solid var(--border); border-radius: 0; margin: 0 0 18px; overflow-x: auto; }
.view-strip--pages a { min-height: 44px; padding: 0 16px; border-radius: 0; background: transparent;
  font-size: 13.5px; font-weight: 600; color: var(--text-2);
  /* the tab's own 2px rule sits ON the container's 1px rule, not above it */
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease); }
.view-strip--pages a:hover { background: transparent; color: var(--text); border-bottom-color: var(--border-2); }
.view-strip--pages [aria-current="page"] { background: transparent; color: var(--text); font-weight: 700;
  border-bottom-color: var(--blue); }
.view-strip--pages [aria-current="page"] .vs-n { color: var(--text-2); }
.view-strip--pages a:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; }
/* The base component drops to 40px on touch; page navigation keeps the full 44px target. */
@media (pointer: coarse) { .view-strip--pages a { min-height: 44px; } }

/* R4 — the one banner grammar: neutral surface, icon slot, restrained accent EDGE (never a wash),
   one line by default. Warnings are not gold; gold means money and Premium. */
.banner { display: flex; align-items: center; gap: 10px; padding: 9px 14px; background: var(--bg-card); border: 1px solid var(--border); border-left: 3px solid var(--border-2); border-radius: var(--r-surface); font-size: 12.5px; line-height: 1.5; color: var(--text-2); margin: 0 0 14px; }
.banner--warn { border-left-color: var(--red); }
.banner--info { border-left-color: var(--blue); }
.banner--premium { border-left-color: var(--gold); }
.banner .bn-act { margin-left: auto; flex-shrink: 0; display: flex; gap: 8px; align-items: center; }
/* flex-shrink:0 above pins the action cluster at max-content, which ran a banner's buttons past
   a 320px viewport (found on the /my-alerts advisory). At phone width the banner stacks instead:
   text first, actions on their own wrapped line. */
@media (max-width: 480px) {
  .banner { flex-wrap: wrap; }
  .banner .bn-act { flex-shrink: 1; flex-wrap: wrap; margin-left: 0; }
}
.banner b { color: var(--text); }

/* R7 — stats as ROWS, not boxes: cells sized to their information, separated by rules. Replaces
   the half-page cards that stretched two digits across 1150px. */
.stat-strip { display: flex; flex-wrap: wrap; border: 1px solid var(--border); border-radius: var(--r-surface); background: var(--bg-card); overflow: hidden; }
.stat-strip .ss-cell { display: flex; flex-direction: column; gap: 3px; padding: 12px 18px; border-right: 1px solid var(--border); min-width: 110px; flex: 1 1 auto; }
.stat-strip .ss-cell:last-child { border-right: 0; }
.stat-strip .ss-k { font-size: var(--fs-meta); font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .08em; color: var(--text-3); font-weight: 600; }
.stat-strip .ss-v { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 18px; font-weight: 600; color: var(--text); line-height: 1.2; }
.stat-strip .ss-s { font-size: 11.5px; color: var(--text-3); }

/* R4 — the credits chip that replaced the site-wide banner. Lives in .rail-foot above the Premium
   entry; the full pitch lives on /me and /rewards, which own the message in-page. */
/* .rail-foot a.rail-credits, not bare .rail-credits: the chip lives inside the foot cluster,
   whose base anchor rule (.rail-foot a, 0-1-1) silently outranked this class (0-1-0) — the chip
   rendered at the foot's 13.5px/12px-gap instead of its own 11.5px/7px, which is half of how a
   "covers ..." line ended up wider than the rail. Same specificity family as the UA-pink
   Help & Guide bug; the foot's own comment already warns about it. */
.rail-foot a.rail-credits { display: flex; align-items: center; gap: 7px; margin: 0 0 6px; padding: 7px 10px; border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg-1); color: var(--text-3); font-size: 11.5px; text-decoration: none; min-width: 0; max-width: 100%; transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease); }
.rail-foot a.rail-credits:hover { border-color: var(--border-2); color: var(--text-2); }
/* The text is the one flexible part of the chip and it CLIPS rather than widening the chip past
   the rail: a flex item's default min-width:auto let a long "covers ..." variant push the whole
   anchor through the rail's right border (the .rail-foot a nowrap made sure it never wrapped).
   The full sentence lives on the chip's title/aria either way. */
.rail-credits .rc-txt { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-credits b { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--gold); font-weight: 600; }
.rail-credits .rc-bar { flex: 1 1 auto; height: 3px; border-radius: 2px; background: var(--veil-2); overflow: hidden; min-width: 24px; }
.rail-credits .rc-bar i { display: block; height: 100%; background: var(--gold); opacity: .7; }
html.rail-collapsed .rail-credits { justify-content: center; padding: 7px 4px; }
html.rail-collapsed .rail-credits .rc-txt, html.rail-collapsed .rail-credits .rc-bar { display: none; }

.section-heading { font-size: clamp(24px, 4.5vw, 38px); font-weight: 800; letter-spacing: -0.03em; line-height: 1.12; margin: 0 0 14px; }
.section-sub { font-size: 15px; color: var(--text-2); line-height: 1.65; max-width: 540px; }

.tg-blue   { color: var(--blue-bright); }
.tg-gold   { color: var(--gold); }
.tg-white  { color: var(--text); }

hr.divider { border: none; border-top: 1px solid var(--border); margin: 32px 0; }
.line-glow { height: 1px; border: none; margin: 0; background: linear-gradient(90deg, transparent, var(--border-2) 25%, var(--border-2) 75%, transparent); }

.skeleton { background: linear-gradient(90deg, var(--bg-2) 25%, var(--veil-3) 50%, var(--bg-2) 75%); background-size: 200% 100%; animation: shimmer 1.4s infinite; border-radius: var(--r-sm); }
@keyframes shimmer { to { background-position: -200% 0; } }
.spinner { width: 20px; height: 20px; border: 2px solid var(--border); border-top-color: var(--blue); border-radius: 50%; animation: spin 0.7s linear infinite; flex-shrink: 0; }
@keyframes spin { to { transform: rotate(360deg); } }

.live-dot { position: relative; width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--green); }
.live-dot::after { content: ''; position: absolute; inset: 0; border-radius: 50%; border: 1px solid var(--green); opacity: 0; animation: live-pulse 2s var(--ease-out) infinite; }
.live-dot.slow { background: var(--text-3); opacity: 0.5; }
.live-dot.slow::after { animation: none; }
@keyframes live-pulse { 0% { transform: scale(1); opacity: 0.7; } 100% { transform: scale(3); opacity: 0; } }

.reveal { opacity: 0; transform: translateY(20px); transition: opacity var(--t-slow) var(--ease-out), transform var(--t-slow) var(--ease-out); will-change: opacity, transform; }

@keyframes reveal-failsafe { to { opacity: 1; transform: none; } }
html:not(.tl-js) .reveal { animation: reveal-failsafe 0.01s linear 1.2s forwards; }
.reveal.delay-1 { transition-delay: 0.07s; }
.reveal.delay-2 { transition-delay: 0.14s; }
.reveal.delay-3 { transition-delay: 0.21s; }
.reveal.delay-4 { transition-delay: 0.28s; }
.reveal.visible { opacity: 1; transform: none; }

* { scrollbar-width: thin; scrollbar-color: var(--border-2) transparent; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: var(--r-pill); border: 2px solid transparent; background-clip: padding-box; }
::-webkit-scrollbar-thumb:hover { background: var(--text-3); background-clip: padding-box; }

.hidden { display: none !important; }
.mt-0 { margin-top: 0; } .mt-1 { margin-top: 8px; } .mt-2 { margin-top: 16px; } .mt-3 { margin-top: 24px; }
.mb-0 { margin-bottom: 0; } .mb-1 { margin-bottom: 8px; } .mb-2 { margin-bottom: 16px; } .mb-3 { margin-bottom: 24px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .reveal { opacity: 1; transform: none; }
}

.section-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--r-xl); padding: var(--space-5); margin-bottom: var(--space-5); box-shadow: var(--shadow-sm); }
.section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: var(--space-4); }
.section-title { font-family: var(--font-display); font-size: var(--fs-h3, 18px); font-weight: 700; margin: 0; letter-spacing: -0.02em; color: var(--text); }
.section-card > .section-sub, .section-head .section-sub { font-size: var(--fs-micro); color: var(--text-2); }

.section-header { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-6); }
.section-header h2 { font-family: var(--font-display); font-size: var(--fs-h2); font-weight: 700; letter-spacing: -0.02em; line-height: 1.12; margin: 0; color: var(--text); }
.section-header p { font-family: var(--font-body); font-size: var(--fs-body-lg); line-height: 1.6; color: var(--text-2); max-width: var(--measure-tight); margin: 0; }
.section-header.center { align-items: center; text-align: center; }
.section-header.center p { max-width: var(--measure); }

.card--stat { padding: var(--space-4); border-radius: var(--r-md); }
.card--stat:hover { transform: none; box-shadow: none; border-color: var(--border); }
.card--region { border-top: 2px solid var(--accent, var(--border-2)); }
.card--feature { padding: var(--space-5); }
.card--premium { padding: var(--space-6); border-color: rgba(232,163,61,0.3); }

/* An interstitial between a board's filters and its first row. Measured at 1280x800 it cost a
   319px footprint -- 207px of band plus 48px and 64px of margin -- and its h2 rendered at 30px
   while /database's own page title is 18px. A CTA heading 67% larger than the title of the page it
   interrupts is a hierarchy inversion, and 112px of pure margin on a data board is a lot of nothing.
   Tightened, not removed: same copy, same button, same gradient. It just stops out-shouting the
   page and gives about 90px back to the rows. */
.cta-band { display: flex; align-items: center; justify-content: space-between; gap: var(--space-5); flex-wrap: wrap; padding: var(--space-5); border: 1px solid var(--border-2); border-radius: var(--r-lg); margin: var(--space-5) 0 var(--space-6); background: var(--bg-card); }
.cta-band .cta-band-copy { max-width: 52ch; }
.cta-band h2 { font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 700; letter-spacing: -0.01em; line-height: 1.25; margin: 0 0 6px; color: var(--text); }
.cta-band p { font-size: var(--fs-body); color: var(--text-2); margin: 0; line-height: 1.55; }
.cta-band--upgrade { background: linear-gradient(135deg, var(--gold-dim), transparent 72%); }
.cta-band--action  { background: linear-gradient(135deg, var(--blue-dim), transparent 72%); }
@media (max-width: 640px) { .cta-band { padding: var(--space-5); } }

.chip { display: inline-flex; align-items: center; gap: 6px; min-height: 34px; padding: 0 13px; border: 1px solid var(--border); border-radius: var(--r-pill); background: var(--bg-card); color: var(--text-2); font: inherit; font-size: var(--fs-small); font-weight: 600; cursor: pointer; transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), background var(--t-fast) var(--ease); }
.chip:hover { border-color: var(--border-2); color: var(--text); }
.chip.active { border-color: var(--blue); color: var(--blue-bright); background: var(--blue-dim); }

.region-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; display: inline-block; flex-shrink: 0; }
.region-dot.eu { color: var(--region-eu); } .region-dot.as, .region-dot.jp { color: var(--region-jp); } .region-dot.us { color: var(--region-na); }

/* ── Region lamps ───────────────────────────────────────────────────────────────
   Three dots in the rail, each opening a status card. The card carries a state pill, a freshness
   meter (age as a fraction of the liveness threshold, so a fetcher falling behind is visible as a
   creeping bar minutes before the lamp turns), and three figures. Four states, not two: "lagging"
   and "offline" are different events and no longer share one red dot. */
.region-leds { display: flex; align-items: center; gap: 4px; }
.region-led-wrap {
  position: relative; display: flex; align-items: center; gap: 6px;
  padding: 5px 8px; border-radius: var(--r-sm); cursor: default;
  /* Focusable (it reveals the health card), so it owes the 24x24 floor; the lamp+label content
     alone measured 15x17. The floor grows the hit box, not the lamp. */
  min-width: 24px; min-height: 24px; box-sizing: border-box;
  transition: background var(--t-fast) var(--ease);
}
.region-led-wrap:hover { background: var(--veil-3); }
/* The card was hover-only, which put the whole readout out of reach of a keyboard and left a screen
   reader with a coloured dot and a two-letter label. The wrap is focusable and carries the spoken
   form of the card in aria-label; focus opens it exactly as hover does. */
.region-led-wrap:focus-visible {
  outline: none; background: var(--veil-3);
  box-shadow: 0 0 0 2px var(--blue-glow);
}

.region-led { position: relative; width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--text-3); }
/* A ring that expands and fades out of the dot: the lamp reads as a live instrument rather than a
   static graphic, without the anxious flashing a fault state needs to keep for itself. */
.region-led::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  border: 1px solid currentColor; opacity: 0;
}
.is-live    .region-led { background: var(--green); box-shadow: 0 0 7px var(--green-glow); color: var(--green); }
.is-live    .region-led::after { animation: led-ping 2.6s var(--ease-out) infinite; }
.is-lagging .region-led { background: var(--gold);  box-shadow: 0 0 7px var(--gold-glow);  animation: led-pulse 1.8s ease-in-out infinite; }
.is-down    .region-led { background: var(--red);   box-shadow: 0 0 7px rgba(var(--red-rgb),0.8); animation: led-pulse 1s ease-in-out infinite; }
.is-unknown .region-led { background: var(--text-3); }
@keyframes led-pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
@keyframes led-ping  { 0%{transform:scale(1);opacity:.7} 70%,100%{transform:scale(2.9);opacity:0} }
@media (prefers-reduced-motion: reduce) {
  .region-led, .region-led::after { animation: none !important; }
}

.region-led-label {
  font-size: 10px; font-weight: 700; letter-spacing: 0.04em; color: var(--text-3);
  transition: color var(--t-fast) var(--ease);
}
/* The label carries the fault too, so a rail glanced at from across the desk still says which region
   is in trouble when the 7px dot is below the threshold of noticing. */
.is-lagging .region-led-label { color: var(--gold); }
.is-down    .region-led-label { color: var(--red-bright); }

.region-led-tooltip {
  position: absolute; top: calc(100% + 8px); left: 50%; transform: translateX(-50%);
  background: var(--bg-elevated); border: 1px solid var(--border-2); border-radius: var(--r-md);
  padding: 10px 12px 9px; width: 232px; font-size: 11px; text-align: left;
  pointer-events: none; opacity: 0; visibility: hidden;
  transition: opacity var(--t-fast) var(--ease), visibility 0s linear var(--t-fast);
  z-index: 200; box-shadow: var(--shadow-lg);
}
.region-led-wrap:hover .region-led-tooltip,
.region-led-wrap:focus-visible .region-led-tooltip {
  opacity: 1; visibility: visible; transition-delay: 0s;
}

.rl-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.rl-name { font-family: var(--font-display); font-weight: 700; font-size: 12.5px; color: var(--text); letter-spacing: -0.01em; }
.rl-pill {
  font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.09em;
  padding: 2px 7px; border-radius: var(--r-pill); border: 1px solid transparent; white-space: nowrap;
  color: var(--text-3); background: var(--veil-3); border-color: var(--border);
}
.is-live    .rl-pill { color: var(--green);      background: var(--green-dim); border-color: rgba(var(--green-rgb),0.30); }
.is-lagging .rl-pill { color: var(--gold);       background: var(--gold-dim);  border-color: rgba(232,163,61,0.32); }
.is-down    .rl-pill { color: var(--red-bright); background: var(--red-dim);   border-color: rgba(var(--red-rgb),0.32); }

/* The TRACK carries the state and the fill carries the freshness left in the budget. Past the
   threshold there is no fill at all, so an empty bar always reads the same way and the colour under
   it says which side of the line the region is on. */
.rl-meter { height: 3px; border-radius: var(--r-pill); background: var(--veil-3); overflow: hidden; margin-bottom: 9px; }
.is-lagging .rl-meter { background: var(--gold-dim); box-shadow: inset 0 0 0 1px rgba(232,163,61,0.22); }
.is-down    .rl-meter { background: var(--red-dim);  box-shadow: inset 0 0 0 1px rgba(var(--red-rgb),0.24); }
/* Widened by the 1s age ticker in site.js, so the transition has to outlast one tick or the bar
   arrives in visible steps. */
.rl-meter i { display: block; height: 100%; border-radius: inherit; background: var(--text-3);
  transition: width 1.05s linear, background var(--t-med) var(--ease); }
.is-live    .rl-meter i { background: linear-gradient(90deg, rgba(var(--green-rgb),0.55), var(--green)); }
.is-lagging .rl-meter i { background: linear-gradient(90deg, rgba(232,163,61,0.55), var(--gold)); }
.is-down    .rl-meter i { background: var(--red); }

.rl-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-top: 4px; color: var(--text-3); white-space: nowrap; }
.rl-v { font-family: var(--font-mono); font-size: 11px; font-variant-numeric: tabular-nums; color: var(--text); font-weight: 500; }
/* The sweep age is the number that answers the question the lamp raises -- "is it actually
   working right now" -- so it carries the weight, and it changes colour as it ages past the
   loop's own period rather than staying green while the figure beside it quietly drifts. */
.rl-hi   { color: var(--text); font-weight: 700; }
.rl-warn { color: var(--gold); }
.rl-bad  { color: var(--red-bright); }
.is-live .rl-hi { color: var(--green); }
.rl-sub { color: var(--text-3); opacity: 0.72; font-size: 9.5px; letter-spacing: 0.02em; }
.rl-note { margin-top: 8px; padding-top: 7px; border-top: 1px solid var(--hairline); font-size: 10px; color: var(--text-3); letter-spacing: 0.02em; }

.t-ok   { color: var(--green); }
.t-bad  { color: var(--red-bright); }  
.t-warn { color: var(--gold); }
.t-dim  { color: var(--text-3); }

.img-failed { display: none; }

.tlc-tooltip {
  position: absolute; top: 0; left: 0; z-index: 20; pointer-events: none;
  opacity: 0; transition: opacity var(--t-fast) var(--ease);
  background: var(--bg-elevated); border: 1px solid var(--border-2); border-radius: var(--r-sm);
  padding: 8px 10px; font-size: 11px; white-space: nowrap; box-shadow: var(--shadow-lg);
}
.tlc-tooltip.on { opacity: 1; }
.tlc-tooltip .tlc-time { color: var(--text-3); font-weight: 600; margin-bottom: 4px; }
.tlc-tooltip .tlc-row { display: flex; align-items: center; gap: 6px; color: var(--text-2); margin-top: 2px; }
.tlc-tooltip .tlc-row b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.tlc-tooltip .tlc-dim { color: var(--text-3); font-weight: 400; }
.tlc-tooltip .tlc-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.tlc-tooltip .tlc-dot[data-r="EU"] { background: var(--region-eu); }
.tlc-tooltip .tlc-dot[data-r="JP"] { background: var(--region-jp); }
.tlc-tooltip .tlc-dot[data-r="US"] { background: var(--region-na); }

.state-box { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-3); text-align: center; padding: var(--space-8) var(--space-4); border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--bg-card); color: var(--text-2); }
.state-box .state-icon { color: var(--text-3); line-height: 0; }
.state-box .state-icon svg { width: 34px; height: 34px; }
.state-box .state-msg { font-size: var(--fs-body); color: var(--text-2); margin: 0; max-width: 42ch; line-height: 1.5; }
.state-box--error { border-color: rgba(var(--red-rgb),0.35); }
.state-box--error .state-icon, .state-box--error .state-msg { color: var(--red); }
/* Loading variant, promoted from /me's page markup so the class means the same thing everywhere:
   a quieter box (data is coming, nothing is wrong), spinner and message on the base layout. */
.state-box--loading { border-style: dashed; color: var(--text-3); }
.state-box--loading .state-msg { color: var(--text-3); }

.spread { position: relative; width: 100%; height: 24px; }
.spread .sp-axis { position: absolute; left: 0; right: 0; top: 50%; height: 2px; background: var(--border-2); border-radius: 2px; transform: translateY(-50%); }
.spread .sp-gap { position: absolute; top: 50%; height: 4px; left: var(--sp-buy, 25%); width: calc(var(--sp-sell, 75%) - var(--sp-buy, 25%)); background: var(--gold); border-radius: 2px; transform: translateY(-50%); }
.spread .sp-tick { position: absolute; top: 50%; width: 9px; height: 9px; border-radius: 50%; transform: translate(-50%, -50%); border: 2px solid var(--bg-card); box-sizing: border-box; }
.spread .sp-tick.eu { left: var(--sp-eu, 50%); background: var(--region-eu); }
.spread .sp-tick.as { left: var(--sp-as, 50%); background: var(--region-jp); }
.spread .sp-tick.us { left: var(--sp-us, 50%); background: var(--region-na); }
.spread .sp-cap { position: absolute; top: 0; font-family: var(--font-mono); font-size: 10px; color: var(--text-3); transform: translateX(-50%); }

.spread--hero { height: 44px; }
.spread--hero .sp-axis { height: 3px; }
.spread--hero .sp-gap { height: 6px; background: linear-gradient(90deg, var(--gold-hover), #ffd166, var(--gold-hover)); background-size: 220% 100%; animation: sp-sheen 3s linear infinite; }
.spread--hero .sp-tick { width: 13px; height: 13px; }
@keyframes sp-sheen { from { background-position: 0% 0; } to { background-position: 220% 0; } }

.spread--stack { height: 20px; }

.spread--skeleton .sp-axis { background: var(--border); }
.spread--skeleton .sp-gap, .spread--skeleton .sp-tick { opacity: 0.4; animation: sp-pulse 1.4s ease-in-out infinite; }
@keyframes sp-pulse { 0%, 100% { opacity: 0.25; } 50% { opacity: 0.6; } }

.spread--broken .sp-gap { background: repeating-linear-gradient(90deg, var(--gold) 0 6px, transparent 6px 12px); opacity: 0.55; }
.spread--broken .sp-tick.us { opacity: 0.4; }

@media (prefers-reduced-motion: reduce) {
  .spread--hero .sp-gap, .spread--skeleton .sp-gap, .spread--skeleton .sp-tick { animation: none; }
}

img { -webkit-user-drag: none; -khtml-user-drag: none; -moz-user-drag: none; user-drag: none; }

.skip-link{position:absolute;left:-9999px;top:0;z-index:300}
.skip-link:focus{left:16px;top:8px;background:var(--bg-elevated);color:var(--text);padding:8px 14px;border-radius:var(--r-sm);box-shadow:var(--shadow-md);outline:2px solid var(--blue-bright);text-decoration:none}

/* The 72-token Radix scale block that sat here was dead weight (one consumer estate-wide,
   inlined at its call sites) and is deleted per the design direction R7. */

:root {
  --tap-min: 44px;   /* the --bp-* tokens that lived here had zero consumers (var() cannot reach a media query) */
}

@media (max-width: 768px), (pointer: coarse) {
  
  
  .tl-nav-auth a,
  .tl-footer-col a {
    min-height: var(--tap-min);
    display: inline-flex;
    align-items: center;
  }
  .tl-hamburger { min-width: var(--tap-min); min-height: var(--tap-min); }

  button:not(.th-sort), .btn,
  input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
  select, textarea { min-height: var(--tap-min); }
}

.input:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[contenteditable]:focus-visible,
.custom-select-val:focus-visible {   
  
  outline: 2px solid var(--blue-bright) !important;
  outline-offset: 1px !important;
  border-color: var(--blue);
}

@media (forced-colors: active) {
  :focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
}

body.is-guest .tl-nav-links a[data-auth-required]::before,
body.is-guest .tl-footer-col a[data-auth-required]::before,

body.is-guest .lp-index-list a[data-auth-required] h3::before {
  content: '';
  display: inline-block;
  width: 9px; height: 11px;
  margin-right: 6px;
  vertical-align: -1px;
  background: currentColor;
  opacity: 0.5;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 10V8a6 6 0 0 1 12 0v2h1v11H5V10z'/%3E%3C/svg%3E") center/contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 10V8a6 6 0 0 1 12 0v2h1v11H5V10z'/%3E%3C/svg%3E") center/contain no-repeat;
  flex-shrink: 0;
}
body.is-guest .tl-nav-links a[data-auth-required],
body.is-guest .tl-footer-col a[data-auth-required] { color: var(--text-3); }

body.is-guest .lp-index-list a[data-auth-required] h3::before { opacity: 0.42; }

:root { --rail-w: 212px; --rail-collapsed-w: 64px; }
html.rail-collapsed { --rail-w: var(--rail-collapsed-w); }

body { padding-left: var(--rail-w); transition: padding-left .22s var(--ease); }

.tl-nav {
  /* 100dvh, not 100vh. On a phone 100vh is the viewport height with the URL bar HIDDEN -- the
     largest it can ever be -- so a fixed drawer sized to it extends BEHIND the browser chrome and
     the bottom of it cannot be reached. That is why the pinned cluster and the account row were
     reported as unreachable on a real device while measuring fine in a headless viewport, which
     has no URL bar to hide. dvh tracks the visible area; the 100vh line above it is the fallback
     for anything that does not support dvh. */
  position: fixed; inset: 0 auto 0 0; width: var(--rail-w); height: 100vh; height: 100dvh;
  background: var(--bg-1); border-bottom: 0; border-right: 1px solid var(--border);
  box-shadow: none; z-index: 120;
  transition: width .22s var(--ease), transform .22s var(--ease);
}

.tl-nav.scrolled { background: var(--bg-1); border-bottom-color: transparent; box-shadow: none; }

.tl-nav-inner {
  max-width: none; margin: 0; padding: 0; height: 100%;
  flex-direction: column; align-items: stretch; gap: 0;
  
  overflow: visible;
}
/* Scoped to the rail. Unscoped, this reached the FOOTER wordmark too and imposed the nav's own
   height and padding on it -- which is why that link measured 55x19, then 20x64, and would not
   respond to anything written about the footer. The leak was the defect; the footer rules below
   were compensating for it. */
.tl-nav .tl-brand {
  height: var(--nav-h); padding: 0 18px; display: flex; align-items: center; gap: 10px;
  flex-shrink: 0; white-space: nowrap;
}
.tl-nav-links {
  flex-direction: column; align-items: stretch; gap: 2px;
  
  padding: 12px 10px; flex: 1 1 auto; min-height: 0; overflow-y: auto;
}
.tl-nav-links a {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 10px; border-radius: var(--r-sm); white-space: nowrap; position: relative;
}

.tl-nav-links a.active::after { display: none; }
.tl-nav-links a.active {
  background: var(--blue-dim);
}
.tl-nav-links a.active::before {
  content: ''; position: absolute; left: -10px; top: 6px; bottom: 6px; width: 3px;
  border-radius: 0 3px 3px 0; background: var(--blue);
}
/* The pill no longer lives in the scrolling list, so it no longer needs margin-top:auto to pin
   itself to the bottom of one -- .rail-foot does that structurally. */
.tl-nav-links a.nav-upgrade { margin-top: 4px; }

.tl-nav-links .rail-group {
  margin: 14px 0 5px; padding: 0 10px;
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .11em; color: var(--text-3);
  white-space: nowrap;
}
.tl-nav-links .rail-group:first-child { margin-top: 2px; }

html.rail-collapsed .tl-nav-links .rail-group {
  height: 0; margin: 10px 8px; padding: 0; overflow: hidden;
  border-top: 1px solid var(--hairline);
}
html.rail-collapsed .tl-nav-links .rail-group:first-child { margin-top: 2px; border-top: 0; }

.tl-nav-links a.nav-upgrade { margin-top: auto; }

body.is-guest .tl-nav-links a[data-auth-required]::before {
  order: 9; margin-right: 0; margin-left: auto;
}
html.rail-collapsed body.is-guest .tl-nav-links a[data-auth-required]::before { display: none; }

/* Signed out, ON a gated page, the padlock and the active accent bar were fighting over one
   ::before. The guest rule wins the shared properties (background, width, mask) but position,
   left, top and bottom come only from .active and still applied -- so the padlock was absolutely
   positioned at left:-10px, half-clipped by the rail's edge, and the blue bar was gone. Visible on
   /deals, /nix, /alerts, /potentials, /rewards, /ledger, /import and /my-alerts.
   One pseudo-element cannot be two things, so the padlock keeps it: the reader is standing on a
   page they cannot use, and that is the more useful message. "You are here" is not lost -- .active
   also tints the row with --blue-dim, which is untouched. */
body.is-guest .tl-nav-links a.active[data-auth-required]::before {
  position: static; left: auto; top: auto; bottom: auto;
  border-radius: 0;
  order: 9; margin-left: auto; margin-right: 0;
}

/* Short viewports -- a landscape phone, a small laptop, or a portrait phone whose browser chrome
   eats 120px. The destination list already absorbs the squeeze because it is the only flexible
   child, but below ~560px of usable height the fixed chrome around it is most of the drawer. The
   brand and the account row give up some padding so the cluster never has to. */
@media (max-height: 620px) {
  .tl-brand { height: 52px; }
  .rail-foot { padding-top: 4px; padding-bottom: 2px; gap: 1px; }
  .rail-foot a, .rail-foot .rail-settings { padding-top: 7px; padding-bottom: 7px; min-height: 0; }
  .tl-nav-auth { padding-top: 6px; padding-bottom: max(6px, env(safe-area-inset-bottom)); }
}

/* The pinned utility cluster: upgrade, Discord, Settings. Never scrolls, so the CTA and the way
   into Settings are reachable at any height without hunting. flex-shrink:0 is what guarantees it --
   the destination list above absorbs every squeeze instead. */
.rail-foot {
  flex: 0 0 auto; display: flex; flex-direction: column; gap: 2px;
  padding: 6px 10px 4px; border-top: 1px solid var(--hairline);
}
/* The cluster's links need the same base a rail link gets. Moving the pill out of .tl-nav-links
   took its flex row, its gap and its text-decoration:none with it -- which is why it rendered as a
   stacked icon above an underlined word instead of a pill. */
.rail-foot a {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 10px; border-radius: var(--r-sm); white-space: nowrap;
  position: relative; text-decoration: none;
  /* Sized here, for the cluster, rather than per pill. The Premium pill carried no font-size of
     its own -- it inherited the 16px body size while the Discord pill declared 13.5px -- so two
     buttons sitting one above the other rendered at different text sizes and different heights
     (44px against 40px). Neither one asked for that; it was simply the size nobody set. */
  font-size: 13.5px;
}
/* margin, not margin-top/left: the base pill rule above is .rail-foot a.nav-upgrade and carries
   margin-left:6px, which outranked the two-property reset that used to live here. The pill was
   therefore inset 6px from its neighbour and 6px narrower than it -- 185px against Discord's 191 --
   for the whole time the two have shared this cluster. */
.rail-foot a.nav-upgrade { margin: 0; }
/* buildFoot() moves the /support anchor out of .tl-nav-links into this cluster, which strips it of
   every author color rule — with color-scheme:dark that leaves the UA dark link palette (#9E9EFF,
   visited #D0ADF0: the "pink Help & Guide"). An author color here also covers :visited. Scoped to
   .rail-help on purpose: a color on the base .rail-foot a (0,1,1) would outrank .rail-discord's
   blurple (0,1,0). Voice matches .tl-nav-links a / .rail-settings:hover. */
.rail-foot a.rail-help { color: var(--text-2); }
.rail-foot a.rail-help:hover,
.rail-foot a.rail-help:focus-visible { color: var(--text); background: var(--veil-2); }
.rail-foot a.rail-help.active { color: var(--text); background: var(--blue-dim); }
/* The destination list is a scroller, and a row sliced in half at its lower edge reads as broken
   rather than as "more below". A short fade says the same thing without the ragged cut. */
.tl-nav-links {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent 100%);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent 100%);
}
/* The fade applies collapsed too: a 17px glyph sliced in half at the scroll boundary reads as a
   rendering fault, which is worse than the same cut on a labelled row. */
.rail-foot .rail-discord { margin-left: 0; margin-right: 0; }
.rail-foot .rail-settings { width: 100%; margin: 0; }
html.rail-collapsed .rail-foot { padding: 6px 0 4px; align-items: center; }
html.rail-collapsed .rail-foot .rail-discord,
html.rail-collapsed .rail-foot .nav-upgrade { width: calc(100% - 12px); margin: 0 6px; }
/* Centring, and the zeroed side padding that lets it work. The Premium pill was the one foot row
   that never got either: Discord and Settings both centre their lone glyph collapsed, while this
   one kept the labelled layout's `padding: 9px 10px` and default flex-start, so its star sat 6px
   left of the rail's centre line -- the only thing in the 64px column that wasn't on it. */
html.rail-collapsed .rail-foot .nav-upgrade { justify-content: center; padding: 9px 0; }

/* Table sort controls carried padding:0, so the button was exactly its text -- 31x16 -- against a
   24x24 minimum. The header cell is already ~34px tall, so giving the button a 24px floor and
   centring it costs no header height and makes the whole label hittable rather than the glyphs. */
/* Footer links get min-height:var(--tap-min) on touch already, and nothing on desktop -- so they
   rendered 17-20px there against the 24x24 WCAG 2.2 asks for regardless of pointer type. This was
   the single largest remaining finding in the audit, 55 of them, and it costs no layout: the footer
   columns are a gapped flex column, so a floor on the inline box moves nothing. */
/* Every footer anchor, not just the column ones -- the brand link and the bottom row are in
   sibling containers and were still landing at 19-20px. */
/* Standalone link lists: the footer, the FAQ's follow-on links, and the guide's on-this-page
   table of contents. All are links-as-navigation rather than links inside a sentence. */
/* .pot-note a and .mk-tier a are the upgrade CTA that closes a delay notice -- a standalone
   call to action rather than a link inside a sentence. (.mk-note does not exist; the class was
   guessed once and the /nix CTA stayed 17px tall because of it.) */
/* min-width too: the floor is 24x24 and a three-letter link ("NIX") passes the height floor at
   20px wide. width:fit-content on the column base rule still holds; this only widens the hit box
   of the shortest labels. */
.tl-footer a:not(.tl-brand), .faq-more a, .sup-toc a, .err-links a, .pot-note a, .mk-tier a,
.price-rail-delay a, .rs-note a {
  display: inline-flex; align-items: center; min-height: 24px; min-width: 24px;
}
/* ...but the footer COLUMNS are a stack of one link per line. inline-flex outranked their own
   display:block (0,2,1 vs 0,1,1), so every column ran its links together on one row -- "Deals
   Potentials Region Gaps Alerts" as a single line. Block-level flex keeps the 24px tap floor and
   the vertical centring while restoring one link per row; width:fit-content on the base rule
   still keeps the hit area off the empty column width. */
.tl-footer .tl-footer-col a { display: flex; }

/* ── Text-styled interactive targets ────────────────────────────────────────────────────────
   Item names in rows and link-styled action buttons rendered 16-22px tall against the 24x24 that
   WCAG 2.2 asks for. The obvious objection to fixing that on a data tool is row height -- but each
   of these already sits inside a cell or card that is 40px or taller, so a 24px floor on the
   INLINE box grows nothing. Verified by measuring row heights before and after rather than assumed.
   Inline links inside a sentence are deliberately not in this list: those are exempt, and padding
   them would break the line box of the prose around them. */
/* a.ma-name is the alert's own name on /potentials, and it only renders for an account that HAS
   alerts -- which is why six clean sweeps never saw it: the QA fixtures held none. It is
   navigation to the potential, not a link inside a sentence. */
/* .tl-footer-link-btn is the withdraw-consent control, and it is a BUTTON sitting in a column of
   anchors -- so `.tl-footer a:not(.tl-brand)` above misses it on both the tap floor and the layout.
   Named here rather than left to inherit, because that selector list is exactly the thing this
   codebase has silently fallen out of before. */
/* a.b-name is the item name on every /alerts feed row -- navigation in a 40px+ row, same case as
   a.item-name; it measured 95x18 across all states in the 2026-08-27 sweep. */
/* button.dfc-x / button.dfc-clear are the /deals active-filter chip dismissers (the ×) and their
   Clear all — new interactive text-styled controls, so they join this list the day they ship. */
a.item-name, a.pl-link, a.pot-name, a.alert-name-link, a.db-name-link, a.ma-name,
a.ma-pot-more, a.lpf-link, a.pd-back, a.me-card-link, a.b-name,
button.rw-card-link, button.me-card-link, button#imTry, button.tl-footer-link-btn,
button.dfc-x, button.dfc-clear,
.ck-more a {
  display: inline-flex; align-items: center; min-height: 24px;
}

/* Every table sort control on the site, whichever class the page happened to name it. They all
   carried padding:0, so the button was exactly its text -- 31x16 on /deals, 39x16 and 26x18 on
   /database -- against a 24x24 minimum. The header cells are already ~34px tall, so a 24px floor
   costs no header height and makes the whole label hittable instead of the glyphs. */
th .th-sort, thead th .th-sort,
.db-sort-btn, .db-treg-btn {
  display: inline-flex; align-items: center; gap: 4px;
  min-height: 24px; padding: 0 2px; margin: 0 -2px;
  /* Width 100% of the CELL, not of the label. A three-letter header like "EU" left the button
     21px wide; the th around it is already the column width less 13px of padding each side, so
     filling it reaches 24px without forcing any column wider. min-width is the floor for the one
     case where a column really is narrower than the target. */
  width: 100%; min-width: 24px; box-sizing: border-box;
}
/* Numeric headers align right, so the button has to keep that alignment once it spans the cell. */
th.col-num .th-sort, thead th.col-num .th-sort, th.num .th-sort { justify-content: flex-end; }

/* ── Sortable affordance (2026-08 deslop pass, R6) ──────────────────────────────────────────
   "This is sortable" and "this is sorted" are two different claims. At rest a sortable header
   used to be indistinguishable from an inert caps label (undiscoverable), and permanent arrows
   on every column are noise — so: nothing at rest, a faint ↕ on header hover, and on the column
   actually sorted ONE directional chevron in --blue-bright plus a one-step ink lift on the label
   (--text-3 → --text-2). Drawn as mask data-URIs in the estate's 9px select-chevron grammar, not
   unicode arrows, which wobble on the 10.5px mono baseline. The pseudo-element is a flex item of
   the (already inline-flex, gap:4px) sort button, so it centres itself and reserves its 9px at
   rest — a header never shifts when the glyph lights.
   SCOPE, detected by the marks in the real markup, not by assumption: the shared .th-sort and
   .db-sort-btn machinery on /deals, the admin tables, /database and /nix (whose page-local ↕/↑/↓
   glyphs were retired when it adopted this affordance — its data-key attribute is JS wiring, not
   an exclusion mark any more). One board keeps a competent page-local affordance of its own and
   is excluded by what makes it recognisable — /ledger's buttons carry a .caret span; matching
   them here would paint two glyphs. Sorted state answers BOTH hooks in use estate-wide: the
   sorted-asc/-desc classes and aria-sort (whose rest value on some boards is "none", so only the
   directional values match). */
th :is(.th-sort:not(:has(.caret)), .db-sort-btn)::after,
:is(th.col-num, th.num) :is(.th-sort:not(:has(.caret)), .db-sort-btn)::before {
  content: ''; flex: 0 0 9px; width: 9px; height: 12px;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 12'%3E%3Cpath d='M1 4.5L5 1l4 3.5M1 7.5L5 11l4-3.5' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / 9px 12px;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 12'%3E%3Cpath d='M1 4.5L5 1l4 3.5M1 7.5L5 11l4-3.5' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / 9px 12px;
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease);
}
/* Numeric columns right-align their figures, so the glyph sits LEFT of the label there — the
   label's right edge stays flush over the numbers it captions. */
:is(th.col-num, th.num) :is(.th-sort:not(:has(.caret)), .db-sort-btn)::after { content: none; }
th:hover :is(.th-sort:not(:has(.caret)), .db-sort-btn)::after,
th:hover :is(.th-sort:not(:has(.caret)), .db-sort-btn)::before { opacity: 0.6; }
:is(th.sorted-asc, th[aria-sort="ascending"]) :is(.th-sort:not(:has(.caret)), .db-sort-btn)::after,
:is(th.sorted-asc, th[aria-sort="ascending"]) :is(.th-sort:not(:has(.caret)), .db-sort-btn)::before {
  opacity: 1; background-color: var(--blue-bright);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 12'%3E%3Cpath d='M1 7.5L5 4l4 3.5' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 12'%3E%3Cpath d='M1 7.5L5 4l4 3.5' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
:is(th.sorted-desc, th[aria-sort="descending"]) :is(.th-sort:not(:has(.caret)), .db-sort-btn)::after,
:is(th.sorted-desc, th[aria-sort="descending"]) :is(.th-sort:not(:has(.caret)), .db-sort-btn)::before {
  opacity: 1; background-color: var(--blue-bright);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 12'%3E%3Cpath d='M1 4.5L5 8l4-3.5' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 12'%3E%3Cpath d='M1 4.5L5 8l4-3.5' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
/* The sorted column's label lifts one ink step, on every board (page styles that deliberately go
   further, like /nix's --text, still win at their own equal-and-later specificity). */
:is(th.sorted-asc, th.sorted-desc, th[aria-sort="ascending"], th[aria-sort="descending"]) :is(.th-sort, .db-sort-btn) { color: var(--text-2); }
/* Two page-local indicators this shared affordance supersedes, retired HERE because their pages'
   own sheets load after this one and would win a plain cascade: /database's .db-sort-ind ▲ span
   (the shared chevron would render beside it) and its all-blue sorted label, and the admin
   panel's permanent ↕ on th.sortable::after. */
.db-sort-ind { display: none !important; }
.db-table thead th.sorted-asc .db-sort-btn, .db-table thead th.sorted-desc .db-sort-btn { color: var(--text-2) !important; }
th.sortable::after { content: none !important; }

/* ── Search input ───────────────────────────────────────────────────────────────────────────
   Same story as the filter chip: used on deals.html and statistics.html, defined only on deals.
   The statistics one therefore rendered as a bare browser input, 21px tall, next to controls that
   are 38-44px. One definition, here. */
.search-input {
  padding: 10px 13px; font: inherit; font-size: 13px; min-height: 38px;
  color: var(--text); background: var(--bg-1);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.search-input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-dim); outline: none; }
.search-input::placeholder { color: var(--text-3); }

/* ── Keyboard hint chip ─────────────────────────────────────────────────────────────────────
   The visible half of window.tlSlashFocus (site.js): a small mono kbd chip parked inside the
   right edge of a search input, inside a position:relative wrapper. The wrapper gives the input
   enough padding-right that typed text never runs under the chip (the select-chevron recipe).
   Hidden the moment the wrapper owns focus, and entirely on touch, where there is no key. */
.kbd-hint {
  position: absolute; right: 9px; top: 50%; transform: translateY(-50%);
  font-family: var(--font-mono); font-size: 10.5px; line-height: 1;
  color: var(--text-3); background: var(--bg-1);
  border: 1px solid var(--border); border-radius: var(--r-xs);
  padding: 1px 5px; pointer-events: none;
}
:focus-within > .kbd-hint { display: none; }
@media (pointer: coarse) { .kbd-hint { display: none; } }

/* ── Filter chip ────────────────────────────────────────────────────────────────────────────
   A checkbox-in-a-label used as a filter toggle. It was defined TWICE, in deals.html and
   potentials.html, and used on a THIRD page that defined it nowhere -- so /statistics rendered
   these as bare inline labels with no chip at all, 21px tall, while the two that did define it had
   already drifted apart (12px/7px padding/13px box vs 12.5px/44px min-height/14px box).
   One definition, here. The 44px floor is kept because the label IS the tap target: the input
   inside is 14px and clicking the label is what toggles it. */
.filter-check {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 12.5px; color: var(--text-2); cursor: pointer; user-select: none;
  padding: 0 12px; min-height: 44px;
  border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg-1);
  transition: color var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease);
}
.filter-check:hover { border-color: var(--border-2); color: var(--text); }
.filter-check:has(input:checked) { border-color: var(--blue); color: var(--text); background: var(--blue-dim); }
.filter-check input { accent-color: var(--blue); width: 14px; height: 14px; }
/* Selects that sit inside a chip inherit the chip's height rather than fighting it. The native
   dropdown arrow was the one default-browser glyph left on the boards, so the chevron is drawn
   here instead (appearance:none) — same chip look, our arrow. */
.filter-check select { border: 0; color: inherit; font: inherit; cursor: pointer;
  appearance: none; -webkit-appearance: none;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%237c8aa3' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat right 4px center / 9px;
  padding: 2px 18px 2px 4px; }

/* ── tl-select: the boards' dropdown, drawn over a real <select> (assets/tl-select.js) ─────────
   The native control is kept as the value holder and hidden here rather than removed, so every
   listener already bound to it keeps firing and the page still works if the script never loads.

   `display:none` and not a clip-rect: this is not a field the user should be able to tab into
   twice. The visible trigger carries the role, the name and the focus ring instead. */
/* Stretched to the chip, so the whole 38px chip is the hit area rather than the 23px line of text
   inside it — the linter caught this one at 69x23 against the 24px floor. The chip is what a person
   aims at; the trigger has to be the same shape. */
.tlsel { position: relative; display: inline-flex; align-items: stretch; align-self: stretch; min-width: 0; }
.tlsel-native { display: none !important; }
/* Sized by the SELECTED label, which is the entire point — a native select is sized by its widest
   option, so the Auction house chip was 233px wide to say "Any". The chevron is the same drawn glyph
   .filter-check select used, so nothing about the closed chip changes except that it now fits. */
.tlsel-val {
  display: inline-flex; align-items: center; min-height: 24px; max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: inherit; font: inherit; cursor: pointer; padding: 2px 18px 2px 4px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%237c8aa3' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat right 4px center / 9px;
}
.tlsel-val:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; border-radius: var(--r-xs); }
.filter-check:has(.tlsel.open) { border-color: var(--blue); color: var(--text); }

/* PORTALLED TO <body>, so position:fixed — see the note in tl-select.js. Below 760px the toolbar is
   an overflow-x scroller carrying a mask-image, and a panel rendered inside it would be both clipped
   and faded near the right edge. */
.tlsel-panel {
  position: fixed; z-index: 1200; min-width: 168px; max-width: min(92vw, 340px);
  max-height: 320px; overflow-y: auto; overscroll-behavior: contain;
  background: var(--bg-elevated); border: 1px solid var(--border-2);
  border-radius: var(--r-sm); box-shadow: var(--shadow-lg); padding: 4px;
}
.tlsel-opt {
  padding: 8px 10px; border-radius: var(--r-xs); font-size: 12.5px; line-height: 1.3;
  color: var(--text-2); cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.tlsel-opt:hover { background: var(--bg-3); color: var(--text); }
.tlsel-opt:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; }
.tlsel-opt.sel { color: var(--blue-bright); background: var(--blue-dim); }
.tlsel-opt.is-disabled { color: var(--text-3); cursor: default; font-style: italic; }
/* An optgroup label, not an option: same vocabulary as the axis labels above the boards. */
.tlsel-group {
  padding: 8px 10px 4px; font-family: var(--font-mono); font-size: var(--fs-meta, 10.5px);
  font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: var(--text-3);
}
.tlsel-group:first-child { padding-top: 4px; }

/* The currency icon every lucent VALUE carries. em-sized so it rides the figure's own size. */
img.lucent { display: inline-block; width: 1em; height: 1em; vertical-align: -0.14em;
  margin-left: 3px; }

.rail-ico { width: 17px; height: 17px; flex-shrink: 0; stroke-width: 1.9; }
.rail-txt { overflow: hidden; text-overflow: ellipsis; }

.region-leds { padding: 8px 14px; flex-shrink: 0; }
/* ONE class, and it is a long one on purpose. This box wore `class="tl-nav-auth auth"` until
   2026-08-29, and the bare `auth` half was styled by nothing here -- but seven pages still carried
   a `.auth { ... gap: 9px ... }` block in their own <style>, left behind from before the rail
   existed, when this was a row in a top header. A page <style> is parsed AFTER the linked sheet,
   so at equal specificity those blocks beat the `gap: 6px` below and the box came out 96px tall on
   five routes, 95 on two (their copy said `var(--space-2)`) and 93 on the rest.

   Nothing looked broken on any single page. The cost was only visible while MOVING between them:
   .tl-nav-links is flex:1 and swallows the difference, so every pixel the box gained came out of
   the list, and the whole bottom cluster -- Premium, Discord, Help & Guide, Settings, the region
   LEDs -- sat 3px higher on /deals than on /alerts and jumped on every navigation.

   The seven blocks are deleted and the class is gone with them, which is the part that keeps it
   fixed: a generic name on a shared element, in an estate where all 23 pages ship their own
   <style>, is a collision waiting for the next person who needs a class called "auth". Style this
   box from here, by its own name, or not at all. */
.tl-nav-auth {
  flex-shrink: 0; padding: 10px; border-top: 1px solid var(--hairline);
  flex-direction: column; align-items: stretch; gap: 7px;
}
.tl-nav-auth .auth-identity {
  display: flex; align-items: center; gap: 6px; min-width: 0;
  padding: 7px 8px; border-radius: var(--r-sm); background: var(--veil-1);
}
.tl-nav-auth .auth-me {
  flex: 1 1 auto; min-width: 0; padding: 0; border: 0; background: none;
  border-radius: 0; gap: 9px;
}
.tl-nav-auth .auth-me:hover { background: none; box-shadow: none; }
.tl-nav-auth .auth-me:focus-visible { outline-offset: 3px; }
.tl-nav-auth .auth-avatar, .tl-nav-auth .auth-avatar-chip {
  flex: none; width: 30px; height: 30px;
}
.tl-nav-auth .auth-copy { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; gap: 2px; }
.tl-nav-auth .auth-name { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tl-nav-auth .auth-copy .tier { padding: 0; border: 0; background: none; font-size: 11px; line-height: 1; }
.tl-nav-auth .logout-btn {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; border: 0; background: transparent;
}
.tl-nav-auth .logout-btn svg, .tl-nav-auth .admin-btn svg { flex: none; width: 16px; height: 16px; }
.tl-nav-auth .admin-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: 38px; padding: 8px 10px; white-space: nowrap;
}

/* Empty-state CTAs. "See the paid plans" closes the message shown when a panel has nothing to
   list, so it is the only action on offer there -- a standalone call to action, not a link inside
   a sentence, and it was rendering 17px tall. */
.ma-pot-empty a, .empty-state a, .pot-empty a { display: inline-flex; align-items: center;
  min-height: 24px; }

/* Two landing-page anchors carry no class of their own, so the by-class rules above never reached
   them: the featured item's name inside its h3, and the "open its price history" line that closes
   an alert card. Both are the primary action of the block they sit in. Selecting on the PARENT is
   what works when the link itself is unclassed. */
/* .ma-name a is the alert's item name on /my-alerts and /me, where the anchor is a CHILD of the
   h3 rather than carrying the class itself -- the /potentials variant is a.ma-name and was
   floored separately. .al-trouble-go closes the 'some of your alerts look unreachable' notice
   on /alerts. All three only render for an account that HAS alerts, which is why they were
   invisible until the QA fixtures were given data. */
.lpf-name a, .lp-alert-src a, .ma-name a, .al-trouble-go {
  display: inline-flex; align-items: center; min-height: 24px; }

/* The wordmark is excluded from the rule above and floored on its own. Forcing it to inline-flex
   collapsed it to the 20px mark with the lettering gone -- that rule was written for one-line text
   links, and the brand is a flex row of mark plus wordmark, which is a different shape. It keeps
   its own display and takes only the height floor. */
.tl-footer .tl-brand { min-height: 24px; }

html.rail-collapsed .tl-brand { font-size: 0; }
html.rail-collapsed .tl-brand > :not(.tl-brand-mark),
html.rail-collapsed .rail-txt,
html.rail-collapsed .tl-nav-auth .auth-name,
html.rail-collapsed .tl-nav-auth .tier { display: none; }

html.rail-collapsed .tl-brand { justify-content: center; padding: 0; margin-right: 0; gap: 0; }
html.rail-collapsed .tl-nav-links a { justify-content: center; padding: 9px 0; }
html.rail-collapsed .tl-nav-auth { justify-content: center; }
/* The guest login and the logout button both carry 12px side padding sized for the open rail;
   inside 64px that left the login 2px off both edges and Logout 1px off. Collapsed keeps the
   chip, sheds the width it no longer has. */
.tl-nav-auth .auth-login { display: inline-flex; align-items: center; gap: 8px; }
html.rail-collapsed .tl-nav-auth .auth-login { padding: 8px; }
html.rail-collapsed .tl-nav-auth { align-items: center; }
html.rail-collapsed .tl-nav-auth .auth-identity {
  flex-direction: column; gap: 4px; padding: 4px; background: none;
}
html.rail-collapsed .tl-nav-auth .auth-me { flex: none; }
html.rail-collapsed .tl-nav-auth .auth-copy { display: none; }
html.rail-collapsed .tl-nav-auth .logout-btn { width: 36px; height: 36px; padding: 0; }
html.rail-collapsed .tl-nav-auth .admin-btn { width: 40px; height: 38px; min-height: 38px; padding: 0; }
html.rail-collapsed .tl-nav-auth .auth-action-label { display: none; }
/* Column, not row: the wraps carry a 24x24 focus-target floor, and three of those side by side
   are 72px inside a 64px rail (measured spilling to x:-8). Stacked they read as three lamps in
   the icon column, which is what everything else collapsed already looks like. */
html.rail-collapsed .region-leds { justify-content: center; align-items: center; flex-direction: column; gap: 0; padding: 6px 0; }

/* The collapsed rail's label. It used to be `a:hover::after`, flown out past the rail's right
   edge -- and not one pixel of it had ever been on screen. Two independent clippers, both of them
   load-bearing and neither removable:

     .tl-nav-links carries overflow-y:auto so the destination list can scroll, and a box that is
     not `visible` on ONE axis computes to `auto` on both. It clipped at x=63. The tooltip began
     at x=63.

     The same element carries the bottom-fade mask-image, and a mask hides everything painted
     outside its box whatever overflow says -- so widening the clip alone would not have helped.

   A pseudo-element cannot leave its originating box, so the label is now a real element parked on
   <body> and positioned fixed, which no ancestor's overflow or mask can reach. Measured, not
   assumed: tools/qa/railtip.js reports the box, where it lands, and which ancestor clips it. */
.rail-tip {
  position: fixed; z-index: 200; pointer-events: none;
  background: var(--bg-3); color: var(--text); border: 1px solid var(--border-2);
  border-radius: var(--r-sm); padding: 5px 10px; font-size: 12px; white-space: nowrap;
  box-shadow: var(--shadow-md);
  opacity: 0; transform: translateY(-50%) translateX(calc(var(--rail-motion) * -4px));
  transition: opacity var(--rail-mo) var(--rail-ease),
              transform var(--rail-mo) var(--rail-ease);
}
/* Slides the last few pixels out of the rail rather than appearing, which is the same
   overshoot-free arrival the glyphs use. Under reduced motion --rail-motion is 0, so it fades in
   place: the label still arrives, it just does not travel. */
.rail-tip.on { opacity: 1; transform: translateY(-50%) translateX(0); }
.rail-tip[hidden] { display: none; }

.rail-discord {
  display: flex; align-items: center; gap: 11px; margin: var(--space-3) 10px 0; padding: 9px 11px;
  border-radius: var(--r-sm); border: 1px solid rgba(88,101,242,0.35); background: rgba(88,101,242,0.10);
  color: #aab4f8; font-size: 13.5px; font-weight: 600; text-decoration: none; white-space: nowrap;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.rail-discord:hover { background: rgba(88,101,242,0.20); border-color: rgba(88,101,242,0.6); color: #fff; }
.rail-discord .rail-ico { flex: none; width: 17px; height: 17px; }
.rail-collapsed .rail-discord { justify-content: center; margin-left: 6px; margin-right: 6px; padding: 9px 0; }

.rail-toggle {
  flex-shrink: 0; display: flex; align-items: center; gap: 12px;
  margin: 0 10px 10px; padding: 9px 10px; border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer; font: inherit; font-size: 12.5px;
  color: var(--text-3); text-align: left;
}
.rail-toggle:hover { color: var(--text); background: var(--veil-2); }
.rail-toggle svg { width: 17px; height: 17px; flex-shrink: 0; }
html.rail-collapsed .rail-toggle { justify-content: center; padding: 9px 0; margin: 0 0 10px; }

html.rail-collapsed .page-wrap,
html.rail-collapsed .page-wrap-wide { max-width: none; }

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   RAIL MICRO-INTERACTIONS
   One system, not sixteen tricks.

   Every glyph in the rail answers the cursor in the same time, on the same curve, over the same
   budget: 1-2px of travel, 5-8 degrees of turn, or a 1.05-1.08 scale. What changes per icon is
   only WHICH part moves and in which direction, and that is read off what the icon means -- the
   trend line rises off its axis, the arrow drops into the import tray, the lid comes off the
   gift, the chevron leans the way the rail is about to go. Icons whose meaning suggests nothing
   in particular move as a whole, and nothing moves further than it has to.

   Transitions, never keyframes. A transition retargets from wherever the glyph currently is, so
   a cursor swept down thirteen rows reads as one soft wave and no icon can be left stranded
   part-way through an animation or restarted from the top. That restart is the failure mode that
   makes hover-animated navigation feel broken rather than alive, and it is designed out here
   rather than patched around.

   Nothing in this block changes layout. Transforms do not reflow, .rail-ico already carries
   flex-shrink:0, and the row's text, padding and width are untouched -- so a rail full of moving
   glyphs is still a rail whose entries never move under the pointer.
   ══════════════════════════════════════════════════════════════════════════════════════════════ */

/* Base. The resting rule carries the LEAVE curve; the hover rule below swaps in the faster enter
   curve, which is what makes arriving feel immediate and leaving feel like settling. */
.rail-ico {
  /* Parts travel a pixel or two and several of them start on the edge of the viewBox -- the
     database disc sits at y=2 under a 1.9 stroke, the gift's bow at y=2. Clipping at 24 would
     shave the stroke off exactly the part the animation is drawing attention to. Nothing here
     moves far enough to reach the label 12px away. */
  overflow: visible;
  transition: transform var(--rail-mo-out) var(--rail-ease-out),
              filter    var(--rail-mo-out) var(--rail-ease-out);
}
.rail-ico > * {
  /* view-box, so 50% 50% means the middle of the 24x24 grid the paths are drawn on rather than
     the middle of whatever bounding box that one path happens to have. */
  transform-box: view-box; transform-origin: 50% 50%;
  transition: transform var(--rail-mo-out) var(--rail-ease-out);
}
.tl-nav :is(:hover, :focus-visible) > .rail-ico,
.tl-nav :is(:hover, :focus-visible) > .rail-ico > * {
  transition-duration: var(--rail-mo);
  transition-timing-function: var(--rail-ease);
}
/* One rule composes every gesture in the rail; a variant further down declares only its keyframes
   plus, where it needs them, a pose, duration, curve or delay. Sixteen `animation:` declarations
   would have been sixteen places to keep the fill mode and the fallbacks in step -- and, more to
   the point, sixteen rules to restate for Performance Mode at the bottom of this file instead of
   one.

   LONGHANDS, not the `animation:` shorthand. In the shorthand, the `none` that --ri-anim falls
   back to is claimed by animation-FILL-MODE, which also accepts `none` -- so the trailing `both`
   was parsed as the animation NAME on every un-animated glyph in the rail. It named no keyframes,
   so it was silent; the probe printed `anim=both` and that is the only reason it was ever seen. */
.rail-ico, .rail-ico > *, .rail-toggle svg, .rail-settings svg, .tl-nav-auth a {
  animation-name: var(--ri-anim, none);
  animation-duration: var(--ri-dur, var(--rail-g));
  animation-timing-function: var(--ri-ease, var(--rail-ease));
  animation-delay: var(--ri-delay, 0s);
  animation-fill-mode: both;
  animation-iteration-count: 1;   /* nothing in this rail ever loops */
}
/* Custom properties inherit, so --ri-anim set on the <svg> for a whole-icon gesture reached every
   <path> inside it as well -- the bell rotated 14 degrees AND the path within it rotated 14
   degrees, for 28. Reset on the children; the part variants below set it on the path directly and
   still win. */
.rail-ico > * { --ri-anim: initial; }

/* ── The glyphs ─────────────────────────────────────────────────────────────────────────────
   Each entry performs a short GESTURE on hover -- the bell rings and damps out, the trace beats
   twice, the arrow drops into the tray, the lid comes off the gift -- rather than sliding to a
   new resting pose. One-shot keyframes, never infinite, 0.6-0.8s, and every one of them is a
   motion the icon's own subject would actually make.

   Two shapes of gesture, and which one an icon gets is decided by whether the movement has an
   OUTCOME:

     round trip  the motion is the whole point and the glyph ends exactly where it started: the
                 bell, the heartbeat, the question mark's double-take, the cube's turn.

     travel      the motion arrives somewhere and stays there while the pointer does -- the line
                 has risen, the lid is off, the arrow is in the tray. Every travel gesture names
                 its destination ONCE, in --ri-pose, and that value is used twice: as the
                 keyframes' 100% frame, and as a plain transitioned transform on the hover rule.
                 That is not belt-and-braces, it is the only way the return works. Removing an
                 animation does NOT start a transition from the animated value -- the before-change
                 style a transition is computed against excludes running animations -- so a glyph
                 held by fill-mode alone SNAPS back to rest the instant the cursor leaves. With the
                 pose also declared as a transform, the animation ends exactly where the transition
                 starts, and leaving eases home from there. The probe flagged all nine of these as
                 "SNAPS HOME" before the pose existed.

   Overshoot-then-settle is the shared vocabulary: every travel gesture passes its mark and comes
   back to it. That is what makes sixteen different motions read as one family.

   A variant declares only what it CHANGES -- its keyframes, its pose, and a duration, curve or
   delay when it needs one. The animation longhands that compose them live in the base rule above.

   Whole-icon keyframes transform the <svg>, where a CSS pixel is a screen pixel. Part keyframes
   transform a <path>, where it is a viewBox unit -- hence var(--rail-u). Mixing the two up is the
   easy mistake here: it does not error, it just moves 30% less than it reads. */

/* Deals -- the trend line sweeps up past its mark, settles above a fixed axis, and stays up while
   you are on the row. The axis holding still is the whole trick: without it the glyph just drifts
   and means nothing. */
[data-ico="deals"] .ri-trend { --ri-pose: translateY(calc(var(--rail-motion) * -2.1 * var(--rail-u))); }
@keyframes riTrendRise {
  0%   { transform: translateY(0); }
  50%  { transform: translateY(calc(var(--rail-motion) * -3.6 * var(--rail-u))); }
  100% { transform: var(--ri-pose); }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="deals"] .ri-trend {
  --ri-anim: riTrendRise; transform: var(--ri-pose);
}

/* NIX -- two beats through the trace, hard then soft, the way a heartbeat reads on a monitor. The
   flat arms sit on the vertical centre, so scaling from there moves the peaks and leaves the
   baseline where it is. Ends where it started: the beat IS the feedback. */
@keyframes riPulse {
  0%   { transform: scale(1, 1); }
  16%  { transform: scale(0.97, 1.42); }
  32%  { transform: scale(1.02, 0.88); }
  48%  { transform: scale(0.98, 1.24); }
  66%  { transform: scale(1.01, 0.96); }
  100% { transform: scale(1, 1); }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="nix"] {
  --ri-anim: riPulse; --ri-dur: var(--rail-g-long); --ri-ease: var(--rail-ease-out);
}

/* Alerts -- a bell is struck at its crown, not its middle, and it does not stop on the first
   swing. Five decreasing arcs and it is upright again. The clapper is part of the same path, so
   it swings with the bell rather than independently of it, which is what a real bell does. */
[data-ico="alerts"] { transform-origin: 50% 14%; }
@keyframes riRing {
  0%   { transform: rotate(0deg); }
  12%  { transform: rotate(-20deg); }
  30%  { transform: rotate(15deg); }
  48%  { transform: rotate(-9deg); }
  66%  { transform: rotate(5deg); }
  84%  { transform: rotate(-2deg); }
  100% { transform: rotate(0deg); }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="alerts"] {
  --ri-anim: riRing; --ri-dur: var(--rail-g-long); --ri-ease: var(--rail-ease-out);
}

/* Potentials -- the cube turns on its vertical axis. The turn is a horizontal squeeze plus a few
   degrees of lean, NOT perspective/rotateY: a 3D transform promotes the icon to its own composited
   layer, which is rasterised once and then scaled, and 17px of line art re-sampled that way goes
   visibly soft. This reads as the same rotation and stays crisp. */
@keyframes riTurn {
  0%   { transform: rotate(0deg) scaleX(1); }
  32%  { transform: rotate(-8deg) scaleX(0.74); }
  62%  { transform: rotate(5deg) scaleX(1.06); }
  100% { transform: rotate(0deg) scaleX(1); }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="potentials"] {
  --ri-anim: riTurn; --ri-ease: var(--rail-ease-out);
}

/* Database -- the stack loosens from the top down. Disc and waistline rise by different amounts
   and the waistline starts late, so it reads as a ripple travelling up a stack rather than a lid
   lifting off a box. The barrel never moves; it is what the other two rise against. */
[data-ico="database"] .ri-disc { --ri-pose: translateY(calc(var(--rail-motion) * -1.7 * var(--rail-u))); }
[data-ico="database"] .ri-band { --ri-pose: translateY(calc(var(--rail-motion) * -0.85 * var(--rail-u))); }
@keyframes riStackTop {
  0%   { transform: translateY(0); }
  45%  { transform: translateY(calc(var(--rail-motion) * -2.8 * var(--rail-u))); }
  100% { transform: var(--ri-pose); }
}
@keyframes riStackMid {
  0%   { transform: translateY(0); }
  45%  { transform: translateY(calc(var(--rail-motion) * -1.5 * var(--rail-u))); }
  100% { transform: var(--ri-pose); }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="database"] .ri-disc {
  --ri-anim: riStackTop; transform: var(--ri-pose);
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="database"] .ri-band {
  --ri-anim: riStackMid; --ri-delay: 70ms; transform: var(--ri-pose);
}

/* Market Trends -- the arrow shoots along its own diagonal, past the mark, and holds. */
[data-ico="statistics"] {
  --ri-pose: translate(calc(var(--rail-motion) * 1.2px), calc(var(--rail-motion) * -1.8px));
}
@keyframes riClimb {
  0%   { transform: translate(0, 0); }
  50%  { transform: translate(calc(var(--rail-motion) * 2.6px), calc(var(--rail-motion) * -3.6px)); }
  100% { transform: var(--ri-pose); }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="statistics"] {
  --ri-anim: riClimb; transform: var(--ri-pose);
}

/* Dashboard -- the four panes tap in and close on the centre, clockwise from top-left. Each tile
   scales about ITS OWN centre (transform-box:fill-box), so the pane dips rather than the grid
   shrinking. The stagger runs on the enter only; on leave all four release together, so a fast
   mouse-out never leaves the grid visibly lopsided on its way home. */
[data-ico="me"] .ri-tile {
  transform-box: fill-box; transform-origin: 50% 50%;
  --ri-pose: translate(calc(var(--ri-x) * var(--rail-motion) * 1.05 * var(--rail-u)),
                       calc(var(--ri-y) * var(--rail-motion) * 1.05 * var(--rail-u)));
}
[data-ico="me"] .ri-tile:nth-child(1) { --ri-x:  1; --ri-y:  1; }
[data-ico="me"] .ri-tile:nth-child(2) { --ri-x: -1; --ri-y:  1; }
[data-ico="me"] .ri-tile:nth-child(3) { --ri-x: -1; --ri-y: -1; }
[data-ico="me"] .ri-tile:nth-child(4) { --ri-x:  1; --ri-y: -1; }
@keyframes riTileTap {
  0%   { transform: translate(0, 0) scale(1); }
  45%  { transform: translate(calc(var(--ri-x) * var(--rail-motion) * 1.9 * var(--rail-u)),
                              calc(var(--ri-y) * var(--rail-motion) * 1.9 * var(--rail-u))) scale(0.82); }
  100% { transform: var(--ri-pose); }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="me"] .ri-tile {
  --ri-anim: riTileTap; transform: var(--ri-pose);
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="me"] .ri-tile:nth-child(2) { --ri-delay:  55ms; }
.tl-nav :is(:hover, :focus-visible) > [data-ico="me"] .ri-tile:nth-child(3) { --ri-delay: 110ms; }
.tl-nav :is(:hover, :focus-visible) > [data-ico="me"] .ri-tile:nth-child(4) { --ri-delay: 165ms; }

/* Rewards -- the lid pops off the box, tips, drops back most of the way and rests ajar. The ribbon
   is cut at the lid line in site.js so its top half travels with the lid; a ribbon drawn as one
   stroke would tear away from the box or stop short of the lid's edge. */
[data-ico="rewards"] .ri-lid { --ri-pose: translateY(calc(var(--rail-motion) * -2.3 * var(--rail-u))); }
@keyframes riLidOff {
  0%   { transform: translateY(0) rotate(0deg); }
  38%  { transform: translateY(calc(var(--rail-motion) * -4.4 * var(--rail-u))) rotate(-5deg); }
  72%  { transform: translateY(calc(var(--rail-motion) * -1.4 * var(--rail-u))) rotate(2deg); }
  100% { transform: var(--ri-pose); }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="rewards"] .ri-lid {
  --ri-anim: riLidOff; --ri-ease: var(--rail-ease-out); transform: var(--ri-pose);
}

/* Ledger -- the sheet is picked up: it lifts, tips off the corner it was resting on, and settles
   held. Up and slightly right, the way a page is lifted rather than pushed. */
[data-ico="ledger"] {
  --ri-pose: translate(calc(var(--rail-motion) * 0.7px), calc(var(--rail-motion) * -1.7px));
}
@keyframes riPageLift {
  0%   { transform: translate(0, 0) rotate(0deg); }
  45%  { transform: translate(calc(var(--rail-motion) * 1.6px), calc(var(--rail-motion) * -3.2px))
                    rotate(-4.5deg); }
  100% { transform: var(--ri-pose); }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="ledger"] {
  --ri-anim: riPageLift; --ri-ease: var(--rail-ease-out); transform: var(--ri-pose);
}

/* Import Build -- the arrow drops the last of the way into the tray and the tray takes the weight.
   The tray's dip starts a beat after the arrow leaves, so the two read as one event rather than
   two things moving at once. The tray is a round trip; only the arrow stays where it landed. */
[data-ico="import"] .ri-arrow { --ri-pose: translateY(calc(var(--rail-motion) * 2.4 * var(--rail-u))); }
[data-ico="import"] .ri-tray  { transform-origin: 50% 85%; }
@keyframes riDropIn {
  0%   { transform: translateY(0); }
  50%  { transform: translateY(calc(var(--rail-motion) * 3.6 * var(--rail-u))); }
  78%  { transform: translateY(calc(var(--rail-motion) * 1.6 * var(--rail-u))); }
  100% { transform: var(--ri-pose); }
}
@keyframes riCatch {
  0%   { transform: scaleY(1); }
  45%  { transform: scaleY(0.82); }
  100% { transform: scaleY(1); }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="import"] .ri-arrow {
  --ri-anim: riDropIn; transform: var(--ri-pose);
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="import"] .ri-tray {
  --ri-anim: riCatch; --ri-dur: 0.34s; --ri-delay: 0.2s; --ri-ease: var(--rail-ease-out);
}

/* Help & Guide -- the book turns a page, right side to left. Two earlier glyphs are recorded in
   RAIL_ICONS along with why each failed; what matters here is that the gesture is now the thing the
   object is FOR, rather than a shape being moved about.

   The turn is a scaleX, the only scale in the rail, and it earns the exception because for a page a
   scale is the honest projection: swinging up towards the spine is getting narrower, laying down on
   the far side is that width passing through zero and out to -1. Tilting instead would shear the
   page, which is a thing no book does.

   WHY A FOURTH PATH RATHER THAN MOVING THE RIGHT LEAF. An open book looks the same after a page
   turns as it did before -- that is what a book IS -- so the two leaves are a stack and must not
   move. Sweeping the right leaf itself was an earlier version, and it ends its run with the right
   half of the book missing. Here the leaves hold still and one page crosses them, so the glyph is
   the same at 780ms as it was at rest and the only thing that happened is the thing you watched.

   THE PAGE IS FILLED, and that is what makes this visible rather than merely correct. Drawn as an
   outline it was a hairline sliding about INSIDE a shape already made of hairlines -- geometrically
   a page turn, and at 17px very nearly nothing. Ink is what reads at this size, not position: a
   62% tint sweeping from one half to the other changes the glyph's whole value, which the eye
   catches from across the rail, where 2px of edge travel does not. Two quieter fixes were measured
   and dropped for not earning their keyframes: arcing the page over the spine (it puts a thin spike
   above the book's top edge for a third of the run, which reads as a rendering fault -- and this is
   a front elevation, where a page turning about a vertical spine has no vertical travel anyway),
   and having the stacks hand it over by 16% of their width (indistinguishable beside the fill).

   THE TIMING LINGERS WIDE AND CROSSES FAST. The keyframes sit at 0.9 / 0.3 / -0.3 / -0.9, not at a
   single midpoint, because near scaleX 0 the page is edge-on and there is nothing to see: an even
   sweep spent three of eight sampled frames invisible at the spine. It now gives a third of the run
   to each side, where the page is broad, and 20% to the crossing. Over 0.6s that is 204ms with the
   page broad on each half and 120ms crossing -- the proportions are what carry the read, so they
   survive the shorter clock unchanged.

   The page rests at opacity 0 and rides back to 0 as it lands, which is what keeps the doubled
   stroke from ever being seen: at scaleX 1 it lies exactly on the right leaf, so fading it in there
   changes nothing on screen, and by 94% scaleX has put it exactly on the left leaf, so fading it
   out there changes nothing either. It appears out of one stack and disappears into the other.

   opacity: var(--rail-motion) rather than a literal 1 is not decoration. With motion off, every
   frame here is scaleX(1) at opacity 0: the turning page cannot appear even if the animation runs,
   which is a stronger guarantee than the `animation: none` rule further down and does not depend
   on it. The fill rides that same opacity, so it cannot leak either. */
[data-ico="support"] .ri-flip { opacity: 0; fill: currentColor; fill-opacity: 0.62; }
@keyframes riPageFlip {
  0%   { transform: scaleX(1); opacity: 0; }
  6%   { transform: scaleX(calc(1 - var(--rail-motion) * 0.1));  opacity: var(--rail-motion); }
  40%  { transform: scaleX(calc(1 - var(--rail-motion) * 0.7));  opacity: var(--rail-motion); }
  60%  { transform: scaleX(calc(1 - var(--rail-motion) * 1.3));  opacity: var(--rail-motion); }
  94%  { transform: scaleX(calc(1 - var(--rail-motion) * 1.9));  opacity: var(--rail-motion); }
  100% { transform: scaleX(calc(1 - var(--rail-motion) * 2));    opacity: 0; }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="support"] .ri-flip {
  /* No --ri-dur: it takes the rail's own --rail-g, 0.6s. It ran at --rail-g-long (0.78s) while the
     turn was being drawn, and once the shape read at a glance the extra 180ms was just the page
     hanging about. Dropping the declaration rather than writing 0.62s puts this gesture back on the
     same clock as the ten that never needed a long one. */
  --ri-anim: riPageFlip; --ri-ease: var(--rail-ease-out);
}

/* Premium -- the CTA, and the only glyph allowed to be louder than the rest: the star pops, spins
   back through the upright and holds slightly enlarged, under a gold bloom. The bloom is NOT gated
   on the motion switch; it is light, not movement, and it is the half that has to survive a
   reduced-motion preference. */
[data-ico="upgrade"] { --ri-pose: scale(calc(1 + var(--rail-motion) * 0.14)); }
@keyframes riStarPop {
  0%   { transform: scale(1) rotate(0deg); }
  38%  { transform: scale(1.32) rotate(-16deg); }
  70%  { transform: scale(1.04) rotate(7deg); }
  100% { transform: var(--ri-pose); }
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="upgrade"] {
  --ri-anim: riStarPop; --ri-ease: var(--rail-ease-out); transform: var(--ri-pose);
  filter: drop-shadow(0 0 6px rgba(232,163,61,0.6));
}
/* Restated here rather than left to the pill's own hover rule so the lift obeys the master switch
   like every other movement in the rail. */
.tl-nav-links a.nav-upgrade:hover,         .rail-foot a.nav-upgrade:hover,
.tl-nav-links a.nav-upgrade:focus-visible, .rail-foot a.nav-upgrade:focus-visible {
  transform: translateY(calc(var(--rail-motion) * -1.5px));
}

/* Join Discord -- the mark bobs and settles raised, and the button rises the same 1.5px as the
   Premium pill above it, so two CTAs stacked on one another move by the same amount. */
[data-ico="discord"] {
  --ri-pose: translateY(calc(var(--rail-motion) * -1.2px)) scale(calc(1 + var(--rail-motion) * 0.09));
}
@keyframes riBob {
  0%   { transform: translateY(0) scale(1); }
  40%  { transform: translateY(-3px) scale(1.16); }
  72%  { transform: translateY(0.5px) scale(1.01); }
  100% { transform: var(--ri-pose); }
}
.rail-discord {
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease),
              transform  var(--rail-mo-out) var(--rail-ease-out),
              box-shadow var(--rail-mo-out) var(--rail-ease-out);
}
.rail-discord:hover, .rail-discord:focus-visible {
  transform: translateY(calc(var(--rail-motion) * -1.5px));
  box-shadow: 0 5px 16px -8px rgba(88,101,242,0.85);
}
.tl-nav :is(:hover, :focus-visible) > [data-ico="discord"] {
  --ri-anim: riBob; --ri-ease: var(--rail-ease-out); transform: var(--ri-pose);
}

/* Collapse -- the chevron leans the way the rail is about to go, twice, and holds. Collapsed, the
   glyph is already flipped 180deg, and that flip is STATE, not decoration: it is carried in --chev
   and composed into both the pose and the keyframes, so the same -2.6px lean points the other way
   in screen terms and the hover gesture can never overwrite which way the arrow points. */
.rail-toggle svg {
  --chev: 0deg;
  --ri-pose: rotate(var(--chev)) translateX(calc(var(--rail-motion) * -2.6px));
  transform: rotate(var(--chev));
  transition: transform var(--rail-mo-out) var(--rail-ease-out);
}
html.rail-collapsed .rail-toggle svg { --chev: 180deg; }
@keyframes riLean {
  0%   { transform: rotate(var(--chev)) translateX(0); }
  40%  { transform: rotate(var(--chev)) translateX(calc(var(--rail-motion) * -4px)); }
  68%  { transform: rotate(var(--chev)) translateX(calc(var(--rail-motion) * -0.5px)); }
  100% { transform: var(--ri-pose); }
}
.rail-toggle:hover svg, .rail-toggle:focus-visible svg {
  --ri-anim: riLean; transform: var(--ri-pose);
}

/* ── Row feedback ───────────────────────────────────────────────────────────────────────────
   The glyph is not the whole interaction. Colour and background stay on --t-fast, quicker than
   the movement, on purpose: the row acknowledges the cursor at once and the glyph then settles
   into place behind it. Matching the two would make the whole rail feel a beat slower. */

/* Hovered + ACTIVE. .active sets its background later in this sheet than :hover does and at the
   same specificity, so the page you are standing on was the one row in the rail that did not
   acknowledge the cursor at all. It deepens its own tint rather than borrowing the neutral one,
   which would have read as losing the "you are here" mark on hover. */
.tl-nav-links a.active:hover,
.tl-nav-links a.active:focus-visible { background: rgba(108,140,255,0.19); }

/* The active glyph carries the accent the bar at the row's edge already uses. Two marks saying
   one thing, in one colour -- and it survives hover, because nothing above touches icon colour. */
.tl-nav-links a.active .rail-ico { color: var(--blue-bright); }

/* ── The account row ────────────────────────────────────────────────────────────────────────
   Neither control has a glyph to animate, and neither should grow one: collapsed to 64px this
   row is already the tightest thing in the rail. They move as buttons instead -- Admin lifts into
   a faint ring of its own blue, Logout slides toward the exit. */
.tl-nav-auth a {
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease),
              box-shadow var(--rail-mo-out) var(--rail-ease-out),
              transform  var(--rail-mo-out) var(--rail-ease-out);
}
.tl-nav-auth a:hover, .tl-nav-auth a:focus-visible {
  transition-duration: var(--t-fast), var(--t-fast), var(--t-fast), var(--rail-mo), var(--rail-mo);
  transition-timing-function: var(--ease), var(--ease), var(--ease), var(--rail-ease), var(--rail-ease);
}
.tl-nav-auth .admin-btn  { --ri-pose: translateY(calc(var(--rail-motion) * -1.5px)); }
.tl-nav-auth .logout-btn { --ri-pose: translateX(calc(var(--rail-motion) * 2px)); }
@keyframes riRise {
  0%   { transform: translateY(0); }
  45%  { transform: translateY(calc(var(--rail-motion) * -3px)); }
  100% { transform: var(--ri-pose); }
}
@keyframes riExit {
  0%   { transform: translateX(0); }
  45%  { transform: translateX(calc(var(--rail-motion) * 4px)); }
  100% { transform: var(--ri-pose); }
}
.tl-nav-auth .admin-btn:hover, .tl-nav-auth .admin-btn:focus-visible {
  --ri-anim: riRise; --ri-ease: var(--rail-ease-out); transform: var(--ri-pose);
  box-shadow: 0 0 0 3px rgba(108,140,255,0.12);
}
.tl-nav-auth .logout-btn:hover, .tl-nav-auth .logout-btn:focus-visible {
  --ri-anim: riExit; --ri-ease: var(--rail-ease-out); transform: var(--ri-pose);
}

/* ── Reduced motion ─────────────────────────────────────────────────────────────────────────
   One declaration, because every transform above is multiplied by --rail-motion. The rail keeps
   all of its colour, background, ring and bloom feedback and loses only the travel -- a reader
   who has asked for less movement still needs to be able to tell that a row is interactive.

   data-motion is the in-site Settings control, and data-motion="off" already kills transitions
   site-wide; without this the glyphs would still JUMP to their hovered geometry, instantly,
   which is the one thing worse than animating them. */
@media (prefers-reduced-motion: reduce) {
  :root { --rail-motion: 0; }
  /* The gestures are keyframes, so the multiplier alone does not reach them. The in-site Settings
     values need no equivalent line: html[data-motion="reduced"] already sets animation:none on
     every element on the page, which is why only the media-query branch is spelled out here. */
  .tl-nav .rail-ico, .tl-nav .rail-ico > *,
  .rail-toggle svg, .rail-settings svg, .tl-nav-auth a { animation: none !important; }
}
html[data-motion="reduced"], html[data-motion="off"] { --rail-motion: 0; }



@media (max-width: 880px) {
  
  /* The reservation and the painted bar below must be the SAME number, so both speak the token.
     They shipped as 64 against a hardcoded 60 — a 4px see-through slit under the bar and a
     hamburger chip centred in the wrong box, on every app page. */
  body { padding-left: 0; padding-top: var(--nav-h); }
  
  .tl-nav {
    transform: translateX(-100%); width: var(--rail-collapsed-w);
    visibility: hidden;
    transition: width .22s var(--ease), transform .22s var(--ease), visibility 0s .22s;
  }
  html.rail-open .tl-nav { visibility: visible; transition-delay: 0s; }
  html.rail-open .tl-nav { transform: none; width: 240px; }
  html.rail-open .rail-txt,

  html.rail-open .rail-txt { display: block; }
  html.rail-open .tl-brand { font-size: 15px; justify-content: flex-start; padding: 0 18px; }
  html.rail-open .tl-brand > :not(.tl-brand-mark) { display: block; }
  
    /* 10px, not 12: at 360x780 the destination list held 688px of content in a 572px box. Two
       pixels a row across 13 rows is 26px back, which with the region dots removed below is most
       of the difference between a list that scrolls and one that does not. */
    html.rail-open .tl-nav-links a { justify-content: flex-start; padding: 10px 10px; }

    /* Three unlabelled dots. The labels are display:none at this width, so the region health
       readout was 33px of permanently pinned space showing the reader nothing they could read --
       while the upgrade CTA sat below the fold. It is a desktop-rail instrument; it goes. */
    .tl-nav .region-leds { display: none; }

    /* Settings was centred while all 13 destinations above it were left-aligned, which read as a
       different component rather than the same rail. The centring comes from html.rail-collapsed,
       which stays on <html> from the desktop preference even though the mobile drawer is never
       visually collapsed, so the override has to out-specify that and not just the base rule. */
    html.rail-open .rail-foot .rail-settings,
    html.rail-collapsed .rail-foot .rail-settings {
      justify-content: flex-start; padding: 9px 10px; width: 100%; margin: 0;
    }
    html.rail-collapsed .rail-foot { padding: 6px 10px 4px; align-items: stretch; }
    /* padding too, for the same reason the Settings row above needs it: the desktop-collapsed
       rule zeroes the Discord pill's side padding to centre a lone glyph, and that class stays on
       <html> inside the drawer, where the label IS shown. Its icon sat flush against the box edge
       while the Premium pill beside it kept its 10px. */
    html.rail-collapsed .rail-foot .rail-discord,
    html.rail-collapsed .rail-foot .nav-upgrade {
      width: 100%; margin: 0; padding: 9px 10px; justify-content: flex-start;
    }
    html.rail-collapsed .rail-txt { display: block; }

    /* The account row is the last thing above the home indicator on iOS. */
    .tl-nav-auth { padding-bottom: max(10px, env(safe-area-inset-bottom)); }

    /* A visible scrollbar inside a 240px drawer is noise; the list still scrolls. */
    .tl-nav-links { scrollbar-width: thin; scrollbar-color: var(--border-2) transparent; }
    .tl-nav-links::-webkit-scrollbar { width: 3px; }
    .tl-nav-links::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
    .tl-nav-links::-webkit-scrollbar-track { background: transparent; }
  
  html.rail-open .tl-nav-links .rail-group {
    height: auto; overflow: visible; border-top: 0;
    margin: 14px 0 5px; padding: 0 10px;
  }
  html.rail-open .tl-nav-links .rail-group:first-child { margin-top: 2px; }
  .tl-nav-links { display: flex; }   
  .rail-toggle { display: none; }    
  
  /* The phone header is a BAR, not two floating chips. The chips went opaque first, but scrolled
     rows still slid through the gap between them, which read as content leaking through chrome.
     Every app page already pads its first 64px for this row (measured across all eleven routes),
     so at rest the bar covers only padding; while scrolling, content passes under one solid
     surface. body::before is free here — only body.page-marketing uses it (for the wash), so the
     :not() guard below doubles as the reason marketing documents keep chip-only chrome. */
  /* z 105: above all page content (which caps at z:1), below the drawer scrim (110) so the bar
     dims with the rest of the page when the nav opens, below the chips (130) that sit on it. */
  body:not(.page-marketing)::before {
    content: ''; position: fixed; top: 0; left: 0; right: 0; height: var(--nav-h); z-index: 105;
    background: var(--bg-1); border-bottom: 1px solid var(--border);
  }
  html { scroll-padding-top: calc(var(--nav-h) + 8px); }
  .tl-hamburger {
    display: block; position: fixed; top: 10px; left: 10px; z-index: 130;
    background: var(--bg-elevated); border: 1px solid var(--border-2); border-radius: var(--r-sm);
    box-shadow: var(--shadow-md);
  }
  html.rail-open .tl-hamburger { left: 250px; }
  .rail-scrim {
    position: fixed; inset: 0; background: var(--scrim-3); z-index: 110;
    opacity: 0; pointer-events: none; transition: opacity .2s var(--ease);
  }
  html.rail-open .rail-scrim { opacity: 1; pointer-events: auto; }
}
@media (min-width: 881px) { .rail-scrim { display: none; } }
@media (prefers-reduced-motion: reduce) {
  body, .tl-nav, .rail-toggle svg { transition: none !important; }
}

.tl-nav-inner { overflow: visible; }

.tl-nav-auth { margin-left: 0; overflow: hidden; }

.tl-nav .region-leds { position: relative; }
.tl-nav .region-led-wrap { position: static; }

html.rail-collapsed .tl-nav .region-led-label { display: none; }
html.rail-collapsed .tl-nav .region-led-wrap { padding: 5px 4px; }
.tl-nav .region-led-tooltip {
  top: 50%; bottom: auto; left: calc(100% + 10px); right: auto;
  transform: translateY(-50%);
  z-index: 200;
}

@media (max-width: 880px) {
  
  .tl-nav .region-led-tooltip {
    top: auto; bottom: calc(100% + 8px); left: 12px; right: auto; transform: none;
  }
  
  /* Was: show the region lamps when the drawer is open. Removed -- at this width their labels
     are display:none, so it was 33px of permanently pinned space showing three dots the reader
     cannot identify, sitting below an upgrade CTA they had to scroll to reach. The lamps are a
     desktop-rail instrument and stay there. */
}


/* ── Pending-referral banner ───────────────────────────────────────────────────
   Injected by site.js as the first child of <main id="main">, so it appears on whichever page a
   referral link happened to point at. Gold rather than blue: this design system already uses gold
   for money (lucent floors, savings figures), so the bar reads as a reward waiting rather than as
   a warning. Deliberately NOT given .reveal — that class starts at opacity 0 and only fades in via
   the IntersectionObserver, which would make an acknowledgement flicker in after paint. */
.tl-ref-note { position: relative; display: flex; flex-wrap: wrap; gap: var(--space-3);
  align-items: center; margin: var(--space-5) 0 0; padding: 14px var(--space-4); padding-right: 44px;
  border: 1px solid var(--gold-dim); background: color-mix(in srgb, var(--gold) 7%, transparent);
  border-radius: var(--r-lg); }
.tl-ref-note.is-bad { border-color: var(--border-2); background: var(--veil-2); }
.tl-ref-msg { margin: 0; flex: 1 1 260px; font-size: 14px; line-height: 1.55; color: var(--text); }
.tl-ref-msg b { color: var(--gold); font-family: var(--font-mono); font-weight: 600; }
.tl-ref-act { flex: 0 0 auto; }
.tl-ref-x { position: absolute; top: 8px; right: 10px; background: none; border: 0;
  color: var(--text-3); font-size: 20px; line-height: 1; cursor: pointer; padding: 4px 8px;
  border-radius: var(--r-xs); }
.tl-ref-x:hover { color: var(--text); }
.tl-ref-x:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }


/* ── Credits-in-hand banner ───────────────────────────────────────────────────
   Injected by site.js as the first thing inside <main id="main"> (below the referral note when one
   is already up), on every page except /rewards, for a signed-in member holding credits that
   RewardEngine would actually honour.

   It borrows the referral note's language deliberately — same gold frame, same tint, same dismiss
   affordance — because it is the same kind of object: something already earned, sitting unclaimed.
   Its own class rather than a second use of .tl-ref-note, because it carries a part that bar does
   not: most of the people who see this cannot afford the cheapest store line yet, so the SHORTFALL
   is the common case and it gets a meter.

   Also deliberately NOT given .reveal, for the reason stated above the referral note: that class
   starts at opacity 0 and only fades in on intersection, which would make this flicker in after
   paint. Nothing here animates, so the only movement on load is the insertion itself. */
.tl-credit-note { position: relative; display: flex; flex-wrap: wrap; gap: var(--space-3);
  align-items: center; margin: var(--space-5) 0 0; padding: 14px var(--space-4); padding-right: 44px;
  border: 1px solid var(--gold-dim); background: color-mix(in srgb, var(--gold) 7%, transparent);
  border-radius: var(--r-lg); }
.tl-credit-body { flex: 1 1 260px; min-width: 0; }
.tl-credit-msg { margin: 0; font-size: 14px; line-height: 1.55; color: var(--text); }
.tl-credit-msg b { color: var(--gold); font-family: var(--font-mono); font-weight: 600; }
/* The meter is aria-hidden and only rendered short of the floor: the balance and the gap are both
   already words in the sentence above it, so this adds a shape, not information. */
.tl-credit-bar { display: block; height: 4px; margin-top: 10px; border-radius: var(--r-pill);
  background: var(--veil-3); overflow: hidden; }
.tl-credit-bar i { display: block; height: 100%; border-radius: inherit; background: var(--gold); }
/* Was a bare inline container, which was fine while it held a single button. The permanent
   opt-out added a second control and the two ran straight into each other. */
.tl-credit-act { flex: 0 0 auto; display: flex; align-items: center; gap: 16px; }
.tl-credit-x { position: absolute; top: 8px; right: 10px; background: none; border: 0;
  color: var(--text-3); font-size: 20px; line-height: 1; cursor: pointer; padding: 4px 8px;
  border-radius: var(--r-xs); }
.tl-credit-x:hover { color: var(--text); }
.tl-credit-x:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }
/* On a phone the row has already wrapped, and a button hugging its own text next to a 44px gutter
   reads as an afterthought. Give it the line. */
@media (max-width: 520px) {
  .tl-credit-note { padding: 12px var(--space-3); padding-right: 38px; gap: var(--space-2); }
  /* Narrow: the CTA takes the width and the opt-out sits under it, still spaced. */
  .tl-credit-act { flex: 1 1 100%; flex-wrap: wrap; gap: 10px; }
  .tl-credit-act .btn { width: 100%; }
  .tl-credit-never { width: 100%; text-align: center; }
}


/* The mobile Premium chip. Mirrors the hamburger across the top bar: the menu button is pinned
   left, this sits right, and between them they are the only two fixed affordances a phone visitor
   has. Created by site.js only for someone who is not already paying, and removed again on
   /upgrade itself.

   Not shown above 880px, where the rail is on screen and its own pill does this job -- two Premium
   entry points in view at once would just look anxious. */
.tl-up-chip { display: none; }
@media (max-width: 880px) {
  .tl-up-chip {
    display: inline-flex; align-items: center; position: fixed; top: 10px; right: 10px; z-index: 130;
    padding: 9px 14px; border-radius: var(--r-sm);
    font-family: var(--font-display); font-size: 13.5px; font-weight: 600; letter-spacing: .01em;
    color: var(--gold); text-decoration: none;
    /* Opaque over scrolling content (R9): the translucent gold wash let page text bleed through
       the chip. Solid elevated ground, gold identity kept in the border and the label. */
    background: var(--bg-elevated);
    border: 1px solid rgba(232,163,61,0.45);
    box-shadow: var(--shadow-md);
    /* No animation: an infinite gold pulse floating over price rows was the loudest thing on
       every phone screen — the chip is chrome, and chrome holds still (R2/R9). */
  }
  .tl-up-chip:active { transform: translateY(1px); }
  .tl-up-chip:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
  /* Out of the way while the drawer is open -- the rail's own pill is on screen then. */
  html.rail-open .tl-up-chip { opacity: 0; pointer-events: none; }
}
/* ============================================================================================
   USER SETTINGS LAYER
   Every switch in the Settings drawer lands here as an attribute on <html>, set before first
   paint by settings-boot.js. Nothing below reads JavaScript state at runtime -- the attribute IS
   the state, so a setting costs one attribute write and the cascade does the rest.

   Attributes, all optional (absent == the site's historic behaviour, so an existing reader who
   never opens Settings sees exactly what they saw before):
     data-theme=dark|light      data-density=compact
     data-perf=1                data-motion=reduced|off
     data-contrast=high         data-fontscale=lg
     data-focus=always          data-sticky=0
     data-colorblind=1
   ============================================================================================ */

:root {
  /* Chart gridlines. Read by charts.js, which previously hardcoded a white wash -- fine on a
     near-black ground, invisible on paper. */
  --chart-grid: rgba(255,255,255,0.035);

  /* Surface veils. These were 31 different hand-written rgba(255,255,255,x) values used for hover
     tints, hairlines and raised panels. On a dark ground a white wash lifts a surface; on a light
     ground it has to become a black wash or the hover state simply vanishes. Three tokens is
     enough resolution -- the alphas they replaced differed by amounts nobody can see. */
  --veil-1: rgba(255,255,255,0.022);
  --veil-2: rgba(255,255,255,0.045);
  --veil-3: rgba(255,255,255,0.065);

  /* Modal backdrops and drop shadows. Dark in both themes: a scrim is meant to read as absence. */
  --scrim-1: rgba(0,0,0,0.28);
  --scrim-2: rgba(0,0,0,0.45);
  --scrim-3: rgba(0,0,0,0.62);

  /* The background layers motion.js paints. Light mode dims the aurora and drops the additive
     canvas entirely -- see the [data-theme="light"] block for why. */
  --aurora-opacity: 1;

  color-scheme: dark;
}

/* --------------------------------------------------------------------------------------------
   LIGHT THEME
   Not an inversion. The dark palette is a near-black ground with cool grey text; inverting it
   gives the washed-out grey-on-grey that makes light modes feel like an afterthought. This is a
   slightly cool paper with near-black text, and every accent is re-picked for contrast against
   white rather than reused from the dark set -- gold at #e8a33d reads at 1.9:1 on white, which
   is unreadable, so the light theme uses a deeper amber for text-bearing gold.
   -------------------------------------------------------------------------------------------- */
html[data-theme="light"] {
  color-scheme: light;

  --bg:          #f4f6fa;
  --bg-1:        #ffffff;
  --bg-2:        #eef1f7;
  --bg-3:        #e3e8f1;
  --bg-glass:    rgba(255,255,255,0.92);
  --bg-card:     #ffffff;
  --bg-elevated: #ffffff;

  --border:      #dde3ec;
  --border-2:    #c3ccdb;
  --border-glow: rgba(165,112,26,0.22);
  --hairline:    rgba(16,24,40,0.09);

  --text:        #111725;
  --text-2:      #4b5768;
  --text-3:      #68748a;

  /* Triplets first, same contract as :root -- keep each one in step with the hex below it. */
  --blue-rgb:       60,91,214;
  --green-rgb:      15,115,80;
  --gold-rgb:       150,101,15;
  --red-rgb:        196,40,45;
  --red-bright-rgb: 168,31,36;
  --purple-rgb:     114,57,189;
  --region-eu-rgb:  60,91,214;
  /* Both a shade deeper than the #0d7a68 / #b83f68 they were. On paper a region tile is the colour
     on a 12% wash of itself, and at the old values that read 4.45:1 and 4.48:1 -- under AA by a
     rounding error, on the page whose job is teaching a newcomer to read the board. */
  --region-jp-rgb:  11,106,92;
  --region-na-rgb:  168,56,94;

  --blue:        #3c5bd6;
  --blue-bright: #2c47bd;
  --blue-hover:  #2a44b4;
  --blue-dim:    rgba(var(--blue-rgb),0.10);
  --blue-glow:   rgba(var(--blue-rgb),0.22);

  /* Darker than the #12805a it was, and everything that touches it moves with it. The old value
     failed WCAG AA three separate ways on paper and none of them were visible from the dark board:
     green text on its own 10% tint measured 4.31:1, green text on the page 4.56:1, and the
     near-black --on-green label on a solid .btn-success only 3.38:1. Darkening fixes the first two
     and would have made the third worse, which is why the label flips with it.

     The rule on paper is the opposite of the rule on the dark board: the positive here is a DARK
     fill, so what sits ON it is white and what blends INTO it is darker still, not lighter. That
     is also why --green-lift and --green-sink stop being theme-independent -- their dark-theme
     values bracket a light --green, and against this one they made .pm-submit a light-to-dark
     ramp that no single label colour could cross.

       green on its own tint  4.31 -> 5.07     white on the solid fill   3.38 -> 5.85
       green on the page      4.56 -> 5.41     white across the gradient 1.93 -> 4.91 worst */
  --green:       #0f7350;
  --green-dim:   rgba(var(--green-rgb),0.10);
  --green-glow:  rgba(var(--green-rgb),0.22);
  --green-lift:  #1e8055;
  --green-sink:  #0a5238;
  --on-green:    #ffffff;
  /* Both swatch labels invert on paper for the same reason: here the fill is the dark thing. */
  --on-pos-tag:  #ffffff;
  --on-neg-tag:  #ffffff;
  /* And .btn-danger's fill stops being translucent. Its 55% maroon exists to darken whatever card
     it sits on, which works on the dark board and does the opposite here: 55% of #7f1d1d over a
     white card is a dusty pink, and the pale label on it measured 1.67:1. Opaque, with a white
     label, the same button reads 10.02:1 and its hover 8.31:1. */
  --red-fill:       #7f1d1d;
  --red-fill-hover: #991b1b;
  --red-pale:       #ffffff;
  /* The hover has to stay dark enough for that white label, and :root's #46c78e is a LIGHT green
     built for a dark board -- white on it is 2.14:1. One step lighter than the resting fill is
     exactly the value the resting fill used to be, which is tidy and reads 4.93:1. */
  --green-hover: #12805a;

  --gold:        #96650f;
  --gold-hover:  #7c520a;
  --gold-dim:    rgba(var(--gold-rgb),0.12);
  --gold-glow:   rgba(var(--gold-rgb),0.20);

  --red:         #c4282d;
  --red-bright:  #a81f24;
  --red-dim:     rgba(var(--red-rgb),0.10);

  --purple:      #7239bd;
  --purple-dim:  rgba(var(--purple-rgb),0.10);

  --region-eu:   #3c5bd6;
  --region-jp:   #0b6a5c;
  --region-na:   #a8385e;

  --chart-grid: rgba(16,24,40,0.07);

  --veil-1: rgba(16,24,40,0.025);
  --veil-2: rgba(16,24,40,0.045);
  --veil-3: rgba(16,24,40,0.075);

  --shadow-sm:  0 1px 2px rgba(16,24,40,0.07);
  --shadow-md:  0 6px 20px rgba(16,24,40,0.09);
  --shadow-lg:  0 14px 44px rgba(16,24,40,0.12);
  --shadow-xl:  0 28px 80px rgba(16,24,40,0.16);

  /* The aurora is three soft colour washes and survives a light ground at reduced strength. */
  --aurora-opacity: 0.55;
}

/* The nav gains .scrolled on scroll and hardcodes a near-black wash there, so in light mode
   the sidebar stayed dark against a light page. It was one of two hardcoded dark surfaces in
   this file; the other styled .tl-mobile-menu, and that element and its rules are now
   gone. */
html[data-theme="light"] .tl-nav.scrolled { background: rgba(255,255,255,0.97); }
/* Additive compositing is invisible-to-muddy on a light ground, so light theme drops the canvas
   and keeps only the (dimmed) aurora. */
html[data-theme="light"] .lux-field { display: none !important; }
.lux-aurora { opacity: var(--aurora-opacity); }

/* --------------------------------------------------------------------------------------------
   PERFORMANCE MODE
   The brief: keep it looking clean, not stripped. So this removes the things that cost GPU time
   continuously (a rAF canvas, infinite keyframes, permanent compositor layers, large blurred
   shadows) and keeps the things that carry meaning (borders, colour, the active-row accent,
   focus rings, and short state transitions so the UI still feels responsive rather than abrupt).
   -------------------------------------------------------------------------------------------- */
html[data-perf="1"] .lux-field,
html[data-perf="1"] .lux-aurora { display: none !important; }

html[data-perf="1"] * {
  /* Permanent compositor layers are the quiet cost: eight elements declaring will-change hold GPU
     memory for animations that mostly are not running. */
  will-change: auto !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

html[data-perf="1"] *,
html[data-perf="1"] *::before,
html[data-perf="1"] *::after {
  /* Kill infinite/ambient animation only. Transitions survive below. */
  animation: none !important;
}
/* ...with the rail's hover gestures put back. The rule above states its own scope -- ambient
   animation, with transitions deliberately spared -- but `*` also catches the rail's one-shot
   hover responses, which are discrete interaction feedback in exactly the category it spares, not
   ambient motion. Left as-is, Performance Mode returned a sidebar in which nothing at all
   acknowledged the cursor, which is not what a reader asking for fewer repaints is asking for.
   Restating the composed shorthand is what makes this ONE rule; carving the rail out of the `*`
   above with :not() would need a complex-selector :not() that Safari only learned in 16.4, and an
   invalid selector there would silently switch ambient animation back on for everyone on an older
   one. */
@media (prefers-reduced-motion: no-preference) {
html[data-perf="1"]:not([data-motion="reduced"]):not([data-motion="off"]) .rail-ico,
html[data-perf="1"]:not([data-motion="reduced"]):not([data-motion="off"]) .rail-ico > *,
html[data-perf="1"]:not([data-motion="reduced"]):not([data-motion="off"]) .rail-toggle svg,
html[data-perf="1"]:not([data-motion="reduced"]):not([data-motion="off"]) .rail-settings svg,
html[data-perf="1"]:not([data-motion="reduced"]):not([data-motion="off"]) .tl-nav-auth a {
  animation-name: var(--ri-anim, none) !important;
  animation-duration: var(--ri-dur, var(--rail-g)) !important;
  animation-timing-function: var(--ri-ease, var(--rail-ease)) !important;
  animation-delay: var(--ri-delay, 0s) !important;
  animation-fill-mode: both !important;
  animation-iteration-count: 1 !important;
}
}

html[data-perf="1"] {
  /* Flatten the shadow scale rather than deleting it: a card still separates from the page, but
     with a 1px edge instead of an 80px blurred spread that repaints on every scroll. */
  --shadow-sm:  0 1px 1px var(--scrim-1);
  --shadow-md:  0 1px 2px var(--scrim-1);
  --shadow-lg:  0 2px 4px var(--scrim-1);
  --shadow-xl:  0 2px 6px var(--scrim-2);
  --t-fast: 0.10s; --t-med: 0.12s; --t-slow: 0.14s;
}

html[data-perf="1"] .tl-nav-links a.nav-upgrade,
html[data-perf="1"] .rail-foot a.nav-upgrade { background: var(--gold-dim); }

/* 124 of the site's 161 box-shadows resolve through the shadow and glow custom properties and
   are already
   flattened by the block above. The button glows are hand-written values, so the tokens cannot
   reach them -- and they are the worst of the set: 26-32px blurred spreads that repaint on hover,
   paired with a translateY that promotes the button to its own layer. Gradients, colour and border
   all stay, so a button still reads as a button; it just stops glowing. */
html[data-perf="1"] .btn-primary,
html[data-perf="1"] .btn-premium,
html[data-perf="1"] .btn-success,
html[data-perf="1"] .tl-up-chip { box-shadow: none; }
html[data-perf="1"] .btn-primary:hover,
html[data-perf="1"] .btn-premium:hover,
html[data-perf="1"] .btn-success:hover,
html[data-perf="1"] .btn-success:active,
html[data-perf="1"] .btn:hover { box-shadow: none; transform: none; }

/* --------------------------------------------------------------------------------------------
   MOTION
   data-motion=reduced keeps meaningful state transitions and stops ambient movement.
   data-motion=off stops everything, including transitions.
   Both are also driven by the OS setting when the reader leaves Motion on "Auto".
   -------------------------------------------------------------------------------------------- */
/* Background effects, switched off independently of Performance Mode. CSS removes the pixels
   without a reload; motion.js's frame() loop separately parks itself via backgroundOn(), so the
   canvas stops being DRAWN as well as being seen. data-motion=reduced keeps the static aurora
   (nothing about it moves) but drops the animated field. */
html[data-bg="off"] .lux-field,
html[data-bg="off"] .lux-aurora,
html[data-motion="off"] .lux-field,
html[data-motion="off"] .lux-aurora { display: none !important; }
html[data-motion="reduced"] .lux-field { display: none !important; }

html[data-motion="reduced"] *,
html[data-motion="reduced"] *::before,
html[data-motion="reduced"] *::after { animation: none !important; }

html[data-motion="off"] *,
html[data-motion="off"] *::before,
html[data-motion="off"] *::after {
  animation: none !important;
  transition: none !important;
  scroll-behavior: auto !important;
}

/* --------------------------------------------------------------------------------------------
   DENSITY
   Compact buys roughly a third more rows per screen on the data tables, which is the whole point
   of the mode -- it is not applied to prose pages, where tighter leading just hurts.
   -------------------------------------------------------------------------------------------- */
html[data-density="compact"] table th,
html[data-density="compact"] table td { padding-top: 6px; padding-bottom: 6px; font-size: 12.5px; }
html[data-density="compact"] .section-card { padding: 14px 16px; }
html[data-density="compact"] .stat-card { padding: 12px 14px; }
html[data-density="compact"] .tl-nav-links a { padding-top: 7px; padding-bottom: 7px; }
/* /potentials and Region Gaps draw their rows as a CSS grid, not a table, so the table rule above
   never reached the site's second-biggest board -- compact did nothing there. Height is set by
   min-height on the row, not padding, so that is what has to move. */
html[data-density="compact"] .pl-row { min-height: 32px; padding: 0 8px; }
html[data-density="compact"] .pl-row,
html[data-density="compact"] .pl-ghead { font-size: 12.5px; }
/* min-height alone moved the row by one pixel: the height is set by the 26px item icon and the
   gap beside it, not by the row box. Compact has to reach those or it is a setting that does
   nothing on the board it matters most on. */
html[data-density="compact"] .pl-ico { width: 20px; height: 20px; flex-basis: 20px; }
html[data-density="compact"] .pl-nm { gap: 8px; }
/* And the row's floor is actually the 40px action button, not the icon -- shrinking the icon alone
   left the row a pixel shorter. 32px keeps it comfortably above the 24px minimum target size for a
   pointer, which is the line that matters: compact must not make a control hard to hit. */
html[data-density="compact"] .pl-btn { min-height: 32px; padding: 0 12px; }
/* The real floor is the 44px favourite star, which is the minimum comfortable TOUCH target and
   must stay 44px on a touch device -- trading an accessibility property for four rows a screen is
   not a trade worth making. On a fine pointer the requirement is far smaller, so compact reclaims
   the height only where a mouse is driving. */
@media (pointer: fine) {
  html[data-density="compact"] .pl-star { width: 30px; height: 30px; min-width: 30px; }
}

/* Shown only when a cross-region board has fewer than two regions to compare. Styled as a calm
   explanation rather than a warning: the reader did this on purpose, they just may not have
   connected the setting to the empty board. */
.tl-xregion-note {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  margin: 0 0 var(--space-4); padding: 13px 16px;
  border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--bg-card);
}
.tl-xregion-note p { margin: 0; flex: 1 1 260px; font-size: 13px; color: var(--text-2); }

/* Regions hidden by preference. One quiet line beside the region control that already exists on
   the page -- the place a reader looks when a region they expected is missing -- rather than a
   banner at the top of every board. It is also the contextual way into Settings: the panel stays
   the source of truth, this just points at it. */
.tl-region-note {
  display: inline-flex; align-items: center; gap: 8px; margin-left: 10px;
  font-size: 11.5px; color: var(--text-3); white-space: nowrap;
}
.tl-region-note button {
  background: none; border: 0; padding: 0; cursor: pointer; font: inherit; font-size: 11.5px;
  color: var(--blue); text-decoration: underline; text-underline-offset: 2px;
}
.tl-region-note button:hover { color: var(--blue-bright); }
@media (max-width: 560px) {
  .tl-region-note { margin-left: 0; margin-top: 8px; width: 100%; }
}


/* --------------------------------------------------------------------------------------------
   ACCESSIBILITY
   -------------------------------------------------------------------------------------------- */
html[data-fontscale="lg"] { font-size: 112.5%; }

html[data-contrast="high"] {
  --text-2: var(--text);
  --border:   var(--border-2);
  --hairline: var(--border-2);
}
html[data-contrast="high"][data-theme="light"] { --text-3: #3b4658; }
html[data-contrast="high"]:not([data-theme="light"]) { --text-3: #c3cddd; }
html[data-contrast="high"] .tl-nav-links a.active { outline: 1px solid var(--blue); }

/* --------------------------------------------------------------------------------------------
   COLORBLIND-FRIENDLY MODE
   Asked for by a reader with deuteranopia, the common form: red and green land on nearly the same
   hue, and what is left to tell them apart is lightness, which this palette had deliberately
   matched so a gain and a loss would weigh the same on a board.

   Measured on the shipped palette, in CIEDE2000 after a Vienot dichromat projection (20 is
   comfortably distinct, 10 is the floor of usable):

     positive #3fb984 vs negative #e5484d      68.7 normal ->  13.6 deuteranopic
     region AS #34d0b4 vs region US #e5739a    66.8 normal ->   8.5 deuteranopic

   The second line is the chart in the report: two price series a deuteranope cannot separate at
   all. This block re-points the SAME tokens the rest of the sheet already uses, so it reaches
   every board, badge, pill, meter and chart without a second rule anywhere:

     positive -> azure    negative -> orange     region AS -> amber   region US -> pale steel

     positive #4cc9f0 vs negative #ff8a4c       54.5 normal ->  54.4 deuteranopic
     region AS #f0b429 vs region US #dfe6f5     36.3 normal ->  40.1 deuteranopic
     region EU keeps its blue; all three regions now sit >= 30 apart for every dichromat type.

   Blue against orange is the pairing that survives all three deficiencies, because it rides the
   one axis every dichromat keeps. Region EU is untouched: it was already the safe end.

   Two attribute selectors, not one, and both carry :not()/[data-theme] so they outrank the theme
   block that precedes them -- otherwise the dark set would win on a light page, where azure on
   paper is 1.8:1. Every value below clears 4.5:1 against its own ground. --gold is NOT re-pointed:
   it is the Premium accent, brand rather than state, and moving it would restyle the site instead
   of making it readable.

   Where a palette alone is not enough, a second channel is added rather than a second colour: the
   region lamps below get four silhouettes, and charts.js gives the AS and US series a dash pattern
   the EU series does not have. Most figures on this site already print their own sign.

   WARNING STAYS GOLD, and not for want of trying. A dichromat has one usable hue axis, and this
   palette already spends it: positive azure and region EU at the blue end, negative orange and
   region AS at the yellow end. A search of every colour that clears 4.5:1 on this ground found
   nothing left for a fifth category -- the best warm candidate sits 2.0 from the negative, and the
   best overall (#9696d2) only scores by escaping into the blue end, where it lands 7.8 from the
   positive and 7.3 from --blue-bright. So gold against the negative is 2.0 deuteranopic and stays
   that way. What carries warning instead is not colour: .is-lagging is a rounded square where
   .is-down is a hollow ring, .rl-pill prints the state as a word, and .da-note.warn is a sentence.
   If you are tempted to add a warning hue, measure it against BOTH ends first.
   -------------------------------------------------------------------------------------------- */
html[data-colorblind="1"]:not([data-theme="light"]) {
  --green-rgb:      76,201,240;
  --green:          #4cc9f0;
  --red-rgb:        255,138,76;
  --red:            #ff8a4c;
  --red-bright-rgb: 255,164,115;
  --red-bright:     #ffa473;
  --red-pale:       #ffd0b0;
  --green-hover:    #6fd4f5;

  --region-jp-rgb:  240,180,41;
  --region-jp:      #f0b429;
  /* Pale steel rather than the near-white first tried: at #dfe6f5 the US marker measured 5.1
     CIEDE2000 from --text, so a US dot read as a white dot -- a marker with no colour assigned
     rather than one of three. This sits 12.9 from body text and still 23.4 from EU blue and 43.0
     from AS amber, so all three regions stay comfortably apart for every dichromat type. */
  --region-na-rgb:  184,196,217;
  --region-na:      #b8c4d9;


}

html[data-colorblind="1"][data-theme="light"] {
  --green-rgb:      3,105,161;
  --green:          #0369a1;
  --red-rgb:        180,71,12;
  --red:            #b4470c;
  --red-bright-rgb: 143,57,8;
  --red-bright:     #8f3908;
  /* --red-pale is inherited from the light theme block, which already made it white: on paper the
     danger fill is opaque and dark in this mode too, for the same reason. */
  --red-fill:       #8c4c2a;
  --red-fill-hover: #a85b32;
  --green-hover:    #0472ad;

  --region-jp-rgb:  138,101,8;
  --region-jp:      #8a6508;
  --region-na-rgb:  47,58,77;
  --region-na:      #2f3a4d;

  /* The positive on paper is dark in this mode too (#0369a1), so the whole filled-control set
     inverts exactly as it does in Standard light: white label, and gradient stops that sit BELOW
     the fill rather than above it. --on-pos-tag is inherited from the light theme block, which
     already made it white for the same reason. */
  --on-green:       #ffffff;
  --green-lift:     #1d7ea8;
  --green-sink:     #02486e;
}

/* Theme-independent, because what they feed is: the filled positive controls paint one gradient on
   paper and on the dark board alike, and always did. #04202b keeps the label near-black on azure
   the way #052318 was near-black on green. */
html[data-colorblind="1"] {
  --green-lift: #7cd9f6;
  --green-sink: #2a9dc4;
  --on-green:   #04202b;
  /* The same two fills, darkened from the orange negative in the same proportion the maroon
     versions were darkened from the red one. */
  --red-fill:       rgba(140,76,42,0.55);
  --red-fill-hover: rgba(168,91,50,0.8);
}

/* The region lamps are the one place a palette swap alone makes something WORSE. There are four
   states, and two of them -- lagging (--gold) and down (--red) -- both land on yellow for a
   deuteranope: 14.8 CIEDE2000 apart today, 2.0 once the negative moves to orange. So this mode
   gives the four lamps four SILHOUETTES and stops relying on hue at 7px, where nobody judges hue
   well anyway:

     live      filled disc      lagging   rounded square      down   hollow ring      unknown  disc

   The other channel that already separated them -- 1.8s pulse against 1s -- is switched off by
   prefers-reduced-motion, so it could never be the answer on its own. */
html[data-colorblind="1"] .is-lagging .region-led { border-radius: 1.5px; }
html[data-colorblind="1"] .is-down .region-led,
html[data-colorblind="1"] .lp-pulse-led.bad {
  background: transparent;
  box-shadow: inset 0 0 0 2px var(--red);
}

/* Focus rings are visible on :focus-visible already. "Always" also shows them for mouse users,
   which some people want and most do not -- hence a setting rather than a global change. */
html[data-focus="always"] :focus { outline: 2px solid var(--blue) !important; outline-offset: 2px !important; }

/* Sticky headers are worth a switch: they help on a long board and hurt on a short viewport,
   where a pinned header eats rows that are already scarce. */
html[data-sticky="0"] thead th { position: static !important; }

/* ============================================================================================
   SETTINGS DRAWER
   A right-hand drawer rather than a centred modal. Settings here are mostly about how the boards
   look, and a drawer leaves the board visible behind it -- flipping Density or Theme shows its
   own result immediately, which a modal covering the page would hide.
   Built from the tokens the rest of the site uses, so it inherits both themes for free.
   ============================================================================================ */

.set-scrim {
  position: fixed; inset: 0; z-index: 2400; background: var(--scrim-2);
  opacity: 0; pointer-events: none; transition: opacity var(--t-med) var(--ease);
}
.set-scrim.open { opacity: 1; pointer-events: auto; }

.set-drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 2401;
  width: min(430px, 100vw);
  display: flex; flex-direction: column;
  background: var(--bg-1); border-left: 1px solid var(--border);
  box-shadow: var(--shadow-xl);
  transform: translateX(100%);
  transition: transform var(--t-med) var(--ease-out);
}
.set-drawer.open { transform: translateX(0); }

.set-head {
  flex-shrink: 0; display: flex; align-items: center; gap: 10px;
  padding: 16px 18px 12px; border-bottom: 1px solid var(--border);
}
.set-head h2 {
  font-family: var(--font-display); font-size: 17px; font-weight: 700;
  letter-spacing: -0.01em; margin: 0; color: var(--text); flex: 1;
}
.set-close {
  flex-shrink: 0; width: 30px; height: 30px; display: flex; align-items: center;
  justify-content: center; border-radius: var(--r-sm);
  background: none; border: 1px solid transparent; color: var(--text-3); cursor: pointer;
}
.set-close:hover { background: var(--veil-2); color: var(--text); border-color: var(--border); }
.set-close svg { width: 16px; height: 16px; }

.set-search-wrap { flex-shrink: 0; padding: 12px 18px; border-bottom: 1px solid var(--border); }
.set-search {
  width: 100%; box-sizing: border-box; padding: 8px 11px;
  background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--r-sm);
  color: var(--text); font: inherit; font-size: 13px;
}
.set-search::placeholder { color: var(--text-3); }
.set-search:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-dim); }

.set-body { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 4px 0 18px; }

.set-section { padding: 14px 18px 6px; }
.set-section + .set-section { border-top: 1px solid var(--hairline); }
.set-section-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 4px; }
.set-section-head h3 {
  flex: 1; margin: 0;
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .11em; color: var(--text-3);
}
/* Only rendered when the section holds a non-default value, so its presence is also the
   "you have changed something here" indicator. */
.set-reset {
  background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
  font-size: 11px; color: var(--text-3); text-decoration: underline;
  text-underline-offset: 2px;
}
.set-reset:hover { color: var(--blue); }

.set-row { padding: 11px 0; border-bottom: 1px solid var(--hairline); }
.set-row:last-child { border-bottom: 0; }
.set-row-top { display: flex; align-items: center; gap: 14px; }
.set-label { flex: 1; min-width: 0; font-size: 13.5px; color: var(--text); font-weight: 500;
  display: flex; align-items: center; gap: 7px; }
/* Marks a setting that is no longer at its default. Small and unlabelled by design: it is an
   orientation aid, not a warning, and it carries an aria-label for anyone who cannot see it. */
.set-mod { width: 6px; height: 6px; border-radius: 50%; background: var(--blue); flex-shrink: 0; }

.set-desc { margin: 3px 0 0; font-size: 11.5px; line-height: 1.45; color: var(--text-3); max-width: 44ch; }
.set-control { flex-shrink: 0; }
/* Wide controls (4 options, or the region chips) drop below the label instead of squeezing it. */
.set-row.stack .set-row-top { display: block; }
.set-row.stack .set-control { margin-top: 9px; }

/* -- segmented ------------------------------------------------------------------------------- */
.set-seg { display: inline-flex; background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 2px; }
.set-seg button {
  border: 0; background: none; cursor: pointer; font: inherit; font-size: 12px;
  padding: 5px 11px; border-radius: 6px; color: var(--text-3); white-space: nowrap;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.set-seg button:hover { color: var(--text); }
.set-seg button[aria-pressed="true"] { background: var(--blue-dim); color: var(--blue-bright); font-weight: 600; }
.set-row.stack .set-seg { display: flex; width: 100%; }
.set-row.stack .set-seg button { flex: 1; text-align: center; }

/* -- switch ---------------------------------------------------------------------------------- */
/* -- select ---------------------------------------------------------------------------------- */
.set-select {
  padding: 6px 24px 6px 9px; border-radius: var(--r-sm); font: inherit; font-size: 12.5px;
  background-color: var(--bg-2); border: 1px solid var(--border); color: var(--text); cursor: pointer;
  max-width: 190px; appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%237c8aa3' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 8px center; background-size: 9px;
}
.set-select:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-dim); }

/* The track is drawn by ::before rather than on the button box itself, and that is load-bearing:
   this site applies min-height:var(--tap-min) to every button under (max-width:768px) or
   (pointer:coarse), which is the right rule -- a 22px switch is not a fair tap target on a phone.
   Painting the box directly meant the switch became 38x44 there and the pill radius rendered it as
   a CIRCLE. Now the button stays the hit area, at 44px where it should be, and the pill inside it
   is 38x22 and vertically centred at every width. */
.set-switch {
  position: relative; width: 38px; height: 22px; flex-shrink: 0;
  border: 0; background: none; padding: 0; cursor: pointer;
}
.set-switch::before {
  content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 38px; height: 22px; box-sizing: border-box;
  border-radius: var(--r-pill); border: 1px solid var(--border-2); background: var(--bg-3);
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.set-switch::after {
  content: ''; position: absolute; left: 3px; top: 50%; margin-top: -8px;
  width: 16px; height: 16px; border-radius: 50%; background: var(--text-3);
  transition: transform var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.set-switch[aria-checked="true"]::before { background: var(--blue-dim); border-color: var(--blue); }
.set-switch[aria-checked="true"]::after { transform: translateX(15px); background: var(--blue); }
.set-switch:focus-visible::before { outline: 2px solid var(--blue); outline-offset: 2px; }

/* -- region chips ---------------------------------------------------------------------------- */
.set-chips { display: flex; gap: 8px; }
.set-chip {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 7px;
  padding: 8px 6px; border-radius: var(--r-sm); cursor: pointer; font: inherit; font-size: 12.5px;
  background: var(--bg-2); border: 1px solid var(--border); color: var(--text-3);
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.set-chip:hover { color: var(--text); }
.set-chip .dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; opacity: .45; }
.set-chip[aria-pressed="true"] { color: var(--text); border-color: var(--border-2); background: var(--bg-3); }
.set-chip[aria-pressed="true"] .dot { opacity: 1; }
.set-chip[data-region="EU"][aria-pressed="true"] .dot { background: var(--region-eu); }
.set-chip[data-region="JP"][aria-pressed="true"] .dot { background: var(--region-jp); }
.set-chip[data-region="US"][aria-pressed="true"] .dot { background: var(--region-na); }
/* The last visible region cannot be switched off -- an empty board with no explanation reads as
   an outage. The chip stays interactive for screen readers but says why it will not move. */
.set-chip[data-locked="1"] { cursor: not-allowed; opacity: .75; }

.set-foot {
  flex-shrink: 0; display: flex; align-items: center; gap: 12px;
  padding: 12px 18px; border-top: 1px solid var(--border); background: var(--bg-2);
}
.set-foot-note { flex: 1; font-size: 11px; color: var(--text-3); }
.set-reset-all {
  padding: 7px 12px; border-radius: var(--r-sm); cursor: pointer; font: inherit; font-size: 12px;
  background: var(--bg-1); border: 1px solid var(--border-2); color: var(--text-2);
}
.set-reset-all:hover { border-color: var(--red); color: var(--red); }

/* The Performance Mode suggestion. Bottom-anchored and non-modal on purpose: it is unsolicited,
   so it must never sit between the reader and what they came for. It appears at most once ever. */
/* luxToast is built in JS and used to hard-code left:50% inline, where no stylesheet could
   reach it. It carries a class now and is positioned here with everything else. */
.lux-toast { left: calc(50% + var(--rail-w, 0px) / 2); }

/* Centred fixed layers have to centre on the CONTENT, not the viewport. The rail is a fixed
   212px column, so left:50% put the toast and this hint 106px left of where the page they
   belong to actually sits. --rail-w follows the collapse state on its own, and drops to 0
   below 880px where the rail is off-canvas. */
.perf-hint {
  position: fixed; left: calc(50% + var(--rail-w, 0px) / 2); bottom: 20px; z-index: 2300;
  transform: translate(-50%, 20px); opacity: 0;
  transition: opacity var(--t-med) var(--ease), transform var(--t-med) var(--ease-out);
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  max-width: min(620px, calc(100vw - 32px));
  padding: 13px 16px; border-radius: var(--r-lg);
  background: var(--bg-elevated); border: 1px solid var(--border-2);
  box-shadow: var(--shadow-lg);
}
.perf-hint.on { opacity: 1; transform: translate(-50%, 0); }
.perf-hint-msg { margin: 0; flex: 1 1 260px; font-size: 12.5px; line-height: 1.5; color: var(--text-2); }
.perf-hint-act { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.perf-hint-no {
  background: none; border: 0; padding: 6px 2px; cursor: pointer; font: inherit; font-size: 12px;
  color: var(--text-3); text-decoration: underline; text-underline-offset: 2px; white-space: nowrap;
}
.perf-hint-no:hover { color: var(--text-2); }
/* It suggests turning effects off; it must not be the one thing still animating when they are. */
html[data-perf="1"] .perf-hint, html[data-motion="off"] .perf-hint { transition: none; }

.set-empty { padding: 26px 18px; text-align: center; font-size: 12.5px; color: var(--text-3); }

/* The credits bar's permanent opt-out. Deliberately quieter than the CTA beside it: it sits in
   the same action group so it is findable, but a reader looking for the offer should land on the
   offer first. */
.tl-credit-never {
  background: none; border: 0; padding: 6px 2px; cursor: pointer; font: inherit; font-size: 12px;
  color: var(--text-3); text-decoration: underline; text-underline-offset: 2px; white-space: nowrap;
}
.tl-credit-never:hover { color: var(--text-2); }

/* The rail entry. Deliberately the same component as Collapse directly beneath it -- it is a
   utility control, not a destination, and styling it like a nav link would put a 14th entry in a
   rail we just spent two commits shortening. */
.rail-settings {
  flex-shrink: 0; display: flex; align-items: center; gap: 12px;
  margin: 0 10px 2px; padding: 9px 10px; border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer; font: inherit; font-size: 12.5px;
  color: var(--text-3); text-align: left; width: calc(100% - 20px);
}
.rail-settings:hover { color: var(--text); background: var(--veil-2); }
/* The reference the rest of the rail was tuned against, and the one glyph on --rail-mo-turn:
   40 degrees is six times the travel of anything else in the system, and the shared 0.26s over
   that arc reads as a flick rather than a turn. The rotation itself is unchanged; the enter curve
   is now a decelerate, so the gear starts moving the instant the cursor lands rather than easing
   in from nothing, which is the only part of it that was not already smooth. */
@keyframes riGear {
  0%   { transform: rotate(0deg); }
  72%  { transform: rotate(calc(var(--rail-motion) * 126deg)); }
  100% { transform: var(--ri-pose); }
}
/* 112 degrees, and the odd number is the point. This gear has EIGHT lobes, so it repeats every
   45 -- at the 90 it was first given, the settled pose was pixel-identical to rest and the whole
   turn read as a glyph that got brighter. 112 lands halfway between two teeth, which is the
   furthest a gear can ever look from where it started. */
.rail-settings svg { width: 17px; height: 17px; flex-shrink: 0;
  --ri-pose: rotate(calc(var(--rail-motion) * 112deg));
  transition: transform var(--rail-mo-turn) var(--rail-ease-out); }
.rail-settings:hover svg, .rail-settings:focus-visible svg {
  --ri-anim: riGear; --ri-ease: var(--rail-ease-out); transform: var(--ri-pose);
}
html.rail-collapsed .rail-settings { justify-content: center; padding: 9px 0; margin: 0 0 2px; width: 100%; }

@media (max-width: 560px) {
  .set-drawer { width: 100vw; border-left: 0; }
  .set-row-top { gap: 10px; }
  .set-desc { max-width: none; }
}

/* ══ USAGE-MEASUREMENT CONSENT ══════════════════════════════════════════════════════════════════
   A SMALL CARD IN A CORNER, not a full-width sheet and not a centred modal. The first build spanned
   the whole content column and took a 110px band off the bottom of every page; nobody arrives here
   to answer a question about measurement, so it takes a corner and leaves the rest of the screen
   doing the job the reader actually came for. The page stays scrollable and readable throughout --
   this is never a wall, and there is no overlay behind it.

   BOTH BUTTONS ARE THE SAME BUTTON. Identical box, weight, size and colour; the only difference is
   the word. Every other surface on this site uses .btn-primary's gradient to mark the intended
   action, which is exactly why that class is not used here: a Decline that reads as the quieter
   option is what makes consent invalid, and a number we talked somebody into is worse than a
   smaller one we can trust. */
.ck {
  /* UNDER EVERY OVERLAY, NOT OVER THEM. At 3000 this outranked the whole stack -- .modal-overlay
     (1000), the settings drawer and its scrim (2401/2400), the toast (2300) and the phone nav
     drawer (120) -- and both it and the alert composer are direct children of body, so nothing
     intervened. Measured: at 375x812 the card covered Cancel and Create Alert outright, and it
     covered the drawer's sign-in cluster too; the composer's buttons were simply dead until the
     card was answered. 1920 was clear only because the modal starts right of the card's edge,
     which is luck, not design. 100 keeps it above page content (.page-wrap is 1) and below
     everything that is meant to be in front of it, so an open dialog just hides it and the
     question is answered afterwards. */
  position: fixed; z-index: 100;
  left: calc(var(--rail-w) + var(--gutter));
  bottom: var(--space-4);
  width: min(420px, calc(100vw - var(--rail-w) - var(--gutter) * 2));
  padding: var(--space-4);
  background: var(--bg-card); border: 1px solid var(--border-2);
  border-radius: var(--r-lg); box-shadow: var(--shadow-lg);
  transform: translateY(calc(100% + var(--space-4)));
  transition: transform 340ms var(--ease-out);
}
.ck.is-in { transform: translateY(0); }
.ck-title {
  margin: 0 0 5px; font-size: 14px; font-weight: 700; letter-spacing: -0.01em; color: var(--text);
}
.ck-body { margin: 0; font-size: 12.5px; line-height: 1.55; color: var(--text-2); }
/* The 24px floor for this link is enforced in the tap-target list further up, NOT here. It was
   missed on the first pass and the sweep found it 368 times -- once on every route x state x width,
   because the card renders on all of them. That list is this codebase's most repeatedly-forgotten
   contract: a new interactive class is invisible to it until somebody adds the name by hand. */
.ck-more { margin: 8px 0 0; font-size: 12px; }
.ck-more a { color: var(--text-3); text-decoration: underline; text-underline-offset: 2px; }
.ck-more a:hover { color: var(--text); }
.ck-acts { display: flex; gap: var(--space-2); margin-top: var(--space-4); }
.ck-btn {
  flex: 1 1 0;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap-min); padding: 10px 16px;
  font-family: inherit; font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em;
  color: var(--text); background: var(--bg-3);
  border: 1px solid var(--border-2); border-radius: var(--r-md);
  cursor: pointer;
  transition: background var(--t-med) var(--ease), border-color var(--t-med) var(--ease);
}
.ck-btn:hover { background: var(--bg-elevated); border-color: var(--text-3); }
.ck-btn:active { transform: translateY(1px); }

/* Below the rail breakpoint the rail is off-canvas, so the card measures from the viewport edge. */
@media (max-width: 880px) {
  .ck { left: var(--gutter); width: calc(100vw - var(--gutter) * 2); }
}
/* On a phone a floating card wastes the two side margins it cannot use, so it becomes a sheet
   across the bottom -- still small, and still one tap per answer with neither button nearer the
   thumb than the other. */
@media (max-width: 560px) {
  .ck {
    left: 0; bottom: 0; width: 100%;
    border-radius: var(--r-lg) var(--r-lg) 0 0; border-left: 0; border-right: 0; border-bottom: 0;
    padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
    transform: translateY(101%);
  }
}

/* No slide for anyone who asked motion to stop: it appears where it belongs and stays there. */
@media (prefers-reduced-motion: reduce) {
  .ck { transition: none; transform: translateY(0); }
}

/* A link-styled button in a column of links. Inherits the column's own anchor treatment so the
   withdraw control reads as part of the list rather than a stray form element. */
.tl-footer-link-btn {
  background: none; border: 0; padding: 0; margin-bottom: 10px;
  font-family: inherit; font-size: 13px; color: var(--text-2); text-align: left; cursor: pointer;
  width: fit-content;
  transition: color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.tl-footer-link-btn:hover { color: var(--text); transform: translateX(3px); }

/* The phone drawer is always full width, even when the desktop rail preference is collapsed. */
@media (max-width: 880px) {
  html.rail-open .tl-nav-auth { align-items: stretch; }
  html.rail-open .tl-nav-auth .auth-identity {
    flex-direction: row; gap: 6px; padding: 7px 8px; background: var(--veil-1);
  }
  html.rail-open .tl-nav-auth .auth-me { flex: 1 1 auto; }
  html.rail-open .tl-nav-auth .auth-copy { display: flex; }
  html.rail-open .tl-nav-auth .auth-name,
  html.rail-open .tl-nav-auth .tier { display: inline-block; }
  html.rail-open .tl-nav-auth .logout-btn { width: 44px; height: 44px; }
  html.rail-open .tl-nav-auth .admin-btn {
    width: 100%; min-height: 44px; padding: 8px 10px;
  }
  html.rail-open .tl-nav-auth .auth-action-label { display: inline; }
}
