/* Recorder website — one stylesheet, no framework, no build step.
 *
 * Deliberately external rather than inline: nginx serves this site with
 * `Content-Security-Policy: default-src 'self'`, which forbids inline <script>
 * and inline style attributes. Keeping every rule here is what lets that header
 * stay strict, and a strict CSP is the single cheapest defence for pages that
 * hold a session token.
 */

/* ── fonts ──────────────────────────────────────────────────────────── */

/* Self-hosted in /assets/fonts — the CSP is `default-src 'self'` with no
 * font-src carve-out, so a Google Fonts <link> would simply be blocked.
 * latin + latin-ext subsets only: the non-Latin locales (ja, ko, zh, th, hi,
 * ar…) fall through the stacks below to their system fonts, which ship better
 * native coverage than any subset we could serve. */
@font-face {
    font-family: "Public Sans";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/assets/fonts/public-sans-400.woff2") format("woff2");
}
@font-face {
    font-family: "Public Sans";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("/assets/fonts/public-sans-500.woff2") format("woff2");
}
@font-face {
    font-family: "Public Sans";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("/assets/fonts/public-sans-600.woff2") format("woff2");
}
@font-face {
    font-family: "Manrope";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("/assets/fonts/manrope-500.woff2") format("woff2");
}
@font-face {
    font-family: "Manrope";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("/assets/fonts/manrope-600.woff2") format("woff2");
}
@font-face {
    font-family: "Manrope";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/assets/fonts/manrope-700.woff2") format("woff2");
}
@font-face {
    font-family: "IBM Plex Mono";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/assets/fonts/plex-mono-400.woff2") format("woff2");
}

/* ── reset ──────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html { -webkit-text-size-adjust: 100%; }

body {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    font-family: var(--font-body);
    /* 18px, not 16 (owner, 2026-09-15: "the website should be more zoomed
     * in"). Every size below is rem, so this one number is the zoom. */
    font-size: 18px;
    line-height: 1.6;
    background: var(--bg);
    color: var(--text);
    -webkit-font-smoothing: antialiased;
}

