/* === _fonts.css === */
/* apps/web/static/css/_fonts.css
   Self-hosted per Phase 35 GDPR posture — no third-party CDN. WOFF2
   format. font-display: swap so first paint isn't blocked. */

@font-face {
  font-family: 'Satoshi';
  src: url('/static/fonts/satoshi-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Satoshi';
  src: url('/static/fonts/satoshi-500.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Satoshi';
  src: url('/static/fonts/satoshi-700.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'JetBrains Mono';
  src: url('/static/fonts/jetbrains-mono-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('/static/fonts/jetbrains-mono-500.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}


/* === design-system.css === */
/* =====================================================
   Rewards Design System — Foundations
   Maps to Flask/Tailwind/Flowbite stack already in base.html
   - Primary scale = Tailwind 'primary' (green) already wired
   - Adds: surface scale, status colors, dark/light parity, RTL
   ===================================================== */

:root {
  color-scheme: light;

  /* primary — Last Z hazmat lime */
  --p-50:#F7FEE7; --p-100:#ECFCCB; --p-200:#D9F99D; --p-300:#BEF264;
  --p-400:#A3E635; --p-500:#84CC16; --p-600:#65A30D; --p-700:#4D7C0F;
  --p-800:#3F6212; --p-900:#365314; --p-950:#1A2E05;

  /* neutral surface scale — light theme */
  --sf-canvas:#F7F8FA;          /* page bg */
  --sf-surface:#FFFFFF;          /* cards */
  --sf-surface-2:#F1F3F6;        /* inset / table head */
  --sf-border:#E4E7EC;
  --sf-border-strong:#CDD2DA;
  --sf-text:#0F172A;             /* slate-900 */
  --sf-text-2:#475569;           /* slate-600 */
  --sf-text-3:#94A3B8;           /* slate-400 */
  --sf-text-on-primary:#FFFFFF;

  /* status */
  --st-success:#16A34A; --st-success-bg:#DCFCE7; --st-success-border:#86EFAC;
  --st-danger:#DC2626;  --st-danger-bg:#FEE2E2;  --st-danger-border:#FCA5A5;
  --st-warn:#D97706;    --st-warn-bg:#FEF3C7;    --st-warn-border:#FCD34D;
  --st-info:#2563EB;    --st-info-bg:#DBEAFE;    --st-info-border:#93C5FD;
  --st-pending:#7C3AED; --st-pending-bg:#EDE9FE; --st-pending-border:#C4B5FD;

  /* type */
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* scale */
  --r-sm: 6px; --r-md: 10px; --r-lg: 14px; --r-xl: 20px; --r-pill: 999px;
  --sh-sm: 0 1px 2px rgba(15,23,42,0.06);
  --sh-md: 0 1px 3px rgba(15,23,42,0.08), 0 1px 2px rgba(15,23,42,0.04);
  --sh-lg: 0 10px 30px -10px rgba(15,23,42,0.18), 0 2px 6px rgba(15,23,42,0.06);
  --sh-focus: 0 0 0 3px rgba(34,197,94,0.30);
}

.dark {
  --sf-canvas:#0B1220;
  --sf-surface:#111827;
  --sf-surface-2:#1F2937;
  --sf-border:#1F2937;
  --sf-border-strong:#374151;
  --sf-text:#F1F5F9;
  --sf-text-2:#CBD5E1;
  --sf-text-3:#94A3B8;

  --st-success-bg:rgba(22,163,74,0.15);  --st-success-border:rgba(22,163,74,0.4);
  --st-danger-bg:rgba(220,38,38,0.15);   --st-danger-border:rgba(220,38,38,0.4);
  --st-warn-bg:rgba(217,119,6,0.15);     --st-warn-border:rgba(217,119,6,0.4);
  --st-info-bg:rgba(37,99,235,0.15);     --st-info-border:rgba(37,99,235,0.4);
  --st-pending-bg:rgba(124,58,237,0.15); --st-pending-border:rgba(124,58,237,0.4);

  /* Foreground colours: the default :root values (#DC2626 etc.) read
     OK at AA but are visually muted against the deep dark canvas.
     Lift each status colour by ~2 shades so the pill / button text
     stays vivid in dark mode without losing semantic meaning. */
  --st-success:#4ADE80;
  --st-danger:#F87171;
  --st-warn:#FBBF24;
  --st-info:#60A5FA;
}

/* Dark mode override — gunmetal / concrete vibe */
.dark {
  color-scheme: dark;
  --sf-canvas:#0A0E0A;
  --sf-surface:#121712;
  --sf-surface-2:#1B221B;
  --sf-border:#1F2A1F;
  --sf-border-strong:#374237;
}
:root { --sh-focus: 0 0 0 3px rgba(132,204,22,0.30); }

/* Native <dialog>'s UA stylesheet resets `color: CanvasText` and
   `background-color: Canvas`, which Chromium paints as black-on-white
   when `color-scheme` isn't declared on the host doc — making every
   modal that doesn't set its own `color` unreadable in dark mode
   (titles, table cells, <pre> blocks).  Reset both to design tokens
   so any text inside a dialog inherits the active theme by default.
   Per-dialog inline overrides (background: transparent on the
   dashboard's card-wrapped dialogs) continue to win via specificity. */
dialog {
  color: var(--sf-text);
  background: var(--sf-surface);
}

/* ─── Base reset for the doc itself ─── */
.ds-page {
  font-family: var(--font-sans);
  background: var(--sf-canvas);
  color: var(--sf-text);
  -webkit-font-smoothing: antialiased;
  /* Sticky-footer scaffolding: page is a flex column at least as tall
     as the viewport. The site footer sits at the end of the column;
     `margin-block-start: auto` on `.lz-site-footer` (set in lastz.css)
     pushes it to the bottom when content is shorter than the viewport.
     Without this, the footer floats up against short pages (e.g.
     /admin/cache, /admin/page-alerts) and leaves a blank gap below. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* RTL helpers — most spacing flips naturally via logical properties */
[dir="rtl"] .ds-flip { transform: scaleX(-1); }

/* ─── Type ─── */
.t-display { font-size: 40px; line-height: 1.1; font-weight: 700; letter-spacing: -0.025em; }
.t-h1      { font-size: 30px; line-height: 1.15; font-weight: 700; letter-spacing: -0.02em; }
.t-h2      { font-size: 22px; line-height: 1.25; font-weight: 600; letter-spacing: -0.015em; }
.t-h3      { font-size: 17px; line-height: 1.35; font-weight: 600; }
.t-body    { font-size: 14px; line-height: 1.55; }
.t-sm      { font-size: 13px; line-height: 1.5; color: var(--sf-text-2); }
.t-xs      { font-size: 11.5px; line-height: 1.4; color: var(--sf-text-3); letter-spacing: 0.02em; }
.t-eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--sf-text-3); }
.t-mono    { font-family: var(--font-mono); }
.t-num     { font-variant-numeric: tabular-nums; }

/* ─── Buttons ─── */
.btn {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 16px; border-radius: var(--r-md);
  font: 500 14px/1 var(--font-sans);
  border: 1px solid transparent; cursor: pointer;
  transition: background .15s, border-color .15s, color .15s, box-shadow .15s, transform .05s;
  white-space: nowrap;
  /* `min-height: 44px` (set in lastz.css mobile rule for WCAG 2.5.5)
     was being treated as a content-area floor instead of a border-box
     floor, so padding+border stacked on top — `.btn-lg` rendered at
     ~69-70px tall on mobile instead of the intended ~44px. */
  box-sizing: border-box;
  /* `.btn` is used on both <button> and <a> elements; <a> brings the
     default text-decoration: underline which reads as a link, not a
     button, and stacks oddly with the pill shape on hero CTAs. */
  text-decoration: none;
}
.btn:focus-visible { outline: none; box-shadow: var(--sh-focus); }
.btn:active { transform: translateY(0.5px); }
.btn-primary   { background: var(--p-600); color: #fff; }
.btn-primary:hover { background: var(--p-700); }
.btn-secondary { background: var(--sf-surface); color: var(--sf-text); border-color: var(--sf-border-strong); }
.btn-secondary:hover { background: var(--sf-surface-2); }
.btn-ghost     { background: transparent; color: var(--sf-text-2); }
.btn-ghost:hover { background: var(--sf-surface-2); color: var(--sf-text); }
.btn-danger    { background: var(--st-danger); color: #fff; }
.btn-danger:hover { filter: brightness(0.95); }
.btn-sm  { padding: 6px 11px; font-size: 12.5px; border-radius: var(--r-sm); }
.btn-lg  { padding: 12px 22px; font-size: 15px; }

/* ─── Inputs ─── */
.field-label { display: block; font-size: 12px; font-weight: 600; color: var(--sf-text-2); margin-bottom: 6px; }
.field-input, .field-select, .field-textarea,
input[type="text"],
input[type="email"],
input[type="url"],
input[type="number"],
input[type="password"],
input[type="search"],
input[type="tel"],
input[type="date"],
input[type="datetime-local"],
input[type="time"],
input:not([type]),
textarea,
select {
  box-sizing: border-box;
  padding: 8px 12px; border-radius: var(--r-md);
  background: var(--sf-surface); border: 1px solid var(--sf-border-strong);
  color: var(--sf-text); font: 400 14px/1.4 var(--font-sans);
  transition: border-color .15s, box-shadow .15s;
}
.field-input, .field-select, .field-textarea { width: 100%; }
.field-input:focus, .field-select:focus, .field-textarea:focus,
input[type="text"]:focus,
input[type="email"]:focus,
input[type="url"]:focus,
input[type="number"]:focus,
input[type="password"]:focus,
input[type="search"]:focus,
input[type="tel"]:focus,
input[type="date"]:focus,
input[type="datetime-local"]:focus,
input[type="time"]:focus,
input:not([type]):focus,
textarea:focus,
select:focus {
  outline: none; border-color: var(--p-500); box-shadow: var(--sh-focus);
}
input::placeholder, textarea::placeholder { color: var(--sf-text-3); opacity: 0.85; }
input:hover:not(:focus), textarea:hover:not(:focus), select:hover:not(:focus) {
  border-color: var(--sf-text-3);
}
.field-help { font-size: 12px; color: var(--sf-text-3); margin-top: 6px; }
.field-error { font-size: 12px; color: var(--st-danger); margin-top: 6px; }

/* ─── Cards ─── */
.card {
  background: var(--sf-surface);
  border: 1px solid var(--sf-border);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-sm);
}
.card-pad { padding: 20px; }
.card-pad-lg { padding: 28px; }
.card-elev { box-shadow: var(--sh-md); }

/* ─── Status pills ─── */
.pill {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 7px; border-radius: var(--r-pill);
  font: 600 11px/1.4 var(--font-sans);
  letter-spacing: 0.04em; text-transform: uppercase;
  border: 1px solid transparent;
}
.pill::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: currentColor;
}
.pill-success { background: var(--st-success-bg); color: var(--st-success); border-color: var(--st-success-border); }
.pill-danger  { background: var(--st-danger-bg);  color: var(--st-danger);  border-color: var(--st-danger-border); }
.pill-warn    { background: var(--st-warn-bg);    color: var(--st-warn);    border-color: var(--st-warn-border); }
.pill-info    { background: var(--st-info-bg);    color: var(--st-info);    border-color: var(--st-info-border); }
.pill-pending { background: var(--st-pending-bg); color: var(--st-pending); border-color: var(--st-pending-border); }
.pill-muted   { background: var(--sf-surface-2);  color: var(--sf-text-2);  border-color: var(--sf-border-strong); }
.pill-muted::before { background: var(--sf-text-3); }

/* ─── Table ─── */
.tbl { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.tbl thead th {
  text-align: start; padding: 11px 14px;
  background: var(--sf-surface-2);
  font-weight: 600; font-size: 11.5px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--sf-text-2);
  border-bottom: 1px solid var(--sf-border);
}
.tbl tbody td { padding: 12px 14px; border-bottom: 1px solid var(--sf-border); color: var(--sf-text); }
.tbl tbody tr:hover td { background: var(--sf-surface-2); }
.tbl tbody tr:last-child td { border-bottom: 0; }

/* ─── Empty state ─── */
.empty {
  padding: 48px 24px; text-align: center;
  border: 1px dashed var(--sf-border-strong);
  border-radius: var(--r-lg);
  background: var(--sf-surface);
}
.empty-icon {
  width: 48px; height: 48px; margin: 0 auto 12px;
  border-radius: 12px;
  background: var(--sf-surface-2);
  display: flex; align-items: center; justify-content: center;
  color: var(--sf-text-3); font-size: 22px;
}

/* ─── Nav (header) ─── */
.nav {
  display: flex; align-items: center; gap: 18px;
  padding: 14px 24px;
  background: var(--sf-surface);
  border-bottom: 1px solid var(--sf-border);
}
.nav a { color: var(--sf-text-2); font-size: 14px; font-weight: 500; text-decoration: none; padding: 6px 2px; border-bottom: 2px solid transparent; }
.nav a:hover { color: var(--sf-text); }
.nav a.active { color: var(--p-600); border-bottom-color: var(--p-600); }

/* ─── Toast / Alert ─── */
.alert {
  display: flex; gap: 12px; padding: 12px 14px;
  border-radius: var(--r-md);
  border: 1px solid;
  font-size: 13.5px;
}
.alert-success { background: var(--st-success-bg); border-color: var(--st-success-border); color: var(--st-success); }
.alert-danger  { background: var(--st-danger-bg);  border-color: var(--st-danger-border);  color: var(--st-danger); }
.alert-warn    { background: var(--st-warn-bg);    border-color: var(--st-warn-border);    color: var(--st-warn); }
.alert-info    { background: var(--st-info-bg);    border-color: var(--st-info-border);    color: var(--st-info); }

/* ─── Swatches (for the doc) ─── */
.swatch-grid { display: grid; grid-template-columns: repeat(11, 1fr); gap: 6px; }
.swatch { aspect-ratio: 1.4; border-radius: 8px; display: flex; flex-direction: column; justify-content: flex-end; padding: 6px 8px; font: 600 10px/1 var(--font-mono); }

/* ─── Demo grid scaffolding ─── */
.ds-section { padding: 40px 48px; border-bottom: 1px solid var(--sf-border); }
.ds-section h2.ds-title { font-size: 28px; font-weight: 700; letter-spacing: -0.02em; margin: 0 0 6px; }
.ds-section p.ds-sub { color: var(--sf-text-2); font-size: 14px; margin: 0 0 28px; max-width: 60ch; }
.ds-grid { display: grid; gap: 20px; }
.ds-row  { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
.ds-stack { display: flex; flex-direction: column; gap: 14px; }
.ds-label { font-size: 11px; font-weight: 600; color: var(--sf-text-3); letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 8px; }

/* Topbar of the foundations doc */
.ds-topbar {
  position: sticky; top: 0; z-index: 10;
  background: color-mix(in srgb, var(--sf-surface) 88%, transparent);
  backdrop-filter: saturate(140%) blur(10px);
  border-bottom: 1px solid var(--sf-border);
  display: flex; align-items: center; gap: 16px; padding: 12px 28px;
}
.ds-topbar .seg { display: inline-flex; background: var(--sf-surface-2); border-radius: var(--r-pill); padding: 3px; gap: 2px; }
.ds-topbar .seg button {
  border: 0; background: transparent; color: var(--sf-text-2);
  padding: 6px 12px; border-radius: var(--r-pill);
  font: 600 12px/1 var(--font-sans); cursor: pointer;
}
.ds-topbar .seg button.on { background: var(--sf-surface); color: var(--sf-text); box-shadow: var(--sh-sm); }

/* === _tokens.css === */
/* apps/web/static/css/_tokens.css
   Phase 37 design tokens. Layered ON TOP of design-system.css (which
   ships the canvas's --p-*, --sf-*, --st-* color tokens). This file
   adds the FLUID layer (type scale + spacing scale + motion) and
   semantic aliases that newer component CSS uses.

   Load order in base.html:
       _fonts.css  →  design-system.css  →  _tokens.css  →  lastz.css
   So semantic aliases here can reference the canvas color tokens. */

:root {
  /* ─── Type scale (fluid, clamp-based) ──────────────────────
     Replaces fixed-px font-sizes throughout the site. h1 stops
     wrapping one-word-per-line on mobile; large-display headlines
     don't go absurdly large on 4K. */
  --fz-h1:    clamp(2rem, 1.4rem + 3vw, 4rem);          /* 32 → 64 */
  --fz-h2:    clamp(1.625rem, 1.2rem + 2vw, 2.5rem);    /* 26 → 40 */
  --fz-h3:    clamp(1.25rem, 1.05rem + 1vw, 1.5rem);    /* 20 → 24 */
  --fz-body:  clamp(0.9375rem, 0.9rem + 0.2vw, 1rem);   /* 15 → 16 */
  --fz-small: 0.8125rem;                                /* 13 */
  --fz-xs:    0.75rem;                                  /* 12 */
  --fz-data:  clamp(1.75rem, 1.2rem + 2.5vw, 3rem);     /* 28 → 48 */

  /* line-heights paired with the scale */
  --lh-tight: 1.05;
  --lh-snug:  1.25;
  --lh-body:  1.6;
  --lh-data:  1;

  /* ─── Spacing scale ────────────────────────────────────────
     Component-internal spacing in 4-px multiples; fluid section /
     gutter tokens for page-level rhythm. */
  --sp-1: 0.25rem;  /* 4 */
  --sp-2: 0.5rem;   /* 8 */
  --sp-3: 0.75rem;  /* 12 */
  --sp-4: 1rem;     /* 16 */
  --sp-5: 1.5rem;   /* 24 */
  --sp-6: 2rem;     /* 32 */
  --sp-7: 3rem;     /* 48 */

  --sp-section:      clamp(2.5rem, 1.5rem + 4vw, 5rem);     /* 40 → 80 */
  --sp-shell-gutter: clamp(1rem, 4vw, 2rem);                /* 16 → 32 */
  --sp-card-pad:     clamp(1rem, 0.6rem + 1.5vw, 1.75rem);  /* 16 → 28 */
  --sp-hero-y:       clamp(2rem, 1rem + 4vw, 4.5rem);       /* 32 → 72 */

  /* ─── Motion ───────────────────────────────────────────────
     Subtle = Linear-grade. Always pair a duration with an easing. */
  --ease-out:  cubic-bezier(0.22, 1, 0.36, 1);
  --ease-snap: cubic-bezier(0.4, 0, 0.2, 1);
  --dur-fast: 120ms;
  --dur-mid:  220ms;
  --dur-slow: 380ms;

  /* ─── Semantic aliases ─────────────────────────────────────
     Bridge the canvas's --sf-* / --p-* / --st-* tokens to the
     semantic names component CSS uses. Changing one of these
     re-themes a category without hunting through component files. */
  --bg-page:        var(--sf-canvas);
  --bg-surface:     var(--sf-surface);
  --bg-surface-2:   var(--sf-surface-2);
  --bg-elevated:    var(--sf-surface);
  --fg-default:     var(--sf-text);
  --fg-secondary:   var(--sf-text-2);
  --fg-tertiary:    var(--sf-text-3);
  --border-default: var(--sf-border);
  --border-strong:  var(--sf-border-strong);

  --accent:       var(--p-600);  /* light theme: darker lime for AA on white */
  --accent-fg:    #ffffff;
  --accent-soft:  rgba(101, 163, 13, 0.10);
  --accent-glow:  rgba(101, 163, 13, 0.18);

  --success: var(--st-success);
  --warning: var(--st-warn);
  --danger:  var(--st-danger);
  --info:    var(--st-info);

  /* Compact shadows (canvas has --sh-sm/md/lg/focus; reuse via alias) */
  --sh-1:    var(--sh-sm);
  --sh-2:    var(--sh-md);
  --sh-3:    var(--sh-lg);
  --sh-glow: 0 6px 22px var(--accent-glow);
}

/* ─── Dark theme overrides ─ flip accent + accent-soft/glow ─── */
html.dark {
  --accent:      var(--p-400);     /* brighter lime on dark for contrast */
  --accent-fg:   #0c0d10;
  --accent-soft: rgba(163, 230, 53, 0.10);
  --accent-glow: rgba(163, 230, 53, 0.22);
  /* All other --bg-* / --fg-* / --border-* tokens already flip via the
     existing design-system.css .dark rules — semantic aliases reflect
     those because they use var() chains. */
}

/* ─── prefers-reduced-motion ─ zero out duration tokens ────── */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: 0ms;
    --dur-mid:  0ms;
    --dur-slow: 0ms;
  }
}

/* === lastz.css === */
/* =====================================================
   Last Z Rewards — atmosphere + UI overlays
   Layered on top of design-system.css
   ===================================================== */

/* Doc shell */
body { margin: 0; }
.lz-doc { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; }
.lz-side {
  position: sticky; top: 0; align-self: start;
  height: 100vh; overflow-y: auto;
  background: var(--sf-surface);
  border-inline-end: 1px solid var(--sf-border);
  padding: 18px 14px 22px;
}
.lz-side .brand {
  display: flex; align-items: center; gap: 10px;
  padding: 4px 8px 14px;
  border-bottom: 1px solid var(--sf-border);
  margin-bottom: 12px;
}
.lz-side .brand-mark {
  width: 32px; height: 32px; border-radius: 8px;
  background: linear-gradient(135deg, var(--p-300), var(--p-700));
  position: relative; overflow: hidden;
  display: grid; place-items: center;
}
.lz-side .brand-mark::after {
  content: ""; position: absolute; inset: 0;
  background:
    repeating-linear-gradient(45deg, transparent 0 4px, rgba(0,0,0,0.15) 4px 5px);
  mix-blend-mode: multiply;
}
.lz-side .brand-mark svg { position: relative; z-index: 1; }
.lz-side .brand-name { font: 800 14px/1 var(--font-sans); letter-spacing: -0.01em; color: var(--sf-text); }
.lz-side .brand-tag { font: 700 9px/1 var(--font-mono); letter-spacing: 0.18em; color: var(--p-700); text-transform: uppercase; margin-top: 3px; }
.dark .lz-side .brand-tag { color: var(--p-400); }

.lz-side .seg { display:flex; gap:4px; padding:3px; background:var(--sf-surface-2); border-radius:var(--r-pill); margin-bottom:6px; }
.lz-side .seg button { flex:1; border:0; background:transparent; color:var(--sf-text-2); padding:6px 0; border-radius:var(--r-pill); font:600 11px/1 var(--font-sans); cursor:pointer; }
.lz-side .seg button.on { background:var(--sf-surface); color:var(--sf-text); box-shadow:var(--sh-sm); }

.lz-side .grp { font: 700 10px/1 var(--font-sans); letter-spacing: 0.14em; text-transform: uppercase; color: var(--sf-text-3); margin: 16px 10px 6px; }
.lz-side a {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px; border-radius: 6px;
  font-size: 13px; color: var(--sf-text-2); text-decoration: none;
}
.lz-side a:hover { background: var(--sf-surface-2); color: var(--sf-text); }
.lz-side a .badge { margin-inline-start: auto; font: 700 9px/1 var(--font-mono); padding: 2px 5px; border-radius: 99px; background: var(--p-100); color: var(--p-800); }
.dark .lz-side a .badge { background: rgba(132,204,22,0.18); color: var(--p-300); }

.lz-main { background: var(--sf-canvas); }

/* Section / page-frame */
.lz-section {
  padding: 24px 32px 40px;
  border-bottom: 1px solid var(--sf-border);
}
.lz-section.alt { background: color-mix(in srgb, var(--p-50) 30%, var(--sf-canvas)); }
.dark .lz-section.alt { background: color-mix(in srgb, rgba(132,204,22,0.06) 50%, var(--sf-canvas)); }
.lz-tag {
  display: inline-flex; align-items: center; gap: 8px;
  font: 700 10px/1 var(--font-sans); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--sf-text-3); margin-bottom: 14px;
}
.lz-tag::before { content: ""; width: 6px; height: 6px; border-radius: 1px; background: var(--p-500); transform: rotate(45deg); }
.lz-tag .route { font-family: var(--font-mono); color: var(--p-700); letter-spacing: 0.05em; }
.dark .lz-tag .route { color: var(--p-400); }

.lz-frame {
  background: var(--sf-surface);
  border: 1px solid var(--sf-border);
  border-radius: var(--r-xl);
  /* overflow: clip preserves the rounded-corner clip on children without
     making .lz-frame a CSS "scroll container".  On a tall page
     (Phase 11 /dashboard with multiple subscription groups) the previous
     `overflow: hidden` caused Chrome to consume mouse-wheel events that
     landed inside the frame instead of bubbling them up to the document
     root, leaving the page un-scrollable via wheel / trackpad even
     though window.scrollTo() worked from JS.  `overflow: clip` clips
     identically but is NOT a scroll container, so wheel events bubble
     normally.  Chrome 90+ / Firefox 81+ / Safari 16+. */
  overflow: clip;
  box-shadow: var(--sh-md);
}

/* Public nav inside frame */
.lz-nav {
  display: flex; align-items: center; gap: 22px;
  padding: 14px 24px;
  background: var(--sf-surface);
  border-bottom: 1px solid var(--sf-border);
  position: relative;
}
.lz-nav::after {
  content:""; position:absolute; left:0; right:0; bottom:-1px; height:2px;
  background: repeating-linear-gradient(90deg, var(--p-500) 0 12px, transparent 12px 24px);
  opacity: 0.15;
}
.lz-nav .brand { display:flex; align-items:center; gap:10px; font:800 14px/1 var(--font-sans); letter-spacing:-0.01em; }
.lz-nav .brand-mark {
  width: 26px; height: 26px; border-radius: 6px;
  background: linear-gradient(135deg, var(--p-300), var(--p-700));
  position: relative; overflow:hidden;
}
.lz-nav .brand-mark::after {
  content:""; position:absolute; inset:0;
  background: repeating-linear-gradient(45deg, transparent 0 3px, rgba(0,0,0,0.18) 3px 4px);
}
.lz-nav a { color: var(--sf-text-2); font-size:13px; text-decoration:none; padding: 4px 0; }
.lz-nav a:hover { color: var(--sf-text); }
.lz-nav a.active { color: var(--p-700); font-weight: 600; }
.dark .lz-nav a.active { color: var(--p-400); }
.lz-nav .spacer { flex: 1; }
.lz-nav .acct { display:inline-flex; align-items:center; gap:8px; padding:4px 10px 4px 4px; border-radius: var(--r-pill); background: var(--sf-surface-2); font-size: 12px; font-weight:500; }
.lz-nav .avatar { width: 22px; height: 22px; border-radius: 50%; background: linear-gradient(135deg, var(--p-400), var(--p-700)); display:grid; place-items:center; color:#fff; font:700 10px/1 var(--font-sans); }

/* Public nav responsiveness (≤720px). 13 nav children (brand + 8
   links + spacer + sign-in + theme toggle) overflow at 375px. Make
   the nav horizontally scrollable so every link stays reachable
   via swipe, shrink padding + gap, and let the brand anchor not
   shrink so it's always visible on the left. */
@media (max-width: 720px) {
  .lz-nav {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    gap: 14px;
    padding: 10px 14px;
    flex-wrap: nowrap;
  }
  .lz-nav .brand {
    flex-shrink: 0;
  }
  .lz-nav a,
  .lz-nav .acct {
    flex-shrink: 0;
    white-space: nowrap;
    font-size: 12px;
  }
  .lz-nav .spacer {
    /* Spacer takes no room on mobile — items pack left-to-right and
       scroll. */
    flex: 0 0 4px;
  }
}

/* Sidebar (admin) */
.lz-asidenav { background: var(--sf-surface-2); border-inline-end: 1px solid var(--sf-border); display: flex; flex-direction: column; }
.lz-asidenav .head { padding: 16px 16px 12px; border-bottom: 1px solid var(--sf-border); display: flex; align-items: center; gap: 10px; }
.lz-asidenav .head .dot { width: 26px; height: 26px; border-radius: 7px; background: linear-gradient(135deg, var(--p-400), var(--p-800)); display: grid; place-items:center; color:#fff; font: 800 11px/1 var(--font-sans); position:relative; overflow:hidden; }
.lz-asidenav .head .dot::after { content:""; position:absolute; inset:0; background: repeating-linear-gradient(45deg, transparent 0 3px, rgba(0,0,0,0.2) 3px 4px); }
.lz-asidenav nav { padding: 10px 8px; flex: 1; display: flex; flex-direction: column; gap: 1px; }
.lz-asidenav nav a { display: flex; align-items: center; gap: 10px; padding: 7px 12px; border-radius: 8px; font-size: 13px; color: var(--sf-text-2); text-decoration: none; }
.lz-asidenav nav a:hover { background: var(--sf-surface); color: var(--sf-text); }
.lz-asidenav nav a.active { background: var(--p-50); color: var(--p-700); font-weight: 600; }
.dark .lz-asidenav nav a.active { background: rgba(132,204,22,0.15); color: var(--p-300); }
.lz-asidenav .grp { font: 700 10px/1 var(--font-sans); letter-spacing: 0.14em; text-transform: uppercase; color: var(--sf-text-3); margin: 12px 12px 4px; }
.lz-asidenav .cnt { margin-inline-start: auto; font: 700 9px/1 var(--font-mono); padding: 2px 5px; border-radius: 99px; background: var(--sf-surface); color: var(--sf-text-2); }
.lz-asidenav a.active .cnt { background: var(--p-100); color: var(--p-700); }
.dark .lz-asidenav a.active .cnt { background: rgba(132,204,22,0.25); color: var(--p-200); }
.lz-asidenav .foot { padding: 10px 12px; border-top: 1px solid var(--sf-border); display:flex; align-items:center; gap: 8px; font-size: 12px; }

.lz-admin { display: grid; grid-template-columns: 220px minmax(0, 1fr); min-height: 680px; }
.lz-admin-main { display: flex; flex-direction: column; min-width: 0; }
.lz-admin-head { display:flex; align-items:center; gap: 14px; padding: 14px 22px; border-bottom: 1px solid var(--sf-border); background: var(--sf-surface); flex-wrap: wrap; }

/* ─── Responsive admin layout (≤960px) ─────────────────────────────
   At narrow viewports the 220px sidebar leaves ~150px for the main
   content (unusable). Collapse the layout to a single column: the
   aside becomes a horizontally-scrollable nav strip at the top with
   nav items laid out in a row, the main content takes full width
   below. Group headers ("PEOPLE", "INFRA", etc.) collapse to inline
   separators so the strip stays one line. Keeps every link reachable
   without JS / a hamburger toggle. */
@media (max-width: 960px) {
  .lz-admin {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    min-height: auto;
  }
  .lz-asidenav {
    border-inline-end: none;
    border-bottom: 1px solid var(--sf-border);
    /* Without min-width:0 the flex-row nav inside expands the grid
       track to its intrinsic width (~2500px with 22 nav links),
       blowing out the layout. Clamp it. */
    min-width: 0;
    width: 100%;
  }
  .lz-asidenav nav {
    flex-direction: row;
    overflow-x: auto;
    padding: 8px 10px;
    gap: 4px;
    /* Hide the scrollbar visually but keep it functional. */
    scrollbar-width: thin;
    -webkit-overflow-scrolling: touch;
    min-width: 0;
    max-width: 100%;
  }
  .lz-asidenav nav a {
    flex-shrink: 0;
    white-space: nowrap;
    font-size: 12px;
    padding: 6px 10px;
  }
  .lz-asidenav .grp {
    flex-shrink: 0;
    align-self: center;
    margin: 0 4px;
    font-size: 9px;
    /* Hide group labels on mobile — they take horizontal real estate
       without helping nav (links are already grouped visually by being
       adjacent). The scroll position carries spatial grouping. */
    display: none;
  }
  .lz-asidenav .head,
  .lz-asidenav .foot {
    padding: 8px 12px;
    font-size: 12px;
  }
  .lz-admin-head {
    padding: 12px 14px;
    gap: 10px;
  }
  .lz-admin-head h1 {
    font-size: 16px;
  }
  .lz-admin-body {
    padding: 14px 14px !important;
  }
  /* Tables in admin cards are usually wider than the mobile viewport
     (10+ columns). The default inline `overflow:hidden` clips the
     overflow making the right-hand columns inaccessible. Force the
     containing `.card` to scroll horizontally on mobile so the
     operator can swipe through every column. */
  .lz-admin-body .card:has(> table.tbl) {
    overflow-x: auto !important;
    -webkit-overflow-scrolling: touch;
  }
}

/* ─── Responsive site frame (≤960px tablet, ≤720px mobile) ───────
   The .lz-frame wrapper enforces a max-width and gutter padding that
   wastes space on phones. Shrink the gutter so cards/grids inside
   actually have room. Grid stacks: 3+ columns collapse to 2 on tablet,
   everything collapses to 1 column on phones. Inline
   `grid-template-columns` styles are caught by the attribute selector
   so templates don't need to know about breakpoints individually. */
@media (max-width: 960px) {
  /* Tablet: drop 3+ column grids to 2-up. Multi-col inline grids
     (which we can't introspect from CSS) also flatten to 2-up. */
  .grid3,
  .grid4,
  [style*="grid-template-columns"] {
    grid-template-columns: 1fr 1fr !important;
  }
}
@media (max-width: 720px) {
  .lz-frame {
    padding-inline: 12px !important;
  }
  /* Inline `style="padding:..."` overrides on cards/sections force
     desktop spacing on phones — counter with !important under the
     mobile breakpoint. */
  .card.card-pad {
    padding: 14px !important;
  }
  .card.card-pad-lg {
    padding: 20px !important;
  }
  /* Phones: every multi-column layout collapses to one column. The
     attribute selector also catches inline `grid-template-columns`
     styles on cards / promo blocks (e.g. home.html's pro-tier teaser)
     without making the template aware of breakpoints. Pair with
     `.no-stack` on the parent if a future layout opts out. */
  .grid2,
  .grid3,
  .grid4,
  :not(.no-stack) > [style*="grid-template-columns"] {
    grid-template-columns: 1fr !important;
  }
  /* Multi-column inline grids (KPI tiles, button rows) collapse to
     a single column on phones unless the template opts out via a
     `.no-stack` class. */
  .row {
    flex-wrap: wrap;
  }
  /* Inline `padding: 0 48px 48px` style on the home.html pro-tier
     teaser wrapper wastes 96px of horizontal real estate on a 375px
     viewport. Shrink it. Same idea: cards/wrappers with desktop-only
     horizontal padding need a mobile cap. */
  div[style*="padding:0 48px"],
  div[style*="padding: 0 48px"] {
    padding-inline: 12px !important;
  }
}
.lz-admin-head h1 { margin:0; font: 600 17px/1 var(--font-sans); letter-spacing: -0.01em; }
.lz-admin-head .search { flex: 1; max-width: 380px; display:flex; align-items:center; gap: 8px; padding: 6px 12px; background: var(--sf-surface-2); border-radius: var(--r-md); color: var(--sf-text-3); font-size: 13px; }
.lz-admin-head .search input { flex: 1; background: transparent; border: 0; outline: none; color: var(--sf-text); font-size: 13px; }
.lz-admin-body { padding: 22px 26px; flex: 1; background: var(--sf-canvas); }

.crumb { font-size: 12px; color: var(--sf-text-3); display:flex; gap:6px; align-items:center; flex-wrap: wrap; }
.crumb a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dashed transparent;
  padding-bottom: 1px;
  transition: color 0.15s, border-color 0.15s;
}
.crumb a:hover {
  color: var(--p-700);
  border-bottom-color: var(--p-500);
}
.crumb a:focus-visible {
  outline: 2px solid var(--p-500);
  outline-offset: 2px;
  border-radius: 2px;
}
.crumb .sep { opacity: 0.5; }

/* Hero (Last Z stencil + scanline atmosphere) */
.lz-hero {
  position: relative; overflow: hidden;
  padding: 52px 48px 56px;
  background:
    radial-gradient(700px 320px at 88% -10%, rgba(132,204,22,0.18), transparent 70%),
    radial-gradient(640px 360px at 0% 110%, rgba(217,119,6,0.06), transparent 70%),
    linear-gradient(180deg, var(--sf-surface) 0%, var(--sf-canvas) 100%);
  border-bottom: 1px solid var(--sf-border);
}
.dark .lz-hero {
  background:
    radial-gradient(800px 380px at 88% -10%, rgba(132,204,22,0.22), transparent 70%),
    radial-gradient(640px 360px at 0% 110%, rgba(217,119,6,0.08), transparent 70%),
    linear-gradient(180deg, #131A11 0%, #0A0E0A 100%);
}
.lz-hero::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(rgba(0,0,0,0.04) 1px, transparent 1px),
    linear-gradient(90deg, rgba(0,0,0,0.04) 1px, transparent 1px);
  background-size: 32px 32px;
  mask-image: radial-gradient(ellipse 100% 60% at 50% 20%, black 30%, transparent 80%);
}
.dark .lz-hero::before { background-image: linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px); }

.lz-hero .hero-row { display: grid; grid-template-columns: 1.2fr 1fr; gap: 40px; align-items: center; position: relative; }
.lz-hero h1 {
  font: 800 60px/1.02 var(--font-sans); letter-spacing: -0.03em; margin: 0;
  color: var(--sf-text);
}
/* Phase 38 review #23 — hero accent had a generic lime-on-lime
   gradient text-fill (Tailwind-tutorial vibe). Switched to a
   hand-painted highlight: text body keeps full --sf-text contrast,
   a skewed accent slab sits behind. Looks like brush stroke rather
   than CSS gradient. */
.lz-hero h1 .accent {
  position: relative;
  display: inline-block;
  isolation: isolate;
  color: var(--sf-text);
  padding: 0 0.08em;
}
.lz-hero h1 .accent::before {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0.06em;
  height: 0.22em;
  background: linear-gradient(
    100deg,
    color-mix(in srgb, var(--accent) 65%, transparent) 4%,
    color-mix(in srgb, var(--accent) 92%, transparent) 50%,
    color-mix(in srgb, var(--accent) 55%, transparent) 96%
  );
  transform: skew(-3deg);
  z-index: -1;
  border-radius: 4px 8px 6px 10px / 6px 4px 10px 6px;
}
.lz-hero .lede { font-size: 16px; line-height: 1.55; color: var(--sf-text-2); max-width: 52ch; margin: 18px 0 0; }

/* Redeem field */
.redeem {
  margin-top: 26px; padding: 14px; border-radius: var(--r-lg);
  background: var(--sf-surface);
  border: 1px solid var(--sf-border-strong);
  box-shadow: var(--sh-md);
}
.dark .redeem { background: color-mix(in srgb, var(--sf-surface) 85%, transparent); backdrop-filter: blur(20px); }
.redeem .row { display: flex; gap: 8px; align-items: stretch; }
.redeem .input-wrap {
  flex: 1; display: flex; align-items: center; gap: 10px;
  padding: 4px 14px;
  background: var(--sf-surface-2); border: 1px solid var(--sf-border); border-radius: var(--r-md);
}
.redeem .input-wrap:focus-within { border-color: var(--p-500); box-shadow: var(--sh-focus); }
.redeem .input-wrap .player-chip { display:inline-flex; align-items:center; gap:6px; padding: 4px 8px 4px 4px; border-radius: var(--r-pill); background: var(--sf-surface); border: 1px solid var(--sf-border); font: 600 12px/1 var(--font-sans); color: var(--sf-text); }
.redeem .input-wrap .player-chip .av { width: 18px; height: 18px; border-radius: 50%; background: linear-gradient(135deg, var(--p-400), var(--p-700)); color:#fff; display:grid; place-items:center; font: 700 9px/1 var(--font-mono); }
.redeem .input-wrap .player-chip .x { color: var(--sf-text-3); cursor:pointer; }
.redeem .input-wrap input { flex: 1; background: transparent; border: 0; outline: none; font: 600 15px/1.3 var(--font-mono); color: var(--sf-text); padding: 10px 0; min-width: 0; }
.redeem .input-wrap input::placeholder { color: var(--sf-text-3); }
.redeem .helps { display:flex; gap: 14px; margin-top: 10px; font-size: 11.5px; color: var(--sf-text-3); }
.redeem .helps strong { color: var(--sf-text-2); font-weight: 600; }

/* Reward preview pop (before paste) */
.reward-preview {
  margin-top: 14px; padding: 14px 16px; border-radius: var(--r-md);
  background: color-mix(in srgb, var(--p-50) 70%, var(--sf-surface));
  border: 1px dashed var(--p-300);
  display: flex; align-items:center; gap: 14px;
}
.dark .reward-preview { background: rgba(132,204,22,0.08); border-color: rgba(132,204,22,0.3); }
.reward-preview .icons { display:flex; gap:6px; }
.reward-preview .icons span {
  width: 28px; height: 28px; border-radius: 6px;
  display: grid; place-items: center; font-size: 14px;
  background: var(--sf-surface); border: 1px solid var(--sf-border);
  box-shadow: var(--sh-sm);
}
.reward-preview .icons .ammo  { background: linear-gradient(135deg, #FCD34D, #B45309); color: #fff; border-color: transparent; }
.reward-preview .icons .med   { background: linear-gradient(135deg, #FCA5A5, #B91C1C); color: #fff; border-color: transparent; }
.reward-preview .icons .food  { background: linear-gradient(135deg, #BEF264, #4D7C0F); color: #fff; border-color: transparent; }
.reward-preview .icons .fuel  { background: linear-gradient(135deg, #93C5FD, #1D4ED8); color: #fff; border-color: transparent; }
.reward-preview .icons .parts { background: linear-gradient(135deg, #D6D3D1, #57534E); color: #fff; border-color: transparent; }
.reward-preview .label { font-size: 13px; color: var(--sf-text); }
.reward-preview .label strong { font-weight: 700; }

/* Live ticker */
.live-ticker {
  display:flex; align-items:center; gap: 10px;
  font: 500 12px/1 var(--font-sans);
  color: var(--sf-text-2); padding: 8px 14px;
  background: var(--sf-surface-2); border-radius: var(--r-pill);
  border: 1px solid var(--sf-border);
}
.live-ticker .live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--st-success); box-shadow: 0 0 8px var(--st-success); animation: pulse 1.5s ease-in-out infinite; }
@keyframes pulse { 50% { opacity: 0.4; } }
.live-ticker .ticker-track { overflow:hidden; max-width: 360px; }
.live-ticker .ticker-track .item { white-space: nowrap; display: inline-block; }

/* Stat / KPI cards */
.kpi { display: grid; gap: 14px; }
.kpi-card { background: var(--sf-surface); border:1px solid var(--sf-border); border-radius: var(--r-md); padding: 16px; }
.kpi-card .e { font: 700 10.5px/1 var(--font-sans); letter-spacing: 0.14em; text-transform: uppercase; color: var(--sf-text-3); }
.kpi-card .n { font: 800 28px/1 var(--font-sans); letter-spacing: -0.025em; margin-top: 8px; font-variant-numeric: tabular-nums; color: var(--sf-text); }
.kpi-card .d { font-size: 11.5px; margin-top: 4px; color: var(--sf-text-3); }
.kpi-card .d.up { color: var(--st-success); } .kpi-card .d.down { color: var(--st-danger); } .kpi-card .d.warn { color: var(--st-warn); }
.spark { width: 100%; height: 32px; margin-top: 8px; }

/* Code chip — Phase 16.1 polish: each row is one compact card; the
   parent `.active-codes-grid` lays them out responsively (1-4 columns
   depending on viewport).  Internal layout: pill (auto) | content
   (auto) | action (1fr w/ justify-self:end) so the Copy button always
   right-aligns within whatever cell width the grid allocates. */
.code-row {
  display: grid; grid-template-columns: auto auto 1fr;
  align-items: center; gap: 14px; padding: 12px 14px;
  background: var(--sf-surface); border: 1px solid var(--sf-border); border-radius: var(--r-md);
}
.code-row > button { justify-self: end; }

/* Responsive grid for the home-page Active codes section.  Each card
   gets at least 300px and grows to fill its track up to 1fr; the grid
   auto-fills as many tracks as the viewport allows (1 col on phones,
   2 on tablets, 3-4 on desktop, more on ultra-wide).  Use auto-fill
   not auto-fit so sparse states (e.g. 1 live code) don't stretch the
   single card to full row width — empty tracks stay reserved.  The
   `min(280px, 100%)` floor lets the track shrink below 300px on
   sub-300 viewports (e.g. 320px-wide phones) so .code-row doesn't
   overflow into the clipped gutter and trap the Copy button. */
.active-codes-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  gap: 10px;
}
/* Inside the grid the inter-row vertical margin would conflict with
   `gap`, so reset it for cards that live in the grid. */
.active-codes-grid .code-row + .code-row { margin-top: 0; }
.code-row + .code-row { margin-top: 8px; }
.code-row .code { font: 700 14px/1.3 var(--font-mono); letter-spacing: -0.005em; color: var(--sf-text); }
.code-row .meta { font-size: 11.5px; color: var(--sf-text-3); margin-top: 2px; }
.code-row.expired { opacity: 0.55; }
.code-row.expired .code { text-decoration: line-through; text-decoration-color: var(--st-danger); }
.code-row .reward-icons { display: flex; gap: 4px; }
.code-row .reward-icons span {
  width: 22px; height: 22px; border-radius: 5px;
  display: grid; place-items: center; font-size: 11px;
}

/* Tier card */
.tier {
  background: var(--sf-surface); border: 1px solid var(--sf-border); border-radius: var(--r-lg);
  padding: 24px; position: relative; display: flex; flex-direction: column;
}
.tier.featured {
  border-color: var(--p-400);
  box-shadow: 0 0 0 1px var(--p-400), var(--sh-md);
}
.tier .price { font: 800 36px/1 var(--font-sans); letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
.tier .per { font-size: 13px; color: var(--sf-text-2); font-weight: 500; }
.tier ul { margin: 16px 0 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 8px; }
.tier li { font-size: 13.5px; color: var(--sf-text); display: flex; gap: 8px; align-items: start; }
.tier li::before { content: "✓"; color: var(--p-600); font-weight: 800; flex-shrink: 0; }
.tier li.no { color: var(--sf-text-3); }
.tier li.no::before { content: "—"; color: var(--sf-text-3); }
/* `.tier` is a flex column already; pushing the CTA via `margin-top:
   auto` floats it to the bottom of the card so every tier's button
   sits at the same Y regardless of how many feature bullets the tier
   has. `padding-top` preserves the gap from the bullets above on
   short cards (otherwise the auto would collapse to 0). */
.tier .tier-cta { margin-top: auto; padding-top: 20px; }

/* Phase 39 — manage-subscription Player-IDs meter. Replaces the
   inline `<div style="height:6px;...">` track that vanished at 0%
   into the surface-2 background. The new component renders an
   explicit "no Player IDs yet" hint inside the empty track and
   gives the track a visible border so it reads as a container
   even at 0/N. */
.lz-uid-meter {
  display: flex;
  align-items: center;
  gap: 14px;
}
.lz-uid-meter__count {
  font: 700 18px/1 var(--font-mono);
  color: var(--sf-text);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.lz-uid-meter__cap {
  color: var(--sf-text-3);
  font-size: 14px;
  font-weight: 500;
  margin-inline-start: 2px;
}
.lz-uid-meter__track {
  position: relative;
  flex: 1;
  height: 10px;
  background: var(--sf-surface-2);
  border: 1px solid var(--sf-border);
  border-radius: 5px;
  overflow: hidden;
}
.lz-uid-meter__fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  background: linear-gradient(90deg, var(--p-500), var(--p-400));
  border-radius: inherit;
  transition: width 240ms ease;
}
.lz-uid-meter__hint {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 600 10px/1 var(--font-sans);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sf-text-3);
  pointer-events: none;
}
.lz-uid-meter.is-empty .lz-uid-meter__track {
  border-style: dashed;
  border-color: var(--sf-border-strong);
  background: transparent;
  height: 22px;
  border-radius: 6px;
}
@media (max-width: 480px) {
  /* Narrower hint to fit on a 320 viewport's truncated track. */
  .lz-uid-meter__hint { font-size: 9.5px; letter-spacing: 0.04em; }
}

/* Phase 39 — /autoredemption pricing-tier compaction on phones.
   At 320 the five stacked tiers eat ~250px each (24px box padding,
   36px price, 8px gap × 4 bullets, 20px CTA margin) for ~1250px
   of scroll. Halve the box padding, drop the bullet gap, and pull
   the CTA up — keeps every comparison fact visible but trims the
   vertical span by ~80px per tier. */
@media (max-width: 480px) {
  .tier { padding: 18px 16px; }
  .tier .price { font-size: 30px; }
  .tier ul { margin-top: 12px; gap: 6px; }
  .tier li { font-size: 13px; }
  .tier .tier-cta { margin-top: 14px; }
}

.savings {
  display: grid; grid-template-columns: 60px 1fr auto; gap: 12px; align-items: center;
  padding: 10px 14px; background: var(--p-50); border: 1px solid var(--p-200); border-radius: var(--r-md);
  margin-bottom: 14px;
}
.dark .savings { background: rgba(132,204,22,0.1); border-color: rgba(132,204,22,0.25); }
.savings .icon { width: 36px; height: 36px; border-radius: 8px; background: var(--p-100); display: grid; place-items: center; color: var(--p-700); font-weight: 800; font-size: 14px; }
.dark .savings .icon { background: rgba(132,204,22,0.18); color: var(--p-300); }
.savings .body { font-size: 13px; color: var(--sf-text); }

/* Manual queue */
.queue-list { display: flex; flex-direction: column; gap: 6px; max-height: 480px; overflow-y: auto; }
.queue-item {
  display: grid; grid-template-columns: 16px 1fr auto; gap: 12px; align-items: center;
  padding: 12px 14px; border: 1px solid var(--sf-border); border-radius: var(--r-md);
  background: var(--sf-surface); cursor: pointer;
}
.queue-item:hover { border-color: var(--sf-border-strong); }
.queue-item.selected { background: var(--p-50); border-color: var(--p-400); border-inline-start: 3px solid var(--p-500); }
.dark .queue-item.selected { background: rgba(132,204,22,0.12); }
.queue-item .age { font: 700 11px/1 var(--font-mono); padding: 3px 7px; border-radius: 99px; background: var(--sf-surface-2); color: var(--sf-text-2); }
.queue-item .age.over { background: var(--st-warn-bg); color: var(--st-warn); }
.queue-item .age.critical { background: var(--st-danger-bg); color: var(--st-danger); }

/* Differentiated error states */
.err-frame {
  min-height: 460px; display: grid; grid-template-columns: 1fr 1fr; gap: 0;
  overflow: hidden;
}
.err-art {
  background: var(--sf-surface-2);
  padding: 40px; display: flex; align-items: center; justify-content: center;
  position: relative; overflow: hidden;
  border-inline-end: 1px solid var(--sf-border);
}
.err-art .mono {
  font: 900 200px/0.9 var(--font-mono); letter-spacing: -0.05em;
  color: var(--sf-text-3); opacity: 0.5;
}
.err-art.danger { background: linear-gradient(135deg, var(--st-danger-bg), var(--sf-surface) 70%); }
.err-art.warn { background: linear-gradient(135deg, var(--st-warn-bg), var(--sf-surface) 70%); }
.err-art.info { background: linear-gradient(135deg, var(--st-info-bg), var(--sf-surface) 70%); }
.err-art .stencil {
  position: absolute; inset: 0;
  background-image:
    repeating-linear-gradient(-45deg, transparent 0 12px, rgba(0,0,0,0.04) 12px 14px);
  mask-image: linear-gradient(180deg, transparent 0%, black 50%, transparent 100%);
}
.err-body { padding: 40px 48px; display: flex; flex-direction: column; justify-content: center; min-width: 0; }
.err-body h2 { font: 800 28px/1.15 var(--font-sans); letter-spacing: -0.025em; margin: 12px 0 8px; }
.err-body p { font-size: 14px; color: var(--sf-text-2); margin: 0 0 6px; line-height: 1.55; max-width: 44ch; }
.err-body .actions { display: flex; gap: 8px; margin-top: 18px; flex-wrap: wrap; }
.err-body .ref { font: 600 11px/1 var(--font-mono); color: var(--sf-text-3); margin-top: 18px; }

/* On phones the 1fr|1fr split crushes the body column to ~160px, then
   the 48px horizontal padding plus the centered "404" mono leaves no
   room for the actual quarantine message — content clips off the right
   gutter and the action buttons spill past the viewport. Stack the
   frame vertically and shrink the chrome below 640. */
@media (max-width: 640px) {
  .err-frame {
    grid-template-columns: 1fr;
    min-height: 0;
  }
  .err-art {
    padding: 24px;
    border-inline-end: 0;
    border-block-end: 1px solid var(--sf-border);
  }
  .err-art .mono { font-size: 120px; }
  .err-body { padding: 24px 20px; }
  .err-body h2 { font-size: 22px; }
}

/* Mobile preview frame */
.mobile-frame {
  width: 360px; height: 720px; border-radius: 36px;
  border: 10px solid #18181B; box-shadow: var(--sh-lg);
  overflow: hidden; background: var(--sf-canvas);
  position: relative;
}
.mobile-frame .notch { position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 96px; height: 22px; background: #18181B; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; z-index: 2; }
.mobile-body { height: 100%; overflow-y: auto; padding: 36px 16px 80px; }
.mobile-tabbar {
  position: absolute; bottom: 0; left: 0; right: 0;
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 0;
  padding: 8px 0 18px; background: color-mix(in srgb, var(--sf-surface) 92%, transparent);
  backdrop-filter: blur(20px); border-top: 1px solid var(--sf-border);
}
.mobile-tabbar .tab { display: flex; flex-direction: column; align-items: center; gap: 3px; font: 600 10px/1 var(--font-sans); color: var(--sf-text-3); }
.mobile-tabbar .tab.on { color: var(--p-700); }
.dark .mobile-tabbar .tab.on { color: var(--p-400); }

/* ── Dashboard UID list (horizontally-scrollable table) ───────────
   Renders as a single <table> with one <tr> per Player ID. Sticky-left
   first column carries avatar + nickname + UID; subsequent columns
   (HQ / Server / Country / VIP / Z-pts / Updated / Actions) scroll
   horizontally on narrow viewports. Looks like a clean data grid at
   desktop widths (everything visible without scroll) and as a
   swipeable mobile table below ~700px. */
.lz-uid-table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--sf-border);
  border-radius: 10px;
  background: var(--sf-surface);
  /* Keep the horizontal scroll on this element; don't chain to the
     page when the user scrolls past either end. */
  overscroll-behavior-x: contain;
  /* Right-edge fade so it's visible that more content is to the right.
     Disappears once the user scrolls to the end (no JS — the
     background-attachment trick keeps the fade pinned to the visible
     viewport of the scroller). */
  background-image:
    linear-gradient(to right, var(--sf-surface) 30%, rgba(255,255,255,0) 70%),
    linear-gradient(to right, rgba(255,255,255,0) 30%, var(--sf-surface) 70%),
    linear-gradient(to right, rgba(0,0,0,0.18), rgba(0,0,0,0)),
    linear-gradient(to left,  rgba(0,0,0,0.18), rgba(0,0,0,0));
  background-position: left center, right center, left center, right center;
  background-repeat: no-repeat;
  background-color: var(--sf-surface);
  background-size: 24px 100%, 24px 100%, 12px 100%, 12px 100%;
  background-attachment: local, local, scroll, scroll;
}
/* Always-visible thin scrollbar so the swipe affordance is obvious on
   mobile. Webkit covers iOS Safari + Chrome Android + Edge; Firefox
   uses scrollbar-color. */
.lz-uid-table-wrap::-webkit-scrollbar {
  height: 6px;
}
.lz-uid-table-wrap::-webkit-scrollbar-track {
  background: var(--sf-surface-2);
}
.lz-uid-table-wrap::-webkit-scrollbar-thumb {
  background: var(--sf-border);
  border-radius: 3px;
}
.lz-uid-table-wrap::-webkit-scrollbar-thumb:hover {
  background: var(--sf-text-3);
}
.lz-uid-table-wrap {
  scrollbar-width: thin;
  scrollbar-color: var(--sf-border) var(--sf-surface-2);
}
.lz-uid-table {
  width: 100%;
  min-width: max-content;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 13px;
  color: var(--sf-text-2);
}
.lz-uid-table thead th {
  text-align: left;
  padding: 8px 10px;
  font: 700 10.5px/1 var(--font-sans);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sf-text-3);
  background: var(--sf-surface-2);
  border-bottom: 1px solid var(--sf-border);
  white-space: nowrap;
}
.lz-uid-table tbody td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--sf-border);
  vertical-align: middle;
  white-space: nowrap;
  background: var(--sf-surface);
}
.lz-uid-table tbody tr:last-child td { border-bottom: 0; }
.lz-uid-table tbody tr:hover td {
  background: var(--sf-surface-2);
}

/* Sticky-left first column — avatar + name only. Kept narrow so on
   mobile (393px viewport) the user has enough non-sticky width to
   grab a horizontal swipe. UID moves to its own scrollable column. */
.lz-uid-table .lz-uid-row__id {
  position: sticky;
  left: 0;
  z-index: 1;
  min-width: 160px;
  max-width: 200px;
  border-right: 1px solid var(--sf-border);
}
.lz-uid-table thead th.lz-uid-row__id {
  z-index: 2; /* above body sticky cells when they scroll under */
}
.lz-uid-row__id-inner {
  display: flex;
  align-items: center;
  gap: 10px;
}
.lz-uid-row__avatar {
  width: 36px;
  height: 36px;
  border-radius: 8px;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--sf-surface-2);
}
.lz-uid-row__avatar--fallback {
  display: grid;
  place-items: center;
  font: 700 13px/1 var(--font-sans);
  color: var(--sf-text-2);
}
.lz-uid-row__name {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.25;
}
.lz-uid-row__nick {
  font-weight: 600;
  font-size: 14px;
  color: var(--sf-text);
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 120px;
}
.lz-uid-row__uid {
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--sf-text-2);
}

/* Meta cells — labels are part of the column header so the values are
   bare numbers/strings. Z-pts uses a heavier weight for the digits. */
.lz-uid-row__hq,
.lz-uid-row__sv,
.lz-uid-row__country,
.lz-uid-row__vip {
  font-size: 12.5px;
  color: var(--sf-text-2);
}
.lz-uid-row__zpts .mono {
  font-weight: 600;
  color: var(--sf-text);
}
.lz-uid-row__updated {
  font-size: 11.5px;
  color: var(--sf-text-3);
}

/* Actions cell — every button + the Auto toggle on one line. */
.lz-uid-row__actions {
  text-align: right;
}
.lz-uid-row__actions-th { text-align: right; }
.lz-uid-row__actions .lz-uid-card__actions {
  display: flex;
  gap: 4px;
  align-items: center;
  justify-content: flex-end;
}
.lz-uid-row__actions .btn {
  font-size: 11.5px;
  padding: 4px 8px;
  border-radius: var(--r-sm);
}
.lz-uid-row__actions .js-toggle-uid-wrap {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 11px;
  color: var(--sf-text-2);
  white-space: nowrap;
  padding: 0 2px;
}

/* Bulk-mode selected row — paint each cell, including the sticky one,
   so the highlight extends across the full visible row width. */
.js-uid-card.is-selected td {
  background: color-mix(in srgb, var(--p-50) 60%, var(--sf-surface)) !important;
}

/* ── Tasks table wrap (/taskstatus) ───────────────────────────────
   The /taskstatus table renders 4 columns (When / Player ID / Status /
   Result-or-Error). On mobile the 4 columns together exceed the
   viewport, and the parent .card was using `overflow: hidden` which
   silently clipped columns 3-4 with no scroll affordance. This wrap
   uses the same swipe-friendly pattern as .lz-uid-table-wrap: visible
   scrollbar, right-edge fade hint, overscroll-behavior contain. The
   wrap IS the .card so the card's border/radius/background stay. */
.lz-tasks-table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  background-image:
    linear-gradient(to right, var(--sf-surface) 30%, rgba(255,255,255,0) 70%),
    linear-gradient(to right, rgba(255,255,255,0) 30%, var(--sf-surface) 70%),
    linear-gradient(to right, rgba(0,0,0,0.18), rgba(0,0,0,0)),
    linear-gradient(to left,  rgba(0,0,0,0.18), rgba(0,0,0,0));
  background-position: left center, right center, left center, right center;
  background-repeat: no-repeat;
  background-color: var(--sf-surface);
  background-size: 24px 100%, 24px 100%, 12px 100%, 12px 100%;
  background-attachment: local, local, scroll, scroll;
}
.lz-tasks-table-wrap > .tbl {
  /* Without this the table tries to fit the wrap width and cell
     content wraps + the column header order goes weird. min-content
     lets each cell size to its actual content; the wrap scrolls
     instead. */
  min-width: max-content;
}
.lz-tasks-table-wrap::-webkit-scrollbar { height: 6px; }
.lz-tasks-table-wrap::-webkit-scrollbar-track { background: var(--sf-surface-2); }
.lz-tasks-table-wrap::-webkit-scrollbar-thumb { background: var(--sf-border); border-radius: 3px; }
.lz-tasks-table-wrap::-webkit-scrollbar-thumb:hover { background: var(--sf-text-3); }
.lz-tasks-table-wrap { scrollbar-width: thin; scrollbar-color: var(--sf-border) var(--sf-surface-2); }

@media (max-width: 640px) {
  /* Tighter cells on mobile so more of the table peeks into view
     before the user has to swipe. */
  .lz-tasks-table-wrap .tbl th,
  .lz-tasks-table-wrap .tbl td {
    padding: 8px 10px;
    font-size: 12.5px;
  }
}

/* ── Dashboard mobile — toolbar, bulk-actions, add-UID form ───────
   Below 640px the per-subscription card gets tighter padding and the
   toolbar/bulk-actions/add-form switch to layouts that don't leave
   awkward gaps when buttons wrap. The .lz-flex-spacer divs that push
   "Bulk edit" + "Cancel" to the right on desktop disappear on mobile
   so buttons flow naturally. */
@media (max-width: 640px) {
  /* Tighter card padding gives the inner toolbar + table more room.
     Desktop keeps the spacious 28px from .card-pad-lg. */
  .js-sub-group.card-pad-lg { padding: 16px; }

  /* Hide the flex-spacer divs that justify-end "Bulk edit" + "Cancel"
     on desktop — on mobile they create a giant empty gap above the
     last button when the toolbar wraps. */
  .lz-flex-spacer { display: none; }

  /* Toolbar buttons — half-width per row so they line up in clean
     pairs instead of wrapping awkwardly. flex-grow:1 lets them
     stretch to fill any leftover space evenly. */
  .js-group-toolbar { gap: 6px; }
  .js-group-toolbar .btn {
    flex: 1 1 calc(50% - 6px);
    min-width: 0;
    justify-content: center;
  }

  /* Bulk-actions row: same half-width pattern. The "N selected"
     pill takes its own full-width row so the count is readable. */
  .js-bulk-actions { gap: 6px; }
  .js-bulk-actions .btn {
    flex: 1 1 calc(50% - 6px);
    min-width: 0;
    justify-content: center;
  }
  .js-bulk-actions .js-bulk-count-wrap {
    flex: 1 1 100%;
    text-align: center;
    order: -1;            /* "0 selected" reads above the buttons */
  }

  /* Add-UID form: stack input above the submit button, both full-width.
     The .row's existing flex layout cramped the input + "Add to this
     subscription →" button into one row at 393px viewports. */
  .js-add-uid-form .row {
    flex-direction: column;
    gap: 8px;
    align-items: stretch;
  }
  .js-add-uid-form .js-add-uid-input,
  .js-add-uid-form .js-add-uid-submit {
    width: 100%;
  }
}

/* Inline helpers */
.row { display: flex; gap: 12px; align-items: center; }
.col { display: flex; flex-direction: column; gap: 12px; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
.grid4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
/* Templates often override `.grid2 / .grid3 / .grid4` with inline
   `style="grid-template-columns:1.2fr 1fr"` for a designer-tuned
   desktop split. At 320 there's no room for two tracks of a card +
   gap; force single-column below 640 — !important is load-bearing to
   beat the inline style. */
@media (max-width: 640px) {
  .grid2,
  .grid3,
  .grid4 {
    grid-template-columns: 1fr !important;
  }
}
/* The legacy page wrappers (`<div style="padding:36px 40px; ...">`)
   eat 80px of horizontal real estate on every section — a quarter of
   a 320px viewport just for breathing room. Targeting the inline
   pattern lets us shrink it without rewriting every template; the
   attribute-substring match is precise enough that other inline
   `padding:` rules (e.g. 12px 14px, 28px 36px) are untouched. */
@media (max-width: 640px) {
  [style*="padding:36px 40px"],
  [style*="padding: 36px 40px"] {
    padding-inline: 16px !important;
  }
  [style*="padding:28px 36px"],
  [style*="padding: 28px 36px"] {
    padding-inline: 16px !important;
  }
  /* /autoredemption wraps its panels in <div style="padding:0 56px Xpx">
     (1280px-design column gutter). On 320 that's 112px of horizontal
     padding — eats the pricing grid down to ~150px and the FAQ /
     contact panels with it. Drop the inline horizontal gutter on
     mobile; vertical padding is preserved. */
  [style*="padding:0 56px"],
  [style*="padding: 0 56px"] {
    padding-inline: 0 !important;
  }
  /* /autoredemption hero uses `padding:48px 56px 24px` (the 56px
     gutter again, plus a chunky 48px top). Trim to 24px top + 0
     inline on mobile so the hero starts close to the nav and
     spans full content width. */
  [style*="padding:48px 56px"] {
    padding: 24px 0 16px !important;
  }
}
/* /autoredemption hero h1 is inline-styled `font: 800 44px/1.1` — at
   320 the title "Link your Player ID. We do the rest." renders 290px
   tall and pushes the next section ("What's included" eyebrow at
   y=600) below the fold of a 640px viewport. User has to scroll the
   whole hero before they see any pricing. Shrink to 28px on the
   smallest viewports so the lede + first section live above the fold. */
@media (max-width: 480px) {
  h1[style*="44px"] {
    font-size: 28px !important;
    line-height: 1.15 !important;
    letter-spacing: -0.02em !important;
  }
}
.gap-2 { gap: 6px; }
.eq { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
/* `.eq` is "two equal-priority blocks side-by-side" (eyebrow+heading on
   the left, secondary action/search on the right). At narrow widths
   the two blocks have no room — stack vertically so each gets the
   full content column. Mirrors the same intent as `.row` flex-wrap. */
@media (max-width: 640px) {
  .eq { flex-direction: column; align-items: stretch; }
}
/* Inline-flex search forms (taskstatus + admin/audit-log) hard-code
   an input width via `style="width:220px|240px"`. At 320 viewport the
   input + Search + Clear blow ~140px past the gutter, page does not
   h-scroll, search field becomes unreachable. Allow the form row to
   wrap and force the search input to full width below 640. */
@media (max-width: 640px) {
  form[style*="display:flex"] { flex-wrap: wrap !important; }
  form[style*="display:flex"] input[type="search"],
  form[style*="display:flex"] input[type="text"] {
    width: 100% !important;
    min-width: 0;
  }
}
.divider { height: 1px; background: var(--sf-border); margin: 14px 0; }
.muted { color: var(--sf-text-3); }
.dim  { color: var(--sf-text-2); }
.mono { font-family: var(--font-mono); }

/* Tags */
.tag-source {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 8px; border-radius: var(--r-pill); font: 600 10.5px/1.4 var(--font-sans);
  background: var(--sf-surface-2); border: 1px solid var(--sf-border); color: var(--sf-text-2);
}
.tag-source.tg::before { content:"✈"; color: #229ED9; }
.tag-source.yt::before { content:"▶"; color: #DC2626; }
.tag-source.dc::before { content:"⌘"; color: #5865F2; }
.tag-source.off::before { content:"●"; color: var(--p-600); }

/* ── Phase 19: Global subscription-expiry banner ──────────────────────
   Renders directly inside <body> when an active subscription is within
   7 days of expiry or post-end-at (within the pg_cron 7-day grace).
   Non-dismissible by design: presence IS the action prompt; the CTA
   button is the explicit response.  Skipped on /login, /logout,
   /auth/verify and the static endpoint by the context processor.
   CSS lives in lastz.css (not the inline <style> block) so that
   anonymous pages — which always include base.html's <head> — do not
   contain the 'lz-sub-banner' class name in their HTML output. */
.lz-sub-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 20px;
  font: 500 13.5px/1.5 var(--font-sans);
  border-bottom: 1px solid transparent;
}
.lz-sub-banner__msg { flex: 1 1 auto; }
.lz-sub-banner__cta {
  flex: 0 0 auto;
  padding: 6px 14px;
  border-radius: 6px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.lz-sub-banner--warning {
  background: var(--st-warn-bg, #FEF3C7);
  color: var(--st-warn, #92400E);
  border-bottom-color: var(--st-warn-border, #FCD34D);
}
.lz-sub-banner--warning .lz-sub-banner__cta {
  background: var(--st-warn, #92400E);
  color: #FFFFFF;
}
.lz-sub-banner--danger {
  background: var(--st-danger-bg, #FEE2E2);
  color: var(--st-danger, #991B1B);
  border-bottom-color: var(--st-danger-border, #FCA5A5);
}
.lz-sub-banner--danger .lz-sub-banner__cta {
  background: var(--st-danger, #991B1B);
  color: #FFFFFF;
}
@media (max-width: 560px) {
  .lz-sub-banner {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    padding: 10px 14px;
  }
}

/* ─── Phase 22: page alerts ─────────────────────────────────────────
   Sibling of .lz-sub-banner (Phase 19).  Stacks BELOW the subscription
   banner via document order in base.html.  Each alert is full-bleed,
   matches subscription-banner padding/typography for visual coherence.
   CSS lives in lastz.css (not the inline <style> block) for the same
   reason as .lz-sub-banner: keep anonymous-page HTML free of the
   class-name footprint when no active rows render. */
.lz-page-alerts {
  display: flex;
  flex-direction: column;
}
.lz-page-alert {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 20px;
  font: 500 13.5px/1.5 var(--font-sans);
  border-bottom: 1px solid transparent;
  box-sizing: border-box;
}
.lz-page-alert__msg { flex: 1 1 auto; min-width: 0; }
.lz-page-alert__msg strong { margin-right: 6px; }
.lz-page-alert__body { display: inline; }
.lz-page-alert__body p { display: inline; margin: 0; }
.lz-page-alert__body a { color: inherit; text-decoration: underline; }
.lz-page-alert__dismiss {
  flex: 0 0 auto;
  background: transparent;
  border: none;
  color: inherit;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: 4px;
}
.lz-page-alert__dismiss:hover { background: rgba(0, 0, 0, 0.08); }
.lz-page-alert--info {
  background: var(--st-info-bg, #DBEAFE);
  color: var(--st-info, #1E3A8A);
  border-bottom-color: var(--st-info-border, #93C5FD);
}
.lz-page-alert--success {
  background: var(--st-success-bg, #DCFCE7);
  color: var(--st-success, #166534);
  border-bottom-color: var(--st-success-border, #86EFAC);
}
.lz-page-alert--warning {
  background: var(--st-warn-bg, #FEF3C7);
  color: var(--st-warn, #92400E);
  border-bottom-color: var(--st-warn-border, #FCD34D);
}
.lz-page-alert--danger {
  background: var(--st-danger-bg, #FEE2E2);
  color: var(--st-danger, #991B1B);
  border-bottom-color: var(--st-danger-border, #FCA5A5);
}
@media (max-width: 560px) {
  .lz-page-alert {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    padding: 10px 14px;
  }
  .lz-page-alert__dismiss { align-self: flex-end; }
}

/* ── Page breadcrumb (back-to-parent link) ─────────────────────────
   Small inline link rendered above the eyebrow on sub-pages of a
   logical parent route. Currently used by /youtubers and /bots to
   route back to /community. Kept generic so future sub-pages can
   reuse the class with their own href. */
.lz-breadcrumb {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-bottom: 14px;
  font-size: 12.5px;
  color: var(--sf-text-3);
  text-decoration: none;
  transition: color 0.15s;
}
.lz-breadcrumb:hover {
  color: var(--p-700);
  text-decoration: underline;
}

/* ── /youtubers card grid ──────────────────────────────────────────
   YouTube-channel-style cards: gradient banner across the top, a
   round avatar overlapping the banner, channel title + @handle, a
   3-up stats grid (subs / videos / views) using the compactnum
   filter (181 → "181", 6646 → "6.6K", 1_300_000 → "1.3M"), and a
   red-play "Watch on YouTube" CTA at the bottom.

   The channel description is intentionally OMITTED — raw channel
   descriptions from the YouTube API are long blobs that overflow
   the card and dominate the visual hierarchy.  Metrics tell the
   story better. */
.lz-yt-grid {
  display: grid;
  /* P2-9: use min(320px, 100%) so the auto-fit floor doesn't create a
     phantom second column at viewport widths between ~340-639px (the
     pre-existing bug was minmax(320, 1fr) producing one 320px column
     plus a ~14px squashed pair for every card on narrow phones). */
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: 22px;
}
/* P2-9: when the last row holds exactly one orphan card in a 3-col
   layout, center it. Scoped to @≥1024 so it never fires on the 1- or
   2-col layouts where there's no orphan to center. */
@media (min-width: 1024px) {
  .lz-yt-grid > .lz-yt-card:last-child:nth-child(3n+1) {
    grid-column: 2 / 3;
  }
}
.lz-yt-card {
  position: relative;
  background: var(--sf-surface);
  border: 1px solid var(--sf-border);
  border-radius: 14px;
  overflow: hidden;
  padding: 0 0 18px;
  display: flex;
  flex-direction: column;
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.lz-yt-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 14px 30px -12px rgba(0, 0, 0, 0.28);
}
.lz-yt-card--promoted {
  border-color: var(--p-500);
  box-shadow: 0 0 0 1px var(--p-500) inset;
}
/* Top banner — gradient using the brand greens.  Decorative; the
   <div> has aria-hidden so screen readers skip it. */
.lz-yt-card__banner {
  height: 70px;
  background:
    radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.10), transparent 60%),
    linear-gradient(135deg, #15803D 0%, #22C55E 60%, #4ADE80 100%);
}
.lz-yt-card__promoted-pill {
  position: absolute;
  top: 10px;
  right: 10px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  color: #FACC15;
  font: 700 11px/1.2 var(--font-sans);
  letter-spacing: 0.02em;
  backdrop-filter: blur(2px);
  z-index: 2;
}
.lz-yt-card__avatar-wrap {
  /* Pull the avatar UP so it overlaps the banner / surface seam. */
  margin: -34px auto 0;
  width: 76px;
  height: 76px;
  display: grid;
  place-items: center;
}
.lz-yt-card__avatar {
  width: 76px;
  height: 76px;
  border-radius: 50%;
  object-fit: cover;
  border: 4px solid var(--sf-surface);
  background: var(--sf-surface-2);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.18);
}
.lz-yt-card__avatar--fallback {
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, #EF4444, #B91C1C);
  color: #fff;
  font: 800 24px/1 var(--font-sans);
}
.lz-yt-card__head {
  margin-top: 10px;
  text-align: center;
  padding: 0 18px;
}
.lz-yt-card__title {
  margin: 0;
  font-size: 17px;
  font-weight: 700;
  line-height: 1.25;
  color: var(--sf-text);
  /* P2-9: clamp to 2 lines instead of single-line ellipsis so titles
     like "DrAGoN Gaming" don't truncate mid-word ("DrAGoN G…"). */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
  /* Reserve the full 2-line slot for every title so cards equalise in
     height regardless of channel name length. Without this, a 1-line
     title card sits ~21px shorter than a wrapping-title card; on the
     single-column mobile grid every row's a different size, and on
     desktop the grid only stretches WITHIN a row so cross-row
     mismatches stay visible. The trade-off is ~21px of breathing room
     below single-line titles, sitting cleanly between the avatar and
     handle — reads as intentional spacing, not dead space. */
  min-height: calc(1.25em * 2);
}
.lz-yt-card__handle {
  margin-top: 2px;
  font-size: 12px;
  color: var(--sf-text-3);
}
.lz-yt-card__stats {
  margin: 16px 18px 0;
  display: grid;
  /* minmax(0, 1fr) — `1fr` resolves to `minmax(auto, 1fr)` which lets
     min-content widen a column when a label like "SUBSCRIBERS" is
     wider than its 1/3 share, producing visibly unequal columns
     (73/47/47 at 320). minmax(0,…) lets the columns collapse to true
     1/3-each and the label can wrap or ellipsize within its cell. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  background: var(--sf-surface-2);
  border: 1px solid var(--sf-border);
  border-radius: 10px;
  overflow: hidden;
}
.lz-yt-stat {
  padding: 10px 4px;
  text-align: center;
  border-right: 1px solid var(--sf-border);
}
.lz-yt-stat:last-child { border-right: none; }
.lz-yt-stat__num {
  font-size: 18px;
  font-weight: 700;
  color: var(--sf-text);
  line-height: 1.15;
}
.lz-yt-stat__label {
  margin-top: 2px;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--sf-text-3);
}
.lz-yt-card__cta {
  margin: 16px 18px 0;
  padding: 10px 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border-radius: 10px;
  background: var(--sf-surface-2);
  border: 1px solid var(--sf-border-strong, var(--sf-border));
  color: var(--sf-text);
  font-weight: 600;
  font-size: 13.5px;
  text-decoration: none;
  transition: background 0.15s, border-color 0.15s, transform 0.15s;
  margin-top: auto;
}
.lz-yt-card__cta:hover {
  background: var(--sf-surface);
  border-color: var(--p-500);
  transform: translateY(-1px);
}
.lz-yt-card__cta svg { flex-shrink: 0; }
/* `.lz-yt-card__cta` is a custom class, not `.btn-sm`, so the
   mobile-44px tap-target rule that lives on `.btn-sm` doesn't cover
   it. At 39px the CTA was below WCAG 2.5.5 (target size); bump to
   44px floor on phones to match every other interactive on the page. */
@media (max-width: 640px) {
  .lz-yt-card__cta { min-height: 44px; }
  /* Compact card chrome on phones — the desktop banner+avatar combo
     eats ~140px of vertical real estate before any content shows, and
     with 10+ cards on the page that's ~3000px of scroll. Shrink the
     banner from 70→50, the avatar from 76→60, and pull the avatar up
     ~22px (was 34) so it still overlaps the banner cleanly. Card
     drops from ~309px to ~275px tall — same density treatment we
     applied to /giftcodes live cards in 3aa943c. */
  .lz-yt-card__banner { height: 50px; }
  .lz-yt-card__avatar-wrap {
    width: 60px;
    height: 60px;
    margin: -22px auto 0;
  }
  .lz-yt-card__avatar {
    width: 60px;
    height: 60px;
    border-width: 3px;
  }
  .lz-yt-card__avatar--fallback {
    font-size: 20px;
  }
  /* Declutter stats on phones — the desktop look is an inset panel
     (background + border + per-cell border-right) inside an already-
     bordered card. At 64px-wide cells the uppercase letter-spaced
     labels read as visual noise and "Total views" wraps to 2 lines
     while siblings stay on 1, giving uneven row heights. On mobile,
     strip the panel chrome (no bg/border/dividers) so the stats sit
     directly on the card surface, and lowercase the labels with no
     letter-spacing so they read as data rather than UI labels. */
  .lz-yt-card__stats {
    margin: 10px 16px 0;
    background: transparent;
    border: none;
    border-radius: 0;
  }
  .lz-yt-stat {
    padding: 4px 0;
    border-right: none;
  }
  .lz-yt-stat__num { font-size: 16px; }
  .lz-yt-stat__label {
    margin-top: 1px;
    font-size: 10px;
    text-transform: none;
    letter-spacing: 0;
  }
}

/* ── Bots-page card grid ───────────────────────────────────────────
   Horizontal cards (logo-left, content-right) that fill the row well
   even when only 2 are listed.  Uses auto-fit so the grid scales
   from 1 to 2 columns depending on viewport, never stranding cards
   in a too-narrow column.

   Card structure:
     ┌─────────────────────────────────────────┐
     │ ┌──────┐  Title                          │
     │ │ logo │  description                    │
     │ │      │  [Visit website →]              │
     │ │      │  [coupon input] [Copy]          │
     │ └──────┘  cupon_text                     │
     └─────────────────────────────────────────┘

   Bot images in MAIN are typically logos / favicons (square-ish,
   sometimes transparent).  object-fit: contain inside a padded box
   shows the full logo at native aspect ratio instead of stretching. */
.lz-bots-grid {
  display: grid;
  /* P2-8: cap each card at 480px so two cards don't stretch across the
     whole frame at 1440 leaving a dead zone on either side, and cap the
     wrapper itself at 1000px + center it so the two-card row sits in
     the centre of the page instead of left-aligned. Min uses
     min(380px, 100%) so on viewports narrower than 380px the card
     shrinks instead of overflowing. */
  grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 480px));
  max-width: 1000px;
  margin-inline: auto;
  justify-content: center;
  gap: 18px;
}
.lz-bot-card {
  display: grid;
  grid-template-columns: 132px 1fr;
  background: var(--sf-surface);
  border: 1px solid var(--sf-border);
  border-radius: 12px;
  overflow: hidden;
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.lz-bot-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 22px -10px rgba(0,0,0,0.25);
}
.lz-bot-card__logo {
  display: grid;
  place-items: center;
  background: var(--sf-surface-2);
  padding: 14px;
  border-right: 1px solid var(--sf-border);
}
.lz-bot-card__logo img {
  max-width: 100%;
  max-height: 104px;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
}
.lz-bot-card__logo-fallback {
  width: 88px;
  height: 88px;
  border-radius: 16px;
  background: linear-gradient(135deg, #22C55E, #15803D);
  color: #fff;
  display: grid;
  place-items: center;
  font: 800 28px/1 var(--font-sans);
}
.lz-bot-card__body {
  padding: 14px 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}
.lz-bot-card__title {
  margin: 0;
  font-size: 16px;
  line-height: 1.2;
}
.lz-bot-card__desc {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--sf-text-2);
  /* Clamp to two lines so a verbose description doesn't blow up the
     card height relative to its neighbours. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.lz-bot-card__actions {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.lz-bot-card__visit {
  align-self: flex-start;
  text-decoration: none;
}
.lz-bot-card__coupon {
  display: flex;
  align-items: stretch;
}
.lz-bot-card__coupon-input {
  flex: 1;
  min-width: 0;
  padding: 6px 10px;
  border: 1px solid var(--sf-border);
  border-right: none;
  border-radius: 8px 0 0 8px;
  background: var(--sf-surface-2);
  color: var(--sf-text);
  font-size: 13px;
}
.lz-bot-card__coupon .js-bot-copy {
  border-radius: 0 8px 8px 0;
  white-space: nowrap;
}
.lz-bot-card__coupon-note {
  margin: 2px 0 0;
  font-size: 11.5px;
  color: var(--sf-text-3);
}
@media (max-width: 520px) {
  .lz-bot-card { grid-template-columns: 1fr; }
  .lz-bot-card__logo {
    border-right: none;
    border-bottom: 1px solid var(--sf-border);
    padding: 18px;
  }
  .lz-bot-card__logo img { max-height: 80px; }
}
/* Phones: the Visit button defaults to `align-self: flex-start` so it
   hugs its text (~108px wide), but on mobile the coupon row below it
   is full-width — the imbalance reads as a stunted CTA. Stretch it to
   the row so the stacked CTAs equalise, matching the pattern shipped
   in 7536697 for stacked .btn-lg / .actions > .btn rows. */
@media (max-width: 640px) {
  .lz-bot-card__visit { align-self: stretch; }
}

/* ── Site footer ────────────────────────────────────────────────────
   Ported from the design-canvas mockup. Single-row flex layout on
   wide screens; stacks vertically under 720px so labels keep readable
   width without overflowing.  Color tokens come from the theme so the
   footer matches light + dark without per-mode overrides. */
.lz-site-footer {
  border-top: 1px solid var(--sf-border);
  background: var(--sf-surface-2);
  padding: 18px 32px;
  /* `margin-block-start: auto` is the second half of the sticky-footer
     pattern (the first half is `body.ds-page { min-height:100vh;
     display:flex; flex-direction:column }` in design-system.css). Flex
     auto-margins consume the leftover space along the main axis, so a
     footer that's last in document flow gets pushed to the bottom of
     the viewport on short pages — and behaves normally (sits below
     content) on long pages. Falls back gracefully on browsers that
     don't understand logical properties (no UA without `margin-top` in
     2026), but using the logical property keeps RTL behavior correct. */
  margin-block-start: auto;
  font-size: 12px;
  color: var(--sf-text-3);
}
/* Phase 38 review #15: two-column desktop, single-column mobile.
   Left = copyright + non-affiliation disclaimer. Right = legal links
   + credit/flag. On mobile (≤640) the disclaimer + credit hide so
   the footer collapses to the bare legal nav + 1-line copyright. */
.lz-site-footer__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  flex-wrap: wrap;
  max-width: 1280px;
  margin: 0 auto;
}
.lz-site-footer__left,
.lz-site-footer__right {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.lz-site-footer__brand {
  /* Phase 38 review #27: footer brand link no longer competes with
     accent-green CTAs; uses default fg color and reveals as a link
     only on hover (dashed underline → solid). */
  color: var(--sf-text-2);
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px dashed transparent;
}
.lz-site-footer__brand:hover {
  color: var(--sf-text);
  border-bottom-style: solid;
  border-bottom-color: var(--sf-text-3);
}
.lz-site-footer__disclaimer { color: var(--sf-text-3); }
.lz-site-footer__credit {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  color: var(--sf-text-3);
}
.lz-site-footer__flag {
  display: inline-block;
  width: 20px;
  height: 14px;
  vertical-align: -2px;
  border-radius: 2px;
}
/* Tablet — keep two-column shape, drop disclaimer to second line. */
@media (max-width: 960px) {
  .lz-site-footer__inner { gap: 10px; }
  .lz-site-footer__left { gap: 8px; }
}
/* Mobile — minimal footer: copyright + legal nav, nothing else. */
@media (max-width: 640px) {
  .lz-site-footer { padding: 14px 18px; }
  .lz-site-footer__inner {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }
  .lz-site-footer__disclaimer,
  .lz-site-footer__credit { display: none; }
  .lz-site-footer__legal { gap: 12px; flex-wrap: wrap; }
  /* WCAG 2.5.5 floor on small text links — Privacy/Terms/Cookies/
     Manage/Support us rendered at 16×~40px each (12px font, no
     padding), way below the 44 tap target. Padding-only bump keeps
     the visual scale unchanged. */
  .lz-site-footer__legal a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-block: 6px;
  }
}

/* Phase 35 — footer legal navigation. Four links (Privacy / Terms /
   Cookie policy / Manage cookies) live to the left of the credit/flag
   on wide screens; wrap below the copyright row on narrow. */
.lz-site-footer__legal {
  display: inline-flex;
  align-items: center;
  gap: 14px;
}
.lz-site-footer__legal a {
  color: var(--sf-text-2);
  text-decoration: none;
  font-size: 12px;
}
.lz-site-footer__legal a:hover {
  color: var(--p-700);
  text-decoration: underline;
}
.dark .lz-site-footer__legal a:hover {
  color: var(--p-400);
}

/* Phase 39: footer "Support us" link drops its ♥ prefix — the heart
   was paired with the corner Ko-fi pill's heart label, but the
   corner pill is now cup-only (lz_cookie_consent.js sets the
   floating-chat text to empty), so the footer heart was a lone
   orphan. Keep the brand-green colour as the subtle nudge cue. */
.lz-site-footer__support {
  color: var(--p-600) !important;
}
.dark .lz-site-footer__support {
  color: var(--p-400) !important;
}

/* Phase 35 — orestbida/cookieconsent v3 theme glue. The vendor library
   ships its own structural CSS at vendor/cookieconsent/cookieconsent.css;
   we override only the brand-colour variables so the modal matches the
   dark surface tokens used by the rest of the site. Light mode uses the
   default variables; dark mode swaps to surface tokens. */
#cc-main {
  --cc-btn-primary-bg: var(--p-700);
  --cc-btn-primary-hover-bg: var(--p-800);
  --cc-btn-primary-color: #fff;
  --cc-btn-primary-hover-color: #fff;
  --cc-btn-secondary-bg: var(--sf-surface-2);
  --cc-btn-secondary-hover-bg: var(--sf-surface);
  --cc-btn-secondary-color: var(--sf-text);
  --cc-btn-secondary-hover-color: var(--sf-text);
  --cc-btn-secondary-border-color: var(--sf-border);
}
/* ═══════════════════════════════════════════════════════════════
   Phase 35 — Legal pages (Privacy / Terms / Cookies)
   Two-column shell with sticky ToC + structured prose body. Auto-
   numbered sections via CSS counters. Tightened tables, badges, and
   metadata chips for a document-grade reading experience.
   ═══════════════════════════════════════════════════════════════ */
.lz-legal-shell {
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 28px 60px;
}
.lz-legal-shell .lz-breadcrumb {
  padding: 18px 0 6px;
  font-size: 13px;
}
.lz-legal-shell .lz-breadcrumb a {
  color: var(--sf-text-2);
  text-decoration: none;
  padding: 4px 8px;
  margin-left: -8px;
  border-radius: 6px;
  transition: background 0.12s, color 0.12s;
}
.lz-legal-shell .lz-breadcrumb a:hover {
  background: var(--sf-surface-2);
  color: var(--sf-text);
}

/* ── Hero ───────────────────────────────────────────── */
.lz-legal-hero {
  padding: 14px 0 22px;
  border-bottom: 1px solid var(--sf-border);
  margin-bottom: 28px;
}
.lz-legal-hero__eyebrow {
  font: 700 11px/1 var(--font-sans);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--p-500);
  margin-bottom: 10px;
}
.lz-legal-hero__title {
  font: 800 38px/1.15 var(--font-sans);
  letter-spacing: -0.02em;
  color: var(--sf-text);
  margin: 0 0 18px;
}
.lz-legal-hero__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}
.lz-legal-meta-chip {
  display: inline-flex;
  gap: 8px;
  align-items: baseline;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--sf-surface-2);
  border: 1px solid var(--sf-border);
  font: 500 13px/1.2 var(--font-sans);
  color: var(--sf-text);
}
.lz-legal-meta-chip__label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--sf-text-2);
  font-weight: 600;
}
.lz-legal-meta-chip time {
  font-variant-numeric: tabular-nums;
}
.lz-legal-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 11px;
  border-radius: 999px;
  font: 700 11px/1 var(--font-sans);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.lz-legal-badge--gdpr {
  background: rgba(34, 197, 94, 0.10);
  color: var(--st-success, #15803d);
  border: 1px solid rgba(34, 197, 94, 0.30);
}
html.dark .lz-legal-badge--gdpr {
  background: rgba(34, 197, 94, 0.15);
  color: #4ade80;
  border-color: rgba(34, 197, 94, 0.40);
}
.lz-legal-badge--draft {
  background: rgba(234, 179, 8, 0.10);
  color: #a16207;
  border: 1px solid rgba(234, 179, 8, 0.30);
}
html.dark .lz-legal-badge--draft {
  background: rgba(234, 179, 8, 0.15);
  color: #fcd34d;
  border-color: rgba(234, 179, 8, 0.40);
}

/* ── Layout grid ────────────────────────────────────── */
.lz-legal-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 28px;
  align-items: start;
}
@media (min-width: 960px) {
  .lz-legal-layout {
    grid-template-columns: 248px minmax(0, 1fr);
    gap: 40px;
  }
}

/* ── ToC sidebar ────────────────────────────────────── */
.lz-legal-toc {
  background: var(--sf-surface);
  border: 1px solid var(--sf-border);
  border-radius: 12px;
  padding: 16px 14px;
  font-size: 13px;
}
/* Below 960 the .lz-legal-mini-toc <details> block handles ToC duty;
   hide the desktop sidebar so its grid-min-content shape (long ToC
   labels force min-width past viewport on /privacy + /cookies) doesn't
   push a 420-435px column onto a 320px viewport. */
@media (max-width: 959px) {
  .lz-legal-toc {
    display: none;
  }
}
@media (min-width: 960px) {
  .lz-legal-toc {
    position: sticky;
    top: 80px;
    max-height: calc(100vh - 100px);
    overflow-y: auto;
  }
}
.lz-legal-toc__title {
  font: 700 11px/1 var(--font-sans);
  text-transform: uppercase;
  letter-spacing: 0.10em;
  color: var(--sf-text-2);
  padding: 4px 8px 10px;
  border-bottom: 1px solid var(--sf-border);
  margin-bottom: 8px;
}
.lz-legal-toc__list {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: lz-toc;
}
.lz-legal-toc__list li {
  counter-increment: lz-toc;
}
.lz-legal-toc__link {
  display: block;
  padding: 8px 10px 8px 30px;
  color: var(--sf-text-2);
  text-decoration: none;
  border-radius: 6px;
  font-size: 13px;
  line-height: 1.35;
  position: relative;
  transition: background 0.12s, color 0.12s;
}
.lz-legal-toc__link::before {
  content: counter(lz-toc);
  position: absolute;
  left: 8px;
  top: 8px;
  width: 18px;
  font: 700 11px/1.35 var(--font-mono);
  color: var(--sf-text-3, var(--sf-text-2));
  opacity: 0.7;
  text-align: right;
}
.lz-legal-toc__link:hover {
  background: var(--sf-surface-2);
  color: var(--sf-text);
}
.lz-legal-toc__link:hover::before { opacity: 1; color: var(--p-500); }
/* Phase 38 review #20 — scrollspy current state. JS sets .is-current
   on the link whose section is closest to the viewport top. */
.lz-legal-toc__link.is-current {
  background: color-mix(in srgb, var(--accent) 12%, var(--sf-surface-2));
  color: var(--sf-text);
  font-weight: 600;
}
.lz-legal-toc__link.is-current::before {
  opacity: 1;
  color: var(--accent);
}

/* Phase 38 review #32 — inline "Manage cookie preferences" button
   rendered inside the /privacy markdown body. Visually a button but
   structurally an anchor so it remains keyboard-activatable when
   the consent JS is unloaded. */
.lz-inline-cookie-btn {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 8px;
  padding: 8px 14px;
  border-radius: var(--r-md);
  background: var(--sf-surface-2);
  border: 1px solid var(--sf-border-strong);
  color: var(--sf-text) !important;
  text-decoration: none !important;
  font-size: 13px;
  font-weight: 600;
}
.lz-inline-cookie-btn:hover {
  background: color-mix(in srgb, var(--accent) 10%, var(--sf-surface-2));
  border-color: var(--accent);
  color: var(--accent) !important;
}
.lz-inline-cookie-btn::before {
  content: "⚙";
  display: inline-block;
}

/* Phase 38 review #20 — mobile mini-ToC (collapsed by default).
   Hidden on desktop (sidebar carries the load); on tablet/mobile
   it sits between the header and the body so anchored sections
   are reachable without scrolling past the prose. */
.lz-legal-mini-toc { display: none; }
@media (max-width: 959px) {
  .lz-legal-mini-toc {
    display: block;
    margin: 0 0 18px;
    background: var(--sf-surface);
    border: 1px solid var(--sf-border);
    border-radius: 10px;
    padding: 0;
  }
}
.lz-legal-mini-toc__summary {
  list-style: none;
  cursor: pointer;
  padding: 10px 14px;
  font: 600 13px/1.4 var(--font-sans);
  color: var(--sf-text-2);
  display: flex; align-items: center; gap: 8px;
}
.lz-legal-mini-toc__summary::-webkit-details-marker { display: none; }
.lz-legal-mini-toc__chev {
  display: inline-block;
  transition: transform 180ms ease;
  color: var(--sf-text-3);
}
.lz-legal-mini-toc[open] .lz-legal-mini-toc__chev { transform: rotate(90deg); }
.lz-legal-mini-toc__count { color: var(--sf-text-3); font-weight: 400; }
.lz-legal-mini-toc__list {
  list-style: none;
  margin: 0;
  padding: 4px 8px 10px 8px;
  border-top: 1px solid var(--sf-border);
}
.lz-legal-mini-toc__list li { padding: 0; }
.lz-legal-mini-toc__list a {
  display: block;
  padding: 7px 10px;
  font-size: 13px;
  color: var(--sf-text-2);
  text-decoration: none;
  border-radius: 6px;
}
.lz-legal-mini-toc__list a:hover {
  background: var(--sf-surface-2);
  color: var(--sf-text);
}
.lz-legal-toc__cta {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--sf-border);
}
.lz-legal-toc__cta-link {
  display: block;
  padding: 8px 10px;
  font-size: 12px;
  color: var(--p-500);
  text-decoration: none;
  border-radius: 6px;
  transition: background 0.12s, color 0.12s;
}
.lz-legal-toc__cta-link:hover {
  background: var(--sf-surface-2);
  color: var(--p-400, var(--p-500));
}

/* ── Body prose ─────────────────────────────────────── */
.lz-legal-body {
  font-size: 15px;
  line-height: 1.7;
  color: var(--sf-text);
  max-width: 72ch;
  counter-reset: lz-sec;
  scroll-behavior: smooth;
  /* Without `min-width: 0` a wide child element (markdown <table>, long
     unbroken URL, etc.) forces the grid track's `auto` minimum past
     viewport width, dragging the whole article off-screen at 320. */
  min-width: 0;
}
.lz-legal-body > h2:first-child { margin-top: 0; }
.lz-legal-body h2 {
  font: 800 22px/1.25 var(--font-sans);
  letter-spacing: -0.01em;
  margin: 44px 0 14px;
  padding-top: 10px;
  color: var(--sf-text);
  counter-reset: lz-subsec;
  scroll-margin-top: 80px;
  display: flex;
  align-items: baseline;
  gap: 14px;
}
.lz-legal-body h2::before {
  counter-increment: lz-sec;
  content: counter(lz-sec);
  font: 800 14px/1 var(--font-mono);
  color: var(--p-500);
  background: var(--sf-surface-2);
  border: 1px solid var(--sf-border);
  border-radius: 7px;
  padding: 8px 10px;
  flex: 0 0 auto;
  letter-spacing: 0;
}
.lz-legal-body h3 {
  font: 700 16px/1.4 var(--font-sans);
  margin: 26px 0 8px;
  color: var(--sf-text);
  scroll-margin-top: 80px;
}
.lz-legal-body h3::before {
  counter-increment: lz-subsec;
  content: counter(lz-sec) "." counter(lz-subsec) "  ";
  color: var(--sf-text-2);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.lz-legal-body h4 {
  font: 700 13.5px/1.35 var(--font-sans);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 18px 0 6px;
  color: var(--sf-text-2);
}
.lz-legal-body p {
  margin: 0 0 14px;
}
.lz-legal-body ul,
.lz-legal-body ol {
  margin: 0 0 14px;
  padding-inline-start: 22px;
}
.lz-legal-body li {
  margin-bottom: 6px;
}
.lz-legal-body a {
  color: var(--p-700);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.dark .lz-legal-body a { color: var(--p-400); }
.lz-legal-body a:hover { text-decoration-thickness: 2px; }
.lz-legal-body code {
  background: var(--sf-surface-2);
  border: 1px solid var(--sf-border);
  border-radius: 4px;
  padding: 1px 5px;
  font: 500 12.5px/1 var(--font-mono);
}
.lz-legal-body pre {
  background: var(--sf-surface-2);
  border: 1px solid var(--sf-border);
  border-radius: 6px;
  padding: 10px 12px;
  overflow-x: auto;
  font: 500 12.5px/1.5 var(--font-mono);
  margin: 0 0 14px;
}
.lz-legal-body blockquote {
  margin: 0 0 14px;
  padding: 10px 16px;
  border-inline-start: 3px solid var(--p-500);
  background: var(--sf-surface-2);
  color: var(--sf-text-2);
  border-radius: 0 6px 6px 0;
  font-style: italic;
}
.lz-legal-body hr {
  border: 0;
  border-top: 1px solid var(--sf-border);
  margin: 24px 0;
}
.lz-legal-body table {
  width: 100%;
  border-collapse: collapse;
  margin: 6px 0 16px;
  font-size: 13.5px;
  /* `display: block` removes the table's intrinsic min-content
     contribution to the grid track so a long cell stops dragging the
     whole article off-screen at narrow widths. The table still
     renders rows + columns; only its outer flow box changes. */
  display: block;
  overflow-x: auto;
  max-width: 100%;
}
/* Phase 39 — on phones the 3-column markdown tables in legal pages
   (Categories / Why / Who / Retention) shrunk each column to ~80px
   and wrapped every cell mid-word ("phone.email/Privacy/Policy"
   stacking on one word per line). Convert tables to a card-list
   layout below 560: hide the thead, stack each row as a bordered
   card, render the first cell as a bold title with the remaining
   cells flowing as labelled paragraphs underneath. */
@media (max-width: 560px) {
  .lz-legal-body table,
  .lz-legal-body tbody,
  .lz-legal-body tr,
  .lz-legal-body td {
    display: block;
    width: 100%;
  }
  .lz-legal-body table {
    overflow: visible;
    border: 0;
  }
  .lz-legal-body thead {
    display: none;
  }
  .lz-legal-body tr {
    border: 1px solid var(--sf-border);
    border-radius: 8px;
    background: var(--sf-surface-2);
    padding: 12px 14px;
    margin-bottom: 10px;
  }
  .lz-legal-body td {
    padding: 4px 0;
    border-bottom: 0;
    line-height: 1.55;
  }
  .lz-legal-body td:first-child {
    font-weight: 700;
    color: var(--sf-text);
    font-size: 14px;
    padding-bottom: 6px;
    margin-bottom: 4px;
    border-bottom: 1px solid var(--sf-border);
  }
  .lz-legal-body td:not(:first-child) {
    color: var(--sf-text-2);
    font-size: 13px;
  }
}
.lz-legal-body th,
.lz-legal-body td {
  text-align: start;
  padding: 8px 10px;
  border-bottom: 1px solid var(--sf-border);
  vertical-align: top;
}
.lz-legal-body thead th {
  background: var(--sf-surface-2);
  font-weight: 700;
  color: var(--sf-text);
  border-bottom: 1px solid var(--sf-border);
}
/* Markdown's toc extension auto-injects header anchor link spans —
   keep them invisible-but-accessible (focusable on keyboard nav). */
.lz-legal-body .toclink,
.lz-legal-body .headerlink { color: transparent; }
.lz-legal-body h2:hover .headerlink,
.lz-legal-body h3:hover .headerlink { color: var(--sf-text-2); }

/* Inline emphasis pulled out of the prose so it reads more like a
   handbook than a wall of paragraphs. */
.lz-legal-body strong {
  font-weight: 700;
  color: var(--sf-text);
}

/* ── Foot block ─────────────────────────────────────── */
.lz-legal-foot {
  margin-top: 56px;
  padding: 22px 24px;
  background: var(--sf-surface);
  border: 1px solid var(--sf-border);
  border-radius: 12px;
  font-size: 13.5px;
  color: var(--sf-text-2);
  line-height: 1.55;
}
.lz-legal-foot p { margin: 0 0 8px; }
.lz-legal-foot p:last-child { margin-bottom: 0; }
.lz-legal-foot a {
  color: var(--p-500);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
html.dark .lz-legal-foot a { color: var(--p-400); }
.lz-legal-foot__related {
  font-size: 12.5px;
  padding-top: 10px;
  border-top: 1px solid var(--sf-border);
  margin-top: 10px;
}
.lz-legal-foot__dot {
  margin: 0 8px;
  color: var(--sf-border);
}

html.dark #cc-main {
  --cc-bg: var(--sf-surface);
  --cc-primary-color: var(--sf-text);
  --cc-secondary-color: var(--sf-text-2);
  --cc-separator-border-color: var(--sf-border);
  --cc-toggle-bg-off: var(--sf-surface-2);
  --cc-toggle-bg-on: var(--p-700);
  --cc-toggle-bg-readonly: var(--sf-surface-2);
  --cc-toggle-knob-bg: var(--sf-surface);
  --cc-toggle-knob-icon-color: var(--sf-text);
  --cc-section-category-border: var(--sf-border);
  --cc-cookie-category-block-bg: var(--sf-surface-2);
  --cc-cookie-category-block-border: var(--sf-border);
  --cc-cookie-category-block-bg-hover: var(--sf-surface);
  --cc-cookie-category-block-border-hover: var(--sf-border);
  --cc-cookie-category-expanded-block-bg: var(--sf-surface-2);
  --cc-overlay-bg: rgba(0, 0, 0, 0.55);
  --cc-footer-bg: var(--sf-surface-2);
  --cc-footer-color: var(--sf-text-2);
  --cc-footer-border-color: var(--sf-border);
}

/* ═══════════════════════════════════════════════════════════════
   PHASE 37 — design-system foundation
   ─────────────────────────────────────────────────────────────
   Everything below this header is the Phase 37 rewrite. New
   patterns (.lz-shell, refreshed components, drawer nav, widget
   auto-hide) live here so the canvas's pre-existing styles above
   stay intact as a reference + safety-net. Wave 4/5 template
   polish will gradually retire pre-Phase-37 rules.
   ═══════════════════════════════════════════════════════════════ */

/* ─── .lz-shell ─ centered page container (max-width 1280px) ──
   AUDIT.md category A — every page now wraps content in
   <main class="lz-shell"> via base.html. Padding-inline scales
   fluidly via clamp(16→32) so phone gutters shrink while desktop
   keeps room. Footer keeps its own centered inner via the
   pre-existing .lz-site-footer__inner rule. */
.lz-shell {
  box-sizing: border-box;
  max-width: 1280px;
  width: 100%;
  min-width: 0;
  margin-inline: auto;
  padding-inline: var(--sp-shell-gutter);
  overflow-x: hidden;  /* Last-resort guard: long unbreakable content
                          inside the shell cannot push the page beyond
                          viewport width. */
}

/* Hero matches the fluid horizontal gutter used by the rest of the
   home page (Active codes wrapper at 48px, How-it-works at the same
   clamp). Without this the hero content sits flush against the
   .lz-frame edge while every following section indents 48px, leaving
   the hero "squished left" relative to the page. */
.lz-shell .lz-hero {
  padding-block: var(--sp-hero-y);
  padding-inline: clamp(20px, 4vw, 48px);
}

/* Lede max-width converted from 52ch (~525px) to 60ch + relative
   units so it grows with type rather than capping at a fixed px.
   Audit category L — hero was authored for one viewport. */
.lz-shell .lz-hero .lede {
  max-width: 60ch;
  font-size: var(--fz-body);
  line-height: var(--lh-body);
}

/* ─── Type-scale token overrides ─────────────────────────────
   Hero h1 was 60px/1.02 at every viewport — looked like a stacked
   one-word-per-line column on 320 phones, and small on 4K. Token
   replacement: clamp(32, 1.4rem+3vw, 64). Two-line "Outlast the
   dead. Claim every reward." now scales smoothly across breakpoints.
   AUDIT.md categories C + L. */
.lz-shell .lz-hero h1 {
  font-size: var(--fz-h1);
  line-height: var(--lh-tight);
}

/* Phase 39 — /contact email card declutters on phones. Three flex
   children (icon | body | Copy) squeezed each other on 320: the
   body shrunk to fit ~120px, masked email truncated mid-tail,
   blurb wrapped into 2-3 lines, Copy clung to the right edge. On
   mobile, switch to a stacked layout: icon + h3 + (Copy if fits)
   on the first row, then the masked address and blurb each get
   their own row + the Copy button drops below as a full-row CTA. */
@media (max-width: 480px) {
  .lz-channel-card--email {
    flex-wrap: wrap;
    row-gap: 8px;
  }
  .lz-channel-card--email .lz-channel-card__body {
    flex: 1 1 calc(100% - 54px);  /* full row minus the 40px icon + 14px gap */
    min-width: 0;
  }
  .lz-channel-card--email .lz-channel-card__addr {
    /* Wrap mid-string instead of clipping. The masked address
       (e.g. `l●●●●●●●…`) is short enough to fit one line at 13px
       on a 240-wide body column. */
    overflow: visible;
    white-space: normal;
    word-break: break-all;
    font-size: 12px;
  }
  .lz-channel-card--email .lz-channel-card__copy {
    flex: 1 1 100%;
    justify-content: center;
    min-height: 44px;
  }
}

/* Phase 39 — mobile type-scale floor. The token clamps already
   scale h1/h2/h3 up on wider viewports, but their lower bound
   (32/26/20px) is calibrated for tablets and looked too heavy on
   phones — multi-line h1s ate 100-130px of vertical real estate
   on 320, pushing the lede + first CTA below the fold on
   /community, /quick-claim, /add-gift-code, and home. Pull the
   floor down on the smallest screens; mid-size phones (481-640)
   get a softer step. */
@media (max-width: 480px) {
  :root {
    --fz-h1: 26px;
    --fz-h2: 21px;
    --fz-h3: 17px;
  }
}
@media (min-width: 481px) and (max-width: 640px) {
  :root {
    --fz-h1: 30px;
    --fz-h2: 24px;
    --fz-h3: 18px;
  }
}

/* Hero row was a 2-column class-based grid (1.2fr / 1fr) — doesn't
   match Phase 36-3's grid2/3/4 / inline-style catch-all. Add explicit
   collapse so the second column stacks below the first at <=720. */
.lz-shell .lz-hero .hero-row {
  gap: var(--sp-section);
}
@media (max-width: 720px) {
  .lz-shell .lz-hero .hero-row {
    grid-template-columns: 1fr !important;
    gap: var(--sp-4);
  }
}

/* ─── Section heading scale ──────────────────────────────────
   Pages use either <h2> or .t-h1/.t-h2 classes. Override both. */
.lz-shell h1 { font-size: var(--fz-h1); line-height: var(--lh-tight); letter-spacing: -0.035em; }
.lz-shell h2 { font-size: var(--fz-h2); line-height: var(--lh-snug); letter-spacing: -0.025em; }
.lz-shell h3 { font-size: var(--fz-h3); line-height: var(--lh-snug); letter-spacing: -0.015em; }
.lz-shell .t-h1 { font-size: var(--fz-h1); line-height: var(--lh-tight); }
.lz-shell .t-h2 { font-size: var(--fz-h2); line-height: var(--lh-snug); }
.lz-shell .t-h3 { font-size: var(--fz-h3); line-height: var(--lh-snug); }
/* P2-11: distribute words across lines evenly so short two-word
   fragments don't orphan ('every reward.' on /login was the trigger
   case). Modern browsers honour text-wrap:balance; older browsers
   silently fall back to normal wrapping. Scoped to h1 + .t-h1 since
   h2/h3 rarely orphan. */
.lz-shell h1, .lz-shell .t-h1 { text-wrap: balance; }

/* KPI tile numbers — these are the big "9 / 1,939 / 781" stats on
   home. Convert from fixed px to --fz-data so they scale with
   viewport. The KPI tile pattern lives in templates as
   <div class="card"> > <div class="big">9</div>. */
.lz-shell .card .big,
.lz-shell .card .kpi-value,
.lz-shell .kpi-tile__value {
  font-family: var(--font-mono);
  font-size: var(--fz-data);
  line-height: var(--lh-data);
  font-feature-settings: 'tnum';
  letter-spacing: -0.02em;
}

/* ─── Mobile drawer ─────────────────────────────────────────
   At ≤960px the .lz-asidenav admin sidebar collapses; at ≤720px the
   public nav collapses too. Both replaced by a left-slide drawer
   driven by lz_drawer.js. Shared DOM in base.html, shared JS, shared
   styles below. Replaces the Phase 36-3 horizontal scroll-strip
   (AUDIT.md G + H: cramped, lost spatial memory, no group headers). */
.lz-drawer-toggle {
  display: none;
  background: transparent;
  border: 0;
  padding: var(--sp-2);
  color: var(--sf-text);
  cursor: pointer;
  border-radius: var(--r-sm);
  align-items: center;
  justify-content: center;
}
.lz-drawer-toggle:hover { background: var(--bg-surface-2); }
.lz-drawer-toggle--floating {
  display: none;
  position: fixed;
  top: 14px;
  inset-inline-start: 14px;
  z-index: 999;
  width: 40px;
  height: 40px;
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  box-shadow: var(--sh-1);
}
.lz-drawer-toggle .lz-burger {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 22px;
}
.lz-drawer-toggle .lz-burger span {
  display: block;
  width: 100%;
  height: 2px;
  background: currentColor;
  border-radius: 1px;
  transition: transform var(--dur-fast) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out);
}
.lz-drawer-host {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;
  width: min(320px, 86vw);
  background: var(--bg-page);
  border-inline-end: 1px solid var(--border-default);
  box-shadow: 18px 0 40px rgba(0, 0, 0, 0.5);
  padding: var(--sp-5) var(--sp-3);
  overflow-y: auto;
  z-index: 1001;
  transform: translateX(-100%);
  transition: transform var(--dur-mid) var(--ease-out);
  visibility: hidden;
}
.lz-drawer-backdrop {
  /* P2-14: display:none in the closed state so the fullscreen-fixed
     overlay isn't in the layer tree at all when no one's looking at the
     drawer. visibility/opacity-only kept it paintable + element-tree
     visible (showed up as a clickable overlay in DevTools). */
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  z-index: 1000;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-mid) var(--ease-out);
}
body.lz-drawer-open .lz-drawer-host {
  transform: translateX(0);
  visibility: visible;
}
body.lz-drawer-open .lz-drawer-backdrop {
  display: block;
  opacity: 1;
  pointer-events: auto;
}
body.lz-drawer-open { overflow: hidden; }

.lz-drawer-host__brand {
  font-weight: 700;
  font-size: var(--fz-body);
  letter-spacing: -0.01em;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-4);
  color: var(--sf-text);
  text-decoration: none;
}
.lz-drawer-host__brand-mark {
  width: 24px; height: 24px; border-radius: 6px;
  background: linear-gradient(135deg, var(--p-400), var(--p-700));
  flex-shrink: 0;
}
.lz-drawer-group { margin-bottom: var(--sp-4); }
.lz-drawer-group__label {
  font-size: var(--fz-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
  padding: var(--sp-2) var(--sp-3);
  opacity: 0.85;
  font-weight: 600;
}
.lz-drawer-host a {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3);
  font-size: var(--fz-body);
  color: var(--sf-text-2);
  text-decoration: none;
  border-radius: var(--r-md);
  min-height: 44px;
  transition: background var(--dur-fast) var(--ease-snap),
              color var(--dur-fast) var(--ease-snap);
}
.lz-drawer-host a.active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}
.lz-drawer-host a:hover { background: var(--bg-surface-2); color: var(--sf-text); }
.lz-drawer-host a .glyph { font-size: var(--fz-body); width: 1.2em; text-align: center; opacity: 0.7; }

.lz-drawer-host__account {
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border-default);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.lz-drawer-host__account .acct {
  flex: 1;
  font-size: var(--fz-small);
  color: var(--sf-text-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ─── Mobile rules: collapse desktop nav, show drawer toggle ── */
@media (max-width: 720px) {
  /* Hide existing scroll-strip public-nav at <=720, show hamburger.
     `:not(.brand)` excludes the brand link from the hide rule so the
     logo stays visible on the left — the old rule was catching it as
     'just another anchor' and the brand collapsed to width:0 on every
     mobile load. */
  .lz-nav > a:not(.brand),
  .lz-nav > .acct,
  .lz-nav > .spacer {
    display: none !important;
  }
  .lz-nav {
    overflow: visible !important;
    flex-wrap: nowrap;
    padding: var(--sp-3) var(--sp-4);
    gap: var(--sp-3);
    /* Vertically center the brand text against the icon cluster — at
       desktop the natural ascender alignment is fine, on mobile the
       brand mark + text vs the icon buttons need explicit centering. */
    align-items: center;
  }
  /* Flex order: brand on the left, burger as the rightmost item, theme
     toggle immediately to its left so they read as a top-right cluster.
     margin-inline-end:auto on the brand pushes the remaining items to
     the right edge of the nav. */
  .lz-nav .brand          { flex-shrink: 0; margin-inline-end: auto; order: 0; }
  .lz-nav .theme-toggle   { order: 1; }
  .lz-nav .lz-drawer-toggle { display: inline-flex; order: 2; }
  /* Tighten the gap inside the right cluster — theme + burger should
     read as one widget, not two evenly-spaced nav items. */
  .lz-nav .theme-toggle + .lz-drawer-toggle,
  .lz-nav .lz-drawer-toggle + .theme-toggle {
    margin-inline-start: -4px;
  }
}
@media (max-width: 960px) {
  /* Admin sidebar: hide the navigation + grouping items, show drawer toggle */
  .lz-asidenav nav,
  .lz-asidenav .head,
  .lz-asidenav .foot {
    display: none;
  }
  .lz-asidenav { display: none; }  /* Suppress the whole 220px column */
  .lz-admin {
    grid-template-columns: 1fr !important;
  }
  .lz-admin-head .lz-drawer-toggle { display: inline-flex; }
  /* Reveal the floating hamburger on admin pages at this breakpoint.
     Push admin-head content right so title doesn't sit under it. */
  .lz-drawer-toggle--floating { display: inline-flex; }
  .lz-admin-head { padding-inline-start: 64px; }
  .lz-admin-body { padding-inline-start: var(--sp-4); }
}

/* ─── Sticky first column on admin tables (mobile) ─────────────
   AUDIT.md category F: tables wider than viewport scroll horizontally
   (Phase 36-3 mechanic) but operator loses track of which row they're
   acting on when action buttons are at the right edge. Sticky first
   column pins the row identity (nickname / id / code) while the data
   columns scroll. */
@media (max-width: 960px) {
  .lz-admin-body table.tbl th:first-child,
  .lz-admin-body table.tbl td:first-child {
    position: sticky;
    inset-inline-start: 0;
    background: var(--sf-surface);
    z-index: 1;
    box-shadow: 4px 0 6px -4px rgba(0, 0, 0, 0.35);
  }
  .lz-admin-body table.tbl th:first-child {
    background: var(--sf-surface-2);
    z-index: 2;
  }

  /* Visible scrollbar + right-edge fade hint so operators see that
     the table scrolls horizontally. Mobile auto-hides native
     scrollbars by default, leaving no affordance. */
  .lz-admin-body .card:has(> table.tbl) {
    /* Always-visible thin scrollbar */
    scrollbar-width: thin;
    scrollbar-color: var(--sf-border-strong) transparent;
    /* Hint that there's more to the right */
    position: relative;
    background-image:
      linear-gradient(to left, var(--bg-page), transparent 24px);
    background-attachment: local, scroll;
    background-repeat: no-repeat;
    background-position: right center;
    background-size: 32px 100%;
  }
  /* WebKit scrollbar styling — show on iOS Safari too */
  .lz-admin-body .card:has(> table.tbl)::-webkit-scrollbar {
    height: 8px;
    -webkit-appearance: none;
  }
  .lz-admin-body .card:has(> table.tbl)::-webkit-scrollbar-thumb {
    background: var(--sf-border-strong);
    border-radius: 4px;
  }
  .lz-admin-body .card:has(> table.tbl)::-webkit-scrollbar-track {
    background: transparent;
  }
}

/* ─── Form controls width clamp ──────────────────────────────
   <textarea> defaults to cols=20 → intrinsic min-width ~355px which
   forces the parent shell to expand past viewport on mobile. Cap
   every input/select/textarea inside .lz-shell to its parent width;
   on mobile bump the font-size to 16px to prevent iOS auto-zoom. */
.lz-shell input,
.lz-shell select,
.lz-shell textarea,
.lz-shell pre,
.lz-shell code {
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
}
.lz-shell textarea {
  width: 100%;
}
.lz-shell pre {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
@media (max-width: 640px) {
  .lz-shell input[type='text'],
  .lz-shell input[type='email'],
  .lz-shell input[type='tel'],
  .lz-shell input[type='password'],
  .lz-shell input[type='number'],
  .lz-shell input[type='search'],
  /* Inputs without an explicit `type=` default to type="text" but the
     attribute selector won't match them (dashboard's Add-Player-ID
     input is one such — left bare to give the JS validator full
     control over inputmode/pattern). Match them explicitly. */
  .lz-shell input:not([type]),
  .lz-shell select,
  .lz-shell textarea {
    font-size: 16px;  /* prevent iOS auto-zoom on focus */
    min-height: 44px; /* tap-target floor */
  }
}

/* ─── Phase 37 button polish ────────────────────────────────
   The canvas .btn rules in design-system.css are token-driven and
   solid. We add: (a) mobile tap-target floor 44x44 per WCAG, (b) a
   subtle glow on .btn-primary so the primary action reads as
   "elevated" against the surface. */
@media (max-width: 768px) {
  .btn { min-height: 44px; }
  /* Phase 38 follow-up: WCAG 2.5.5 floors tap targets at 44×44. The
     previous 36px floor here saved a small visual notch on dense
     action rows (Copy / Check / Search) but left ~50 hot buttons
     sub-target on /, /quick-claim, /giftcodes, /contact, /dashboard,
     and admin action rows. Bump to 44 — the visual budget gain wasn't
     worth the accessibility loss. */
  .btn-sm { min-height: 44px; }
}
.btn-primary {
  box-shadow: 0 4px 14px rgba(101, 163, 13, 0.18);
}
.dark .btn-primary {
  box-shadow: 0 4px 14px rgba(163, 230, 53, 0.22);
}
.btn-primary:active { transform: translateY(1px); }

/* Phase 39 — auto-hide-on-scroll for Crisp + Ko-fi widgets retired.
   User feedback: Crisp icon disappeared on scroll and Ko-fi appeared
   to "float everywhere" as the hide/reveal animation chased the
   scroll direction. Pin them statically (Ko-fi already at
   right:16/bottom:88 via P2-13; Crisp owns bottom:16/right:16) and
   trust the user not to mind a sticky help icon. The
   `lz_widget_autohide.js` script reference was removed from base.html
   in the same commit; the .js file is left on disk for now in case we
   want to revive it as an intent-driven (idle-timer + cookie-banner)
   hide later. */

/* ─── REVIEW-PASS-2 P2-4 — corner-stack floating widgets ─────
   Ko-fi's overlay-widget.js positions .floatingchat-container-wrap
   bottom-left by default, which a) overlaps the Pro pricing tile on
   /autoredemption at ≥1280px and b) sits over body copy on mobile.
   Crisp owns the bottom-right corner (.cc-13wro at bottom:16 /
   right:16). Stack Ko-fi above Crisp at right:16 / bottom:88 so the
   two never compete for the same content cell.

   The override has to use !important because Ko-fi sets inline styles
   on its injected wrapper. Mobile keeps the same anchor — at 500px
   the right-edge stack still clears the document gutter. The
   `.lz-shell` padding-bottom keeps the last paragraph of long pages
   from getting hidden under the widget stack on the natural
   scroll-end position. */
.floatingchat-container-wrap {
  left: auto !important;
  right: 16px !important;
  bottom: 88px !important;
  /* P2-13: Ko-fi iframe size is fixed (180×65) — can't shrink via
     width/height. transform:scale lets us render the pill smaller in
     the corner while keeping the inner iframe intact (text + heart
     icon render at native size, just downscaled). Full size restores
     on hover so it's reachable without precision aim. */
  transform: scale(0.78);
  transform-origin: bottom right;
  transition: transform var(--dur-mid) var(--ease-out);
}
.floatingchat-container-wrap:hover,
.floatingchat-container-wrap:focus-within {
  transform: scale(1);
}
main.lz-shell {
  padding-bottom: 96px;
}

/* ─── REVIEW-PASS-2 P2-7 — autoredemption pricing grid ──────
   5 tiers (Free + 4 paid) wrap to 3+2 with the default
   minmax(280px, 1fr) because 5 × 280 = 1400 > 1216 (the .lz-frame
   width at 1440 viewport). Drop the floor to 220 so all five fit
   on the same row at ≥1280, and pin an explicit 5-col grid at
   that breakpoint so the auto-fit math doesn't undershoot. */
.lz-pricing-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: 18px;
}
@media (min-width: 1280px) {
  .lz-pricing-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

/* ─── Phase 38 — spacing utilities (item #11 follow-up) ──────
   Targeted at high-frequency inline declarations
   (padding/margin/max-width). Each is one fluid spacing token —
   pages keep their existing flexbox/grid containers and just swap
   the inline declaration for a class. Public templates first:
   home, dashboard, autoredemption, contact, giftcodes. */
.lz-pad-md   { padding: clamp(14px, 2.5vw, 18px); }
.lz-pad-lg   { padding: clamp(22px, 3vw, 28px) clamp(20px, 4vw, 40px); }
.lz-pad-page { padding: clamp(20px, 3vw, 32px) clamp(16px, 4vw, 40px); }
.lz-mt-md    { margin-top: 14px; }
.lz-mt-lg    { margin-top: 22px; }
.lz-mb-md    { margin-bottom: 14px; }
.lz-mb-lg    { margin-bottom: 22px; }
.lz-max-prose { max-width: 62ch; }
.lz-max-card  { max-width: 540px; }
.lz-stack-tight { display: flex; flex-direction: column; gap: 8px; }
.lz-stack       { display: flex; flex-direction: column; gap: 14px; }

/* ─── Phase 38 — skeleton-loader utility (item #25) ──────────
   Three building blocks: .lz-skel (block), .lz-skel--text
   (rectangle sized like a line of text), .lz-skel--circle
   (avatar-shaped). Shimmer is a single linear-gradient sweep,
   honors prefers-reduced-motion. Use directly in markup or
   inside a card that's rendering a not-yet-loaded resource. */
.lz-skel {
  display: inline-block;
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--sf-surface-2) 80%, transparent),
    color-mix(in srgb, var(--sf-text-3) 20%, var(--sf-surface-2)),
    color-mix(in srgb, var(--sf-surface-2) 80%, transparent)
  );
  background-size: 200% 100%;
  animation: lz-skel-shimmer 1.4s linear infinite;
  border-radius: 4px;
  height: 14px;
  width: 100%;
}
.lz-skel--text { height: 14px; }
.lz-skel--h    { height: 22px; border-radius: 6px; }
.lz-skel--pill { height: 18px; border-radius: 999px; max-width: 84px; }
.lz-skel--circle {
  width: 34px; height: 34px; border-radius: 50%;
}
@keyframes lz-skel-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .lz-skel { animation: none; }
}

/* ─── Phase 38 utility — visually-hidden ────────────────────
   Standard sr-only pattern. Used to attach text labels to
   icon-only buttons and column headers without visible markup.
   WCAG SC 1.3.1 (Info & Relationships) + 4.1.2 (Name/Role/Value).
*/
.sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

/* P2-10: keyboard skip-to-content link. Off-screen by default, slides
   in on :focus as the first interactive element after page load. The
   z-index sits above every other surface (drawer backdrop is 1000, page
   alerts are ~999); cookie banner ~9999, hence 10000. */
.lz-skip-link {
  position: absolute;
  top: -48px;
  left: 8px;
  z-index: 10000;
  padding: 8px 14px;
  border-radius: 6px;
  background: var(--p-500);
  color: #fff;
  font: 600 14px/1 var(--font-sans);
  text-decoration: none;
  transition: top 0.18s ease;
}
.lz-skip-link:focus {
  top: 8px;
  outline: 2px solid #fff;
  outline-offset: 2px;
}
/* Suppress the focus ring on main when reached via skip-link — the
   tabindex="-1" only exists to receive programmatic focus. */
#main-content:focus { outline: none; }

/* ─── Phase 38 — visible field labels ────────────────────────
   Review item #19 — placeholder-as-label fails WCAG 3.3.2.
   .field-label sits above the input as a consistent caption.
   Pair with placeholder text formatted as "e.g. <example>". */
.field-label {
  display: block;
  font: 600 12px/1.2 var(--font-sans);
  letter-spacing: 0.02em;
  color: var(--sf-text);
  margin: 0 0 6px 2px;
}
.field-label .req { color: var(--accent); margin-inline-start: 2px; }
.field-hint {
  display: block;
  font: 500 11px/1.4 var(--font-sans);
  color: var(--sf-text-3);
  margin: 4px 0 0 2px;
}

/* ─── Phase 38 — section heading inside .card ───────────────
   Review item #10 — heading-order failed because every admin
   "form" card titled itself with an h3 below the page h1. Cards
   that act as sections promote to h2 with this consistent shape. */
.lz-card-h {
  margin: 0 0 8px;
  font: 700 14px/1.2 var(--font-sans);
  color: var(--sf-text);
  letter-spacing: -0.005em;
}

/* ─── Phase 38 — section heading + meta rhythm (item #28) ────
   "Active codes" + "9 live · revalidated weekly" no longer
   compete; the meta sits on the heading baseline with a thin
   chevron divider and matches t-sm color. */
.lz-section-head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.lz-section-head__h { margin: 0; }
.lz-section-head__meta {
  font-size: 13px;
  color: var(--sf-text-2);
  position: relative;
  padding-inline-start: 12px;
}
.lz-section-head__meta::before {
  content: "›";
  position: absolute;
  left: 0;
  color: var(--sf-text-3);
}

/* ─── Phase 38 — KPI delta chip (item #12) ──────────────────
   Small chip rendered under a KPI tile's body copy when the
   corresponding delta is positive. Default green (.lz-delta--up).
   Layout sits the chip on a second line under the description
   so the KPI number stays the headline. */
/* Sibling spacing — the hero CTA row above the KPI grid carries
   `margin-top: 22px` to separate from the paragraph, but nothing
   pushed the KPI grid down off the buttons, leaving the cards
   touching the bottom edge of the buttons (gap=0). */
.lz-kpi-row { margin-top: 28px; }
/* When a flex row wraps its CTAs onto multiple stacked lines below
   640 (home hero, /quick-claim, /banned + /vpn + /google-banned,
   dashboard empty-state), each button shrinks to content-width and
   the row reads as visually mismatched. Pin every CTA in the
   sanctioned containers to full-row width + center the label.
   `white-space: normal` lets long labels ("Sign up free instead →",
   "I need a VPN — contact us") wrap inside the button instead of
   blowing past the parent's content area. */
@media (max-width: 640px) {
  .row > .btn-lg,
  .actions > .btn,
  .lz-empty-hero__cta-row > .btn-lg {
    flex: 1 1 100%;
    min-width: 0;
    justify-content: center;
    white-space: normal;
    text-align: center;
  }
  /* Force the wrappers themselves to fill the parent so the
     100%-basis children get a real full-row width — otherwise the
     flex container shrink-wraps to its widest child and the
     equalisation collapses back to content widths. */
  .row,
  .actions,
  .lz-empty-hero__cta-row {
    width: 100%;
    box-sizing: border-box;
  }
}
.lz-kpi-row .kpi-card .d { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 8px; }
.lz-delta {
  display: inline-flex; align-items: center; gap: 4px;
  font: 700 11px/1.4 var(--font-mono);
  letter-spacing: 0.03em;
  padding: 2px 8px;
  border-radius: 999px;
  white-space: nowrap;
}
.lz-delta--up {
  color: var(--st-success);
  background: var(--st-success-bg);
  border: 1px solid var(--st-success-border);
}
.lz-delta--down {
  color: var(--st-danger);
  background: var(--st-danger-bg);
  border: 1px solid var(--st-danger-border);
}

/* ─── Phase 38 — "How it works" live proof (item #13) ────────
   Each step renders the actual artifact it describes:
   - 01: a faux Player-ID card with avatar circle, nickname,
     16-digit mono ID, "Linked" pill.
   - 02: a 2-line Discord-feed snapshot pulling the newest live
     codes from the actual catalog (server-side props).
   - 03: a 3-item mail list with token icons.
   The cards stay readable on mobile (single-column stacking
   inherited from .grid3). */
.lz-how-it-works {
  padding: 0 clamp(20px, 4vw, 48px) 32px;
}
.lz-how-it-works > h2 { margin: 0 0 18px; }
.lz-how-grid { gap: 14px; }
.lz-how-card { display: flex; flex-direction: column; gap: 10px; }
.lz-how-card__h { margin: 6px 0 0; font-size: 16px; }
.lz-how-card__lede {
  color: var(--sf-text-2);
  margin: 0 0 4px;
}
.lz-proof {
  margin-top: auto;
  padding: 12px 14px;
  background: var(--sf-surface-2);
  border: 1px solid var(--sf-border);
  border-radius: var(--r-md);
  font-size: 13px;
  color: var(--sf-text-2);
}

.lz-proof-pid {
  display: flex; align-items: center; gap: 12px;
}
.lz-proof-pid__avatar {
  width: 34px; height: 34px; border-radius: 50%;
  background: linear-gradient(135deg, var(--p-400), var(--p-700));
  flex-shrink: 0;
  position: relative;
}
.lz-proof-pid__avatar::after {
  content: ""; position: absolute; inset: 2px; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--sf-surface) 60%, transparent);
}
.lz-proof-pid__col { flex: 1; min-width: 0; }
.lz-proof-pid__nick { font-weight: 700; font-size: 13px; color: var(--sf-text); }
.lz-proof-pid__id {
  font-size: 11.5px;
  letter-spacing: 0.04em;
  color: var(--sf-text-3);
  margin-top: 2px;
  word-break: break-all;
}
.lz-proof-pid__pill {
  font-size: 10.5px; padding: 2px 8px;
}

.lz-proof-feed { display: flex; flex-direction: column; gap: 6px; }
.lz-proof-feed__row {
  display: flex; align-items: center; gap: 8px;
}
.lz-proof-feed__row--dim { opacity: 0.7; }
.lz-proof-feed__dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--st-success);
  flex-shrink: 0;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--st-success) 20%, transparent);
  animation: lz-feed-pulse 2.4s ease-in-out infinite;
}
@keyframes lz-feed-pulse {
  0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--st-success) 20%, transparent); }
  50%      { box-shadow: 0 0 0 6px color-mix(in srgb, var(--st-success) 0%,  transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .lz-proof-feed__dot { animation: none; }
}
.lz-proof-feed__code {
  font-weight: 700; color: var(--sf-text); font-size: 12.5px;
  letter-spacing: 0.03em;
}
/* P2-12: bumped from sf-text-3 (slate-400 #94a3b8) → sf-text-2 to
   clear Lighthouse color-contrast AA on the dark panel, and from 11.5px
   → 12px to clear the small-text threshold. Same change on
   __more for consistency. */
.lz-proof-feed__age { font-size: 12px; color: var(--sf-text-2); }
.lz-proof-feed__hint { font-size: 12px; color: var(--sf-text-2); }
.lz-proof-feed__more {
  margin-top: 4px;
  font-size: 12px;
  color: var(--sf-text-2);
}
.lz-proof-feed__more a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.lz-proof-mail { display: flex; flex-direction: column; gap: 6px; }
.lz-proof-mail__row {
  display: flex; align-items: center; gap: 10px;
  font-size: 13px; color: var(--sf-text);
}
.lz-proof-mail__icon {
  width: 22px; height: 22px;
  border-radius: 6px;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--accent) 18%, var(--sf-surface));
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  color: var(--accent);
  font-size: 12px;
  flex-shrink: 0;
}
.lz-proof-mail__more {
  margin-top: 4px;
  font-size: 11.5px;
}
.lz-proof-mail__more a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ─── Phase 38 — admin table scanning aids (item #18) ────────
   Adds sticky thead, alternating row backgrounds, tabular-nums
   on numeric columns, and a brand-accent tinted hover so the
   eye lands on the active row immediately. Scoped to .lz-admin
   tables — public tables (gift-codes, etc.) keep the canvas
   defaults. */
.lz-admin .tbl thead th {
  position: sticky;
  top: 0;
  z-index: 4;
  background: var(--sf-surface-2);
  /* drop-shadow on the bottom edge keeps the thead visually pinned
     when the body scrolls under it (instead of a hard cut). */
  box-shadow: 0 1px 0 var(--sf-border), 0 2px 6px rgba(0,0,0,0.04);
}
.dark .lz-admin .tbl thead th {
  box-shadow: 0 1px 0 var(--sf-border), 0 2px 8px rgba(0,0,0,0.18);
}
.lz-admin .tbl tbody tr:nth-child(odd) td {
  background: color-mix(in srgb, var(--sf-surface-2) 25%, var(--sf-surface));
}
.lz-admin .tbl tbody tr:hover td {
  background: color-mix(in srgb, var(--accent) 8%, var(--sf-surface-2)) !important;
}
.lz-admin .tbl tbody td.mono,
.lz-admin .tbl tbody td.num,
.lz-admin .tbl tbody td[align="right"] {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}
.lz-admin .tbl tbody td.num,
.lz-admin .tbl tbody td[align="right"] {
  text-align: right;
}
/* When a th carries an aria-sort attr (set by future sortable-column
   wiring) render a visual indicator. Today the markup ships without
   aria-sort, so this is a forward-compatible rule. */
.lz-admin .tbl thead th[aria-sort] {
  cursor: pointer;
  user-select: none;
}
.lz-admin .tbl thead th[aria-sort="ascending"]::after  { content: " ↑"; opacity: 0.7; }
.lz-admin .tbl thead th[aria-sort="descending"]::after { content: " ↓"; opacity: 0.7; }
.lz-admin .tbl thead th[aria-sort="none"]::after       { content: " ↕"; opacity: 0.4; }

/* ─── Phase 38 — /quick-claim (item #1) ─────────────────────
   Hero used to host the anon paste form. Now lives on its own
   route so the home hero stays a pure marketing surface. */
.qc-page {
  padding: clamp(24px, 4vw, 36px) clamp(16px, 4vw, 40px) 48px;
  max-width: 760px;
  margin: 0 auto;
}
.lz-breadcrumb {
  display: inline-flex; align-items: center; gap: 6px;
  font: 600 12px/1 var(--font-sans);
  color: var(--sf-text-3);
  text-decoration: none;
  margin-bottom: 14px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.lz-breadcrumb:hover {
  color: var(--accent);
  text-decoration: underline;
}
.qc-lede {
  font-size: 15px;
  line-height: 1.6;
  color: var(--sf-text-2);
  max-width: 60ch;
  margin: 8px 0 22px;
}
.qc-lede strong { color: var(--sf-text); }
.qc-form { padding: clamp(18px, 3vw, 26px); }
.qc-textarea {
  width: 100%;
  box-sizing: border-box;
  font-family: var(--font-mono);
  font-size: 14px;
  padding: 12px 14px;
  border: 1px solid var(--sf-border);
  border-radius: var(--r-md);
  background: var(--sf-surface);
  color: var(--sf-text);
  resize: vertical;
  min-height: 104px;
}
.qc-pass { margin-top: 14px; }
.qc-pass summary {
  cursor: pointer;
  font-size: 13px;
  color: var(--sf-text-2);
}
.qc-pass__input {
  flex: 1; max-width: 240px;
  padding: 8px 10px;
  font-family: var(--font-mono);
  font-size: 13px;
  border: 1px solid var(--sf-border);
  border-radius: var(--r-md);
  background: var(--sf-surface);
  color: var(--sf-text);
}

/* ── Pass-code input ───────────────────────────────────────────────
   Wraps an <input data-pass-field> + <button data-pass-toggle> so a
   sensitive pass code renders masked by default and can be revealed
   on demand. Shared by /quick-claim (.qc-pass__input) and /add-gift-code
   (the inline form input). Behaviour delegated by lz_pass_toggle.js. */
.lz-pass-input {
  display: inline-flex;
  align-items: stretch;
  gap: 0;
  position: relative;
  max-width: 240px;
}
.lz-pass-input > [data-pass-field] {
  flex: 1;
  padding-inline-end: 40px;
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  border-right: 0;
  letter-spacing: 0.04em;
}
.lz-pass-input__toggle {
  flex: 0 0 36px;
  min-width: 36px;
  min-height: 36px;
  padding: 0 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--sf-surface);
  border: 1px solid var(--sf-border);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  color: var(--sf-text-2);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.lz-pass-input__toggle:hover {
  background: var(--sf-surface-2);
  color: var(--sf-text);
}
.lz-pass-input__toggle:focus-visible {
  outline: 2px solid var(--p-500);
  outline-offset: 1px;
  z-index: 1;
}
.lz-pass-input__toggle[aria-pressed="true"] {
  color: var(--p-700);
}
.lz-pass-input__eye-on,
.lz-pass-input__eye-off {
  pointer-events: none;
}
.lz-pass-input__eye-off { display: none; }
.lz-pass-input__toggle[aria-pressed="true"] .lz-pass-input__eye-on  { display: none; }
.lz-pass-input__toggle[aria-pressed="true"] .lz-pass-input__eye-off { display: inline; }
/* ≥44px tap target on touch devices. */
@media (hover: none) and (pointer: coarse) {
  .lz-pass-input__toggle { min-width: 44px; min-height: 44px; }
}

.qc-cta-row {
  gap: 10px;
  margin-top: 16px;
  flex-wrap: wrap;
}
.qc-disclaimer {
  font-size: 12px;
  color: var(--sf-text-3);
  margin: 10px 0 0;
}
.qc-uphint {
  margin-top: 14px;
  text-align: center;
}
.qc-uphint a {
  font-size: 13px;
  color: var(--sf-text-2);
  text-decoration: underline;
}

/* Home hero aux block (item #1 follow-up) — replaces the inline
   paste form. Visible to anonymous visitors only. */
.hero-aux { /* row utility already supplies layout */ }
.hero-fineprint {
  margin-top: 12px;
  font-size: 13px;
  color: var(--sf-text-3);
  max-width: 60ch;
}

/* ─── Phase 38 — /giftcodes redesign (item #3) ──────────────
   Live codes get a 3-col card grid. Whole card is the copy
   target (click + keyboard activate). Cards are 18-24px tall
   monospace headlines with status pill + "Just dropped" badge
   for codes added in the last 24h. Expired codes collapse into
   a single <details> that the user can expand on demand. */
.gc-page { padding: clamp(20px, 4vw, 28px) clamp(16px, 4vw, 40px) 40px; }
.gc-head { gap: 20px; flex-wrap: wrap; align-items: flex-start; margin-bottom: 26px; }
.gc-head__lede {
  max-width: 62ch;
  margin: 6px 0 0;
}
.gc-kpis { gap: 10px; }
.gc-kpi { padding: 10px 14px; }
.gc-kpi .n { font-size: 22px; }
.gc-kpi__live { color: var(--st-success); }
.gc-empty { text-align: center; }
.gc-empty__icon { font-size: 48px; margin-bottom: 8px; color: var(--sf-text-3); }
.gc-section { margin-bottom: 28px; }
.gc-section__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
  margin-bottom: 12px;
}
.gc-section__title { margin: 0; font-size: 18px; }
.gc-section__count { color: var(--st-success); }
.gc-section__hint { color: var(--sf-text-3); }

.gc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
}
.gc-card {
  display: flex; flex-direction: column;
  gap: 10px;
  padding: 16px 18px 14px;
  background: var(--sf-surface);
  border: 1px solid color-mix(in srgb, var(--st-success) 28%, var(--sf-border));
  border-radius: var(--r-md);
  cursor: pointer;
  user-select: text;
  text-align: left;
  position: relative;
  transition: border-color 160ms ease, transform 60ms ease, box-shadow 160ms ease;
}
.gc-card:hover {
  border-color: color-mix(in srgb, var(--st-success) 60%, var(--sf-border));
  box-shadow: 0 6px 18px rgba(0,0,0,0.08);
}
.dark .gc-card:hover { box-shadow: 0 6px 18px rgba(0,0,0,0.30); }
.gc-card:active { transform: translateY(0.5px); }
.gc-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.gc-card__head {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.gc-card__pill {
  display: inline-block;
  padding: 2px 9px; border-radius: 999px;
  background: var(--st-success-bg);
  color: var(--st-success);
  border: 1px solid var(--st-success-border);
  font: 600 11px/1.5 var(--font-sans);
}
.gc-card__fresh {
  display: inline-block;
  padding: 2px 9px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 50%, transparent);
  font: 700 10.5px/1.5 var(--font-sans);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.gc-card__code {
  font-weight: 800;
  font-size: clamp(18px, 2.4vw, 22px);
  letter-spacing: 0.04em;
  color: var(--sf-text);
  word-break: break-all;
  line-height: 1.15;
}
.gc-card__meta {
  display: flex; flex-wrap: wrap; gap: 5px;
  font-size: 11.5px; color: var(--sf-text-3);
}
.gc-card__dot { color: var(--sf-text-3); }

/* Phase 39 — /giftcodes live-card compaction on phones.
   Catalog cards on 320 rendered ~145px tall each (pill + 22px
   code + two-line meta + chrome) and the section says "Live · 9"
   already, so the per-card Live pill is redundant. Tighten
   chrome, shrink the code one notch, and drop the pill — keeps
   the "click any card to copy" affordance intact via the hover
   border. */
@media (max-width: 480px) {
  .gc-card {
    gap: 6px;
    padding: 12px 14px 11px;
  }
  .gc-card__pill {
    display: none;
  }
  .gc-card__code {
    font-size: 17px;
    letter-spacing: 0.03em;
  }
  .gc-card__meta {
    font-size: 11px;
    gap: 4px;
  }
}

/* When the lz-copy-feedback delegate fires, .is-copied lights the
   whole card with the brand-success tint. */
.gc-card.is-copied {
  border-color: var(--st-success);
  background: color-mix(in srgb, var(--st-success) 10%, var(--sf-surface));
}

.gc-noop { text-align: center; color: var(--sf-text-3); font-size: 13px; }

/* Expired collapse */
.gc-expired {
  border: 1px solid var(--sf-border);
  border-radius: var(--r-md);
  background: var(--sf-surface);
}
.gc-expired__summary {
  list-style: none;
  cursor: pointer;
  padding: 12px 16px;
  font-weight: 600; font-size: 14px;
  color: var(--sf-text-2);
  display: flex; align-items: center; gap: 8px;
}
.gc-expired__summary::-webkit-details-marker { display: none; }
.gc-expired__chevron {
  display: inline-block;
  transition: transform 180ms ease;
  color: var(--sf-text-3);
}
.gc-expired[open] .gc-expired__chevron { transform: rotate(90deg); }
.gc-expired__hint { color: var(--sf-text-3); font-weight: 400; }
/* Phase 39 — expired-code grid. The previous flat-list rendering of
   43 muted rows scrolled forever and read as one wall of monotype.
   Recast as a responsive grid of compact "graveyard" tiles: each
   tile shows the code (strikethrough red, muted) + relative date +
   a tiny Copy affordance. Hover fades the tile back in so an
   operator can still scan dates / copy a code. */
.gc-expired__body {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 8px;
  padding: 4px 12px 12px;
}
.gc-expired__body .gc-row.is-expired {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 10px;
  row-gap: 2px;
  padding: 9px 11px;
  background: var(--sf-surface-2);
  border: 1px solid var(--sf-border);
  border-radius: var(--r-md);
  opacity: 0.65;
  transition: opacity 0.15s, border-color 0.15s;
}
.gc-expired__body .gc-row.is-expired:hover,
.gc-expired__body .gc-row.is-expired:focus-within {
  opacity: 1;
  border-color: var(--sf-border-strong);
}
/* The parent disclosure already labels these rows as expired —
   drop the redundant per-row pill from the grid view. */
.gc-expired__body .gc-row.is-expired .gc-pill {
  display: none;
}
.gc-expired__body .gc-row.is-expired .gc-code {
  font: 700 13px/1.25 var(--font-mono);
  letter-spacing: -0.005em;
  color: var(--sf-text-2);
  text-decoration: line-through;
  text-decoration-color: var(--st-danger);
  text-decoration-thickness: 1.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  grid-column: 1 / 2;
  grid-row: 1 / 2;
}
.gc-expired__body .gc-row.is-expired .gc-meta {
  font: 500 10.5px/1.3 var(--font-sans);
  color: var(--sf-text-3);
  grid-column: 1 / 2;
  grid-row: 2 / 3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gc-expired__body .gc-row.is-expired .btn {
  grid-column: 2 / 3;
  grid-row: 1 / 3;
  align-self: center;
  min-height: 32px;
  padding: 4px 9px;
  font-size: 11px;
}
@media (max-width: 768px) {
  /* WCAG floor inside the tile — bump the Copy back up to 44 on
     touch viewports, but keep the tile compact via padding. */
  .gc-expired__body .gc-row.is-expired .btn { min-height: 44px; }
}

/* ─── Phase 38 — first-run empty hero (dashboard, etc.) ──────
   Review item #2 — dashboard with 0 linked Player IDs showed
   pure black space below the action bar. Empty hero supplies
   illustration + headline + lede + CTA pair so the next step is
   obvious. Layout flips column-direction at <=720 so the
   illustration sits below copy on mobile. */
.lz-empty-hero {
  display: grid;
  grid-template-columns: minmax(220px, 280px) 1fr;
  gap: clamp(20px, 4vw, 36px);
  align-items: center;
  margin-bottom: 22px;
  background: linear-gradient(140deg,
    color-mix(in srgb, var(--accent) 6%, var(--sf-surface)),
    var(--sf-surface) 70%);
  border-color: color-mix(in srgb, var(--accent) 18%, var(--sf-border));
}
.lz-empty-hero__art {
  display: grid;
  place-items: center;
  color: var(--accent);
}
.lz-empty-hero__art img {
  width: 100%;
  max-width: 240px;
  height: auto;
}
.lz-empty-hero__h {
  margin: 6px 0 8px;
}
.lz-empty-hero__lede {
  color: var(--sf-text-2);
  font-size: 14.5px;
  line-height: 1.6;
  max-width: 56ch;
  margin: 0 0 18px;
}
.lz-empty-hero__lede strong { color: var(--sf-text); }
.lz-empty-hero__help {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.lz-empty-hero__cta-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
@media (max-width: 720px) {
  .lz-empty-hero {
    grid-template-columns: 1fr;
    text-align: left;
  }
  .lz-empty-hero__art { order: 2; }
  .lz-empty-hero__art img { max-width: 200px; }
}
/* The empty hero's decorative SVG isn't load-bearing on a phone — at
   320 it sits below the body with a 20px grid-gap + 20px box-padding,
   creating ~40px of breathing room above the next subscription card
   for ~120px of illustration. Hide it below 480 to tighten the page
   without losing the call-to-action stack. */
@media (max-width: 480px) {
  .lz-empty-hero__art { display: none; }
  .lz-empty-hero { padding: 18px; margin-bottom: 16px; }
}

/* Per-subscription Add-Player-ID form: input flexes to 100% of the
   row on mobile (via the .row width:100% rule earlier), but the
   trailing submit button keeps its content width (~199 for "Add to
   this subscription →") which overhangs the form by ~10px on a
   189-wide card. Force the submit to share the row's full width
   when wrapped. Same pattern is fine for the bulk-add modal trigger
   and any future input+button forms. */
@media (max-width: 640px) {
  .js-add-uid-form .row > .btn,
  .js-add-uid-form .row > input {
    flex: 1 1 100%;
    min-width: 0;
  }
}

/* ─── Phase 38 — kebab "···" menu (account actions) ──────────
   Review item #2 — destructive Delete-account no longer sits at
   the same affordance level as routine actions. Lives inside a
   roving kebab menu with click-outside + ESC handling in JS. */
.lz-kebab { position: relative; display: inline-block; }
.lz-kebab__btn {
  padding: 6px 10px !important;
  font-size: 18px;
  line-height: 1;
}
.lz-kebab__list {
  position: absolute;
  top: calc(100% + 4px);
  inset-inline-end: 0;
  min-width: 200px;
  background: var(--sf-surface);
  border: 1px solid var(--sf-border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--sh-md);
  padding: 6px 0;
  z-index: 80;
}
.lz-kebab__list a,
.lz-kebab__list button {
  display: block; width: 100%;
  padding: 8px 14px; text-align: start;
  background: transparent; border: 0;
  color: var(--sf-text);
  font-size: 13.5px; font-family: inherit;
  text-decoration: none; cursor: pointer;
}
.lz-kebab__list a:hover,
.lz-kebab__list button:hover,
.lz-kebab__list a:focus-visible,
.lz-kebab__list button:focus-visible {
  background: var(--sf-surface-2);
  outline: none;
}
.lz-kebab__list hr {
  margin: 4px 0;
  border: 0;
  border-top: 1px solid var(--sf-border);
}
.lz-kebab__danger { color: var(--st-danger) !important; }
.lz-kebab__danger:hover { background: var(--st-danger-bg) !important; }

/* ─── Phase 38 — copy-button feedback ────────────────────────
   Review item #14 — clicking "Copy" on a gift code gave no
   visible feedback beyond the toast. .is-copied flips the button
   to brand-green + checkmark for 1.6s; .lz-pulse runs on the
   linked code block (data-pulse-target) so the eye lands on
   what was just copied. Loops via lz_copy_feedback.js. */
.btn.is-copied,
button.is-copied {
  background: var(--p-600) !important;
  color: #fff !important;
  border-color: var(--p-600) !important;
}
.dark .btn.is-copied,
.dark button.is-copied {
  background: var(--p-500) !important;
  color: #0B1220 !important;
  border-color: var(--p-500) !important;
}
@keyframes lz-pulse-once {
  0%   { box-shadow: 0 0 0 0   color-mix(in srgb, var(--accent) 50%, transparent); }
  60%  { box-shadow: 0 0 0 12px color-mix(in srgb, var(--accent) 0%,  transparent); }
  100% { box-shadow: 0 0 0 0   color-mix(in srgb, var(--accent) 0%,  transparent); }
}
.lz-pulse {
  animation: lz-pulse-once 1.4s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .lz-pulse { animation: none; }
}

/* ─── Phase 38 — step-number contrast token ─────────────────
   Review item #7 — "01 / 02 / 03" labels on the home "How it
   works" cards used --p-700 (#4D7C0F olive). On dark surface
   #121712 that's 3.42:1 — fails WCAG AA 4.5:1 for normal text.

   .lz-step-num pins to --p-700 in light theme (passes 4.88:1 on
   #FFFFFF) and --p-400 (#A3E635 lime) in dark theme (passes
   10.96:1 on #121712). Same shape used wherever a "small numeral
   accent" is wanted — KPI deltas, sequence counters, etc.

   The .mono / .t-sm dim issue: rewriting prose labels under
   "How it works" to .t-sm (slate-600 / slate-300 dark) lifts
   them from --sf-text-3 (3.5:1) to --sf-text-2 (~9:1 dark). */
.lz-step-num {
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.05em;
  color: var(--p-700);
}
.dark .lz-step-num { color: var(--p-400); }

/* Reinforce .dim contrast on surfaces inside cards: the canvas
   .dim helper uses --sf-text-3 (~slate-400) which AA-fails on
   small font (≤13px) sitting on --sf-surface in dark theme.
   Bump dim text inside .card to --sf-text-2 for the small sizes
   that show up in cards. */
.card .dim,
.card .muted { color: var(--sf-text-2); }

/* ─── Phase 38 — theme-toggle smooth transition ──────────────
   Review item #16 — clicking the theme toggle hard-cut the page
   between light and dark. WCAG 2.1 favors smooth ≤500ms changes
   for cognition; we use 220ms (var(--dur-mid) is ~200ms).

   Applies to body + the heavy-paint hosts (cards, surfaces, nav,
   admin tables). Not '*' — animating every node trips
   prefers-reduced-motion AND can stutter on slower devices. */
@media (prefers-reduced-motion: no-preference) {
  body,
  .card,
  .lz-frame,
  .lz-shell,
  .lz-nav,
  .lz-side,
  .lz-admin-head,
  .lz-admin-main,
  .lz-admin-body,
  .lz-site-footer,
  .tbl thead th,
  .tbl tbody td,
  .pill,
  .btn,
  input, textarea, select {
    transition: background-color 220ms ease,
                border-color     220ms ease,
                color            220ms ease;
  }
}

