/* site.css — BOOST global styles (extracted 2026-08-26).
   Tokens, nav, buttons, footer, invite popup, focus rings.
   Every page links this first, then keeps only its own rules inline; an
   inline rule with the same selector wins, so page tweaks stay local.
   Edit colours/fonts here once and every page follows. */

:root{
  --bg:#141414; --surface:#1d1d1d; --line:#363636;
  --ink:#f0f0f0; --ink-dim:#a8a8a8; --ink-faint:#8a8a8a;
  --ok:#3fb950; --warn:#e3b341; --bad:#f85149;
  /* Informational blue, completing the status set (2026-09-11). Same Primer
     family as the three above, so tinted capsules built from any of them sit
     at the same weight. */
  --info:#58a6ff;
  --accent:#eb3a12;
  /* Accent for TEXT (2026-09-15 ux-audit). --accent #eb3a12 on --surface
     #1d1d1d is 4.13:1 — under AA for body-size text — so links and hover
     text read this lighter step instead: #ff5a33 on #1d1d1d = 5.43:1, on
     --bg #141414 = 5.93:1 (WCAG relative-luminance formula). Fills, borders,
     rules and dots keep --accent; only text changes colour. */
  --accent-text:#ff5a33;
  /* The one disabled opacity (2026-09-15 design audit, P2.10). The consoles
     had drifted to .4 / .45 / .5 / .6 / .7 for the same "you cannot use this"
     meaning; every disabled control and locked line now reads this. */
  --disabled:.5;
  /* One height for every solid/ghost action button, site-wide. Buttons are
     laid out as inline-flex at this fixed height with line-height:1, so the
     font size no longer decides how tall a button is — matching the 32px
     nav .cta and .auth chips, which were already this tall. */
  --btn-h:32px;
  /* THE BUTTON SCALE (2026-09-22, dchan: "can we have a global standard button
     size we can use"). Three sizes, no fourth: pages had been inventing their
     own (28px here, 36px there, a 96px width floor in the wizard) and the same
     control ended up a different size depending which page you opened.
       --btn-h-sm  dense, in-row or in-heading actions beside other content
       --btn-h     the default: every solid/ghost action button
       --btn-h-lg  a decision the page is ASKING for (Yes/No, Finish setup),
                   and the WCAG 2.5.8 target every button grows to under 720px
     Use the classes below — `.btn.sm` / `.btn` / `.btn.lg` — rather than a
     local min-height, so a page cannot drift off the scale again.
     --btn-minw keeps a pair of short labels ("Yes"/"No") a matched width
     instead of two ragged pills. */
  --btn-h-sm:28px;
  --btn-h-lg:44px;
  --btn-minw:80px;
  /* Hero title colours. The h1 sits in --hero-ink; whatever a page wraps in
     <em> takes --hero-em. Both are tokens so a page can re-point them for its
     own identity without touching the rule below. */
  --hero-ink:var(--ink);
  --hero-em:var(--accent);
  /* Header height shared by the arcade front doors — see .lander. */
  --lander-h:500px;
  /* Moved up from index.html and bot.html, which each carried an identical
     copy, and index used neither --amber nor --mono-green (2026-09-22
     ux-audit, P3.3). One declaration, like every other token here. */
  --surface-2:#262626;
  --amber:#f5921b;
  --mono-green:#ffb257;
  /* Discord blurple. Was hardcoded as #5865F2 in site.css and four pages, with
     its hover #4752C4 in two of them — the only brand colour on the site with
     no token (2026-09-22 ux-audit, P3.6). */
  --blurple:#5865F2;
  --blurple-hi:#4752C4;
}
*{box-sizing:border-box;margin:0;padding:0}
/* The `hidden` attribute always hides (2026-09-15, audit bugs 4/5/8). The UA's
   own [hidden]{display:none} loses to ANY author display — .send is
   inline-flex, .frow is flex, .panel is block — so a page setting
   el.hidden=true on one of those got a control that stayed on screen, and
   each page grew its own `.x[hidden]{display:none}` patch. One rule, with
   the weight to win, and the patches are redundant. */
[hidden]{display:none!important}
body{background:var(--bg);color:var(--ink);font:16px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased}
.display{font-family:"Avenir Next Condensed","Arial Narrow",Impact,sans-serif;text-transform:uppercase;letter-spacing:.04em;font-weight:700;text-wrap:balance}
.mono{font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace}
a{color:var(--accent-text,var(--accent));text-decoration:none}
a:hover{text-decoration:underline}
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

/* ---------- the arcade treatment ----------
   FIVE pages carry <body class="arcade">: index (hub), bot, dashboard, install
   and account. Four of them also carry <header class="lander">: index, bot,
   dashboard and account. The comments here said "three" and, further down,
   "all five hero pages" — two different counts, neither of them right
   (corrected 2026-09-22, ux-audit P3.9). They share one
   header: a radial charcoal wash, a fine grain, three slow ember orbs and an
   accent hairline, under CRT scanlines on the page itself. It lives here rather
   than in three inline <style> blocks so the front doors cannot drift apart —
   which is exactly how they drifted in the first place.

   Opt in with <body class="arcade"> plus three .ember divs inside <header>.
   install.html opts in for the treatment but keeps its own header and is not a
   .lander, so it does not take the shared header height.
   Pages that opt in also drop to the 1.65 body line-height the marketing copy
   is set in; the doc pages stay at site.css's 1.7. */
html{scroll-behavior:smooth}
body.arcade{line-height:1.65}
body.arcade::after{content:"";position:fixed;inset:0;z-index:99;pointer-events:none;
  background:repeating-linear-gradient(0deg, rgba(0,0,0,.07) 0 1px, transparent 1px 3px);
  mix-blend-mode:multiply}