img, svg { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
h1, h2, h3, h4 { font-family: var(--font-display); line-height: 1.25; font-weight: 600; letter-spacing: -0.02em; }

/* ── tokens ─────────────────────────────────────────────────────────── */

/* DEEP-INDIGO DARK is the default, everywhere, regardless of the OS setting —
 * the owner's direction (2026-08-28) is to match deepinfra.com, which was
 * scraped directly for these values: canvas #040320, primary #4c59d3, link
 * blue #7d8aff, green #3ddc97, Manrope display over Public Sans body. Light
 * exists only as the explicit [data-theme="light"] override below — there is
 * deliberately no prefers-color-scheme media query. */
:root {
    /* Globe for the language picker, inline so it costs no request and
     * inherits colour. Used as a MASK, not a background, which is why the
     * stroke colour inside is irrelevant -- only its alpha is read.
     * Single quotes inside, because the value is wrapped in double ones. */
    --globe: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M3 12h18'/%3E%3Cpath d='M12 3c2.6 2.8 2.6 15.2 0 18'/%3E%3Cpath d='M12 3c-2.6 2.8-2.6 15.2 0 18'/%3E%3C/svg%3E");

    /* The deepinfra surface stack, hue-shifted from their indigo to the
     * desktop app's blue (owner's choice, 2026-08-28): same depths, blue-black
     * instead of purple-black, so the site and the app read as one product. */
    --bg:        #030d1f;
    --bg-soft:   #071427;
    --panel:     #081a33;
    --panel-2:   #10294e;
    --line:      #1e3050;
    --line-soft: #16263f;

    /* Borders on form controls, separately from --line.
     *
     * WCAG 1.4.11 wants 3:1 for anything that identifies a control, and the
     * edge of a text box is exactly that — it is the only thing telling you
     * where the field is. --line is tuned for card edges and dividers, which are
     * decorative and correctly much fainter; using it on inputs made them fail.
     * Two tokens because the two jobs have genuinely different requirements. */
    --field-line: #52678c;

    --text:      #ffffff;
    --text-dim:  #c7d0e0;
    --muted:     #93a3bd;

    /* Two accent roles. --accent-fill is the app's own blue — white on
     * #2862d7 measures 5.5:1, fine for button labels — but as TEXT on the
     * blue-black canvas that same blue is ~3.6:1 and fails AA, so links and
     * focus use the lifted #6ea8ff (~8:1 on --bg, ~6:1 inside
     * --accent-soft). */
    --accent:      #6ea8ff;
    --accent-hover:#8fbcff;
    --accent-ink:  #ffffff;
    --accent-soft: #0e2444;
    --accent-fill:       #2862d7;
    --accent-fill-hover: #3b74e8;

    /* The aurora glows behind the hero, and nowhere else. Colour only — the
     * shapes live on .hero::before so the effect cannot leak onto the content
     * pages that share this sheet. */
    --glow-a: rgba(59, 116, 232, .28);
    --glow-b: rgba(85, 170, 255, .15);

    /* deepinfra's mint green, and companions tuned for the blue canvas. */
    --ok:      #3ddc97;
    --ok-soft: #0a2b20;
    --warn:      #ffca16;
    --warn-soft: #2b2308;
    --danger:      #ff9592;
    --danger-soft: #33141b;

    --radius:    10px;
    --radius-lg: 14px;
    --shadow:    0 1px 2px rgba(0,0,0,.45), 0 10px 30px rgba(2,10,24,.5);
    --font-body:    "Public Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --font-display: "Manrope", "Public Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;

    color-scheme: dark;
}

/* The light theme, one toggle away (app.js writes data-theme="light" and
 * remembers it). Built from the same scrape: deepinfra's light sections are
 * white with #040320 ink, #475467 body gray and #d0d5dd hairlines, and the
 * indigo primary passes AA as both link text and button fill on white. */
:root[data-theme="light"] {
    --bg:        #f6f8fa;
    --bg-soft:   #ffffff;
    --panel:     #ffffff;
    --panel-2:   #eef1f6;
    --line:      #d0d5dd;
    --line-soft: #eaecf0;
    --field-line: #6f7684;

    --text:      #0a1424;
    --text-dim:  #333d52;
    --muted:     #475467;

    /* #0f5fd0 measured 4.5:1+ as link text on white and under white button
     * labels — the same value carried over from the earlier light theme. */
    --accent:      #0f5fd0;
    --accent-hover:#0a4dab;
    --accent-soft: #e8f0fe;
    --accent-fill:       #0f5fd0;
    --accent-fill-hover: #0a4dab;

    --glow-a: rgba(40, 98, 215, .10);
    --glow-b: rgba(85, 170, 255, .10);

    /* Measured for AA on white and on their own soft backgrounds. */
    --ok:      #15683f;
    --ok-soft: #e6f5ed;
    --warn:      #8a6212;
    --warn-soft: #fbf1dc;
    --danger:      #c23934;
    --danger-soft: #fdeceb;

    --shadow: 0 1px 2px rgba(16,24,40,.06), 0 8px 24px rgba(16,24,40,.06);
    color-scheme: light;
}

/* Present to assistive technology, absent from the page.
 *
 * The clip-path/1px recipe rather than display:none or visibility:hidden —
 * both of those remove the element from the accessibility tree as well, which
 * defeats the entire purpose. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ── skip link ──────────────────────────────────────────────────────── */

/* WCAG 2.4.1 Bypass Blocks. Every page repeats the same navigation before its
 * content, so a keyboard or screen-reader user would otherwise tab through it
 * on every single page. Hidden by clipping rather than display:none, because a
 * display:none element cannot receive focus and the link would never appear. */
.skip-link {
    position: absolute;
    inset-inline-start: -9999px;
    top: 0;
    z-index: 100;
    padding: 10px 16px;
    background: var(--accent-fill);
    color: var(--accent-ink);
    border-end-end-radius: var(--radius);
    font-weight: 600;
    text-decoration: none;
}
.skip-link:focus {
    inset-inline-start: 0;
}

/* ── layout ─────────────────────────────────────────────────────────── */

.wrap { width: 100%; max-width: 1180px; margin-inline: auto; padding-inline: 20px; }
.wrap-narrow { max-width: 500px; }
.wrap-mid    { max-width: 900px; }
.wrap-wide   { max-width: 1320px; }   /* the screenshot carousel */

main { flex: 1; padding-block: 40px 72px; }

.stack   { display: flex; flex-direction: column; gap: 16px; }
.stack-s { display: flex; flex-direction: column; gap: 8px; }

/* Section rhythm for the pages that stack several cards inside one .wrap.
 * A gap on the container rather than a margin on each child, so adding or
 * reordering a section never leaves a double or missing space. */
.page-stack { display: flex; flex-direction: column; gap: 32px; }
.page-stack > .auth-head { margin-bottom: 0; }
.row     { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.row-center  { justify-content: center; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.grow    { flex: 1; }
/* The ATTRIBUTE as well as the class. `hidden` alone relies on the user-agent
 * stylesheet's `[hidden] { display: none }`, and any author rule that sets
 * display outranks it -- .stack above is display:flex, so every
 * `<div class="card stack" hidden>` panel rendered permanently. That is how
 * /register came to show "Check your email" and "That address is already
 * registered" at the same time, above a form that had not been submitted.
 * The HTML uses the attribute and the JS toggles `.hidden = true/false`, so
 * the attribute is the mechanism that has to work. */
[hidden], .hidden { display: none !important; }

/* ── signed-in / signed-out visibility ──────────────────────────────── */

/* app.js stamps html.signed-in before first paint, so the correct nav renders
 * immediately instead of being corrected after the user has seen the wrong one.
 * Signed-out is the default because it is the state of every first visit — and
 * of every visit with JavaScript disabled, where "Sign in" is still the honest
 * thing to show. */
/* Both rules only ever HIDE. Nothing here says `display: revert` or restores a
 * value, because a "show" rule has to name some display value and would clobber
 * whatever the element already had — `<a class="btn" data-auth="in">` is
 * inline-flex, and reverting it to the UA default turns the button into a line
 * of text. Hiding only means every element keeps its own display in the state
 * where it is visible. */
:root:not(.signed-in) [data-auth="in"]  { display: none; }
:root.signed-in       [data-auth="out"] { display: none; }

/* ── Paddle checkout overlay ──────────────────────────────
 *
 * Paddle.js appends its overlay as a DIRECT CHILD OF <body>, and `body` above
 * is `display: flex; flex-direction: column` for the sticky footer. That makes
 * the overlay a flex ITEM: it gets laid out in the column like any other
 * section and lands at the bottom of the page, mostly below the fold, instead
 * of covering the viewport. The checkout works perfectly -- you just cannot
 * see it.
 *
 * Pinning it here is the smallest fix. The alternative was to stop body being
 * a flex container, which would mean wrapping the content of all sixteen pages
 * in a div to keep the footer at the bottom.
 *
 * Class names taken from paddle.js itself, not from documentation:
 * paddle-frame, paddle-frame-overlay, paddle-frame-inline.
 */

.paddle-frame-overlay,
body > .paddle-frame:not(.paddle-frame-inline) {
    position: fixed !important;
    inset: 0 !important;
    /* WIDTH AND HEIGHT EXPLICITLY, not just inset. The overlay is an IFRAME,
     * and an iframe carries intrinsic sizing plus whatever Paddle sets inline;
     * inset alone left it a narrow strip in the top-left corner. */
    width: 100vw !important;
    height: 100vh !important;
    max-width: none !important;
    max-height: none !important;
    border: 0 !important;
    /* Above everything of ours. The nav is z-index 10. */
    z-index: 2147483000 !important;
    /* Body is a flex container; without this the overlay is still sized and
     * ordered as a flex item even once it is positioned. */
    flex: none !important;
}

/* ── nav ────────────────────────────────────────────────────────────── */

/* The floating pill bar from the reference: the <header> itself is invisible
 * and sticky, and the .wrap inside it is the visible rounded capsule. The
 * capsule gets the blur + translucency; the page scrolls behind it. */
.nav {
    position: sticky;
    top: 0;
    z-index: 10;
    padding-block: 10px 4px;
}
.nav-inner {
    display: flex;
    align-items: center;
    gap: 8px;
    height: 56px;
    background: color-mix(in srgb, var(--panel) 82%, transparent);
    backdrop-filter: blur(10px);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding-inline: 18px;
}

.brand {
    display: flex;
    align-items: center;
    gap: 9px;
    font-family: var(--font-display);
    font-weight: 600;
    letter-spacing: -0.02em;
    font-size: 1.05rem;
    color: var(--text);
    text-decoration: none;
    margin-inline-end: auto;
    /* The name is two words and a squeezed nav broke it across two lines,
       which doubles the bar's height and reads as a layout fault. */
    white-space: nowrap;
}
/* The brand mark: the myna, on the page's own background.
 *
 * It carried a light chip for a while, on the theory that a dark-navy bird
 * would vanish against the dark-navy nav. Rendered side by side it does not:
 * the cream wing bars and the yellow beak and feet carry the silhouette on
 * their own, and the chip only ever looked like a sticker. The artwork's
 * transparency is the point -- do not put a background back behind it. */
.brand-mark {
    width: 26px;
    height: 26px;
    flex: none;
}

.nav a.nav-link {
    color: var(--text-dim);
    text-decoration: none;
    font-size: .92rem;
    padding: 7px 12px;
    border-radius: 999px;
}
.nav a.nav-link:hover { color: var(--text); background: var(--panel-2); }
.nav a.nav-link[aria-current="page"] { color: var(--text); font-weight: 500; }

.theme-toggle {
    background: none;
    border: 1px solid var(--line);
    border-radius: 999px;
    width: 32px; height: 32px;
    display: grid; place-items: center;
    cursor: pointer;
    color: var(--muted);
}
.theme-toggle:hover { color: var(--text); border-color: var(--muted); }
.theme-toggle svg { width: 15px; height: 15px; }

/* Discord in the nav: the mark AND the words. It shipped icon-only for a build
 * and a bare glyph in a row of text links reads as decoration rather than as a
 * destination, so it is labelled like everything else beside it.
 *
 * Rides on .nav-link for padding, colour and hover; this adds only the icon's
 * alignment. It must NOT use .theme-toggle, whose circular look the icon-only
 * version copied: app.js binds a click handler to every .theme-toggle, so that
 * class would flip the colour scheme on the way out to Discord. */
.nav a.nav-discord {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    white-space: nowrap;
}
.nav a.nav-discord svg { width: 15px; height: 15px; flex: none; }

/* The nav collapses to the essentials rather than growing a hamburger: there
 * are five links, and a menu that has to be opened is worse than one that
 * wraps.
 *
 * TWO breakpoints, because wrapping and hiding answer different questions.
 * Wrapping is about whether the row FITS: it used to start at 620px, and the
 * row ran out of space long before that -- at 700px "Join Discord" and the
 * theme toggle were simply cut off the right-hand edge, with no scrollbar and
 * nothing to say they were there. A nav that overflows silently is worse than
 * one that is two lines tall, so it now wraps as soon as it is tight.
 * Hiding the optional links stays at 620px: there is room for them in the
 * range between, and dropping them earlier would take away navigation the
 * layout can still afford. */
@media (max-width: 960px) {
    /* The capsule relaxes to a rounded rectangle when the links wrap — a
     * stadium shape around two lines reads as a mistake. */
    .nav-inner { height: auto; padding-block: 10px; flex-wrap: wrap; border-radius: 22px; }
    .brand { margin-inline-end: auto; }
}
@media (max-width: 620px) {
    .nav-hide-sm { display: none; }
}

/* ── footer ─────────────────────────────────────────────────────────── */

.footer {
    border-top: 1px solid var(--line);
    padding-block: 24px;
    font-size: .87rem;
    color: var(--muted);
}
.footer a { color: var(--muted); text-decoration: none; }
.footer a:hover { color: var(--text); text-decoration: underline; }

/* ── typography ─────────────────────────────────────────────────────── */

h1 { font-size: 2rem; }
h2 { font-size: 1.4rem; }
h3 { font-size: 1.05rem; }

.lede    { color: var(--text-dim); font-size: 1.03rem; }
.muted   { color: var(--muted); }
.small   { font-size: .87rem; }
.mono    { font-family: var(--mono); }
.center  { text-align: center; }

a { color: var(--accent); }
a:hover { color: var(--accent-hover); }

.eyebrow {
    font-size: .74rem;
    text-transform: uppercase;
    letter-spacing: .09em;
    font-weight: 600;
    color: var(--muted);
}

/* ── cards ──────────────────────────────────────────────────────────── */

.card {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: 22px;
}
.card-pad-lg { padding: 28px; }
.card-head {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 12px; flex-wrap: wrap;
    margin-bottom: 14px;
}
/* Inside a flex .stack the gap already provides the separation; keeping the
 * margin as well doubles it. */
.stack > .card-head { margin-bottom: 0; }

/* ── buttons ────────────────────────────────────────────────────────── */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 9px 18px;
    /* Rounded-lg, not pills — the reference's buttons ("Let's Go", "Log In")
     * are soft rectangles. Every .btn variant shares the radius so primary,
     * ghost and danger sit on one row without looking mismatched. */
    border-radius: 10px;
    border: 1px solid transparent;
    font-size: .93rem;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background .12s, border-color .12s, color .12s;
}
.btn:disabled { opacity: .55; cursor: not-allowed; }
.btn-block { width: 100%; }

.btn-primary { background: var(--accent-fill); color: var(--accent-ink); }
.btn-primary:hover:not(:disabled) { background: var(--accent-fill-hover); color: var(--accent-ink); }

.btn-ghost { background: transparent; border-color: var(--line); color: var(--text); }
.btn-ghost:hover:not(:disabled) { border-color: var(--muted); background: var(--panel-2); color: var(--text); }

.btn-danger { background: transparent; border-color: var(--line); color: var(--danger); }
.btn-danger:hover:not(:disabled) { border-color: var(--danger); background: var(--danger-soft); }

.btn-sm { padding: 6px 14px; font-size: .85rem; }
.btn svg { width: 15px; height: 15px; }

/* ── forms ──────────────────────────────────────────────────────────── */

.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font-size: .88rem; font-weight: 500; color: var(--text-dim); }
.field .hint { font-size: .8rem; color: var(--muted); }

.input {
    width: 100%;
    padding: 10px 12px;
    background: var(--bg-soft);
    border: 1px solid var(--field-line);
    border-radius: 9px;
    outline: none;
}
.input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.input::placeholder { color: var(--muted); }
.input:disabled { opacity: .6; }

/* Browsers mark every empty required field invalid on load; :user-invalid only
 * fires once the field has actually been interacted with, which is the
 * difference between a helpful form and a form that opens covered in red. */
.input:user-invalid { border-color: var(--danger); }

form .stack { gap: 14px; }

/* ── messages ───────────────────────────────────────────────────────── */

.msg {
    padding: 11px 14px;
    border-radius: 9px;
    font-size: .9rem;
    border: 1px solid transparent;
}
.msg-error { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }
.msg-ok    { background: var(--ok-soft);     border-color: var(--ok);     color: var(--ok); }
.msg-info  { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.msg-warn  { background: var(--warn-soft);   border-color: var(--warn);   color: var(--warn); }

/* ── badges ─────────────────────────────────────────────────────────── */

.badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 9px;
    border-radius: 999px;
    font-size: .76rem;
    font-weight: 600;
    letter-spacing: .02em;
    border: 1px solid var(--line);
    color: var(--text-dim);
    background: var(--panel-2);
}
.badge-ok     { color: var(--ok);     border-color: var(--ok);     background: var(--ok-soft); }
.badge-accent { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.badge-warn   { color: var(--warn);   border-color: var(--warn);   background: var(--warn-soft); }
.badge-danger { color: var(--danger); border-color: var(--danger); background: var(--danger-soft); }

/* ── the licence key block ──────────────────────────────────────────── */

/* Activation needs the email AND the key — see routers/license.py — so they are
 * presented as one unit. Showing the key alone would guarantee a support ticket
 * from everyone who signed up with an address they no longer remember. */
.cred {
    background: var(--bg-soft);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
}
.cred-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 14px;
}
.cred-row + .cred-row { border-top: 1px solid var(--line-soft); }
.cred-label {
    font-size: .74rem;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--muted);
    width: 68px;
    flex: none;
}
.cred-value {
    font-family: var(--mono);
    font-size: .95rem;
    letter-spacing: .04em;
    word-break: break-all;
    flex: 1;
}
.cred-value.key { font-size: 1.05rem; font-weight: 600; letter-spacing: .08em; }

/* ── stat tiles ─────────────────────────────────────────────────────── */

.stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
    gap: 14px;
}
.stat {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 16px 18px;
    transition: border-color .15s, transform .15s;
}
.stat:hover { border-color: var(--field-line); transform: translateY(-2px); }
.stat .value {
    font-family: var(--font-display);
    font-size: 1.75rem;
    font-weight: 600;
    letter-spacing: -0.025em;
    line-height: 1.15;
}
.stat .label {
    font-size: .78rem;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--muted);
    margin-top: 4px;
}

/* ── tables ─────────────────────────────────────────────────────────── */

.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: .9rem; }
th {
    text-align: start;
    font-size: .74rem;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--muted);
    font-weight: 600;
    padding: 0 12px 8px 0;
    white-space: nowrap;
}
td { padding-block: 10px; padding-inline: 0 12px; border-top: 1px solid var(--line-soft); vertical-align: top; }
tr td:last-child, tr th:last-child { padding-inline-end: 0; }

/* ── landing ────────────────────────────────────────────────────────── */

.hero { position: relative; padding-block: 72px 48px; text-align: center; }

/* The aurora. One pseudo-element carrying two radial gradients, sized by the
 * hero itself: the box only ever overflows VERTICALLY (top: -80px), which no
 * browser turns into a scrollbar — glows positioned off the sides of the
 * viewport would. z-index -1 sits it behind the text without touching the
 * stacking of anything else on the page; the gradients are colour tokens so
 * the light themes can fade them without a second set of shapes. */