body.arcade header{position:relative;overflow:hidden;border-bottom:1px solid var(--line);background:radial-gradient(900px 520px at 50% -10%,#161414 0%,#0e0e0e 70%)}
/* Ember field: three slow, soft flame-coloured orbs drifting behind the
   headline, over a fine grain so the charcoal has texture instead of flat
   black. Nothing is aligned to a grid; the light is the decoration. */
body.arcade header::before{content:"";position:absolute;inset:0;pointer-events:none;opacity:.22;mix-blend-mode:soft-light;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .5 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
body.arcade header::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:linear-gradient(90deg,transparent,rgba(235,58,18,.35),transparent)}
.ember{position:absolute;border-radius:50%;filter:blur(80px) saturate(.75);pointer-events:none;opacity:.32;will-change:transform}
.ember.a{width:520px;height:520px;left:-120px;top:-160px;background:radial-gradient(circle,rgba(235,58,18,.55),transparent 65%);animation:drift-a 22s ease-in-out infinite alternate}
.ember.b{width:640px;height:640px;right:-200px;top:-80px;background:radial-gradient(circle,rgba(245,146,27,.38),transparent 65%);animation:drift-b 28s ease-in-out infinite alternate}
.ember.c{width:420px;height:420px;left:38%;bottom:-260px;background:radial-gradient(circle,rgba(235,58,18,.35),transparent 65%);animation:drift-c 26s ease-in-out infinite alternate}
@keyframes drift-a{to{transform:translate(120px,80px) scale(1.15)}}
@keyframes drift-b{to{transform:translate(-140px,60px) scale(.9)}}
@keyframes drift-c{to{transform:translate(-90px,-70px) scale(1.2)}}
/* The hero sits above the ember field. */
body.arcade .hero{position:relative;z-index:1}
/* The four .lander pages — index, bot, dashboard and account — hold ONE header
   height, so moving between them does not jump. The hero is pinned to the TOP
   of that box, not centred: centring made the eyebrows land 86px apart, because
   a taller hero centred pushes its first line up. Top-aligned, the shared
   `.hero` padding-top puts the eyebrow — and the h1 under it — at the same
   height on all three, and the slack falls below the fold instead.

   660px was originally set by bot.html, the tallest, whose header carried a
   scrolling seed ticker beneath the hero. That ticker was removed 2026-08-30,
   so the number is now just the agreed header height rather than a constraint
   any one page imposes — retune it freely in --lander-h.
   Below 840px the constraint lifts — a fixed 660 is most of a phone screen. */
body.arcade header.lander{display:flex;flex-direction:column;justify-content:flex-start;min-height:var(--lander-h)}
@media(max-width:840px){body.arcade header.lander{min-height:0}}
/* ---------- shared hero rhythm ----------
   ONE format for every arcade page. The eyebrow,
   the title under it and the lede under that sit on the same rhythm everywhere,
   so the three front doors read as one site rather than three near-misses.
   Agreed values 2026-08-29: 30px above the eyebrow, 30px under it, 30px under
   the title. Deliberately not responsive — 30px is already tight enough to hold
   at any width. These beat each page's own `.hero` rules on specificity, not
   source order, so the inline blocks can stay as they are. */
body.arcade .hero{padding-top:30px}
/* 70px, not 76 (corrected 2026-09-22, ux-audit P2.11). All FIVE arcade pages
   carried their own `body.arcade .hero h1{…clamp(32px,6.5vw,70px)…}` — same
   specificity as this rule, in a file that loads after it, so every one of
   them won on source order and the 76 here had never rendered anywhere. The
   pages now carry only their own `max-width` (where the headline breaks,
   which genuinely differs per page: 10em / 11em / 12em) and this owns the
   scale. Retune the cap HERE and all five move. */
body.arcade .hero h1{font-size:clamp(32px,6.5vw,70px);line-height:.94}
body.arcade .hero p.lede{margin-top:30px}
/* Title colour, once, for every hero page — was duplicated in all five. */
header .hero h1,
body.arcade header .hero h1{color:var(--hero-ink)}
header .hero h1 em,
body.arcade header .hero h1 em{font-style:normal;color:var(--hero-em)}
/* The eyebrow sits directly under the nav — same distance on every hero page
   and at every width (was 96px desktop / 56px mobile).
   Specificity note: each page carries its own `.hero{padding:96px 0 80px}`
   shorthand and the pages load AFTER this file, so a selector that merely
   matches would lose on source order. `header .hero` outranks account's bare
   `.hero`; the `body.arcade` variant outranks the other four. It also beats
   the pages' own @media(max-width:840px) rule, which is why one value covers
   desktop and mobile with no breakpoint. One number, both selectors. */
header .hero,
body.arcade header .hero{padding-top:20px;padding-bottom:40px}
/* Gap under the eyebrow, before the h1. Same specificity trick as above: the
   pages each set `margin-bottom:18px` and load after this file, so only a
   `header`-prefixed selector wins. One number for all five hero pages. */
header .hero .eyebrow,
body.arcade header .hero .eyebrow{margin-bottom:32px}
/* The CTA row under the lede. All three landers have one now, so it is
   defined here rather than copied into each page. */
body.arcade .hero .actions{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;margin-top:0}
/* Mobile trims the hero's bottom padding only — the 30px above the eyebrow
   holds at every width. Same 840px breakpoint the pages already use. */
@media(max-width:840px){
  body.arcade .hero{padding-bottom:50px}
}
/* The headline is the one thing that grows on a phone: the shared clamp bottoms
   out at 32px, small for the thing each page is built around. */
@media(max-width:720px){
  body.arcade .hero h1{font-size:clamp(37px,10.5vw,55px)}
}

/* The `body.lander-page` type scale — 17 declarations stepping the landing
   pages to a 15px base — was deleted 2026-09-22 (ux-audit, P3.2). No <body>
   on any page ever carried the class: the four landers carry `arcade` plus a
   `header.lander`, which is a different thing. It had been styled, documented
   and inert since it was written, and read as a third type scale to anyone
   auditing this file. Deleting it changes nothing on screen. If the landing
   pages should run tighter, add the class to those four <body> tags FIRST,
   then reintroduce the scale. */
@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto}
  .ember{display:none}
  body.arcade *,body.arcade *::before,body.arcade *::after{animation:none!important;transition:none!important}
}
nav{position:sticky;top:0;z-index:50;background:color-mix(in srgb,var(--bg) 86%,transparent);backdrop-filter:blur(12px);border-bottom:1px solid var(--line)}
/* THE BAR IS FULL-BLEED, THE PAGE IS CENTRED (dchan, 2026-09-16) — the app
   shell every console UI uses, and what was asked for here.
   `nav` was already full width: it is a block, so its ground and its 1px
   bottom border have always run edge to edge. What was capped was its
   CONTENTS — brand and buttons pulled into the same 1120 column as the page
   below. max-width:none frees them, so the brand sits at the true left edge
   and the actions at the true right, while `main.wrap` keeps the page on its
   measure.
   The 24px side padding comes from .wrap and is deliberately kept: it is the
   same gutter the page uses, so the brand stays vertically aligned with the
   page's first character at any width below the cap. */