.hero::before {
    content: "";
    position: absolute;
    inset: -80px 0 0;
    z-index: -1;
    pointer-events: none;
    background:
        radial-gradient(42% 55% at 30% 18%, var(--glow-a), transparent 70%),
        radial-gradient(50% 60% at 72% 8%,  var(--glow-b), transparent 70%);
}
.hero h1 { font-size: clamp(2.1rem, 4.6vw, 3.1rem); font-weight: 700; margin-bottom: 16px; letter-spacing: -0.02em; }

/* The eyebrow above the hero headline — the reference sets one word in caps
 * and sky blue over its big Manrope line. */
.hero-eyebrow {
    font-family: var(--font-display);
    font-size: .95rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .14em;
    color: var(--accent);
    margin-bottom: 10px;
}
.hero .lede { max-width: 620px; font-size: 1.1rem; }
.hero .row { margin-top: 26px; }
.hero-note { margin-top: 14px; }
/* The launch-offer banner, under the hero note. inline-block so the coloured
   panel is only as wide as its sentence: stretched across the full hero column
   it reads as a page-level alert rather than an offer. It ships `hidden` and is
   revealed by app.js, so it holds no space when no offer is running. */
.hero-offer {
    display: inline-block;
    margin-top: 12px;
}

/* Copy on the start side, the script window on the end side. minmax(0, …)
 * on both tracks so the <pre> scrolls inside its column instead of widening
 * the grid — same reasoning as the .docs layout below. */
.hero-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 430px);
    gap: 48px;
    align-items: center;
    text-align: start;
}
@media (max-width: 900px) {
    .hero-grid { grid-template-columns: minmax(0, 1fr); gap: 36px; }
    .macro-win { max-width: 480px; }
}
/* At phone widths the longest script line sits within a pixel or two of the
 * window's edge; one type-size step down keeps the signature element free of
 * a horizontal scrollbar instead of relying on the pre's fallback scroll. */
@media (max-width: 480px) {
    .macro-code { font-size: .76rem; }
}

/* ── the hero script window ─────────────────────────────────────────── */

/* The signature element of the landing page: a macro exactly as the desktop
 * app's Script tab shows it, in the real command language — every command in
 * it appears in the manual's script reference. One deliberate animation, the
 * playhead walking the lines the way a run does; the reduced-motion block at
 * the end of this file freezes it on line one. */
.macro-win {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    overflow: hidden;
}
.macro-bar {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 8px 10px;
    background: var(--bg-soft);
    border-bottom: 1px solid var(--line-soft);
    font-size: .78rem;
}
.macro-bar samp { font-family: inherit; }
.macro-tab { padding: 3px 10px; border-radius: 6px; color: var(--muted); }
.macro-tab.on { background: var(--panel-2); color: var(--text); }
.macro-sync {
    margin-inline-start: auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--ok);
    font-size: .74rem;
}
.macro-sync::before {
    content: "";
    width: 7px; height: 7px;
    border-radius: 50%;
    background: currentColor;
}

.macro-code {
    background: transparent;
    border: 0;
    border-radius: 0;
    padding: 12px 0 14px;
    font-size: .82rem;
    line-height: 1.75;
    counter-reset: mline;
}
.macro-code .ml { display: block; counter-increment: mline; padding-inline: 12px 16px; }
.macro-code .ml::before {
    content: counter(mline);
    display: inline-block;
    width: 1.4em;
    margin-inline-end: 14px;
    text-align: end;
    color: var(--muted);
    opacity: .55;
}

.tk-k { color: var(--accent); }
.tk-f { color: var(--text); }
.tk-s { color: var(--ok); }
.tk-v { color: var(--warn); }
.tk-c { color: var(--muted); }
.macro-code, .macro-code .ml { color: var(--text-dim); }

/* The 404 page's variant: the linter strip replaces the sync badge, because a
 * missing page is a problem, and the window centres itself in the narrow
 * column while its code stays start-aligned. */
.macro-err {
    margin-inline-start: auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--danger);
    font-size: .74rem;
}
.macro-err::before {
    content: "";
    width: 7px; height: 7px;
    border-radius: 50%;
    background: currentColor;
}
.macro-win-404 { max-width: 430px; margin-inline: auto; text-align: start; }

/* The playhead. Eight lines, 1.1s per line; each line carries the same
 * keyframes offset by its position, so exactly one is lit at any moment.
 * nth-child delays rather than per-line style attributes — the CSP forbids
 * those. Declared only under no-preference rather than left to the global
 * reduced-motion override: a .01ms infinite animation still ticks, and a
 * person who asked for no motion should get a plainly static listing. */
@media (prefers-reduced-motion: no-preference) {
    .macro-code .ml { animation: ml-play 8.8s infinite; }
    .macro-code .ml:nth-child(1) { animation-delay: 0s; }
    .macro-code .ml:nth-child(2) { animation-delay: 1.1s; }
    .macro-code .ml:nth-child(3) { animation-delay: 2.2s; }
    .macro-code .ml:nth-child(4) { animation-delay: 3.3s; }
    .macro-code .ml:nth-child(5) { animation-delay: 4.4s; }
    .macro-code .ml:nth-child(6) { animation-delay: 5.5s; }
    .macro-code .ml:nth-child(7) { animation-delay: 6.6s; }
    .macro-code .ml:nth-child(8) { animation-delay: 7.7s; }
    @keyframes ml-play {
        0%, 12.4% { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent); }
        12.5%, 100% { background: transparent; box-shadow: inset 2px 0 0 transparent; }
    }
}

/* The landing page stacks full-width sections; this is the rhythm between them
 * rather than a margin on every individual section. */
main > section + section { margin-top: 56px; }

.features {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 16px;
}
/* The landing page's six-card grid: auto-fit at 240px makes four columns and
 * strands two cards on the second row. A 300px floor caps it at three, so six
 * cards sit 3 + 3. Only that grid — the four-card row below it genuinely wants
 * four across, and the two-card comparison fills any column count. */
.features-3 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }

/* Bordered containers with a hover response, like the reference's model
 * cards: the outline is always there, and the cursor lifts the card and
 * brightens its edge. The reduced-motion block at the end of the file makes
 * the change instant instead of animated, which is the right degradation. */
.feature {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: 20px;
    transition: border-color .15s, background .15s, transform .15s;
}
.feature:hover {
    border-color: var(--field-line);
    background: var(--panel-2);
    transform: translateY(-2px);
}
.feature h3 { margin-bottom: 5px; }
.feature p { color: var(--muted); font-size: .92rem; }
.feature-icon {
    width: 30px; height: 30px;
    border-radius: 8px;
    background: var(--accent-soft);
    color: var(--accent);
    display: grid; place-items: center;
    margin-bottom: 11px;
}
.feature-icon svg { width: 16px; height: 16px; }

/* ── the app screenshot ─────────────────────────────────────────────── */

/* A real capture of the running app. The frame's own background matches the
 * app's chrome, so the dark window reads as a window in the light theme too
 * rather than as a hole in the page. */
.shot-frame {
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    overflow: hidden;
    box-shadow: var(--shadow);
    background: #17191d;
    line-height: 0;
}
.shot-frame img { width: 100%; height: auto; }
.shot-caption { margin-top: 12px; }

/* ── the app carousel ───────────────────────────────────────────────── */

/* Seven captures, one per tab. Each slide is its own framed card, 84% of the
 * viewport wide, so the neighbours peek in dimmed at either edge and say
 * "there is more" without a word. The track is a scroll-snap container: it
 * swipes and scrolls with no script at all, and app.js adds the mouse drag,
 * the arrows, the dots, the arrow keys and the dimming. The caption sits on a
 * gradient over the picture's own bottom edge, so it belongs to the card
 * rather than to the page and its colours are fixed, not themed. */
.carousel { --slide: 84%; --gap: 18px; --peek: 8%; }
.carousel-viewport { position: relative; }
.carousel-track {
    display: flex;
    gap: var(--gap);
    overflow-x: auto;
    overflow-y: hidden;
    padding-inline: var(--peek);
    padding-block: 26px 30px;          /* room for the cards' shadow */
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--peek);
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    touch-action: pan-y;
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
}
.carousel-track::-webkit-scrollbar { display: none; }
.carousel-track.is-dragging { scroll-snap-type: none; cursor: grabbing; scroll-behavior: auto; }
.carousel-track:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; border-radius: var(--radius-lg); }
.carousel-slide {
    position: relative;
    flex: 0 0 var(--slide);
    width: var(--slide);
    margin: 0;
    overflow: hidden;
    border-radius: var(--radius-lg);
    border: 1px solid var(--line);
    background: #17191d;
    box-shadow: 0 30px 70px -30px rgba(0, 0, 0, .75), 0 0 0 1px rgba(255, 255, 255, .03) inset;
    scroll-snap-align: center;
    scroll-snap-stop: always;
    transform-origin: center;
    transition: transform .35s ease, opacity .35s ease, box-shadow .35s ease;
}
.carousel-slide.is-current { box-shadow: 0 34px 80px -28px rgba(0, 0, 0, .85), 0 0 0 1px rgba(255, 255, 255, .06) inset, 0 0 60px -10px var(--glow-a); }
.carousel-slide img {
    display: block;
    width: 100%;
    height: auto;
    -webkit-user-drag: none;
    user-select: none;
    pointer-events: none;              /* the drag is the track's, never the picture's */
}
.carousel-slide figcaption {
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    padding: 56px 26px 18px;
    font-size: .95rem;
    line-height: 1.45;
    color: #fff;
    text-align: center;
    text-shadow: 0 1px 2px rgba(0, 0, 0, .6);
    background: linear-gradient(to top, rgba(6, 9, 14, .94) 0%, rgba(6, 9, 14, .72) 45%, rgba(6, 9, 14, 0) 100%);
}
/* Glass arrows in the peek margins, over the dimmed neighbours. */
.carousel-btn {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 46px;
    height: 46px;
    padding: 0;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, .22);
    background: rgba(12, 18, 30, .55);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 0;
    cursor: pointer;
    box-shadow: 0 8px 24px -8px rgba(0, 0, 0, .6);
    transition: background .15s ease, transform .15s ease, border-color .15s ease;
}
.carousel-btn:hover { background: rgba(12, 18, 30, .8); border-color: rgba(255, 255, 255, .4); transform: translateY(-50%) scale(1.06); }
.carousel-btn:active { transform: translateY(-50%) scale(.97); }
.carousel-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* At either end the arrow that has nowhere to go stays put and fades: an
 * end is an end, and nothing wraps round. */