nav .wrap{display:flex;align-items:center;gap:28px;height:60px;max-width:none}
nav .brand{display:flex;align-items:center;gap:10px;color:var(--ink);font-weight:700}
/* Logo matches the 32px controls beside it, so the nav reads as one band:
   all four items already share a centre, but a 28px logo sat 2px inside the
   buttons top and bottom. */
nav .brand img{height:32px;width:auto;display:block}
nav .brand:hover{text-decoration:none}
/* Auth slot. One control, far right: a ghost button signed out, avatar + name
   signed in. Width is reserved so the nav does not jump when /api/me answers. */
/* margin-left:auto moved here from nav .cta when the Add to Discord button was
   removed from the bar (2026-09-17). The CTA was the element carrying the
   right-alignment for everything after the brand; without it the auth chip
   sat against the logo. It belongs on the chip itself — it is now the last
   thing in the bar. */
nav .auth{margin-left:auto;display:inline-flex;align-items:center;gap:8px;min-width:158px;height:32px;box-sizing:border-box;padding:0 14px;border:1px solid var(--line);border-radius:6px;color:var(--ink-dim);font-size:12px;font-weight:600;line-height:1;white-space:nowrap;justify-content:center}
nav .auth:hover{border-color:var(--accent);color:var(--accent-text,var(--accent));text-decoration:none}
nav .auth:not(.me):not(:empty){background:var(--blurple);border-color:var(--blurple);color:#fff;text-transform:uppercase;letter-spacing:.04em;font-family:"Avenir Next Condensed","Arial Narrow",Impact,sans-serif;font-weight:700}
nav .auth:not(.me):not(:empty):hover{background:var(--blurple-hi);border-color:var(--blurple-hi);color:#fff}
/* Avatar and name match the 32px logo across the bar (dchan, 2026-09-19):
   the brand is a 32px mark beside 16px/700 text, and the identity chip at
   the other end was 18px beside 12px/600 — the same kind of thing rendered
   at half the weight. Both ends of the bar now read at one size. The
   signed-OUT chip keeps 12px: it is a button, not an identity. */
nav .auth img{width:18px;height:18px;border-radius:50%}
nav .auth.me img{width:32px;height:32px}
nav .auth.me{font-size:16px;font-weight:700}
/* Signed in, the button is the SAME 158×32 box as the CTA beside it and always
   was — but a transparent fill behind a --line border on a near-black nav made
   its edge all but disappear, so it read as the smaller of the two (dchan,
   2026-09-02). The geometry was never the problem; the contrast was — an
   outlined box reads smaller than a filled one of identical size, because the
   eye takes the line as an edge AROUND the button rather than part of it.
   Filled blurple settles it: the same solid mass as the CTA, and the same
   #5865F2 the signed-out state uses, so one control does not change colour
   just because somebody signed in. It keeps the ACCENT for the one action on
   this bar. Sentence case, unlike the signed-out label — this is a username,
   and shouting somebody's name back at them is not a style. */
/* Signed in, the chip is NOT a button any more (dchan, 2026-09-17): the filled
   blurple box around an avatar + username read as an action when it is really
   just an identity label that happens to link to the account page. Chrome off
   — no fill, no border, no reserved 158px — leaving avatar + name on the bar.
   The signed-OUT state keeps its filled button; that one IS an action. */
nav .auth.me{color:var(--ink);background:none;border-color:transparent;min-width:0;padding:0;gap:10px}
nav .auth.me:hover{background:none;border-color:transparent;color:var(--accent-text,var(--accent))}
nav .auth:empty{border-color:transparent}
@media(max-width:860px){nav .auth{min-width:0;padding:0 10px}}
/* Touch hit area (ux-audit 2026-09-26 #21): the brand and auth chip stay 32px
   to look, but a phone gets a 44px target — 6px of invisible reach above and
   below, inside the 60px bar. */
/* Screen-reader-only text: a heading the outline needs but the design
   doesn't show (ux-audit 2026-09-26 #24). */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
@media(pointer:coarse){nav .auth,nav .brand{position:relative}nav .auth::after,nav .brand::after{content:"";position:absolute;left:0;right:0;top:-6px;bottom:-6px}}
/* The top bar carries no inline links: brand on the left, then the CTA, auth
   slot and burger grouped hard right. `margin-left:auto` on the CTA is what
   pushes that group over — remove it and the whole row packs left. Every destination lives in the drawer, which is why
   the burger is display:flex at every width.
   Restored 2026-08-30 after a brief experiment with inline links. If they are
   ever added back, add `nav .links` rules with them — markup without the rules
   renders unstyled, ungapped and in the wrong place. */
@media(max-width:860px){
  nav .wrap{gap:14px}
}
/* The right-hand drawer and its burger were removed on 2026-09-16 — site
   links live in the footer now — but ~36 lines of their CSS stayed behind,
   along with comments that still explained the bar in terms of a CTA that
   no longer exists. Deleted 2026-09-22 (ux-audit, P3.1). No page contains
   `class="drawer"`, `burger` or `class="cta"`; verified across all twelve.
   The auth slot is now the only control in the bar — see `nav .auth`. */
.wrap{max-width:1400px;margin:0 auto;padding:0 24px}
/* Skip link (2026-09-22 ux-audit, P3.10). Off-screen, not display:none —
   display:none would take it out of the tab order, which is the whole point
   of it. It lands ON the nav band when focused, above everything. */
.skip{position:absolute;left:8px;top:-60px;z-index:200;padding:10px 16px;
  background:var(--accent);color:var(--bg);border-radius:6px;font-weight:700;
  font-size:13px;line-height:1;transition:top .12s ease}
.skip:focus{top:8px;text-decoration:none}
@media (prefers-reduced-motion:reduce){.skip{transition:none}}

/* ONE PAGE WIDTH, 1400, EVERY PAGE (dchan, 2026-09-16). The cap above is the
   only place it lives — there is deliberately no second rule here.
   It moved five times in one day: 1120 everywhere, then 1120 marketing / 1400
   consoles, then 1400 everywhere, then 1400 / 1500, then 1500 everywhere, and
   settled back on 1400. Twice in that sequence the value was split by page
   kind and twice it converged again, which is the argument for keeping it in
   ONE declaration: every one of those moves was a single edit, and none of
   them could leave a stale twin behind.
   If a console ever needs its own width again, the rule is
   `body.console main.wrap` — scoped to main.wrap, NEVER to `.wrap`.
   `body.console .wrap` is two classes and beats `nav .wrap` (one class, one
   element), which drags the full-bleed header back onto the page's cap. That
   exact bug was introduced and fixed here on 2026-09-16.
   Prose does NOT follow this number: `.lede` is capped at 60ch and body copy
   stays on a character measure. A wider page buys more columns, never longer
   lines.
   THE BAR IS NOT ON THIS CAP — see `nav .wrap` above, which sets
   max-width:none so the header runs edge to edge on every page. */
@media(max-width:720px){.wrap{padding:0 16px}}
/* ── THE PROSE MEASURE (2026-09-22 ux-audit, P2.1) ────────────────────────
   The cap above governs the PAGE. It must never govern a LINE OF TEXT, and
   for six days it did: when `.wrap` went 1120 → 1400 on 2026-09-16 the prose
   went with it, and eight of the twelve pages ended up setting body copy at
   166–200 characters a line on a 1440 screen — privacy and disclaimer, the two
   pages anyone actually reads end to end, at 169ch of legal text.

   DESIGN.md §2 had specified `.lede` 38em and `.sec-sub` 44em all along. Only
   the five arcade heroes ever carried them, each as its own copy; the section
   subs, the doc pages and the card copy never did. `.sec-sub` even kept
   `margin:0 auto` — centring that does nothing without a width to centre
   within, so it read as intentional while doing nothing at all.

   Declared HERE, once, for the same reason the section rhythm is: a measure
   that lives in eight pages is a measure that drifts in eight pages. 68ch is
   the general body cap — comfortably inside the 45–90ch range and wide enough
   that a two-column card still fills. Anything that is deliberately NOT prose
   — a status line, an empty state, fine print — is excluded by name.

   If a page ever needs to run a paragraph wide, the rule is `.measure-none`
   on that element, NOT a new max-width somewhere else. */
.lede{max-width:38em}
.sec-sub{max-width:44em}
/* ONE rule for body prose, not a list of container class names. The first
   attempt scoped this to `main.wrap > p` plus `.card p`, and immediately had
   to grow `.channel`, `.scope`, `.offer` behind it — every panel class on the
   site would have ended up here, and the next one added would have been
   missed. A paragraph is a paragraph wherever it sits.
   72ch binds only where a line is ACTUALLY too long: measured against the
   real grids, a 2-up card runs 614/668px and a 3-up 272/326px, so neither is
   capped by this and no column card gains a ragged right edge. What it does
   catch is the full-width panel — `.card.single`, `.channel`, `.scope` — which
   ran 179ch.
   Excluded by name: things that are NOT prose and are centred or measured by
   their own rule. Use `.measure-none` for a deliberate exception. */
main p:not(.lede):not(.sec-sub):not(.fine):not(.updated):not(.status):not(.empty):not(.dlstatus):not(.form-status),
main li{max-width:72ch}
.note,.glegend{max-width:68ch}
.measure-none{max-width:none}
footer{border-top:1px solid var(--line);padding:40px 0 40px;text-align:center;line-height:1.7;font-size:16px}
/* The footer is identical on every page. Colour, size, spacing and the
   button are pinned here so a page's own h2/.btn/p rules cannot restyle it. */
footer h2{font-size:clamp(22px,3.6vw,36px);margin:0 0 10px;color:var(--ink)}
footer p{color:var(--ink-dim);margin:0 auto 18px;max-width:none;font-size:15px}
footer .fine{font-size:12.5px;color:var(--ink-faint);margin:10px auto 0;max-width:56em;text-align:center}
.btn{display:inline-flex;align-items:center;justify-content:center;height:var(--btn-h,32px);padding:0 20px;border-radius:6px;font-weight:700;font-size:13px;line-height:1;border:1px solid transparent;background:transparent;font-family:inherit;cursor:pointer;text-decoration:none}
.btn.primary{background:var(--accent);color:var(--bg)}
.btn.primary:hover{filter:brightness(1.12);text-decoration:none}
/* invite popup — same as index.html */
.el-overlay{position:fixed;inset:0;z-index:100;background:rgba(0,0,0,.72);display:flex;align-items:center;justify-content:center;padding:24px}
.el-overlay[hidden]{display:none}
/* invite popup */
.inv-modal{background:#191919;border:2px solid var(--accent);border-radius:8px;max-width:480px;width:100%;padding:28px 30px;position:relative}
.inv-modal h3{font-size:24px;margin-bottom:14px}
.inv-modal p{font-size:14.5px;color:var(--ink-dim);line-height:1.6;margin-bottom:12px}
.inv-modal ul{list-style:none;margin:0 0 20px;font-size:14px;color:var(--ink-dim)}
.inv-modal li{padding:5px 0 5px 22px;position:relative}
.inv-modal li::before{content:"▸";position:absolute;left:4px;color:var(--accent)}
.inv-modal .actions{display:flex;gap:12px;flex-wrap:wrap}
.inv-modal .close{position:absolute;top:10px;right:12px;background:none;border:none;color:var(--ink-dim);font-size:20px;cursor:pointer;line-height:1;padding:4px}
.inv-modal .close:hover{color:var(--ink)}
/* The two ends of the scale. Height is `min-height` with `height:auto` so a
   label that wraps grows the button instead of overflowing it — the fixed
   `height` on `.btn` is what forced `.send` to need the same treatment in the
   mobile block. */
.btn.sm{height:auto;min-height:var(--btn-h-sm);padding:0 12px;font-size:12px;font-weight:600}
.btn.lg{height:auto;min-height:var(--btn-h-lg);min-width:var(--btn-minw)}
.btn.ghost{border-color:var(--line);color:var(--ink);background:transparent}
.btn.ghost:hover{border-color:var(--accent);text-decoration:none}
/* the nav/footer buttons are <button>s now; keep their link look */
/* invite modal on small screens — shared by every page */
@media(max-width:480px){.el-overlay{padding:14px}.inv-modal{padding:22px 18px}}
/* doc-style pages (contact, subscription, account, privacy, disclaimer).
   The gap above the footer must be exactly the 88px bottom padding on every
   page, so whatever element comes last must not add its own margin. */
/* Section rhythm. ONE value — 40px — on every facing edge: section top and
   bottom, the hero's bottom, main.wrap, and the footer's top. Because padding
   adds at a join, that makes EVERY boundary exactly 80px: hero→section,
   section→section, section→footer. Change the 40 and they all move together.
   The trap this replaces: 50px per side read as "50px gaps" but produced
   100px between sections, 80px after the hero and 90px before the footer —
   three different gaps from one number. */
section{padding:40px 0}
main.wrap{padding-top:40px;padding-bottom:40px}
main.wrap>:last-child,main.wrap>:last-child>:last-child{margin-bottom:0}


/* ── Status notes (toasts) ─────────────────────────────────────────────────
   One implementation for every console (2026-09-15, dchan: the old
   top-centre pill sat under the sticky nav and away from what was clicked).
   Top-right, just under the nav, stacked newest at the bottom. The corner is
   ONE variable so it can be moved in a line: set --toast-side to `left` or
   swap --toast-top for `bottom`. Motion: 160ms in from 8px above, 120ms out;
   opacity only under reduced motion. Tones: ok / warn / bad, plus neutral.
   Errors never auto-clear and carry a close button; ok clears in 3.6s or
   longer for a long note (the page's toast() scales the time by length),
   warn in 8s.
   Audit 2026-09-15: the stack sits 24px in — the .wrap gutter — and is
   capped at 360px so it no longer covers the sticky search bar on /servers;
   radius 8 (the notice-box size, not a fourth radius); text 500 with the
   leading sentence in <b>; and it enters from 8px BELOW, the direction the
   stack grows, instead of dropping in from above. Close is a 28px target. */
:root{--toast-top:calc(60px + 12px);--toast-gap:8px}
.toasts{position:fixed;top:var(--toast-top);right:24px;z-index:950;display:flex;flex-direction:column;gap:var(--toast-gap);
  width:min(360px,calc(100vw - 48px));pointer-events:none}
.toast{display:flex;align-items:flex-start;gap:10px;padding:10px 12px 10px 14px;border-radius:8px;
  background:var(--surface);border:1px solid var(--line);color:var(--ink);
  font-size:12.5px;font-weight:500;line-height:1.45;text-align:left;
  box-shadow:0 10px 30px rgba(0,0,0,.38);
  opacity:0;transform:translateY(8px);transition:opacity .16s ease,transform .16s ease;pointer-events:auto}
.toast.show{opacity:1;transform:translateY(0)}
.toast.hide{opacity:0;transform:translateY(8px);transition:opacity .12s ease,transform .12s ease}
.toast .tx{flex:1;min-width:0;overflow-wrap:anywhere}
.toast .tx b{font-weight:700}
.toast .x{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:-6px -6px -6px 0;
  font:inherit;font-size:14px;font-weight:700;line-height:1;background:none;border:0;border-radius:6px;color:inherit;opacity:.7;cursor:pointer;padding:0}
.toast .x:hover{opacity:1}
.toast .x:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.toast.ok{border-color:color-mix(in srgb, var(--ok) 55%, transparent);color:var(--ok)}
.toast.warn{border-color:color-mix(in srgb, var(--warn) 55%, transparent);color:var(--warn)}
.toast.bad{border-color:color-mix(in srgb, var(--bad) 55%, transparent);color:var(--bad)}
@media (max-width:720px){.toasts{right:16px;width:min(360px,calc(100vw - 32px))}}
@media (max-width:640px){.toasts{right:8px;left:8px;width:auto}}
@media (prefers-reduced-motion:reduce){.toast,.toast.hide{transition:opacity .16s ease;transform:none}}

/* Row flash: the in-place answer to a write. The control already shows the
   new value; this is the half-second confirmation that the write landed, on
   the row that was touched. Every console row shape (feature, channel/role,
   branding) takes it — the pages call landed(row, ok) after each write
   (2026-09-15 audit, P3.1). */
@keyframes rowflash{0%{background:color-mix(in srgb, var(--ok) 22%, transparent)}100%{background:transparent}}
@keyframes rowflashbad{0%{background:color-mix(in srgb, var(--bad) 22%, transparent)}100%{background:transparent}}
.frow.flash,.crow.flash,.brow.flash{animation:rowflash .9s ease-out}
.frow.flash-bad,.crow.flash-bad,.brow.flash-bad{animation:rowflashbad .9s ease-out}
@media (prefers-reduced-motion:reduce){.frow.flash,.crow.flash,.brow.flash,.frow.flash-bad,.crow.flash-bad,.brow.flash-bad{animation:none}}

/* ── Capsule ──────────────────────────────────────────────────────────────
   The small inline note under a row's description (2026-09-15 audit, P2.6).
   There were four — .w (warn outline), .also (info fill), .req.off (a second
   .w on /manage) and .note2 (italic, left-ruled) — in three fonts. One base
   now, 11.5px/400, 2px 9px, 999, and three tones:
     .cap        neutral — --line border, dim text. A caveat, an "Includes".
     .cap.warn   outline in warn — the one that BLOCKS a switch.
     .cap.info   8% info fill, 40% border — a heads-up that informs only.
   inline-block so it hugs its text; align-self:start keeps it hugging inside
   a flex column. Weight 400 on purpose — the row's own name is what an admin
   scans for, and the capsule must stay beneath it. */
.cap{display:inline-block;align-self:start;font-size:11.5px;font-weight:400;line-height:1.5;font-style:normal;margin-top:4px;
  padding:2px 9px;border-radius:999px;border:1px solid var(--line);color:var(--ink-dim);background:none}
.cap.warn{color:var(--warn);border-color:color-mix(in srgb, var(--warn) 28%, transparent)}
.cap.info{color:var(--info);border-color:color-mix(in srgb, var(--info) 40%, transparent);background:color-mix(in srgb, var(--info) 8%, transparent)}
/* A row of capsules on one line (the wizard's "Includes …" + "Also enables …"). */
.caps{display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start}
/* ── Collapsible capsule (`details.cap`) — 2026-09-15 ────────────────────
   The wizard's "Also enables …" listed fifteen features in full on EVERY row,
   and the list was near-identical each time (the reward economy, the guide
   console and its pages come with almost everything). It was three lines of
   blue text under a one-line choice: the capsule was louder than the feature
   it annotated, and the step scrolled for pages.
   Collapsed it states the count; open it lists them. `<details>` rather than a
   JS toggle so it is keyboard-operable and announced as expandable for free.
   Same three tones as a flat `.cap` — this only adds the disclosure. */
details.cap{padding:0}
/* OPEN, IT IS A RECTANGLE (2026-09-15, dchan). A capsule is a pill because it
   hugs one line of text; three wrapped lines inside a 999px radius is a blob
   with its first and last words pushed inward by the curve. Open, it becomes a
   panel — radius 6, the control radius DESIGN.md uses for every box that holds
   content. Collapsed it is a pill again, so the thing you click still reads as
   a capsule. */
details.cap[open]{border-radius:6px}
details.cap[open]>summary{border-radius:6px 6px 0 0}
details.cap>summary{display:flex;align-items:center;gap:5px;list-style:none;cursor:pointer;padding:2px 9px;border-radius:999px}
details.cap>summary::-webkit-details-marker{display:none}
/* The caret is the affordance, so it must not be the only one — the summary
   text says "more", and the whole capsule is the hit target. */
details.cap>summary::before{content:"";width:0;height:0;border-left:4px solid currentColor;border-top:3.5px solid transparent;border-bottom:3.5px solid transparent;transition:transform .12s ease;flex:none}
details.cap[open]>summary::before{transform:rotate(90deg)}
details.cap>summary:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
details.cap .capbody{padding:0 9px 5px;margin-top:-1px}
@media (prefers-reduced-motion:reduce){details.cap>summary::before{transition:none}}

/* ── Consoles (opt in with <body class="console">) ────────────────────────
   /servers, /manage and /onboarding are consoles, not pages of the site.
   They share one card scale, one tab strip and one server list, defined
   ONCE here so they cannot drift apart again (2026-09-15 audit, §5.6–5.8):
   each of the three used to carry its own copy at slightly different values
   — radius 10, three paddings, an h1 that was 28px flat on one of them. */
body.console .card,body.console .grp{background:var(--surface);border:1px solid var(--line);border-radius:11px;padding:16px 18px;margin:0 0 14px}
body.console .card>h3,body.console .card>h2,body.console .grp .gh h3,body.console .card .ghead h3{font-size:13px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-faint);margin:0 0 4px;font-weight:700}
/* A heading that shares its row with a switch or the Allow/Block buttons is
   centred by the row, so it carries no margin of its own. */
body.console .grp .gh h3,body.console .card .ghead h3{margin:0}
/* The empty / loading line, and the spinner in front of it while a fetch is
   in flight — one 12px ring, accent on line, guarded under reduced motion
   (P3.5). Add `busy` while loading, drop it (or replace the node) after. */
/* 13.5px is THE size (2026-09-22 ux-audit, P3.7). account.html overrode it
   to 14px and DESIGN.md documented a third answer; both now agree. */
.empty{color:var(--ink-faint);font-size:13.5px;margin:0}
.empty.busy::before{content:"";display:inline-block;width:12px;height:12px;margin:0 8px -2px 0;border-radius:50%;
  border:2px solid var(--line);border-top-color:var(--accent);animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
/* A tab switch fades in over the same 160ms the toast uses — one tempo. */
@keyframes fadein{from{opacity:0}to{opacity:1}}
body.console .panel{animation:fadein .16s ease}
@media (prefers-reduced-motion:reduce){.empty.busy::before{animation:none;border-top-color:var(--line)}body.console .panel{animation:none}}
/* Tab strip on a phone: one scrolling row, never three wrapped ones (P1.7).
   The negative margin lets it bleed to the screen edge like the .wrap gutter. */
@media (max-width:720px){
  body.console .tabs{flex-wrap:nowrap;overflow-x:auto;margin-inline:-16px;padding-inline:16px}
  /* BIGGER TYPE, not just a bigger box (2026-09-15, dchan: "the top menu
     selection buttons... they are still hard to see"). The 44px rule below
     grew the tab's HEIGHT and its hit area, but the label stayed at the
     desktop 12px — so the strip became easy to hit and no easier to read.
     14px is the body size; a primary navigation control should not be smaller
     than the text it navigates to. Defined here rather than in the two pages
     that each declare `.tabs button` at 12px: `body.console .tabs button`
     outranks a page's bare `.tabs button`, so one rule covers both and they
     cannot drift. */
  body.console .tabs button{flex:none;font-size:14px;line-height:18px;padding:10px 18px}
  /* NO scrollbar on the selection strips (dchan, 2026-09-16). A thin bar was
     added 2026-09-15 as the "more tabs off the right edge" affordance, over an
     edge fade — the fade lies, because scrolled to the end the last tab stays
     half-faded and, if it is the SELECTED one, reads as disabled. dchan's call
     is that a scrollbar under a row of buttons is worse than no hint at all.
     The affordance that remains is SCROLL-SNAP: a swipe lands a tab flush
     against the left padding, so the strip still feels like a strip. Keep the
     snap if the bar ever goes back. */
  body.console .tabs{scrollbar-width:none;-ms-overflow-style:none}
  body.console .tabs::-webkit-scrollbar{display:none}
  /* Snap so a swipe lands a tab flush against the left padding rather than
     halfway under it. `proximity`, not `mandatory`: a short drag to peek at
     the next tab should not be yanked back. */
  body.console .tabs{scroll-snap-type:x proximity;scroll-padding-inline-start:16px}
  body.console .tabs button{scroll-snap-align:start}
}
/* Server list on a phone: a horizontal strip above the pane, so the list is
   still reachable after a server is opened (P1.8 — it used to stack above
   the pane at full height with the pane out of sight beneath it). */
/* 760, MOVED FROM 820 (dchan, 2026-09-16) — IT MUST MATCH servers.html's
   `.layout` collapse breakpoint, which moved to 760 in the same pass so the
   guild rail survives the whole tablet range.
   These two are ONE decision in two files: this block turns the rail's list
   into a horizontal scroller for the stacked layout, and only the layout query
   knows when the layout is actually stacked. While they disagreed, 760–820 was
   a band where the rail was still a 260px column but its list had gone
   horizontal — two guild buttons side by side, clipped at the column edge.
   Caught by screenshot, not by reading: both files parse perfectly. */
@media (max-width:760px){
  body.console .glist{flex-direction:row;overflow-x:auto;gap:8px;max-height:none;padding-bottom:4px;
    scrollbar-width:none;-ms-overflow-style:none;
    scroll-snap-type:x proximity;scroll-padding-inline-start:16px}
  body.console .glist::-webkit-scrollbar{display:none}
  body.console .glist .g{scroll-snap-align:start}
  body.console .g{flex:none;width:auto;max-width:200px}
  body.console .g .m{display:none}
}
/* Visually hidden, still in the accessibility tree. For a reason the layout
   conveys to sighted users but says nothing about to a screen reader — a
   follower row locked by the parent directly above it (audit 2026-09-16).
   Not `display:none` and not `visibility:hidden`: both remove it from the
   tree, which is the opposite of the point. */
.sronly{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
/* The server list's load failure: message plus a Retry button under it
   (2026-09-15 ux-audit — the only way back used to be a reload). */
body.console .glist .retry{display:flex;flex-direction:column;align-items:flex-start;gap:10px}
/* 44px targets on a phone (2026-09-15 ux-audit). Switches were 42×24, tabs
   ≈33 tall, pills ≈30, row buttons ≈30, search 34, the colour input 34×32,
   selects ≈32. One block: every control in a console gets min-height:44px
   under 720px. The switch keeps its 42×24 track — the design says the
   footprint never changes — and pads its HIT AREA instead with a ::before
   10px outside the box (::after is the thumb). */
@media (max-width:720px){
  body.console .tabs button,body.console .subnav button,body.console .prod button,
  body.console .brow button,body.console .card .actions button,body.console .modal .mfoot button,
  body.console .crow select,body.console .brow select,body.console .f select,
  body.console .brow input[type="text"],body.console .f input,
  /* The onboarding wizard's own row class (2026-09-15): its selects and text
     inputs were ~30px and were never in this list, so every control on the
     nine setup steps missed the 44px rule the consoles already follow. */
  body.console .frow select,body.console .frow input[type="text"],
  body.console .rail button,
  body.console .qbar .hq,body.console .subnav input[type="search"],
  body.console .g{min-height:44px}
  body.console input[type="color"]{min-height:44px;min-width:44px}
  body.console button.sw::before{content:"";position:absolute;inset:-10px}
  /* A short label ("All") meeting min-height:44px inside a 999px radius is a
     CIRCLE, not a pill — it read as a different kind of control beside
     "Features ON". A width floor and roomier sides keep every chip the same
     shape whatever its label length (dchan, 2026-09-16). */
  /* The 72px floor existed only to stop a short label ('All') collapsing into
   a circle at the 999px radius. The chips are rectangles as of 2026-09-16, so
   the floor drops to the WCAG 2.5.8 target size and the roomier sides stay. */
  body.console .subnav button{min-width:44px;padding-left:14px;padding-right:14px}
  /* The "Also enables N more" capsule is a real control and was 138x21 — under
     this block's 44px and under WCAG 2.5.8's 24px floor. It postdates the
     block, which is why it was missed (audit 2026-09-16). */
  body.console details.cap>summary{min-height:44px;align-items:center}
}
/* Every page's buttons, not just the consoles'. `.btn` was raised to 44px
   inside the body.console block on 2026-09-16, which left the marketing and doc
   pages ("Get access", "See what it does", "Start custom setup") at 32px on a
   phone — the same control at two sizes depending which page you were on.
   Scoped to `.btn` alone, so the nav bar is untouched: DESIGN.md fixes the CTA,
   auth slot and burger at 32px as one pair, and that is a deliberate decision,
   not an oversight (dchan, 2026-09-16). */
@media (max-width:720px){
  .btn{min-height:44px}
  /* The contact and subscription forms were the last controls outside the
     rule: `.send` is a fixed 32px (--btn-h) and the inputs 40px, so the only
     form a visitor types into on a phone had the smallest targets on the site
     (mobile pass, 2026-09-21). `.send` needs height:auto or min-height loses
     to the fixed height; the inputs grow through min-height alone. */
  .send{height:auto;min-height:44px}
  form input,form textarea,.field input{min-height:44px}
}
/* The filter chips were `position:sticky` AND six rows tall. Measured on an
   iPhone SE while scrolled: nav 61 + qbar 60 + subnav 360 = 468px of a 667px
   screen pinned, leaving 199px of content — barely more than one feature row.
   The search bar above them stays sticky; the chips scroll away like any other
   content (audit 2026-09-16, P1). */
@media (max-width:720px){
  body.console .subnav{position:static;top:auto}
}
/* The setup rail scrolls 384px past the right edge, so nothing says there are
   eight steps. It gets the same treatment as the `.tabs` strip rather than a
   second answer to one question: SCROLL-SNAP, so a swipe lands a step flush
   against the left padding. No scrollbar on any selection strip — dchan's
   call, 2026-09-16; see the note on `.tabs` above (audit 2026-09-16, P2). */
@media (max-width:720px){
  body.console .rail{scrollbar-width:none;-ms-overflow-style:none;
    scroll-snap-type:x proximity;scroll-padding-inline-start:16px}
  body.console .rail::-webkit-scrollbar{display:none}
  body.console .rail button{scroll-snap-align:start}
}

/* ── The burger and drawer are gone (dchan, 2026-09-16) ─────────────────────
   Site links live in the footer now. Two things had to move with them:

   1. The auth slot used to be `display:none` below 860px and lived in the
      drawer instead — so with the drawer removed it was the ONLY sign-in and
      account route on a phone. It now stays in the bar at every width, made
      compact rather than hidden.
   2. The signed-out label is split: "Sign in" + `<span class="authx">` with
      " with Discord". Desktop reads the full phrase; below 860px the span is
      hidden so the chip fits beside the 158px CTA on a 375px screen.
   Signed in, the bar shows the avatar alone — `font-size:0` collapses the
   username text node (it is a text node, so it cannot be hidden by selector)
   while the 32px avatar is unaffected by font-size. */
@media (max-width:860px){
  nav .auth .authx{display:none}
  /* Avatar-only, but ONLY when there is an avatar. A Discord account with no
     custom avatar returns `avatar: null`, so font-size:0 left an empty blue
     chip with nothing in it at all. :has(img) keeps the collapse for the
     common case and lets the username show when it is the only content. */
  nav .auth.me:has(img){font-size:0;padding:0;gap:0}
  nav .auth.me:not(:has(img)){max-width:34vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;line-height:30px;font-size:14px}
}

/* Footer site links, replacing the drawer menu. */
/* The footer lost its heading and tagline (dchan, 2026-09-16), so the 22px
   top margin that separated the links from that block now just stacks on
   footer's own 40px padding — 63px of empty space above a two-row menu. */
footer .fnav{display:flex;flex-wrap:wrap;justify-content:center;gap:6px 22px;margin:0 0 10px}
/* site.css styles a bare `nav` as the sticky blurred site header, so this one
   opts out explicitly — the same trap `.subnav` documents on /servers. */
footer nav.fnav{position:static;height:auto;background:none;border:0;backdrop-filter:none;z-index:auto}
/* White, not --ink-dim (dchan, 2026-09-16): these are the site's only
   navigation now that the drawer is gone, so they read as links rather than
   fine print. The current page keeps the accent so "you are here" still has a
   signal that is not just weight. */
footer .fnav a{color:var(--ink);font-size:13px;text-decoration:none;padding:6px 2px}
footer .fnav a:hover{color:var(--accent-text,var(--accent))}
footer .fnav a[aria-current="page"]{color:var(--accent-text,var(--accent))}
@media (max-width:720px){
  footer .fnav{gap:2px 18px}
  footer .fnav a{min-height:44px;display:inline-flex;align-items:center}
}

/* ── The switch (button.sw), ONE copy (2026-09-21, parity review) ─────────
   Was pasted verbatim into servers.html, manage.html and onboarding.html. Lives
   here under body.console so a page rule of equal specificity (.frow button)
   can no longer out-order it — the trap DESIGN.md warns about. Pages carry no
   copy of their own any more. */
body.console button.sw,body.console span.sw{flex:none;display:inline-block;vertical-align:middle;width:42px;height:24px;box-sizing:border-box;padding:0;margin:0;
  border-radius:6px;border:1px solid var(--ink-faint);background:var(--bg);position:relative;cursor:pointer;color:inherit;opacity:1;
  transition:background .18s ease,border-color .18s ease,box-shadow .18s ease}
body.console button.sw::after,body.console span.sw::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:4px;background:var(--ink-faint);
  transition:transform .18s cubic-bezier(.2,.8,.2,1),width .12s ease,background .18s ease,border-color .18s ease,opacity .18s ease}
body.console button.sw[aria-checked="true"],body.console span.sw[aria-checked="true"]{background:var(--ok);border-color:var(--ok)}
body.console button.sw[aria-checked="true"]::after,body.console span.sw[aria-checked="true"]::after{transform:translateX(18px);background:var(--bg)}
body.console button.sw:hover:not([disabled]){border-color:var(--ink-faint);color:inherit;box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
body.console button.sw[aria-checked="true"]:hover:not([disabled]){border-color:var(--ok)}
body.console button.sw:active:not([disabled])::after{width:20px}
body.console button.sw[aria-checked="true"]:active:not([disabled])::after{transform:translateX(14px)}
body.console button.sw:focus-visible{outline:none;box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--accent)}
body.console button.sw[disabled]{cursor:not-allowed;opacity:1}
body.console button.sw[disabled]::after{background:transparent;border:2px solid var(--ink-faint);box-sizing:border-box}   /* no .6 opacity — the ring sat at ≈2.8:1 (2026-09-15) */
body.console button.sw[disabled][aria-checked="true"]{background:color-mix(in srgb,var(--ok) 45%,var(--bg));border-color:color-mix(in srgb,var(--ok) 45%,var(--bg))}
body.console button.sw[disabled][aria-checked="true"]::after{border-color:var(--bg);opacity:.75}
body.console button.sw.busy[disabled]::after{animation:swbusy .9s ease-in-out infinite}
@keyframes swbusy{0%,100%{opacity:.3}50%{opacity:.95}}
@media (prefers-reduced-motion:reduce){body.console button.sw,body.console span.sw,body.console button.sw::after,body.console span.sw::after{transition:none}button.sw.busy[disabled]::after{animation:none}}