.carousel-btn[disabled] { opacity: .28; cursor: default; }
.carousel-btn[disabled]:hover { background: rgba(12, 18, 30, .55); border-color: rgba(255, 255, 255, .22); transform: translateY(-50%); }
.carousel-btn svg { width: 20px; height: 20px; }
.carousel-prev { left: calc(var(--peek) - 23px); }
.carousel-next { right: calc(var(--peek) - 23px); }
/* The dots: the current one stretches into a pill. */
.carousel-dots {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px;
    margin-top: 4px;
}
.carousel-dot {
    width: 8px;
    height: 8px;
    padding: 0;
    border-radius: 999px;
    border: 0;
    background: var(--field-line);
    opacity: .6;
    cursor: pointer;
    transition: width .25s ease, background .2s ease, opacity .2s ease;
}
.carousel-dot:hover { opacity: 1; }
.carousel-dot[aria-selected="true"] { width: 28px; background: var(--accent); opacity: 1; }
.carousel-dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
@media (max-width: 720px) {
    .carousel { --slide: 90%; --peek: 5%; --gap: 12px; }
    .carousel-track { padding-block: 16px 20px; }
    .carousel-btn { width: 36px; height: 36px; }
    .carousel-btn svg { width: 16px; height: 16px; }
    .carousel-prev { left: 6px; }
    .carousel-next { right: 6px; }
    .carousel-slide figcaption { padding: 40px 14px 12px; font-size: .84rem; }
}
@media (prefers-reduced-motion: reduce) {
    .carousel-slide, .carousel-btn, .carousel-dot { transition: none; }
}

/* ── pricing ────────────────────────────────────────────────────────── */

.plans {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 18px;
    align-items: start;
}
.plan { position: relative; display: flex; flex-direction: column; gap: 18px; transition: border-color .15s, transform .15s; }
.plan:hover { border-color: var(--field-line); transform: translateY(-2px); }
/* The featured plan's ring is already the accent — hovering must not dim it. */
.plan-featured:hover { border-color: var(--accent-fill); }
/* The recommended plan reads as lifted, not just outlined: the ring is the
 * same blue as its tag and the shadow separates it from its neighbours. */
.plan-featured {
    border-color: var(--accent-fill);
    box-shadow: 0 0 0 1px var(--accent-fill), var(--shadow);
}
.plan-tag {
    position: absolute;
    top: -10px; inset-inline-end: 18px;
    background: var(--accent-fill);
    color: var(--accent-ink);
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .04em;
    padding: 2px 10px;
    border-radius: 999px;
}
/* The plan the signed-in customer is already on.
 *
 * A different colour from .plan-featured on purpose: "most popular" is a
 * marketing claim about other people and "your plan" is a fact about the
 * reader, and painting both in the accent makes the page look like it is
 * recommending what they already bought. --ok is defined in all three theme
 * states (light, dark, and the explicit toggle), so this needs no media query.
 *
 * Both classes can land on one card, and the tag markup decides which label
 * wins -- app.js replaces the tag rather than adding a second one. */
.plan-current { border-color: var(--ok); }
.plan-current .plan-tag { background: var(--ok); color: var(--ok-soft); }

.price { display: flex; align-items: baseline; gap: 7px; }
.price .amount { font-family: var(--font-display); font-size: 2.1rem; font-weight: 600; letter-spacing: -0.03em; }
.price .per { color: var(--muted); font-size: .9rem; }
/* The pre-discount price, shown struck through beside the discounted one
   while a launch offer is running. Hidden unless app.js confirms Paddle
   actually discounted that line, so it never sits at the same size as the
   real price and is never a decoration on a full-price plan. */
.price .was { color: var(--muted); font-size: 1.15rem; text-decoration: line-through; text-decoration-thickness: 2px; opacity: .75; }

/* The price is BAKED IN as USD and then replaced by whatever Paddle says this
   visitor really pays. That takes two sequential round trips -- our own
   /v1/billing/prices, then Paddle.PricePreview -- so first paint and the
   confirmed figure are a second or more apart. Shown at full strength in
   between, the number reads as settled and then changes, which on a payment
   page reads as a bait and switch. Dimmed, it reads as what it is: not
   confirmed yet. The figure stays in the markup throughout, so a visitor with
   no JavaScript, and a search engine, still get a real price. */
.prices-pending .price .amount,
.prices-pending .price .was { opacity: .4; }
.price .amount, .price .was { transition: opacity .18s ease-out; }

/* The launch banner is revealed only when Paddle CONFIRMS a line actually
   moved -- never merely because a discount is configured, or the page could
   promise a saving the checkout will not give. That leaves it appearing late
   and shoving the plan cards down. When a discount id is configured we
   therefore hold its space from the start and only make it visible, so the
   confirmation costs no layout shift; when none is configured, or Paddle
   reports no discount, it is removed from the flow entirely and there is no
   stray gap. */
.offer-reserved { visibility: hidden; }

@media (prefers-reduced-motion: reduce) {
    .price .amount, .price .was { transition: none; }
}

.checks { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 9px; font-size: .92rem; }
.checks li { display: flex; gap: 9px; align-items: flex-start; }
.checks svg { width: 15px; height: 15px; flex: none; margin-top: 4px; color: var(--ok); }
.checks li.off { color: var(--muted); }
.checks li.off svg { color: var(--muted); }

/* ── numbered steps ─────────────────────────────────────────────────── */

.steps { list-style: none; padding: 0; counter-reset: step; display: flex; flex-direction: column; gap: 16px; }
.steps li { display: flex; gap: 14px; }
.steps li::before {
    counter-increment: step;
    content: counter(step);
    flex: none;
    width: 25px; height: 25px;
    border-radius: 50%;
    background: var(--panel-2);
    border: 1px solid var(--line);
    color: var(--text-dim);
    display: grid; place-items: center;
    font-size: .8rem;
    font-weight: 600;
}
.steps h3 { font-size: .97rem; margin-bottom: 2px; }
.steps p { color: var(--muted); font-size: .9rem; }

/* ── code ───────────────────────────────────────────────────────────── */

/* Macro scripts are whitespace-significant — four-space indent, one command per
 * line — so these must never wrap. A wrapped script line reads as a different
 * (and invalid) script, which in a manual is worse than a scrollbar. */
pre {
    background: var(--bg-soft);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 14px 16px;
    overflow-x: auto;
    font-family: var(--mono);
    font-size: .86rem;
    line-height: 1.55;
    tab-size: 4;
}
pre code { background: none; padding: 0; border: none; font-size: inherit; }

code {
    font-family: var(--mono);
    font-size: .88em;
    background: var(--panel-2);
    border: 1px solid var(--line-soft);
    border-radius: 4px;
    padding: 1px 5px;
}

kbd {
    font-family: var(--mono);
    font-size: .82em;
    background: var(--panel-2);
    border: 1px solid var(--line);
    border-bottom-width: 2px;
    border-radius: 5px;
    padding: 2px 6px;
    white-space: nowrap;
}

/* ── legal documents ────────────────────────────────────────────────── */

/* The review banner. Deliberately loud and deliberately at the top of the
 * content rather than in the nav: a legal page that has not been through a
 * lawyer must never be mistaken for one that has, and the person most likely to
 * make that mistake is whoever is about to publish it. Delete the element to
 * remove it — there is no flag, so it cannot be left switched off by accident. */
.draft-banner {
    border: 1px solid var(--warn);
    background: var(--warn-soft);
    color: var(--warn);
    border-radius: var(--radius);
    padding: 14px 16px;
    margin-bottom: 24px;
    font-size: .9rem;
}
.draft-banner strong { display: block; margin-bottom: 4px; letter-spacing: .02em; }

/* Version and effective date. Legal documents are only meaningful with these
 * attached — a consent record naming a version is worthless if the page does
 * not say which version it is. */
.doc-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 20px;
    padding: 12px 0 16px;
    border-bottom: 1px solid var(--line);
    margin-bottom: 8px;
    font-size: .85rem;
    color: var(--muted);
}
.doc-meta dt { font-weight: 600; color: var(--text-dim); display: inline; }
.doc-meta dd { display: inline; margin: 0 16px 0 4px; }

/* The non-binding summary at the head of the long documents. Reading a contract
 * is work; a plain-language precis is the difference between a term being
 * technically disclosed and actually known. Marked as non-binding because a
 * summary that contradicts the text it summarises is worse than no summary. */
.plain-words {
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    padding: 16px 18px;
    margin-bottom: 24px;
}
.plain-words h2 { font-size: 1rem; border: none; padding: 0; margin: 0 0 8px; }
.plain-words ul { margin: 0; padding-inline-start: 20px; font-size: .92rem; }
.plain-words li + li { margin-top: 5px; }
.plain-words .disclaimer { font-size: .82rem; color: var(--muted); margin-top: 10px; }

/* ── consent checkbox ───────────────────────────────────────────────── */

/* Laid out as a grid rather than an inline-block so a two-line label aligns to
 * the text rather than wrapping under the box. */
.field-check {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 10px;
    align-items: start;
}
.field-check label { font-size: .9rem; font-weight: 400; color: var(--text-dim); line-height: 1.5; }
.check {
    width: 17px;
    height: 17px;
    margin: 2px 0 0;
    accent-color: var(--accent);
    cursor: pointer;
    flex: none;
}
/* :user-invalid, not :invalid — an untouched required box must not open the
 * page already marked red. Same reasoning as the .input rule above. */
.check:user-invalid { outline: 2px solid var(--danger); outline-offset: 2px; }

/* ── docs layout ────────────────────────────────────────────────────── */

.docs {
    display: grid;
    grid-template-columns: 210px minmax(0, 1fr);
    gap: 40px;
    align-items: start;
}

/* `top` clears the 60px sticky nav. minmax(0,1fr) on the content column above
 * is what keeps a wide <pre> scrolling inside itself instead of stretching the
 * grid and pushing the whole page sideways. */
.docs-toc {
    position: sticky;
    top: 76px;
    max-height: calc(100vh - 96px);
    overflow-y: auto;
    font-size: .87rem;
}
.docs-toc ol { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.docs-toc a {
    display: block;
    padding: 4px 9px;
    border-radius: 6px;
    color: var(--muted);
    text-decoration: none;
    border-inline-start: 2px solid transparent;
}
.docs-toc a:hover { color: var(--text); background: var(--panel-2); }
.docs-toc a.current { color: var(--accent); border-inline-start-color: var(--accent); }

.docs-body > section + section { margin-top: 44px; }

/* scroll-margin-top must sit on whatever carries the id — here the <section>,
 * not the <h2> inside it. Putting it on the heading does nothing at all, because
 * the browser scrolls the element the URL fragment names and that element's own
 * margin is the only one it consults. Without this the sticky nav covers the
 * heading you just jumped to. */
.docs-body section { scroll-margin-top: 80px; }
.docs-body h2 { padding-bottom: 8px; border-bottom: 1px solid var(--line); margin-bottom: 16px; }
.docs-body h3 { scroll-margin-top: 80px; margin-top: 26px; margin-bottom: 8px; }
/* Fourth level exists only where a section genuinely has sub-topics — Discord
 * and Telegram under Notifications. Sized between h3 and body so the hierarchy
 * is visible without adding another heavy weight to the page. */
.docs-body h4 {
    scroll-margin-top: 80px;
    margin-top: 20px;
    margin-bottom: 6px;
    font-size: .94rem;
    font-weight: 600;
    color: var(--text-dim);
}

/* Smooth only for people who have not asked otherwise — the reduced-motion
 * block near the end of this file kills the animation, not the navigation. */
@media (prefers-reduced-motion: no-preference) {
    html { scroll-behavior: smooth; }
}
.docs-body p, .docs-body ul, .docs-body ol, .docs-body pre,
.docs-body .table-scroll, .docs-body .msg { margin-top: 12px; }
.docs-body ul, .docs-body ol { padding-inline-start: 22px; }
.docs-body li + li { margin-top: 5px; }
.docs-body td code { white-space: nowrap; }

@media (max-width: 800px) {
    .docs { grid-template-columns: 1fr; gap: 20px; }
    .docs-toc {
        position: static;
        max-height: none;
        border: 1px solid var(--line);
        border-radius: var(--radius);
        padding: 12px;
    }
}

/* ── empty state ────────────────────────────────────────────────────── */

.empty {
    text-align: center;
    padding: 32px 20px;
    color: var(--muted);
    border: 1px dashed var(--line);
    border-radius: var(--radius);
    font-size: .92rem;
}

/* ── auth pages ─────────────────────────────────────────────────────── */

.auth-head { text-align: center; margin-bottom: 24px; }
.auth-head h1 { font-size: 1.55rem; margin-bottom: 6px; }
.auth-alt { text-align: center; margin-top: 18px; font-size: .9rem; color: var(--muted); }

/* ── spinner ────────────────────────────────────────────────────────── */

.spinner {
    width: 15px; height: 15px;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: spin .6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* Visible focus for keyboard users, suppressed for pointer users. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Text selection in the brand blue. Background only — forcing a foreground
 * colour would fight the syntax colours in code samples. */
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }

/* ── the funnel glow ────────────────────────────────────────────────────
 *
 * The conversion-funnel pages get a faint echo of the hero's aurora: one
 * centred wash falling from the top edge, behind the card. Same tokens as the
 * hero so the two theme states already know what to do with it. The account
 * page and the manual are deliberately excluded — those are workspaces, and
 * atmosphere behind a licence key or a script reference is noise. */
body[data-page="login"]    main,
body[data-page="register"] main,
body[data-page="forgot"]   main,
body[data-page="reset"]    main,
body[data-page="verify"]   main,
body[data-page="welcome"]  main,
body[data-page="404"]      main { position: relative; }
body[data-page="login"]    main::before,
body[data-page="register"] main::before,
body[data-page="forgot"]   main::before,
body[data-page="reset"]    main::before,
body[data-page="verify"]   main::before,
body[data-page="welcome"]  main::before,
body[data-page="404"]      main::before {
    content: "";
    position: absolute;
    inset: 0 0 auto;
    height: 320px;
    z-index: -1;
    pointer-events: none;
    background: radial-gradient(46% 75% at 50% 0%, var(--glow-b), transparent 70%);
}


/* ── language picker ────────────────────────────────────────────────────
 *
 * <details>/<summary> because it is the only disclosure widget that works
 * with no script at all. A <select> would have needed an onchange handler,
 * and the CSP has no 'unsafe-inline' -- adding one for a language menu would
 * weaken the policy that keeps the session token out of reach.
 *
 * Every entry is a real <a>, which is also what lets a crawler walk from any
 * one language to the other twenty-eight and discover the alternates.
 */
.langpick-row { padding-block: 0 20px; }
.langpick { position: relative; display: inline-block; font-size: .87rem; }

.langpick > summary {
    cursor: pointer;
    color: var(--muted);
    padding: 6px 10px;
    border: 1px solid var(--field-line);
    border-radius: 7px;
    list-style: none;
    user-select: none;
    display: inline-flex;
    align-items: center;
    gap: 7px;
}
/* The default disclosure triangle is removed in favour of a globe, which
 * reads as "language" in every locale and needs no translating. */
.langpick > summary::-webkit-details-marker { display: none; }
.langpick > summary::before {
    content: "";
    width: 14px; height: 14px;
    flex: none;
    background: currentColor;
    /* A globe, inline as a mask so it inherits colour and costs no request.
     * data: is allowed by img-src in the CSP; an external icon font is not. */
    -webkit-mask: var(--globe) center / contain no-repeat;
    mask: var(--globe) center / contain no-repeat;
}
.langpick > summary:hover { color: var(--text); border-color: var(--muted); }

.langpick-menu {
    position: absolute;
    bottom: calc(100% + 6px);
    inset-inline-start: 0;
    z-index: 20;
    display: grid;
    grid-template-columns: repeat(3, minmax(7.5rem, 1fr));
    gap: 2px;
    padding: 8px;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: 10px;
    box-shadow: 0 14px 34px rgb(0 0 0 / .34);
    max-height: 62vh;
    overflow-y: auto;
}
.langpick-menu a {
    padding: 5px 9px;
    border-radius: 6px;
    color: var(--text-dim);
    text-decoration: none;
    white-space: nowrap;
}
.langpick-menu a:hover { background: var(--panel-2); color: var(--text); }
.langpick-menu a[aria-current] { color: var(--accent); font-weight: 600; }

@media (max-width: 620px) {
    .langpick-menu { grid-template-columns: repeat(2, minmax(6.5rem, 1fr)); }
}

/* Scripts whose glyphs sit taller or shorter than Latin. Left at the default
 * everywhere else so the English pages are untouched. */
:root:lang(ja), :root:lang(ko), :root:lang(zh-Hans), :root:lang(zh-Hant),
:root:lang(th), :root:lang(hi) { line-height: 1.72; }

/* RTL needs no rule of its own: every direction-sensitive declaration above
 * is a logical property, so dir="rtl" on <html> mirrors the layout. The two
 * exceptions are things that must NOT mirror. */
[dir="rtl"] .mono,
[dir="rtl"] code,
[dir="rtl"] pre {
    /* A licence key, a file path and a script are read left to right even in
     * an Arabic page. Without this the bidi algorithm reorders runs that
     * start with a digit or a slash and the value shown is not the value
     * stored -- which for a licence key means the customer copies something
     * that does not work. */
    direction: ltr;
    text-align: start;
    unicode-bidi: isolate;
}

/* A translated page's one line under the nav: the English original is the
   accurate version and applies if the two differ (tools/i18n/pages.py). */
.lang-note { font-size: .82rem; color: var(--muted); text-align: center; padding-block: 2px 8px; }
.lang-note a { color: var(--accent); }
