/* Flashcard Factory — the handful of rules Tailwind utilities cannot express.
   All colour lives in custom properties: a dark theme is a matter of
   redefining this block, with no template changes. */

/* The landing page's two faces, self-hosted for the reason index.html gives:
   a font request carries the visitor's address to whoever serves it. */
@font-face {
    font-family: "Bricolage Grotesque";
    src: url("../fonts/bricolage-grotesque-latin-standard.477b564b86e0.woff2") format("woff2");
    font-weight: 200 800;
    font-stretch: 75% 100%;
    font-display: swap;
}
@font-face {
    font-family: "Instrument Sans";
    src: url("../fonts/instrument-sans-latin-wght.fa41c43a3d8e.woff2") format("woff2");
    font-weight: 400 700;
    font-display: swap;
}

:root {
    /* The landing page's paper and pen (landing.css), so the app reads as the
       same place the front door does: a cool paper under white surfaces,
       blue-black ink, and the ballpoint blue for every action. */
    --surface: #ffffff;
    --surface-raised: #ffffff;
    --surface-sunken: #f4f6fb;
    --line: #e1e5ef;
    --line-strong: #c9cfdc;
    --ink: #161a2c;
    --ink-muted: #4f576c;
    --ink-faint: #7d8599;
    --accent: #3346d3;
    --accent-strong: #2737ae;
    --accent-soft: #eceffd;
    --accent-ink: #ffffff;
    --accent-ring: rgba(51, 70, 211, 0.16);

    --font-sans: "Instrument Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --font-display: "Bricolage Grotesque", "Instrument Sans", system-ui, sans-serif;
    /* A sheet lying on the desk: a hairline and a contact shadow, close to the
       paper. The long soft blur it replaced made every panel float at the same
       height, which is what a stack of identical SaaS cards looks like. */
    --shadow-card: 0 1px 0 rgba(22, 26, 44, 0.04), 0 1px 3px -1px rgba(22, 26, 44, 0.10);
    --chip-basic-bg: #eef1f5;
    --chip-basic-ink: #48505d;
    --chip-reverse-bg: #eaf3ee;
    --chip-reverse-ink: #2f6b4a;
    --chip-cloze-bg: #f1ecfb;
    --chip-cloze-ink: #59459b;
    --danger: #b42318;
    --danger-soft: #fef3f2;
    /* A flag somebody raised. Rose and not the lint chip's amber: one is a
       person's own note to self, the other the app's opinion, and the two sit
       side by side in a row's head. */
    --flag-soft: #fdeaef;
    --flag-line: #f6c9d6;
    --flag-ink: #b4235a;
    --shadow-row: 0 1px 2px rgba(22, 26, 44, 0.05);

    /* The eight tones a project may wear. Tokens rather than literals for the
       reason every other colour here is one: a dark theme is then a matter of
       redefining this block, and a project keeps whichever tone it chose. None
       of them is --accent, which already means "selected". */
    --tone-slate-soft: #eef1f5;  --tone-slate-ink: #475569;
    --tone-rose-soft: #fdeaef;   --tone-rose-ink: #a8365c;
    --tone-amber-soft: #fdf3e0;  --tone-amber-ink: #92650d;
    --tone-lime-soft: #edf6e0;   --tone-lime-ink: #4e7314;
    --tone-teal-soft: #e2f4f1;   --tone-teal-ink: #11706a;
    --tone-sky-soft: #e4f1fb;    --tone-sky-ink: #1f6690;
    --tone-indigo-soft: #ebedfc; --tone-indigo-ink: #4348b0;
    --tone-violet-soft: #f2ecfc; --tone-violet-ink: #6d40b5;
}

* { -webkit-tap-highlight-color: transparent; }

/* Alpine is loaded with `defer`, so until it starts every `x-show` that begins
   false is drawn: on the workspace that was the generate, export and draft
   dialogs, the mask editor, the shortcut sheet and three menus, flashing up
   and vanishing on every visit. `x-cloak` holds them back and Alpine removes
   it as it initialises. This rule was missing for as long as the app had
   dialogs: the landing page had its own, and every `x-cloak` here was inert.
   `body` and no `!important`: that outweighs a single Tailwind utility on the
   same element (`flex`, `fixed`), and still loses to the icon picker's
   `<noscript>` rule, which has to show its cloaked grid with the script off. */
body [x-cloak] { display: none; }

/* ── Type ──────────────────────────────────────────────────────────────── */
/* Page and section titles in the landing page's display face. Element plus
   class, so it outweighs the Tailwind size utility it sits beside; the
   bodies of cards and slides are untouched — they are content, not chrome. */
/* The mark is a drawing (templates/_logo.html), sized here so the header, the
   landing page and the legal pages all draw it at the height of the word. */
.brand-mark { width: 22px; height: 22px; flex-shrink: 0; overflow: visible; }
.app-brand {
    font: 700 15px/1 var(--font-display); font-stretch: 90%; letter-spacing: -0.01em;
    color: var(--ink); text-decoration: none;
}
main h1.text-xl, main h1.text-head, .err-title {
    font-family: var(--font-display);
    font-stretch: 86%;
    font-weight: 700;
    letter-spacing: -0.02em;
}
main h1.text-xl { font-size: 26px; line-height: 1.15; }
main h1.text-head { font-size: 20px; line-height: 1.2; }
main h2.text-head, main h2.text-lg, dialog h2, [role="dialog"] h2 {
    font-family: var(--font-display);
    font-stretch: 90%;
    font-weight: 650;
    letter-spacing: -0.01em;
}
main h2.text-head { font-size: 17px; }
main h2.text-lg { font-size: 20px; }

body { text-rendering: optimizeLegibility; }

/* ── Focus ─────────────────────────────────────────────────────────────── */
.focus-ring:focus-visible,
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible,
summary:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 4px;
}

/* ── Buttons ───────────────────────────────────────────────────────────── */
.btn-primary, .btn-secondary, .btn-ghost, .btn-danger, .btn-review {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 30px;
    padding: 0 12px;
    /* Pills, as on the landing page: a control never reads as a card. */
    border-radius: 999px;
    border: 1px solid transparent;
    font-size: 13px;
    font-weight: 550;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease, transform 120ms ease;
}
.btn-primary:active, .btn-secondary:active, .btn-danger:active, .btn-review:active { transform: translateY(1px); }
.btn-primary { background: var(--accent); color: var(--accent-ink); box-shadow: 0 1px 2px rgba(39, 55, 174, 0.25); }
.btn-primary:hover { background: var(--accent-strong); }
.btn-secondary { background: var(--surface); border-color: var(--line-strong); color: var(--ink); }
.btn-secondary:hover { background: var(--surface); border-color: var(--ink); }
.btn-ghost { background: transparent; color: var(--ink-muted); }
.btn-ghost:hover { background: var(--surface-sunken); color: var(--ink); }
.btn-danger { background: var(--surface); border-color: #f0c1bc; color: var(--danger); }
.btn-danger:hover { background: var(--danger-soft); }
.btn-primary[disabled], .btn-secondary[disabled], .btn-ghost[disabled],
.btn-danger[disabled], .btn-review[disabled] {
    opacity: .5; cursor: not-allowed;
}
/* The one thing a finished run leaves to do: keep or bin what it wrote. Soft
   accent rather than a filled primary, because the runs are a list — a column
   of solid buttons would shout the same sentence once per row — and because
   this same markup is rendered into the workspace's 48px toolbar, where a
   third primary button beside Generate and Export competes with both. */
.btn-review { background: var(--accent-soft); color: var(--accent-strong); }
.btn-review:hover { background: var(--accent); color: var(--accent-ink); }
/* The number inside such a button. A pill and not a word, so "Review" stays
   the label and the count stays a quantity you can read at a glance down a
   list of runs; tabular figures so 9 and 12 sit at the same width. */
.btn-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 17px; height: 16px; padding: 0 5px; border-radius: 999px;
    background: var(--accent); color: var(--accent-ink);
    font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
}
.btn-review:hover .btn-count { background: var(--accent-ink); color: var(--accent-strong); }
.btn-sm { height: 24px; padding: 0 9px; font-size: 12px; }
.btn-icon { height: 26px; width: 26px; padding: 0; justify-content: center; }
/* An icon inside a button or a link. Sized here rather than on the <svg> so
   every one of them is the same size, and `flex-shrink: 0` because a button
   that runs out of room must lose label before it loses its drawing. */
.ui-icon { height: 14px; width: 14px; flex-shrink: 0; }
.btn-sm .ui-icon { height: 13px; width: 13px; }
/* A button that is only an icon: square, and its label lives in `data-tip`
   (see Tooltip below) with a copy in `aria-label`. */
.btn-square { width: 30px; padding: 0; justify-content: center; }
.btn-square.btn-sm { width: 24px; }
.btn-link {
    background: none; border: 0; padding: 0; cursor: pointer;
    color: var(--accent); font-size: 13px; font-weight: 500; text-decoration: underline;
    text-underline-offset: 2px;
}
/* An inline text link. base.html has no anchor styling of its own — every link
   in the app until now was a button or a nav item — so prose links need this
   or they render as body text. */
.link {
    color: var(--accent); text-decoration: underline; text-underline-offset: 2px;
}
.link:hover { color: var(--accent-strong); }

/* ── Nav ───────────────────────────────────────────────────────────────── */
.nav-link {
    padding: 5px 11px; border-radius: 999px; color: var(--ink-muted); text-decoration: none;
}
.nav-link:hover { background: var(--surface-sunken); color: var(--ink); }
.nav-link-active { background: var(--accent-soft); color: var(--accent-strong); font-weight: 500; }

/* A small menu of links under a button: the project-and-account menu on the
   two pages that draw their own header, and the slide bar's overflow. Right
   aligned, because both triggers sit at the right of their bar. */
.menu-panel {
    position: absolute; right: 0; top: calc(100% + 4px); z-index: 50; min-width: 13rem;
    padding: 5px; border: 1px solid var(--line); border-radius: 12px;
    background: var(--surface); box-shadow: 0 12px 32px -8px rgba(22, 26, 44, .22);
}
.menu-item {
    display: flex; align-items: center; gap: 8px; padding: 6px 9px; border-radius: 8px;
    color: var(--ink); text-decoration: none; text-align: left; white-space: nowrap;
    background: none; border: 0; font: inherit; cursor: pointer;
}
.menu-item .ui-icon { color: var(--ink-faint); }
.menu-item:hover, .menu-item:focus-visible { background: var(--surface-sunken); outline: none; }
.menu-item:disabled { opacity: .45; cursor: default; background: none; }
.menu-item-danger, .menu-item-danger .ui-icon { color: #b91c1c; }
.menu-sep { height: 1px; margin: 4px 0; background: var(--line); }

/* The project's own tabs, under the breadcrumb on every page about one
   project. Underlined rather than filled, so they read as where you are
   inside the project and not as a second copy of the header's nav. The row
   scrolls sideways by itself on a narrow window — never the page — and the
   current tab is whatever carries aria-current, so markup and look agree. */
.project-tabs {
    display: flex; gap: 2px; margin-bottom: 20px; overflow-x: auto;
    border-bottom: 1px solid var(--line); scrollbar-width: none;
}
.project-tabs::-webkit-scrollbar { display: none; }
.project-tab {
    display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0;
    padding: 7px 10px 8px; margin-bottom: -1px; white-space: nowrap;
    border-bottom: 2px solid transparent; color: var(--ink-muted); text-decoration: none;
}
.project-tab:hover { color: var(--ink); border-bottom-color: var(--line-strong); }
.project-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 4px; }
.project-tab[aria-current="page"] {
    color: var(--accent-strong); border-bottom-color: var(--accent); font-weight: 500;
}
.project-tab-count {
    min-width: 18px; padding: 0 5px; border-radius: 9px; text-align: center;
    background: var(--surface-sunken); color: var(--ink-muted); font-size: 11px; font-weight: 500;
}

/* ── Forms ─────────────────────────────────────────────────────────────── */
.label { display: block; margin-bottom: 4px; font-size: 12px; font-weight: 500; color: var(--ink-muted); }
/* `anywhere` because help text quotes URLs, which have nowhere to break and
   otherwise widen a phone's page by their own length. */
.help { margin-top: 4px; font-size: 12px; color: var(--ink-faint); overflow-wrap: anywhere; }
/* Django's password help_text is a <ul>; Tailwind's preflight strips the
   markers and padding, which leaves three sentences run together. */
.help ul { list-style: disc; margin: 0; padding-left: 16px; }
.help li + li { margin-top: 2px; }
.field-error { margin-top: 4px; font-size: 12px; color: var(--danger); }

/* Scoped to `body` on purpose. Tailwind's preflight sets `padding: 0` on
   input/select/textarea, and the Play CDN injects its stylesheet *after* this
   file — so a bare `select` selector ties on specificity and loses on source
   order, leaving the text flush against the border. */
body input[type="text"], body input[type="password"], body input[type="email"],
body input[type="url"], body input[type="number"], body input[type="search"],
body input[type="tel"], body input[type="date"], body input:not([type]),
body select, body textarea {
    width: 100%;
    min-height: 32px;
    padding: 5px 10px;
    border: 1px solid var(--line-strong);
    border-radius: 8px;
    transition: border-color 120ms ease, box-shadow 120ms ease;
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-size: 13px;
}
body textarea { min-height: 72px; line-height: 1.5; resize: vertical; }
input:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); width: 14px; height: 14px; }

/* A secret field and its show/hide toggle, side by side. */
.secret-field { display: flex; align-items: center; gap: 6px; }
.secret-field input { flex: 1; min-width: 0; }
.secret-field button { flex: 0 0 auto; }

/* The rule above is scoped to `body` to beat Tailwind's preflight, which also
   means it beats Tailwind's own `w-*` utilities on a form control — those are a
   single class and lose on specificity. A field that needs to be narrow has to
   say so at the same weight. */
/* `body input.x` matches the weight of the rule above and comes after it, so it
   wins. `body .x` does not: that rule has one element selector fewer. */
body input.field-narrow, body select.field-narrow, body textarea.field-narrow { width: 9rem; }
body input.field-tiny, body select.field-tiny { width: 5rem; }
/* A control that sits in a row of `.btn-sm` buttons. Every input here is 30px
   tall and every small button is 24px, so a select beside one stood a third
   taller than it and the row read as two rows. Same weight rule as above: a
   bare `.field-sm` loses to the block that sizes every input. */
body input.field-sm, body select.field-sm, body textarea.field-sm {
    min-height: 24px; height: 24px; padding: 2px 8px; font-size: 12px;
}
body select.field-sm { padding-right: 24px; background-position: right 6px center; }
input[type="file"] { font-size: 13px; }
body select { appearance: none; padding-right: 26px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath fill='%235b6472' d='M2.5 4.5 6 8l3.5-3.5z'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 7px center; background-size: 12px;
}

/* ── A line of facts ───────────────────────────────────────────────────── */
/* Dots between the items, drawn by the stylesheet rather than written into the
   markup: half of these facts are conditional, and a separator typed between
   two of them is a dot that survives whichever one goes missing. The dot
   belongs to the item that follows it, so the first never has one. */
.meta-dots { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; }
.meta-dots > * + *::before {
    content: "·"; margin-right: 6px; color: var(--line-strong);
}

/* A role beside what it means. The chip is the one shown on every row and
   card in the app, so the legend and the thing it explains are the same
   object. */
.role-legend { display: grid; gap: 6px; max-width: 42rem; color: var(--ink-muted); }
.role-legend > li { display: flex; align-items: baseline; gap: 8px; }
.role-legend .chip { flex-shrink: 0; }

/* ── Numbered steps ────────────────────────────────────────────────────── */
.steps { display: grid; gap: 10px; color: var(--ink-muted); }
.steps > li { display: flex; align-items: flex-start; gap: 10px; }
.step-n {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    height: 20px; width: 20px; border-radius: 50%;
    background: var(--surface-sunken); border: 1px solid var(--line);
    color: var(--ink-muted); font-size: 11px; font-weight: 600;
}
.step-n .ui-icon { height: 12px; width: 12px; }
/* A step the project has already done: the number becomes a tick, and the
   sentence steps back so the next one to do is the one that reads loudest. */
.step-done { color: var(--ink-faint); }
.step-done .step-n { background: var(--accent-soft); border-color: transparent; color: var(--accent-strong); }
/* A panel that leads with a drawing: the tile is the project icon's shape at
   the project icon's size, so a sidebar of them lines up with everything else
   in the app that wears one. */
.panel-icon {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    height: 32px; width: 32px; border-radius: 8px;
    background: var(--surface-sunken); color: var(--ink-muted);
}
.panel-icon .ui-icon { height: 17px; width: 17px; }

/* ── Picking decks to export ───────────────────────────────────────────── */
/* A list of rows rather than a stack of bare checkboxes: the deck's name, what
   has moved since the last build, and how many cards it would contribute all
   belong on one line, and the whole line is the hit area. */
.deck-picks { border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.deck-picks > li + li { border-top: 1px solid var(--line); }
.deck-pick {
    display: flex; align-items: center; gap: 8px;
    padding: 6px 9px; cursor: pointer;
}
.deck-pick:hover { background: var(--surface-sunken); }
.deck-pick:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: -2px; }
/* A deck with no cards in it. Still listed, so the panel and the project page
   agree about which decks exist, but there is nothing for a tick to mean. The
   row is dimmed rather than hidden, and loses the hover it can no longer
   honour — a row that lights up under the pointer and then refuses the click
   reads as a broken checkbox rather than as an empty deck. */
.deck-pick-off { cursor: default; color: var(--ink-faint); }
.deck-pick-off:hover { background: transparent; }
.deck-pick-off input { cursor: default; }

/* ── Chips ─────────────────────────────────────────────────────────────── */
.chip {
    display: inline-flex; align-items: center; gap: 4px;
    height: 19px; padding: 0 7px; border-radius: 999px;
    font-size: 11px; font-weight: 550; letter-spacing: .01em; white-space: nowrap;
}
.chip-basic { background: var(--chip-basic-bg); color: var(--chip-basic-ink); }
.chip-reverse { background: var(--chip-reverse-bg); color: var(--chip-reverse-ink); }
.chip-cloze { background: var(--chip-cloze-bg); color: var(--chip-cloze-ink); }
.chip-slide { background: var(--surface-sunken); color: var(--ink-muted); border: 1px solid var(--line); }
.chip-tag { background: var(--surface-sunken); color: var(--ink-muted); border: 1px solid var(--line); }
/* A lint smell: visible, but never mistaken for an error on a card that works. */
.chip-warn { background: #fdf6e3; color: #8a6100; border: 1px solid #f0dca8; }
/* Flagged by a person to come back to. */
.chip-flag { background: var(--flag-soft); color: var(--flag-ink); border: 1px solid var(--flag-line); }
.chip .ui-icon { height: 11px; width: 11px; }

/* A flag button that is on: the drawing fills in and takes the flag colour.
   Lucide's flag is one path whose pole is an open stroke, so filling it
   colours the cloth and leaves the pole a line. */
.flag-on { color: var(--flag-ink); }
.flag-on .ui-icon { fill: currentColor; }

/* The flag in a slide's head. Faint until the slide is reached for, like the
   card tools, and worded once it is on, because a filled drawing at 12px on
   a grey strip is easy to scroll past. */
.slide-flag {
    margin-left: auto; display: inline-flex; align-items: center; gap: 4px;
    height: 20px; padding: 0 5px; border: 0; border-radius: 4px; background: none;
    font-size: 11px; font-weight: 600; color: var(--ink-faint); cursor: pointer;
    opacity: .45; transition: opacity 120ms ease, background-color 120ms ease;
}
.slide-flag .ui-icon { height: 12px; width: 12px; }
.slide-row:hover .slide-flag, .slide-row[aria-current="true"] .slide-flag,
.slide-flag:focus-visible, .slide-flag.flag-on { opacity: 1; }
.slide-flag:hover { background: var(--surface); color: var(--ink-muted); }
.slide-flag.flag-on { color: var(--flag-ink); }
.slide-flag.flag-on:hover { background: var(--flag-soft); color: var(--flag-ink); }
@media (hover: none) { .slide-flag { opacity: 1; } }
/* A flagged slide's head takes the tint, so it can be found while scrolling. */
.slide-row[data-flagged="true"] .slide-row-head { background: var(--flag-soft); border-bottom-color: var(--flag-line); }

/* Sharing. The accent, because "this is not only yours" is a fact about the
   project rather than something wrong with it. */
.chip-share { background: var(--accent-soft); color: var(--accent-strong); }
a.chip-share:hover { background: var(--accent); color: var(--accent-ink); }

/* ── Card rows ─────────────────────────────────────────────────────────── */
/* An index card, so the corner is a card's and not a panel's: panels and panes
   round at 14px, cards at 8px, controls are pills — three radii for three kinds
   of thing, rather than one on everything. */
.card-row {
    position: relative;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--surface);
    box-shadow: var(--shadow-row);
    transition: background-color 120ms ease, border-color 120ms ease;
}
.card-row:hover { border-color: var(--line-strong); }
/* Selection is carried by three signals at once, never colour alone: the pen
   border, a halo and the bar. The card stays white — it is still a card, and a
   blue-washed one hid its own blue header line. */
.card-row[aria-selected="true"] {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring), var(--shadow-row);
}
.card-row[aria-selected="true"]::before {
    content: ""; position: absolute; left: -1px; top: 8px; bottom: 8px;
    width: 3px; border-radius: 3px; background: var(--accent);
}
/* The editor shows the same text the preview does, so the preview steps aside
   while it is open rather than the row saying everything twice. The sweep does
   the same thing from JavaScript (triage.js showBody); here the flag is on the
   row, so it survives the summary being swapped by every autosave. The head
   stays: which slide this is and what to press are still wanted. */
.card-row[data-editing="true"] .card-body { display: none; }
.card-row[data-suspended="true"] { opacity: .55; }
.card-row[data-suspended="true"] .card-front-preview { text-decoration: line-through; }

/* One inset for the whole card, so every rule in it starts and ends in the
   same place — the sweep's, which is its card padding (16px in slide mode) with
   the divider inside it. The horizontal padding therefore lives here and not on
   the bands: a border drawn on a child spans this content box, which is what
   makes the lines line up with the text instead of running to the card's own
   edges. */
.card-summary { cursor: pointer; padding: 0 16px; }

/* ── The head: everything that is *about* the card ─────────────────────── */
/* It exists so that nothing belonging to the chrome sits beside the text: a
   rule with a drag handle to the left of it reads as a stray underline rather
   than the fold of a card. Under it is an index card's blue header line, as on
   the sweep card, the landing page and the 404: the row *is* the flashcard,
   and the line is what says so. It spans the summary's content box like every
   other rule here. min-width: 0 on the chips or a long tag stops the tools
   shrinking instead. */
.card-head {
    display: flex; align-items: center; gap: 10px;
    padding: 9px 0 8px;
    border-bottom: 1.5px solid var(--card-margin);
}
.card-chips { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: 6px; }

/* Which slide this card came from: a caption, not a badge. A pill puts it at
   the same weight as a warning, and every row had one, so twenty-five rows
   read as a wall of pills with the one that mattered hidden among them. Set in
   the head's own quiet colour, it labels the card and lifts to the accent when
   it is about to be used — because it is also the way to that slide. */
.card-slide-ref {
    flex-shrink: 0; margin-right: 4px; padding: 0; border: 0; background: none;
    font-size: 12px; font-weight: 600; letter-spacing: .01em; white-space: nowrap;
    color: var(--ink-faint); cursor: pointer; transition: color 120ms ease;
}
.card-slide-ref:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.card-slide-ref-none { cursor: default; font-weight: 500; font-style: italic; }
.card-slide-ref-none:hover { color: var(--ink-faint); text-decoration: none; }
/* Who else last touched the card, in a shared project. The slide reference's
   quiet colour without its pointer: there is nowhere to go. */
.card-byline { font-size: 11px; color: var(--ink-faint); white-space: nowrap; }

/* Stretched to the head's full height so there is something to actually grab,
   and dimmed like the tools until the row is reached for. The negative margin
   pays back its own left padding, so the dots sit on the card's inset and the
   chips, the text below and every rule all start at the same x. */
.drag-handle {
    flex-shrink: 0; align-self: stretch; display: flex; align-items: center;
    margin-left: -4px; padding: 4px; border-radius: 4px;
    cursor: grab; user-select: none;
    color: var(--ink-faint); opacity: .5;
    transition: opacity 120ms ease, background-color 120ms ease;
}
.card-row:hover .drag-handle { opacity: 1; }
.drag-handle:hover { background: var(--surface-sunken); color: var(--ink-muted); }
.drag-handle:active { cursor: grabbing; }

/* Quiet until the row is reached for, so twenty-five rows read as twenty-five
   cards rather than a hundred buttons. Dimmed and never removed: a control
   that only exists on hover cannot be found on a touch screen, and
   `hover: none` brings them back up for good there. */
.card-tools {
    display: flex; flex-shrink: 0; align-items: center; gap: 1px;
    /* -4px on the right for the same reason as the handle's on the left: an
       icon button's glyph is centred in a 26px box, so the box has to hang into
       the inset for the glyph to look level with the text. */
    margin-left: auto; margin-right: -4px; opacity: .4; transition: opacity 120ms ease;
}
.card-row:hover .card-tools,
.card-row[aria-selected="true"] .card-tools,
.card-tools:focus-within { opacity: 1; }
@media (hover: none) { .card-tools { opacity: 1; } }

/* ── Selecting several cards ───────────────────────────────────────────── */
/* The box sits where the drag handle starts, so it reads as part of the head
   rather than as a column bolted onto the list. A picked row is tinted but
   keeps its own border, because the accent border already means "the card you
   are reading", and the two can be different cards. */
.card-pick { flex-shrink: 0; margin: 0 2px 0 -2px; cursor: pointer; }
.card-row[data-picked="true"] { background: var(--accent-soft); }
/* A shift-click picks a run of rows; without this it also selects the text of
   every card between them. Only while selecting — reading must still copy. */
#card-list[data-selecting="true"] .card-summary { user-select: none; }

.bulk-bar {
    display: flex; flex-shrink: 0; flex-wrap: wrap; align-items: center; gap: 6px;
    padding: 6px 12px; border-top: 1px solid var(--line);
    background: var(--accent-soft);
}
.bulk-count { font-size: 13px; color: var(--ink); margin-right: 4px; font-variant-numeric: tabular-nums; }
.bulk-bar .btn-icon[disabled] { opacity: .4; cursor: not-allowed; }
/* A toggle that is on. The selection button stays lit while the mode is. */
.btn-icon-on { background: var(--accent-soft); color: var(--accent-strong); }

/* ── The body: one section per field ───────────────────────────────────── */
/* The padding and the rule live here rather than on the previews themselves,
   because the previews are also the duplicates panel's and the trash's and
   those are summaries, not cards. A section is a *direct* child, which is why
   the occlusion thumbnail and its caption share one wrapper. */
.card-row .card-body > * { padding: 13px 0; }
.card-row .card-body > * + * { border-top: 1px solid var(--line); }

.card-front-preview { font-size: 15px; line-height: 1.45; color: var(--ink); }
.card-back-preview { font-size: 13px; line-height: 1.45; color: var(--ink-muted); }
.card-front-preview p, .card-back-preview p { margin: 0; }
.card-front-preview img, .card-back-preview img { max-height: 44px; width: auto; display: inline-block; }
/* The row is the card view: the answer is an answer, not metadata about the
   question, and the rule above it is what says which is which. */
.card-row .card-front-preview { font-size: 16px; line-height: 1.5; }
.card-row .card-back-preview { font-size: 14px; line-height: 1.55; color: var(--ink); }
/* Extra is a note to the reviewer rather than a third side of the card. */
.card-row .card-back-preview.card-extra { font-size: 13px; color: var(--ink-muted); }

/* A field's own paragraphs and list items, which are flush everywhere else
   because everywhere else is a one-line summary. Here the field is the card,
   and an answer in three paragraphs has to look like three paragraphs. */
.card-row .card-body p + p { margin-top: .55em; }
.card-row .card-body ul, .card-row .card-body ol { margin-top: .4em; }
/* …but a list that *is* the answer has nothing above it to be spaced from, and
   the gap only made the section look bottom-heavy. */
.card-row .card-body ul:first-child, .card-row .card-body ol:first-child { margin-top: 0; }
.card-row .card-body li + li { margin-top: .25em; }

.card-occlusion { display: flex; align-items: flex-start; gap: 14px; }
.card-occlusion-count { margin-top: 5px; }

/* The 44px cap was sized for a row clamped to two lines. Bounded rather than
   the sweep's `none`: a hundred rows of full-height images is a scroll
   marathon, and the slide pane is where a picture is meant to be read. */
.card-row .card-front-preview img, .card-row .card-back-preview img { max-height: 160px; max-width: 100%; }
/* Tailwind's preflight resets every list to no marker and no indent, so a card
   whose answer is "1. … 2. … 3. …" previewed as three unnumbered lines — the
   one thing the numbering was for. Anki's own styling never had the reset, so
   the preview disagreed with the reviewer. */
.card-front-preview ul, .card-back-preview ul,
.card-front-preview ol, .card-back-preview ol { margin: .2em 0 0; padding-left: 1.4em; }
.card-front-preview ul, .card-back-preview ul { list-style: disc; }
.card-front-preview ol, .card-back-preview ol { list-style: decimal; }
.card-front-preview li, .card-back-preview li { margin: 0; }

/* The formula the caret is in, live. A sibling of the editor, never inside it:
   MathJax injecting DOM into a contenteditable fights Quill, which is why
   _mathjax.html leaves automatic typesetting off. */
.fcf-maths {
    margin-top: 4px; padding: 3px 8px; overflow-x: auto;
    border-left: 2px solid var(--accent); background: var(--surface-sunken);
    font-size: 15px; color: var(--ink);
}
.fcf-maths-hint, .fcf-maths-problem { font-size: 12px; }
.fcf-maths-hint {
    border-left-color: var(--line-strong); color: var(--ink-faint);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    white-space: pre;
}
.fcf-maths-problem { border-left-color: #f59e0b; color: #92400e; }
/* Said beside a formula that renders here only because the closer was supplied
   for the preview. */
.fcf-maths-note { margin-left: 10px; font-size: 12px; color: #92400e; }

.cloze { font-weight: 600; color: var(--accent-strong); }

/* ── Answers covered ───────────────────────────────────────────────────── */
/* "Hide the answers" in the workspace: one attribute on the list, and the
   question is what is left. A back simply goes; a cloze deletion is blocked
   out *in place*, because on a cloze card the deletion is the answer and
   removing it would take the sentence with it. `user-select: none` so a drag
   across a covered deletion cannot copy what it is covering. */
#card-list[data-answers="hidden"] [data-answer] { display: none; }
#card-list[data-answers="hidden"] .cloze {
    border-radius: 3px; background: var(--chip-cloze-ink); color: transparent;
    user-select: none;
}
/* Never inside the editor: opening a card is asking to see all of it, and a
   redacted deletion in a contenteditable is a word you cannot edit. The
   preview itself needs no exception — `.card-row[data-editing]` already takes
   the whole `.card-body` off the page while the editor stands in front of it. */
#card-list[data-answers="hidden"] .ql-editor .cloze {
    background: none; color: var(--accent-strong); user-select: text;
}

/* ── Workspace ─────────────────────────────────────────────────────────── */
.splitter {
    flex: 0 0 6px; cursor: col-resize; background: var(--line);
    transition: background-color 120ms ease;
}
.splitter:hover, .splitter[data-dragging="true"] { background: var(--accent); }
.ws-dragging { cursor: col-resize; user-select: none; }
.ws-dragging * { pointer-events: none; }

/* The slides as a scrolling column. --slide-width is set by the pane toolbar;
   past 100% the rows simply overflow and the container scrolls sideways. */
.slide-list {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
    background: var(--surface-sunken);
}
.slide-row {
    display: block;
    /* Flex children shrink by default, which would squash every slide to fit
       the pane instead of letting the column scroll. */
    flex: 0 0 auto;
    width: var(--slide-width, 100%);
    padding: 0;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--surface);
    box-shadow: var(--shadow-row);
    text-align: left;
    cursor: pointer;
    overflow: hidden;
    transition: border-color 120ms ease, box-shadow 120ms ease;
}
.slide-row:hover { border-color: var(--line-strong); }
.slide-row[aria-current="true"] {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent);
}
.slide-row-head {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 5px 8px;
    border-bottom: 1px solid var(--line);
    background: var(--surface-sunken);
    /* This sits between two slides' text layers in the DOM. Without it, a
       selection dragged from one slide into the next swallows "Slide 8" and the
       card-count chip and pastes them into the middle of the text. Selection
       passes straight through; only the copying stops. */
    user-select: none;
}
.slide-row-page {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .02em;
    color: var(--ink-muted);
}
.slide-row[aria-current="true"] .slide-row-page { color: var(--accent-strong); }
.slide-row img {
    display: block; width: 100%; height: auto; background: #fff;
    /* Dragging across an <img> starts a native image drag instead of extending
       the selection, so without this a downward drag dies at the first picture. */
    user-select: none;
    -webkit-user-drag: none;
}

/* ── The selectable text layer ──────────────────────────────────────────────
   pdf.js reads the deck's source PDF in the browser and builds its own text
   layer over each slide render, so the pane selects, copies and finds the way
   Firefox's PDF viewer does. See static/js/slide-text.js.

   The rules below are pdf.js's own, from pdfjs-dist/web/pdf_viewer.css
   (Apache-2.0), trimmed to the text layer and vendored rather than pulled from
   the CDN: the published stylesheet is 194 KB of viewer chrome for the 40 lines
   that matter. They are a contract with TextLayer — do not "tidy" them.

   The image sizes itself, .slide-canvas shrink-wraps it, and .textLayer is
   inset:0 on that wrapper. Sizing the wrapper instead lets the two rectangles
   diverge and every highlight drifts off the words. The occlusion mask layer
   broke exactly this way first. */
.slide-canvas { position: relative; display: block; }

.textLayer {
    position: absolute;
    text-align: initial;
    inset: 0;
    overflow: clip;
    opacity: 1;
    line-height: 1;
    -webkit-text-size-adjust: none;
    -moz-text-size-adjust: none;
    text-size-adjust: none;
    forced-color-adjust: none;
    transform-origin: 0 0;
    caret-color: CanvasText;
    z-index: 0;

    /* pdf.js positions every span as a multiple of these. --scale-factor is
       written by slide-text.js as renderedWidth / pageWidth, so the layer
       follows the pane's zoom without being rebuilt; the rounding steps are
       defaulted here because the viewer sets them from its canvas and we have
       no canvas, and an undefined one makes round() — and the whole
       declaration — invalid. */
    --scale-factor: 1;
    --total-scale-factor: var(--scale-factor);
    --scale-round-x: 1px;
    --scale-round-y: 1px;
}
.textLayer :is(span, br) {
    color: transparent;
    position: absolute;
    white-space: pre;
    cursor: text;
    transform-origin: 0% 0%;
}
.textLayer > :not(.markedContent),
.textLayer .markedContent span:not(.markedContent) { z-index: 1; }
.textLayer span.markedContent { top: 0; height: 0; }
.textLayer span[role="img"] { user-select: none; cursor: default; }
.textLayer ::selection { background: rgba(37, 99, 235, .28); }
.textLayer ::-moz-selection { background: rgba(37, 99, 235, .28); }
.textLayer br::selection { background: transparent; }
.textLayer br::-moz-selection { background: transparent; }

/* A rotated page is laid out UNROTATED and the consumer turns it.
   setLayerDimensions sizes the layer from viewport.rawDims, which is the raw
   cropbox — 584x760 portrait on the Network Optimization decks — and positions
   every span as a percentage of that box, while the render we lay it over is
   760x584 landscape. It marks the layer with data-main-rotation and expects
   these three rules to do the turning; they are pdf.js's own, and omitting them
   leaves a portrait layer of sideways text over a landscape slide.
   Scoped to .textLayer: pdf.js ships them unscoped, and this is not a PDF
   viewer — nothing else here should inherit a transform from an attribute. */
.textLayer[data-main-rotation="90"] { transform: rotate(90deg) translateY(-100%); }
.textLayer[data-main-rotation="180"] { transform: rotate(180deg) translate(-100%, -100%); }
.textLayer[data-main-rotation="270"] { transform: rotate(270deg) translateX(-100%); }

/* pdf.js appends this to <body> to measure text widths. Nothing in the
   distributed stylesheet hides it, so without this a blank 300x150 canvas
   lands at the bottom of every page that builds a layer. */
.hiddenCanvasElement { position: absolute; top: 0; left: 0; width: 0; height: 0; display: none; }
/* The trick that makes a drag past the last line behave: an invisible block
   below the text that joins the selection while the pointer is down. */
.textLayer .endOfContent {
    display: block;
    position: absolute;
    inset: 100% 0 0;
    z-index: 0;
    cursor: default;
    user-select: none;
}
.textLayer.selecting .endOfContent { top: 0; }

/* Scrollbars: visible but quiet. */
.scroll-y { overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
/* Room for the last card to scroll clear of the bottom of the pane once its
   editor is open. Only while one is: otherwise it is a hole under the list. */
#card-list[data-editing="true"] { padding-bottom: 40vh; }
.scroll-y::-webkit-scrollbar { width: 10px; height: 10px; }
.scroll-y::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 6px; border: 3px solid transparent; background-clip: content-box; }

/* ── Quill ─────────────────────────────────────────────────────────────── */
.ql-container { font-family: inherit; font-size: 15px; }
.ql-editor {
    min-height: 76px; padding: 8px 10px; line-height: 1.5;
    background: var(--surface); border-radius: 0 0 6px 6px;
}
.ql-editor.ql-blank::before { color: var(--ink-faint); font-style: normal; left: 10px; right: 10px; }
.ql-toolbar.ql-snow, .fcf-toolbar {
    display: flex; flex-wrap: wrap; align-items: center; gap: 1px;
    border: 1px solid var(--line-strong); border-bottom: 0;
    border-radius: 6px 6px 0 0; padding: 3px 4px; background: var(--surface-sunken);
}
.fcf-editor-shell { border: 1px solid var(--line-strong); border-top: 0; border-radius: 0 0 6px 6px; }
.fcf-editor-shell:focus-within, .fcf-field[data-focused="true"] .fcf-toolbar { border-color: var(--accent); }
.fcf-toolbar button {
    height: 24px; min-width: 24px; padding: 0 5px; border: 0; border-radius: 4px;
    background: transparent; color: var(--ink-muted); cursor: pointer; font-size: 12px; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
}
.fcf-toolbar button:hover { background: var(--line); color: var(--ink); }
.fcf-toolbar button.is-active { background: var(--accent-soft); color: var(--accent-strong); }
.fcf-toolbar .sep { width: 1px; height: 16px; margin: 0 3px; background: var(--line-strong); }
.ql-editor img { max-width: 100%; height: auto; }
.ql-clipboard { position: fixed !important; left: -9999px; top: 0; }

/* ── Occlusion editor ──────────────────────────────────────────────────── */
/* The image sizes itself; the wrapper shrink-wraps it; the mask layer covers
   it exactly. Anything that sizes the wrapper independently reintroduces the
   drawn-here-but-exported-there bug. */
.occ-image {
    max-width: 100%;
    max-height: calc(100dvh - 11rem);
    width: auto;
    height: auto;
}
.occ-canvas { position: absolute; inset: 0; cursor: crosshair; }
/* A touch on the slide draws a mask; without this the browser takes it as a
   scroll or a pinch and cancels the pointer before the mask exists. */
.occ-canvas, .occ-shape, .occ-handle { touch-action: none; }
/* A fingertip is wider than the 11px handle: the drawn square stays, and an
   invisible margin round it takes the touch. */
@media (pointer: coarse) {
    .occ-handle::after { content: ""; position: absolute; inset: -11px; }
}
.occ-shape {
    position: absolute;
    background: rgba(255, 235, 162, .88);
    border: 1px solid #212121;
    cursor: move;
}
.occ-shape-on {
    background: rgba(255, 142, 142, .9);
    border-color: var(--accent-strong);
    box-shadow: 0 0 0 1px var(--accent-strong);
}
.occ-shape-new {
    background: rgba(37, 99, 235, .22);
    border: 1px dashed var(--accent-strong);
}
.occ-handle {
    position: absolute;
    right: -5px;
    bottom: -5px;
    height: 11px;
    width: 11px;
    border: 1px solid #fff;
    border-radius: 2px;
    background: var(--accent);
    cursor: nwse-resize;
}
.occ-label {
    position: absolute;
    left: 2px;
    top: 2px;
    padding: 0 4px;
    border-radius: 3px;
    background: rgba(0, 0, 0, .65);
    color: #fff;
    font-size: 10px;
    font-weight: 600;
    line-height: 15px;
    pointer-events: none;
}
/* Mask outlines on the card-list preview. */
.occ-preview { position: relative; display: inline-block; max-height: 44px; }
.occ-preview img { display: block; max-height: 44px; width: auto; }
/* In the workspace list the row is the card, so the masked slide has to be
   readable rather than merely present. Everywhere else 44px is still right. */
.card-row .occ-preview { max-height: 120px; }
.card-row .occ-preview img { max-height: 120px; }
.occ-preview svg { position: absolute; inset: 0; height: 100%; width: 100%; }
.occ-preview rect { fill: #ffeba2; fill-opacity: .8; stroke: #212121; stroke-width: .004; }

/* ── Skeletons ─────────────────────────────────────────────────────────── */
.skeleton { background: linear-gradient(90deg, #eef0f3 25%, #e4e7eb 37%, #eef0f3 63%);
    background-size: 400% 100%; border-radius: 4px; }
@media (prefers-reduced-motion: no-preference) {
    .skeleton { animation: skeleton 1.4s ease infinite; }
}
@keyframes skeleton { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes row-in { from { opacity: 0; } to { opacity: 1; } }

.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; }
.htmx-indicator { opacity: 0; transition: opacity 120ms ease; }

/* Progress bar */
/* display is explicit so the bar works as a span too, not only as a div. */
.progress { display: block; height: 4px; border-radius: 2px; background: var(--line); overflow: hidden; }
.progress > span { display: block; height: 100%; background: var(--accent); transition: width 150ms ease; }
/* The run panel lives in a 48px toolbar, so its bar is a hairline. */
.progress-thin { height: 3px; }
/* Waiting out a rate limit: amber, and moving, so a run that is deliberately
   idle does not look like a run that has died. */
.progress-waiting {
    background: linear-gradient(
        90deg, var(--accent) 0%, #f59e0b 50%, var(--accent) 100%) !important;
    background-size: 200% 100%;
    animation: progress-drift 2s linear infinite;
}
@keyframes progress-drift { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) {
    .progress-waiting { animation: none; }
}

/* Insert picker: the whole slide beside each figure cropped from it. */
.figure-option {
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    width: 116px; padding: 6px; border: 1px solid var(--line); border-radius: 6px;
    background: var(--surface); font-size: 11px; color: var(--ink-muted);
}
.figure-option:hover { border-color: var(--accent); background: var(--accent-soft); }
.figure-option img {
    display: block; width: 100%; height: 64px; object-fit: contain; background: #fff;
    border-radius: 4px;
}

/* ── Tooltip ──────────────────────────────────────────────────────────────── */
/* `data-tip`, not `title`, and for the reason the deck dot below gives at
   length: a control whose only label is a drawing cannot wait the browser's
   second before it says what it is. Every icon-only button on the project page
   wears this, so the four beside the heading answer at the same speed as the
   badges down the deck list.

   Below and centred rather than to the right, because these sit in a row that
   ends at the right edge of the page and there is nothing to the right of the
   last one. `.tip-end` puts the bubble's right edge over the control's, for
   exactly that one. `pointer-events: none` so a bubble hanging over the next
   control does not eat the click aimed at it. */
.tip { position: relative; }
.tip::after {
    content: attr(data-tip);
    position: absolute; top: calc(100% + 6px); left: 50%; z-index: 30;
    transform: translateX(-50%);
    padding: 3px 8px; border-radius: 5px;
    background: var(--ink); color: var(--surface);
    font-size: 12px; line-height: 1.4; font-weight: 500; white-space: nowrap;
    pointer-events: none;
    display: none;
}
.tip:hover::after, .tip:focus-visible::after { display: block; }
/* Nothing to show when there is nothing to say. */
.tip:not([data-tip])::after, .tip[data-tip=""]::after { content: none; }
.tip-end::after { left: auto; right: 0; transform: none; }
/* Above, for a control at the bottom of a pane, where below is clipped. */
.tip-up::after { top: auto; bottom: calc(100% + 6px); }

/* ── Deck state dot ───────────────────────────────────────────────────────── */
/* One class per tone, rather than Tailwind utilities: the Play CDN only sees
   class names written literally in the source, so a colour picked at render
   time has to be real CSS or it silently renders unstyled. The state that
   chooses it lives in apps/decks/status.py. */
.deck-dot {
    position: relative;
    display: flex; align-items: center; justify-content: center;
    height: 24px; width: 24px; border-radius: 50%;
}
/* Its own tooltip rather than `title`, because the icon is the only thing
   carrying the state and the native one is not up to that job: about a second
   of hover before it appears, nothing at all on a touch screen, and no way to
   tell whether it is late or simply missing. This one shows on the first
   frame of the hover.
   To the right, not below: the row is 24px of icon beside a column of text, so
   there is room there and none underneath. */
.deck-dot::after {
    content: attr(data-tip);
    position: absolute; left: calc(100% + 8px); top: 50%; z-index: 30;
    transform: translateY(-50%);
    padding: 3px 8px; border-radius: 5px;
    background: var(--ink); color: var(--surface);
    font-size: 12px; line-height: 1.4; font-weight: 500; white-space: nowrap;
    display: none;
}
.deck-dot:hover::after, .deck-dot:focus-visible::after { display: block; }
/* Nothing to show when there is nothing to say. */
.deck-dot:not([data-tip])::after, .deck-dot[data-tip=""]::after { content: none; }
/* One size for every state, so the badges sit on the same optical line down
   the list however different the shapes are. */
.deck-dot svg { height: 14px; width: 14px; }
.deck-dot-green { background: #e6f4ec; color: #1c6b45; }
.deck-dot-amber { background: #fdf6e3; color: #8a6100; }
.deck-dot-red { background: var(--danger-soft); color: var(--danger); }
.deck-dot-accent { background: var(--accent-soft); color: var(--accent-strong); }
/* Nothing has happened to this deck yet, and the row should not shout it. */
.deck-dot-grey {
    background: var(--surface-sunken); color: var(--ink-faint);
    border: 1px solid var(--line);
}

/* ── Switch ───────────────────────────────────────────────────────────────── */
/* A real checkbox wearing a slider, so it keeps the keyboard, the label click
   and the form post for nothing: `appearance: none` styles the input itself
   rather than hiding it behind a div, which is what usually costs a switch its
   focus ring and its Tab stop. */
.switch {
    display: inline-flex; align-items: center; gap: 8px;
    color: var(--ink-muted); cursor: pointer; user-select: none; white-space: nowrap;
}
.switch:hover { color: var(--ink); }
body .switch input[type="checkbox"] {
    appearance: none; -webkit-appearance: none;
    position: relative; flex: none; margin: 0; padding: 0;
    width: 34px; height: 20px; border-radius: 999px;
    border: 1px solid var(--line-strong); background: var(--surface-sunken);
    transition: background-color 120ms ease, border-color 120ms ease;
    cursor: pointer;
}
body .switch input[type="checkbox"]::after {
    content: ""; position: absolute; top: 2px; left: 2px;
    width: 14px; height: 14px; border-radius: 50%;
    background: var(--surface); border: 1px solid var(--line-strong);
    transition: transform 120ms ease;
}
body .switch input[type="checkbox"]:checked {
    background: var(--accent); border-color: var(--accent-strong);
}
body .switch input[type="checkbox"]:checked::after {
    transform: translateX(14px); border-color: var(--accent-strong);
}
body .switch input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.switch-on { color: var(--ink); font-weight: 500; }

/* ── Triage sweep ─────────────────────────────────────────────────────────── */
/* Both columns start at the same line. The card's height changes from one card
   to the next, so anything centred would move the question up and down under a
   reader who is holding one key down; and the frame hugs its image rather than
   stretching, which would only move the empty space below it. */
.triage-slide-pane {
    display: flex; align-items: flex-start; justify-content: center;
    align-self: start; max-height: 100%;
    min-height: 0; padding: 8px; background: var(--surface);
    border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow-card);
}
/* The image sizes itself and the wrapper shrink-wraps it, so the mask layer
   cannot drift away from the picture it is masking. Same rule as the occlusion
   editor, and it broke there first. */
.triage-slide { position: relative; display: inline-block; max-width: 100%; }
/* The cap is in viewport units, not percentages: a percentage max-height only
   resolves against a definite height, and every box between here and the page
   is auto-height, so the image would quietly overflow its pane. The image is
   still the thing that sizes, and the wrapper still shrink-wraps it, which is
   what keeps the mask layer on top of the picture.
   The 11rem is the chrome above and below it: header, page padding, the action
   bar and its rule. Change any of those and this has to move with them — the
   short-viewport case is the one that catches it. */
.triage-slide img {
    display: block; max-width: 100%; max-height: calc(100dvh - 11rem);
    width: auto; height: auto; background: #fff; border-radius: 4px;
}
.triage-slide svg { position: absolute; inset: 0; height: 100%; width: 100%; pointer-events: none; }
.triage-slide rect { fill: #ffeba2; fill-opacity: .85; stroke: #212121; stroke-width: .003; }

/* The card hugs its content and scrolls only when it outgrows the pane —
   a two-line question in a full-height box reads as a rendering fault. */
.triage-stack { position: relative; display: flex; align-items: flex-start; min-height: 0; flex: 1; }
.triage-card {
    display: flex; flex-direction: column; width: 100%; max-height: 100%;
    border: 1px solid var(--line); border-radius: 8px; background: var(--surface);
    box-shadow: var(--card-shadow);
    padding: 22px; overflow-y: auto;
    will-change: transform;
}
.triage-card[data-verdict="keep"] { border-color: #86c9a4; box-shadow: 0 0 0 3px #86c9a433; }
.triage-card[data-verdict="reject"] { border-color: #f0a9a1; box-shadow: 0 0 0 3px #f0a9a133; }
.triage-card .card-front-preview { font-size: 20px; line-height: 1.45; }
.triage-card .card-back-preview { font-size: 16px; line-height: 1.5; color: var(--ink); }
.triage-card .card-front-preview img, .triage-card .card-back-preview img { max-height: none; max-width: 100%; }
.triage-divider { height: 1px; margin: 14px 0; background: var(--line); }
.triage-hidden-answer {
    display: flex; align-items: center; justify-content: center; gap: 6px;
    border: 1px dashed var(--line-strong); border-radius: 6px;
    padding: 14px; color: var(--ink-faint);
}

/* Swipe feedback. Transform only, so nothing reflows mid-drag. */
.triage-card[data-dragging="true"] { transition: none; cursor: grabbing; }
.triage-verdict {
    position: absolute; top: 18px; z-index: 2; pointer-events: none;
    border-radius: 6px; padding: 4px 10px; font-size: 13px; font-weight: 600;
    letter-spacing: .04em; text-transform: uppercase; opacity: 0; transition: opacity 90ms ease;
}
.triage-verdict-keep { right: 18px; background: #e6f4ec; color: #1c6b45; border: 1px solid #86c9a4; }
.triage-verdict-reject { left: 18px; background: var(--danger-soft); color: var(--danger); border: 1px solid #f0a9a1; }
.triage-verdict[data-on="true"] { opacity: 1; }

.triage-actions {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    flex-shrink: 0; padding-top: 12px; border-top: 1px solid var(--line);
}
.triage-action {
    display: inline-flex; align-items: center; gap: 8px;
    height: 40px; padding: 0 18px; border-radius: 999px; border: 1px solid var(--line-strong);
    background: var(--surface); color: var(--ink); font-weight: 550;
    transition: border-color 120ms ease, transform 120ms ease;
}
.triage-action:hover { border-color: var(--ink); }
.triage-action:active { transform: translateY(1px); }
.triage-action kbd { font-size: 11px; }

/* A finger swipes the card the way a mouse drags it. Without this the browser
   takes a sideways touch as a pan of its own and cancels the pointer, so the
   swipe did nothing on a phone; a vertical touch still scrolls. */
.triage-stack[data-mode="card"] .triage-card { touch-action: pan-y; }

/* The sweep's header on a narrow screen: a second row for the progress and
   the view controls, broken off by a zero-height item a row wide. */
.tri-head-break { display: none; }
@media (max-width: 1023px) {
    .tri-head-break { display: block; order: 1; flex-basis: 100%; height: 0; }
    .tri-head > .tri-head-row { order: 2; }
}

/* On a phone the two verdicts are the top row, half the width each, where a
   thumb finds them; skip, edit and undo share the row under them. The key
   hints go wherever there are no keys. */
@media (max-width: 640px) {
    .triage-actions { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 6px; padding-top: 10px; }
    .triage-actions > * { grid-column: span 2; justify-content: center; padding: 0 8px; }
    .triage-actions > .triage-reject { grid-column: 1 / 4; grid-row: 1; }
    .triage-actions > .triage-keep { grid-column: 4 / 7; grid-row: 1; height: 44px; }
    .triage-actions > .triage-reject { height: 44px; }
}
@media (hover: none) and (pointer: coarse) {
    .triage-actions kbd { display: none; }
}
.triage-keep { border-color: #86c9a4; background: #e6f4ec; color: #1c6b45; }
.triage-keep:hover { background: #d7ecdf; border-color: #1c6b45; }
.triage-reject { border-color: #f0a9a1; background: var(--danger-soft); color: var(--danger); }
.triage-reject:hover { background: #fadedb; border-color: var(--danger); }

/* ── Triage, a slide at a time ────────────────────────────────────────────── */
/* Same cards, same fragment, different layout: the stack stops being a stack
   and becomes a scrolling column, so one slide's cards can be read against each
   other. The cards must lose their own max-height here — each one scrolling
   inside a scrolling column is two scrollbars and no way to see the set. */
.triage-group-head {
    display: flex; align-items: center; gap: 8px; flex-shrink: 0;
    padding-bottom: 8px; border-bottom: 1px solid var(--line); margin-bottom: 10px;
}
.triage-stack[data-mode="slide"] {
    flex-direction: column; align-items: stretch; gap: 10px;
    overflow-y: auto; padding: 2px;
}
.triage-stack[data-mode="slide"] .triage-card {
    max-height: none; overflow-y: visible; padding: 16px;
    transition: border-color 120ms ease, box-shadow 120ms ease, opacity 120ms ease;
}
/* Which card the keys act on. A ring rather than a colour, because keep and
   reject already own the two colours a card can turn. */
.triage-stack[data-mode="slide"] .triage-card[data-focused="true"] {
    border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
/* A decided card stays in place, faded, until the slide is done. Removing it
   would reflow the column under the reader's eyes on every keystroke, and the
   decision could not be undone by looking at it. */
.triage-stack[data-mode="slide"] .triage-card[data-decided] { opacity: .5; }
.triage-stack[data-mode="slide"] .triage-card[data-decided="keep"] { border-color: #86c9a4; }
.triage-stack[data-mode="slide"] .triage-card[data-decided="reject"] { border-color: #f0a9a1; }
.triage-stack[data-mode="slide"] .triage-card[data-decided]:hover { opacity: 1; }
.triage-stack[data-mode="slide"] .triage-card .card-front-preview { font-size: 17px; }
.triage-stack[data-mode="slide"] .triage-card .card-back-preview { font-size: 15px; }

/* Per-card buttons: only this mode has more than one card to point at. */
.triage-card-actions { display: none; align-items: center; gap: 6px; margin-top: 12px; }
.triage-stack[data-mode="slide"] .triage-card-actions { display: flex; }
.triage-card-verdict {
    margin-left: auto; font-size: 12px; font-weight: 600; letter-spacing: .04em;
    text-transform: uppercase; color: var(--ink-faint);
}
.triage-mini {
    display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 10px;
    border-radius: 6px; border: 1px solid var(--line-strong);
    background: var(--surface); color: var(--ink-muted); font-size: 13px;
}
.triage-mini:hover { background: var(--surface-sunken); color: var(--ink); }
.triage-mini-keep:hover { border-color: #86c9a4; background: #e6f4ec; color: #1c6b45; }
.triage-mini-reject:hover { border-color: #f0a9a1; background: var(--danger-soft); color: var(--danger); }

@media (max-width: 900px) {
    .triage-panes { grid-template-columns: minmax(0, 1fr) !important; }
    .triage-slide-pane { max-height: 32vh; }
    .triage-slide img { max-height: 30vh; }
}

/* ── Possible duplicates ──────────────────────────────────────────────────── */
.dup-pair { border: 1px solid var(--line); border-radius: 8px; background: var(--surface); }
.dup-head {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    padding: 10px 12px; border-bottom: 1px solid var(--line);
}
.dup-score {
    font-variant-numeric: tabular-nums; font-weight: 600;
    color: var(--accent-strong); background: var(--accent-soft);
    border-radius: 6px; padding: 2px 8px; cursor: help;
}
/* Two columns so the pair reads as a comparison; one below the other when
   there is not room for that to be legible. */
.dup-sides { display: grid; grid-template-columns: 1fr 1fr; }
/* The two sides are read as a comparison, so their actions line up even when
   one card has a back field and the other keeps its answer in the front. */
.dup-side { display: flex; flex-direction: column; padding: 12px; min-width: 0; }
.dup-actions { margin-top: auto; padding-top: 12px; }
.dup-side + .dup-side { border-left: 1px solid var(--line); }
.dup-side .card-front-preview img, .dup-side .card-back-preview img { max-height: 120px; }
@media (max-width: 780px) {
    .dup-sides { grid-template-columns: 1fr; }
    .dup-side + .dup-side { border-left: 0; border-top: 1px solid var(--line); }
}

/* The whole matching family is worth showing — recognising the right model is
   the job — so the list scrolls rather than being cut to an arbitrary few. */
.price-options {
    max-height: 260px; overflow-y: auto;
    border: 1px solid var(--line); border-radius: 6px; background: var(--surface);
}
.price-option {
    display: flex; align-items: center; gap: 8px;
    padding: 5px 8px; border-top: 1px solid var(--line);
}
.price-option:first-child { border-top: 0; }
.price-option:hover { background: var(--surface-sunken); }

/* A looked-up price landing in the field, so the change is not silent. */
.field-applied { border-color: var(--accent) !important; background: var(--accent-soft); }

/* ── Statistics ───────────────────────────────────────────────────────────── */
.stat {
    display: flex; flex-direction: column; gap: 2px;
    border: 1px solid var(--line); border-radius: 8px;
    background: var(--surface); padding: 12px 14px;
}
.stat-value { font-size: 24px; font-weight: 600; line-height: 1.15; font-variant-numeric: tabular-nums; }
.stat-label { font-size: 13px; color: var(--ink-muted); }
.stat-note { font-size: 11px; color: var(--ink-faint); }

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

/* The workspace below 900px: the slide above the cards rather than beside
   them, because the job is still reading a card against its slide. The slide
   pane takes a fixed share and scrolls inside it — left to size itself it grew
   to the height of the whole deck and the cards were somewhere below the
   fold. The splitter goes: there is no drag to make on a phone. */
@media (max-width: 900px) {
    .ws-panes { flex-direction: column; }
    .ws-panes > .splitter { display: none; }
    .ws-panes > * { width: 100% !important; }
    .ws-panes > section { flex: 0 0 42%; min-height: 0; border-bottom: 1px solid var(--line-strong); }
    .ws-panes > #card-controls { flex: 1 1 0; min-height: 0; }
}

/* The deck picker is the one control in the workspace header that changes
   what the page is about, so it is the one that gives way on a phone rather
   than wrapping off. `body select` is the weight that beats the rule sizing
   every control. */
body select.ws-deck-select { width: 14rem; min-width: 0; }
@media (max-width: 1023px) {
    body select.ws-deck-select { flex: 1 1 0; width: auto; max-width: 20rem; }
    /* The deck-level actions and the run status take the header's second row;
       the back arrow, the deck and the menus keep the first. */
    .ws-head > .ws-head-row { order: 1; flex-basis: 100%; }
    .ws-head > .ws-head-row:not(#run-status) { overflow-x: auto; scrollbar-width: none; }
}

/* A shortcut list is no use without a keyboard: a phone or a tablet with
   nothing attached is told nothing it can act on. */
@media (hover: none) and (pointer: coarse) {
    .keys-only { display: none !important; }
}

/* ── Create with AI ───────────────────────────────────────────────────────── */
/* The dialog is the workspace in miniature: the slide on the left, the work on
   the right. That is not a visual conceit — it is the comparison the job needs.
   You write the brief against the slide, and then you judge the draft against
   it, so the rail stays put and only the right column changes. */
.draft-backdrop {
    position: fixed; inset: 0; z-index: 40;
    display: flex; align-items: flex-start; justify-content: center;
    padding: 16px; background: rgba(12, 16, 22, .38);
    /* The workspace behind this is a slide render and a column of cards — busy
       enough that a flat scrim still competes with the dialog. */
    backdrop-filter: blur(2px);
}
.draft-dialog {
    display: flex; flex-direction: column; overflow: hidden;
    margin-top: 6vh; width: 100%; max-width: 780px; max-height: 84vh;
    border: 1px solid var(--line-strong); border-radius: 10px;
    background: var(--surface);
    box-shadow: 0 12px 32px rgba(16, 24, 40, .16), 0 2px 6px rgba(16, 24, 40, .08);
}
.draft-head {
    display: flex; align-items: center; gap: 8px; flex-shrink: 0;
    padding: 10px 10px 10px 16px; border-bottom: 1px solid var(--line);
}
.draft-panes { display: flex; min-height: 0; }

/* The rail. Sunken like the pane it stands for, and sized so the slide is
   recognisable rather than merely present. */
.draft-slide {
    flex: 0 0 200px; display: flex; flex-direction: column;
    padding: 16px; border-right: 1px solid var(--line); background: var(--surface-sunken);
}
.draft-slide img {
    width: 100%; height: auto; margin-bottom: 10px; display: block;
    border: 1px solid var(--line); border-radius: 6px; background: #fff;
}
/* min-width: 0, or the caption refuses to wrap and pushes the dialog wider
   than the window when the two columns become a row. */
.draft-slide-text { min-width: 0; }
.draft-slide-name { font-size: 12px; font-weight: 600; color: var(--ink); }
.draft-slide-note { margin-top: 2px; font-size: 12px; line-height: 1.45; color: var(--ink-muted); }

.draft-panes { align-items: stretch; }
/* min-height: 0 on both of these, or a column of six cards never scrolls: a
   flex item refuses to shrink below its content unless told it may, and the
   footer is then pushed off the bottom of the dialog instead. */
.draft-work { display: flex; min-width: 0; min-height: 0; flex: 1; flex-direction: column; }
/* The swap target sits between the work column and the panel inside it, so it
   has to carry the column through rather than break the chain. */
.draft-results { display: flex; min-height: 0; flex: 1; flex-direction: column; }
.draft-form { display: flex; min-height: 0; flex: 1; flex-direction: column; }
/* The body takes the slack so the actions sit on the floor of the dialog. The
   rail is the taller column, and without this the right side stopped where its
   content did and left a panel of white under the buttons. */
.draft-body { flex: 1; min-height: 0; padding: 16px; }
.draft-scroll { overflow-y: auto; }
.draft-foot {
    display: flex; align-items: center; gap: 8px; flex-shrink: 0; flex-wrap: wrap;
    padding: 10px 16px; border-top: 1px solid var(--line); background: var(--surface);
}
/* The actions sit right; whatever comes before them takes the slack. The brief
   uses an empty spacer, the draft uses its "Edit the brief" button. */
.draft-foot > :first-child { margin-right: auto; }
.draft-meta { font-size: 12px; line-height: 1.45; color: var(--ink-faint); }
/* The tokens were spent by the request that produced this, not by the button
   underneath it — so it reads as a receipt above the work, not as a price tag
   on the action. */
.draft-receipt { margin-bottom: 12px; }

body textarea#draft-instruction { min-height: 84px; font-size: 14px; }

/* Waiting for the model. Asking one takes seconds, and the only feedback used
   to be a 12px word going from invisible to faint — which reads as nothing
   happening, because nothing visibly does. While the request is in flight the
   brief steps aside and says what is going on in the same place, and the button
   says it too. htmx puts `htmx-request` on the form for the duration. */
.draft-waiting { display: none; }
.draft-form.htmx-request .draft-fields { display: none; }
.draft-form.htmx-request .draft-waiting { display: block; }
.draft-label-busy { display: none; }
.draft-form.htmx-request .draft-label-idle { display: none; }
.draft-form.htmx-request .draft-label-busy { display: inline; }
/* Indeterminate: one request, nothing to report part-way. A bar that filled to
   a number would be inventing the number. */
.draft-waiting-bar > span {
    width: 40%;
    animation: draft-sweep 1.4s cubic-bezier(.4, 0, .2, 1) infinite;
}
@keyframes draft-sweep {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(350%); }
}
@media (prefers-reduced-motion: reduce) {
    /* Still says "working", without the movement. */
    .draft-waiting-bar > span { width: 100%; animation: none; opacity: .5; }
}
.draft-foot-spacer { flex: 1; }

/* Starters fill the field rather than submitting: the brief that works is
   usually one of these plus a detail only the reader knows. */
.draft-starters { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.draft-starter {
    padding: 4px 9px; border-radius: 999px;
    border: 1px solid var(--line-strong); background: var(--surface);
    font-size: 12px; color: var(--ink-muted); cursor: pointer;
    transition: border-color 120ms ease, color 120ms ease, background-color 120ms ease;
}
.draft-starter:hover { border-color: var(--accent); color: var(--accent-strong); background: var(--accent-soft); }

/* A proposal is drawn as the row it is about to become. */
.draft-card {
    border: 1px solid var(--line); border-radius: 12px; background: var(--surface);
    box-shadow: var(--shadow-row); padding: 12px 14px;
}
.draft-card + .draft-card { margin-top: 10px; }
.draft-card-head { margin-bottom: 8px; }
.draft-card .card-front-preview { font-size: 15px; line-height: 1.5; }
.draft-card .card-back-preview {
    margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line);
    font-size: 13px; line-height: 1.5; color: var(--ink);
}
.draft-card .card-front-preview img, .draft-card .card-back-preview img { max-height: 140px; max-width: 100%; }

.draft-error {
    border: 1px solid #f0dca8; border-radius: 6px; background: #fdf6e3;
    padding: 10px 12px; font-size: 13px; line-height: 1.5; color: #8a6100;
}

/* The one moment of motion: the right column changes because the model
   answered, so the change is shown rather than simply appearing. */
#draft-modal-body.htmx-swapping { opacity: 0; }
#draft-modal-body { transition: opacity 120ms ease; }
@media (prefers-reduced-motion: reduce) {
    #draft-modal-body { transition: none; }
}

@media (max-width: 640px) {
    .draft-panes { flex-direction: column; }
    .draft-slide {
        flex: 0 0 auto; flex-direction: row; align-items: center; gap: 12px;
        border-right: 0; border-bottom: 1px solid var(--line); padding: 10px 16px;
    }
    .draft-slide img { width: 88px; flex-shrink: 0; margin-bottom: 0; }
    .draft-dialog { margin-top: 2vh; max-height: 92vh; }
}
.draft-card-extra { font-size: 12px; color: var(--ink-muted); }
.draft-dropped { margin-top: 10px; list-style: disc; padding-left: 18px; }
.draft-dropped li { margin-top: 2px; }

/* ── A project's identity ─────────────────────────────────────────────────── */
/* A drawing in a colour, shown wherever a project says its own name, so it can
   be found without reading the word. The tone classes are spelled out here and
   never composed as Tailwind utilities: the Play CDN only sees class names
   written literally in the source, which is why the deck dots are plain CSS
   too. */
.project-icon {
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0; height: 32px; width: 32px; border-radius: 8px;
}
.project-icon svg { height: 18px; width: 18px; }
.project-icon-lg { height: 40px; width: 40px; border-radius: 10px; }
.project-icon-lg svg { height: 22px; width: 22px; }
/* Small enough that a 2px stroke would close up, so it thins with the tile. */
.project-icon-sm { height: 24px; width: 24px; border-radius: 6px; }
.project-icon-sm svg { height: 15px; width: 15px; stroke-width: 1.75; }

.project-tone-slate  { background: var(--tone-slate-soft);  color: var(--tone-slate-ink); }
.project-tone-rose   { background: var(--tone-rose-soft);   color: var(--tone-rose-ink); }
.project-tone-amber  { background: var(--tone-amber-soft);  color: var(--tone-amber-ink); }
.project-tone-lime   { background: var(--tone-lime-soft);   color: var(--tone-lime-ink); }
.project-tone-teal   { background: var(--tone-teal-soft);   color: var(--tone-teal-ink); }
.project-tone-sky    { background: var(--tone-sky-soft);    color: var(--tone-sky-ink); }
.project-tone-indigo { background: var(--tone-indigo-soft); color: var(--tone-indigo-ink); }
.project-tone-violet { background: var(--tone-violet-soft); color: var(--tone-violet-ink); }

/* ── Picking one ──────────────────────────────────────────────────────────── */
/* Radios, so the picker submits with the form, works with a keyboard and
   survives with JavaScript off. The input itself is hidden from sight but not
   from focus: :focus-visible lands on the tile through :has(). */
.pick-grid { display: flex; flex-wrap: wrap; gap: 6px; }
.pick-group + .pick-group { margin-top: 14px; }
.pick-group-name {
    margin-bottom: 6px; font-size: 12px; font-weight: 600; color: var(--ink-muted);
}
/* The library is 1,600 icons and the grid is searched, not scrolled through;
   this is the height of "enough to browse", not of the catalogue. */
.pick-scroll {
    max-height: 296px; overflow-y: auto; padding: 10px;
    border: 1px solid var(--line); border-radius: 8px;
    background: var(--surface-sunken);
}
.pick-search { margin-bottom: 8px; }
.pick-status { margin: 0 0 8px; font-size: 12px; color: var(--ink-faint); }

.pick-tile {
    position: relative; display: inline-flex; cursor: pointer; border-radius: 8px;
}
/* The whole tile is the hit area, so the radio is stretched over it rather
   than hidden: `display: none` would take it out of the tab order, and the
   14x14 box the stylesheet gives every radio has to be overridden explicitly
   because a fixed width beats `inset: 0`. */
.pick-tile input[type="radio"] {
    position: absolute; inset: 0; height: 100%; width: 100%;
    opacity: 0; margin: 0; cursor: pointer;
}
.pick-tile .project-icon { border: 1px solid transparent; transition: border-color 120ms ease; }
.pick-tile:hover .project-icon { border-color: var(--line-strong); }
.pick-tile:has(input:checked) .project-icon { border-color: var(--ink); }
.pick-tile:has(input:focus-visible) .project-icon { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The grid is drawn in the colour the project is about to wear, so the two
   choices are made together. It is one attribute on the container and two
   custom properties rather than a tone class per tile, because the tiles are
   rebuilt by a script on every search and the colour is not its business. */
.icon-picker { --pick-soft: var(--tone-slate-soft); --pick-ink: var(--tone-slate-ink); }
.icon-picker[data-tone="slate"]  { --pick-soft: var(--tone-slate-soft);  --pick-ink: var(--tone-slate-ink); }
.icon-picker[data-tone="rose"]   { --pick-soft: var(--tone-rose-soft);   --pick-ink: var(--tone-rose-ink); }
.icon-picker[data-tone="amber"]  { --pick-soft: var(--tone-amber-soft);  --pick-ink: var(--tone-amber-ink); }
.icon-picker[data-tone="lime"]   { --pick-soft: var(--tone-lime-soft);   --pick-ink: var(--tone-lime-ink); }
.icon-picker[data-tone="teal"]   { --pick-soft: var(--tone-teal-soft);   --pick-ink: var(--tone-teal-ink); }
.icon-picker[data-tone="sky"]    { --pick-soft: var(--tone-sky-soft);    --pick-ink: var(--tone-sky-ink); }
.icon-picker[data-tone="indigo"] { --pick-soft: var(--tone-indigo-soft); --pick-ink: var(--tone-indigo-ink); }
.icon-picker[data-tone="violet"] { --pick-soft: var(--tone-violet-soft); --pick-ink: var(--tone-violet-ink); }
.icon-picker .project-icon { background: var(--pick-soft); color: var(--pick-ink); }

/* A colour is chosen as the colour, not as a word beside it: the tile wears
   the tone's own background and the disc is its ink, so the swatch is the two
   values the tile will actually use rather than an approximation of them. */
.pick-swatch .project-icon::after {
    content: ""; height: 13px; width: 13px; border-radius: 50%;
    background: currentColor;
}

/* ── Choosing an icon ─────────────────────────────────────────────────────── */
/* The trigger is the preview: the icon you have, at the size it will be shown,
   rather than a button that only describes one. */
.icon-trigger {
    display: inline-flex; align-items: center; gap: 10px;
    padding: 6px 10px 6px 6px; border: 1px solid var(--line-strong);
    border-radius: 10px; background: var(--surface); cursor: pointer;
    color: var(--ink); font: inherit; font-size: 13px;
}
.icon-trigger:hover { border-color: var(--ink-faint); }
.icon-trigger-name { font-weight: 500; }
.icon-trigger-hint { color: var(--ink-faint); }

.icon-modal-backdrop {
    position: fixed; inset: 0; z-index: 40; padding: 16px;
    display: flex; align-items: flex-start; justify-content: center;
    background: rgba(0, 0, 0, 0.3);
}
.icon-modal {
    display: flex; flex-direction: column; min-height: 0;
    margin-top: 6vh; width: 100%; max-width: 640px; max-height: 84vh;
    border: 1px solid var(--line); border-radius: 12px;
    background: var(--surface); box-shadow: 0 20px 40px rgba(16, 24, 40, 0.18);
}
.icon-modal-head, .icon-modal-foot {
    display: flex; align-items: center; gap: 10px; padding: 14px 16px;
}
.icon-modal-head { justify-content: space-between; border-bottom: 1px solid var(--line); }
.icon-modal-foot { justify-content: flex-end; border-top: 1px solid var(--line); }
/* Every column down to the scroller needs min-height: 0, or a full grid pushes
   the footer off the bottom instead of scrolling — the same chain the draft
   dialog documents. */
.icon-modal-body { display: flex; flex-direction: column; min-height: 0; padding: 14px 16px; }
/* A floor as well as a ceiling: without it the dialog shrinks to fit two
   results and grows again on the next keystroke, and the thing you are reading
   moves while you type. */
.icon-modal .pick-scroll { max-height: none; flex: 1; min-height: 260px; }


/* Fields whose widget names a `row` sit beside each other. The icon keeps its
   natural width — it is one button — and the colour takes the rest and wraps
   its swatches inside its own column rather than pushing the row wider. */
.field-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 20px; }
.field-col-icon { flex: 0 0 auto; }
.field-col-color { flex: 1 1 280px; min-width: 0; }

/* The two columns of that row line up, and say so once. The icon trigger is a
   32px tile in 6px of padding inside a border; the swatches keep the bare tile,
   because a swatch blown up to the height of the control beside it stops
   reading as a palette and starts reading as eight buttons. So the *grid* takes
   the height and centres them in it, rather than the tiles growing into it. */
.field-row { --identity-height: 46px; }
.field-col-icon .icon-trigger { min-height: var(--identity-height); }
.field-col-color .pick-grid { min-height: var(--identity-height); align-items: center; }

/* ── AI model picker ─────────────────────────────────────────────────────── */
/* One card per model a person may use; the chosen one wears the accent ring
   and a tick, read straight off the radio with :has() — a class rendered by
   the server would leave two cards looking chosen while a pick is in flight.
   The radio itself is visually hidden but stays in the form and in
   the tab order, so arrow keys walk the cards like any radio group, and the
   focus ring is drawn on the card it belongs to. */
.model-grid {
    display: grid; gap: 10px;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.model-card {
    position: relative; display: flex; flex-direction: column; gap: 6px;
    padding: 12px 14px; border: 1px solid var(--line); border-radius: 8px;
    background: var(--surface); cursor: pointer;
    transition: border-color 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
}
.model-card:hover { border-color: var(--line-strong); }
.model-card:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.model-card:has(input:checked) {
    border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); background: var(--accent-soft);
}
.model-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.model-card-name { font-weight: 600; }
.model-card-tick {
    display: inline-flex; align-items: center; justify-content: center;
    width: 18px; height: 18px; border-radius: 999px;
    color: var(--accent-ink); background: var(--accent); opacity: 0;
    transition: opacity 120ms ease;
}
.model-card-tick .ui-icon { height: 12px; width: 12px; }
.model-card:has(input:checked) .model-card-tick { opacity: 1; }
.model-card-text { color: var(--ink-muted); font-size: 12px; line-height: 1.45; }
.model-card-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.model-card-tags:empty { display: none; }
/* While the choice is on its way to the server, the grid says so. */
#model-choice.htmx-request .model-grid { opacity: .6; pointer-events: none; }

/* ── deck row: rename and replace ───────────────────────────────────────────
   The pencil beside a deck's name is quiet until the row is hovered, for the
   reason the card row's buttons are: ten decks should read as ten names, not
   ten names and ten pencils. A touch screen has no hover, so there it stays. */
.deck-rename-trigger { opacity: .45; }
li:hover .deck-rename-trigger, .deck-rename-trigger:focus-visible { opacity: 1; }
@media (hover: none) { .deck-rename-trigger { opacity: 1; } }
.deck-rename { max-width: 32rem; }
/* Uploading a PDF takes a moment; the button says so while it does. */
.replace-busy { display: none; }
.replace-form.htmx-request .replace-busy { display: inline; }
.replace-form.htmx-request .replace-idle { display: none; }

/* ── Profile menu and AI meter ─────────────────────────────────────────── */
/* The header's account control: an avatar, the name where there is room, and
   the AI meter as a pill beside it, all one button. The meter's colour is the
   state of the budget — accent while there is room, amber past 80% used, red
   when it is gone — and plain grey where no budget applies and it only reports
   what was spent. */
.profile-button {
    display: inline-flex; align-items: center; gap: 8px; height: 32px;
    padding: 0 6px 0 3px; border: 1px solid transparent; border-radius: 999px;
    background: none; color: var(--ink-muted); cursor: pointer;
}
.profile-button:hover, .profile-button[aria-expanded="true"] {
    background: var(--surface-sunken); border-color: var(--line); color: var(--ink);
}
.profile-name { max-width: 10rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A phone keeps the figure and drops its unit: the ring or the sparkle says
   which figure it is, and the panel one tap away spells it out. */
@media (max-width: 640px) { .profile-name, .ai-meter-unit { display: none; } }
.avatar {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    height: 26px; width: 26px; border-radius: 50%;
    background: var(--accent-soft); color: var(--accent-strong);
    font-size: 11px; font-weight: 600; letter-spacing: .02em;
}
.avatar-lg { height: 40px; width: 40px; font-size: 14px; }

.ai-meter {
    display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
    font-size: 11.5px; font-weight: 500; font-variant-numeric: tabular-nums;
    color: var(--ink-muted);
}
.ai-meter:empty { display: none; }
.ai-meter:not(:empty) {
    height: 22px; padding: 0 8px 0 6px; border-radius: 999px; background: var(--surface-sunken);
}
.profile-button:hover .ai-meter:not(:empty) { background: var(--surface); }
.ai-meter .ui-icon { height: 12px; width: 12px; color: var(--ink-faint); }
.ai-meter-unit { font-weight: 400; color: var(--ink-faint); }
.ai-meter-ring { height: 14px; width: 14px; transform: rotate(-90deg); }
.ai-meter-ring circle { fill: none; stroke-width: 2.5; }
.ai-meter-track { stroke: var(--line); }
.ai-meter-fill { stroke: var(--accent); stroke-linecap: round; transition: stroke-dasharray 300ms ease; }
.ai-meter-ok { color: var(--accent-strong); }
.ai-meter-warn { color: var(--tone-amber-ink); }
.ai-meter-warn .ai-meter-fill { stroke: #d97706; }
.ai-meter-danger { color: var(--danger); }
.ai-meter-danger:not(:empty) { background: var(--danger-soft); }
.ai-meter-danger .ai-meter-fill { stroke: var(--danger); }

.profile-panel { width: 18rem; padding: 0 4px 4px; }
.profile-head { display: flex; align-items: center; gap: 12px; padding: 14px 10px 12px; }
.profile-usage {
    margin: 0 4px 6px; padding: 12px; border: 1px solid var(--line); border-radius: 8px;
    background: var(--surface-sunken);
}
.profile-usage-label {
    font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
    color: var(--ink-faint);
}
.profile-figure { display: flex; align-items: baseline; gap: 6px; margin-top: 4px; }
.profile-amount { font-size: 20px; font-weight: 600; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
.profile-usage .progress { background: var(--line-strong); }
.progress-warn > span { background: #d97706; }
.progress-danger > span { background: var(--danger); }
.profile-model {
    display: flex; align-items: center; gap: 6px; margin-top: 10px; padding-top: 10px;
    border-top: 1px solid var(--line); font-size: 12px; color: var(--ink-muted);
}
.profile-model .ui-icon { color: var(--ink-faint); }

/* ── Error pages ──────────────────────────────────────────────────────────── */
/* The status code is written on index cards — white, a blue rule under the
   title line, faint blue rules below it — because that is what this app makes.
   The cards are the one loud thing on the page; everything under them is the
   ordinary 13px interface. `500.html` carries its own copy of these rules,
   since it may not load this stylesheet; change one and look at the other. */
:root {
    --card-rule: #dde6f5;
    --card-margin: #aab6f0;
    --card-shadow: 0 1px 1px rgba(16, 24, 40, .06), 0 14px 28px -14px rgba(16, 24, 40, .35);
}
.err-page { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 24px 0 40px; }
.err-hand { display: flex; justify-content: center; padding: 12px 0 8px; perspective: 900px; }
.err-card {
    --w: clamp(88px, 24vw, 148px);
    --turn: 0deg;
    position: relative; display: block; flex-shrink: 0;
    width: var(--w); aspect-ratio: 3 / 4; margin: 0 calc(var(--w) * -0.06);
    border: 0; border-radius: 6px; padding: 0;
    background-color: var(--surface);
    background-image:
        linear-gradient(to bottom, transparent 24%, var(--card-margin) 24%, var(--card-margin) calc(24% + 2px), transparent calc(24% + 2px)),
        repeating-linear-gradient(to bottom, transparent 0 calc(var(--w) * 0.12 - 1px), var(--card-rule) calc(var(--w) * 0.12 - 1px) calc(var(--w) * 0.12));
    background-size: 100% 100%, 100% 70%;
    background-position: 0 0, 0 100%;
    background-repeat: no-repeat;
    box-shadow: var(--card-shadow);
    transform: rotate(var(--turn)) translateY(var(--lift, 0));
    transition: transform 180ms ease;
}
.err-card-1 { --turn: -9deg; --lift: 10px; z-index: 1; }
.err-card-2 { --turn: 1.5deg; z-index: 2; }
.err-card-3 { --turn: 8deg; --lift: 14px; z-index: 1; }
.err-digit {
    position: absolute; inset: 24% 0 0; display: flex; align-items: center; justify-content: center;
    font: 800 calc(var(--w) * 0.62) / 1 ui-rounded, system-ui, -apple-system, "Segoe UI", sans-serif;
    letter-spacing: -0.04em; color: var(--ink);
}
/* The answer side is written in the pen the app's accent already is. */
.err-digit-pen { color: var(--accent); }
/* What a real index card keeps above its header line: a heading. Here Q. and A.,
   which is the whole of a flashcard. */
.err-cue {
    position: absolute; top: 7%; left: 10%;
    font-size: calc(var(--w) * 0.1); font-weight: 600; color: var(--ink-faint);
}

/* The card that turns over. Both faces carry the paper, so the back reads as
   the same card rather than a different one. */
.err-flip { cursor: pointer; background: none; box-shadow: none; transform-style: preserve-3d; }
.err-flip .err-face {
    position: absolute; inset: 0; border-radius: 6px;
    background-color: var(--surface);
    background-image:
        linear-gradient(to bottom, transparent 24%, var(--card-margin) 24%, var(--card-margin) calc(24% + 2px), transparent calc(24% + 2px)),
        repeating-linear-gradient(to bottom, transparent 0 calc(var(--w) * 0.12 - 1px), var(--card-rule) calc(var(--w) * 0.12 - 1px) calc(var(--w) * 0.12));
    background-size: 100% 100%, 100% 70%; background-position: 0 0, 0 100%; background-repeat: no-repeat;
    box-shadow: var(--card-shadow);
    backface-visibility: hidden; -webkit-backface-visibility: hidden;
    transition: transform 520ms cubic-bezier(.3, 1.4, .5, 1);
}
.err-flip .err-back { transform: rotateY(180deg); }
.err-flip[aria-pressed="true"] .err-front { transform: rotateY(-180deg); }
.err-flip[aria-pressed="true"] .err-back { transform: rotateY(0deg); }
.err-flip:hover { --lift: -6px; }
.err-flip:focus-visible { outline: none; }
.err-flip:focus-visible .err-face { outline: 2px solid var(--accent); outline-offset: 3px; }

.err-title { margin-top: 36px; font-size: 22px; font-weight: 600; letter-spacing: -0.015em; }
.err-lede { margin-top: 6px; max-width: 30rem; color: var(--ink-muted); }
.err-actions { margin-top: 22px; display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }

/* The one moment of motion: the cards are dealt from a single stack into the
   fan. Off entirely for anybody who has asked for less motion. */
@media (prefers-reduced-motion: no-preference) {
    .err-card { animation: err-deal 560ms cubic-bezier(.2, .9, .3, 1.15) backwards; }
    .err-card-1 { animation-delay: 80ms; }
    .err-card-3 { animation-delay: 160ms; }
}
@keyframes err-deal {
    from { transform: translate(calc(var(--w) * var(--from, 0)), 40px) rotate(0deg); opacity: 0; }
}
.err-card-1 { --from: 1; }
.err-card-3 { --from: -1; }

/* ── Legal pages ───────────────────────────────────────────────────────── */
/* Long prose in the app's own type. Tailwind's preflight zeroes every margin
   and list marker, so the rhythm is put back here rather than per element. */
.legal { font-size: 15px; line-height: 1.6; }
.legal h1 {
    font-family: var(--font-display); font-size: 30px; font-weight: 700;
    font-stretch: 86%; letter-spacing: -0.02em; line-height: 1.1;
}
.legal h2 { margin-top: 1.75em; font-size: 15px; font-weight: 600; }
.legal p, .legal ul { margin-top: .6em; }
.legal ul { list-style: disc; padding-left: 1.4em; }
.legal code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .9em; }
.legal .legal-lead { margin-top: .5em; color: var(--ink-muted); }
.legal a { overflow-wrap: anywhere; }

/* ── Surfaces ──────────────────────────────────────────────────────────── */
/* A white panel on the paper gets the landing page's soft lift, so a page of
   them reads as sheets laid on a desk rather than boxes drawn on a screen. */
main .rounded-card.border.bg-surface { box-shadow: var(--shadow-card); }

/* ── Paper ─────────────────────────────────────────────────────────────── */
/* The landing page's squared paper, rising from the bottom of the window and
   fading out on the way up: the top of a page is where its title, tabs and
   first panels are, and the bottom is where a page runs out of things to say.
   Fixed, so it stays at the foot of the window however far a page scrolls.
   It is drawn by an element inside base.html's chrome block, so the pages that
   replace the chrome — the workspace and the sweep, which are tools filled
   edge to edge — never get it. z-index -1 puts it under main without a
   stacking context to manage: body's background belongs to the canvas, so the
   grid still shows above it. */
.app-paper {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: -1;
    height: 55vh; pointer-events: none;
    background-image:
        linear-gradient(var(--line) 1px, transparent 1px),
        linear-gradient(90deg, var(--line) 1px, transparent 1px);
    background-size: 28px 28px;
    background-position: -1px -1px;
    -webkit-mask-image: linear-gradient(to top, rgba(0, 0, 0, 0.75), transparent);
    mask-image: linear-gradient(to top, rgba(0, 0, 0, 0.75), transparent);
}

/* A header with a little of the page showing through it, as on the landing page. */
header.app-header { background: rgba(255, 255, 255, 0.86); backdrop-filter: saturate(1.4) blur(10px); }

/* ── Figures ───────────────────────────────────────────────────────────── */
/* A headline number is set like the landing page's cost figure. */
.stat-value {
    font-family: var(--font-display); font-size: 32px; font-weight: 650;
    font-stretch: 80%; letter-spacing: -0.02em; line-height: 1.05;
}

/* ── Links ─────────────────────────────────────────────────────────────── */
/* An underline at a third of the ink until the pointer arrives: still
   unmistakably a link in a sentence, no longer a ruled line through it. */
.link, .btn-link {
    text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
    text-underline-offset: 3px;
    transition: text-decoration-color 120ms ease;
}
.link:hover, .btn-link:hover { text-decoration-color: currentColor; }

/* ── Dialogs ───────────────────────────────────────────────────────────── */
/* Every modal is a backdrop element carrying role="dialog" with a panel
   inside it, so this reaches all of them without a class on each. */
[role="dialog"][aria-modal="true"].bg-black\/30,
[role="dialog"][aria-modal="true"].bg-black\/40 {
    background: rgba(22, 27, 68, 0.36);
    backdrop-filter: blur(2px);
}
[role="dialog"][aria-modal="true"] > .rounded-card {
    border-radius: 18px;
    box-shadow: 0 1px 2px rgba(22, 26, 44, 0.06), 0 40px 80px -30px rgba(22, 26, 44, 0.45);
}

/* ── Empty states ──────────────────────────────────────────────────────── */
/* Two blank index cards, dealt, above a sentence saying what would fill the
   space: the 404 page's cards at a smaller size, and the same tokens. */
.empty-deal { display: flex; justify-content: center; margin-bottom: 18px; }
.empty-deal span {
    width: 74px; aspect-ratio: 5 / 3; margin: 0 -6px;
    border-radius: 4px;
    background-color: var(--surface);
    background-image:
        linear-gradient(var(--card-margin), var(--card-margin)),
        repeating-linear-gradient(to bottom, transparent 0 7px, var(--card-rule) 7px 8px);
    background-size: 100% 1.5px, 100% calc(100% - 14px);
    background-position: 0 11px, 0 13px;
    background-repeat: no-repeat;
    box-shadow: var(--card-shadow);
    transform: rotate(var(--turn));
}
.empty-deal span:first-child { --turn: -8deg; }
.empty-deal span:last-child { --turn: 5deg; translate: 0 6px; }

/* The legal pages sit outside the app, beside the landing page, and keep its
   arrangement: paper at the top of the page, under the logo, fading down. */
.app-paper-top {
    position: absolute; top: 0; bottom: auto; height: 420px;
    -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), transparent);
    mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), transparent);
}

/* Tailwind's space-y-* puts its gap above every child after the first, and a
   form's CSRF input is a child the eye cannot see: the first visible field
   then sits a whole gap lower than the panel's padding says it should. */
[class*="space-y-"] > input[type="hidden"]:first-child + :not([type="hidden"]),
[class*="space-y-"] > input[type="hidden"]:first-child + input[type="hidden"] + :not([type="hidden"]) {
    margin-top: 0;
}

/* The sweep card is the flashcard itself, so it keeps an index card's red
   line between the heading (the chips) and what is written on it. */
.triage-card > .triage-body {
    margin-top: 12px; padding-top: 16px;
    border-top: 1.5px solid var(--card-margin);
}

/* A form whose heading sits above its panel: the shared field list's own top
   margin (it expects a heading inside) would only add to the padding. */
.form-flush > div:first-of-type { margin-top: 0; }

/* ── Project tiles ─────────────────────────────────────────────────────── */
/* A course is a stack of index cards, so its tile is one: the name above a
   blue header line, the facts and the ways in written below it. The line runs
   edge to edge, as on a real card, which is why the padding is on the two
   bands rather than on the tile. */
.project-card {
    border: 1px solid var(--line); border-radius: 8px;
    background: var(--surface); box-shadow: var(--shadow-card);
    transition: border-color 120ms ease, box-shadow 120ms ease;
}
.project-card:hover { border-color: var(--line-strong); box-shadow: var(--card-shadow); }
.project-card-head { padding: 14px 16px 12px; border-bottom: 1.5px solid var(--card-margin); }
.project-card-body { padding: 10px 16px 12px; }
.project-card-body > * + * { margin-top: 10px; }
/* The two counts in ink, the rest of the line faint: they are what tells two
   courses apart at a glance. */
.project-card-n { font-weight: 550; color: var(--ink-muted); font-variant-numeric: tabular-nums; }

/* A destructive control that waits its turn: muted until it is reached for,
   red once it is. */
.btn-ghost-danger { color: var(--ink-faint); }
.btn-ghost-danger:hover { background: var(--danger-soft); color: var(--danger); }

/* ── Consent ───────────────────────────────────────────────────────────── */
/* The analytics question (templates/_consent.html): a small sheet at the
   bottom left, out of the way of the content, above everything including the
   toasts. Its own type sizes, because it also sits on the landing page, whose
   16px body would otherwise dwarf the app's 13px. */
.consent {
    position: fixed; left: 16px; bottom: 16px; z-index: 70;
    width: min(380px, calc(100vw - 32px));
    padding: 16px 18px;
    background: var(--surface); color: var(--ink);
    border: 1px solid var(--line); border-radius: 14px;
    box-shadow: 0 1px 2px rgba(22, 26, 44, 0.06), 0 24px 48px -20px rgba(22, 26, 44, 0.4);
    font-family: var(--font-sans); font-size: 13px; line-height: 1.5;
}
.consent[hidden] { display: none; }
.consent-title { font: 650 15px/1.3 var(--font-display); font-stretch: 90%; }
.consent-text { margin-top: 4px; color: var(--ink-muted); }
.consent-actions { display: flex; gap: 8px; margin-top: 12px; }
.consent-actions button { flex: 1 1 0; justify-content: center; height: 34px; }

/* ── Study ─────────────────────────────────────────────────────────────── */
/* Anki's three numbers in Anki's three colours: new in blue, learning in red,
   due in green. Tokens, like every other colour here. */
:root {
    --study-new: #2563c9;
    --study-learn: #c2410c;
    --study-due: #15803d;
    --study-again-soft: #fdecea; --study-again-ink: #b42318; --study-again-line: #f4b4ad;
    --study-hard-soft: #fdf3e0;  --study-hard-ink: #92650d;  --study-hard-line: #efd29b;
    --study-good-soft: #e7f5ec;  --study-good-ink: #1c6b45;  --study-good-line: #a5d9b9;
    --study-easy-soft: #e8effc;  --study-easy-ink: #2347a8;  --study-easy-line: #b4c6f1;
    /* The edges' light after an answer: the buttons' hues, brighter, because
       a glow is seen out of the corner of the eye rather than read. */
    --study-again-glow: #e5484d;
    --study-hard-glow: #f0a020;
    --study-good-glow: #2fb267;
    --study-easy-glow: #3b82f6;
}
.study-new { color: var(--study-new); }
.study-learn { color: var(--study-learn); }
.study-due { color: var(--study-due); }

/* The deck list: a name column and three narrow number columns, one grid
   for the head and every row so the numbers line up down the page. */
.study-list-head, .study-row {
    display: grid; grid-template-columns: minmax(0, 1fr) 3.5rem 3.5rem 3.5rem;
    align-items: center; gap: 4px; padding: 8px 16px;
}
.study-list-head { border-bottom: 1px solid var(--line); padding-top: 10px; padding-bottom: 8px; }
.study-num-head { text-align: right; font-size: 11px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; }
.study-num { text-align: right; font-weight: 650; font-variant-numeric: tabular-nums; }
.study-zero { opacity: .35; font-weight: 500; }
.study-none { color: var(--ink-faint); font-weight: 400; }
.study-project + .study-project { border-top: 1px solid var(--line); }
.study-project > .study-row { padding-top: 11px; padding-bottom: 11px; }
.study-name { color: var(--ink); text-decoration: none; }
.study-name:hover { color: var(--accent); }
.study-deck-row { padding-left: 62px; padding-top: 6px; padding-bottom: 6px; }
.study-deck-row .study-name { color: var(--ink-muted); }
.study-deck-row .study-name:hover { color: var(--accent); }
.study-decks { padding-bottom: 6px; }
.study-caret { width: 24px; height: 24px; padding: 0; justify-content: center; }
.study-caret .ui-icon { width: 15px; height: 15px; transition: transform 120ms ease; }
.study-caret[aria-expanded="false"] .ui-icon { transform: rotate(-90deg); }
.study-caret-spacer { display: inline-block; width: 24px; flex-shrink: 0; }
@media (max-width: 479px) {
    .study-list-head, .study-row { grid-template-columns: minmax(0, 1fr) 2.4rem 2.4rem 2.4rem; padding-left: 10px; padding-right: 10px; }
    .study-deck-row { padding-left: 40px; }
}

/* The overview's three counts, large. */
.study-big-counts { display: flex; justify-content: center; gap: clamp(20px, 8vw, 48px); }
.study-big-counts dt { font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-faint); }
.study-big-counts dd { margin: 2px 0 0; font: 700 40px/1 var(--font-display); font-variant-numeric: tabular-nums; }
.study-now { height: 40px; padding: 0 22px; font-size: 15px; }
.study-now kbd, .study-show kbd, .study-done-actions kbd { font-size: 11px; opacity: .75; }

/* ── The session ── */
.study-remaining { display: inline-flex; align-items: center; gap: 5px; font-weight: 650; }
.study-remaining .study-current { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 4px; }
/* The bar wears the project's tone: the class sets the colour, this paints
   with it, and outweighs the class's own soft background. */
.study-progress > span { background-color: currentColor; }

.study-stage { display: flex; height: 100%; flex-direction: column; }
.study-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 28px 16px 24px; }

/* The card: an index card at review size — 8px corners, the blue header line
   under its head, the same object as the sweep card and the workspace row. */
.study-card {
    max-width: 44rem; margin: 0 auto;
    background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
    box-shadow: var(--card-shadow);
}
.study-card-head {
    display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
    padding: 10px 20px; border-bottom: 1.5px solid var(--card-margin);
}
.study-crumb { min-width: 0; flex: 1; font-size: 12px; color: var(--ink-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.study-leech-chip { background: var(--study-hard-soft); color: var(--study-hard-ink); }
.study-flag-chip { background: var(--flag-soft); color: var(--flag-ink); }
.study-body { padding: 30px clamp(18px, 5vw, 44px) 34px; text-align: center; }
.study-card .study-q { font-size: 22px; line-height: 1.5; }
.study-card .study-q-small { font-size: 17px; margin-bottom: 16px; }
.study-card .study-a, .study-card .study-extra { font-size: 18px; line-height: 1.55; color: var(--ink); }
.study-card .study-extra { font-size: 16px; color: var(--ink-muted); }
.study-card .card-front-preview img, .study-card .card-back-preview img { max-height: 22rem; max-width: 100%; }
/* Centred like Anki's reviewer, except a list: its markers belong at the
   left of its own block, which sits centred. */
.study-card ul, .study-card ol { display: inline-block; text-align: left; }
.study-rule { margin: 22px auto; border: 0; border-top: 1.5px solid var(--line-strong); }
.study-rule-light { border-top: 1px dashed var(--line); margin: 18px auto; }
.study-card .cloze { color: var(--accent-strong); font-weight: 650; }
.study-slide-toggle { margin-top: 22px; display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 12px; }
.study-slide { margin: 12px 0 0; }
.study-slide img { display: block; margin: 0 auto; max-width: 100%; max-height: 60vh; border-radius: 4px; border: 1px solid var(--line); background: #fff; }
.study-slide figcaption { margin-top: 6px; font-size: 12px; color: var(--ink-faint); }

/* Occlusion: the masks are positioned divs over an image that sizes itself —
   the editor's lesson: the layer covers the image, not a box like it. */
.study-occlusion { position: relative; display: inline-block; max-width: 100%; }
.study-occlusion img { display: block; max-width: 100%; max-height: 62vh; width: auto; height: auto; border-radius: 4px; background: #fff; }
.study-masks { position: absolute; inset: 0; pointer-events: none; }
.study-mask { position: absolute; background: #ffeba2; border: 1px solid #212121; border-radius: 2px; }
.study-mask-active { background: #ff8e8e; }

.study-bar {
    flex-shrink: 0; display: flex; justify-content: center; padding: 12px 16px 16px;
    border-top: 1px solid var(--line); background: var(--surface);
}
.study-show { height: 46px; min-width: min(20rem, 100%); justify-content: center; font-size: 15px; }
.study-ratings { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; width: min(36rem, 100%); }
.study-rate {
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
    min-height: 54px; padding: 4px 6px; border-radius: 16px; border: 1px solid;
    font: inherit; cursor: pointer; transition: transform 80ms ease, box-shadow 120ms ease;
}
.study-rate:active { transform: translateY(1px); }
.study-rate-name { font-size: 14px; font-weight: 650; }
.study-ivl { font-size: 11px; font-variant-numeric: tabular-nums; opacity: .8; }
.study-rate kbd { font-size: 10px; opacity: .55; }
.study-again { background: var(--study-again-soft); color: var(--study-again-ink); border-color: var(--study-again-line); }
.study-hard { background: var(--study-hard-soft); color: var(--study-hard-ink); border-color: var(--study-hard-line); }
.study-good { background: var(--study-good-soft); color: var(--study-good-ink); border-color: var(--study-good-line); }
.study-easy { background: var(--study-easy-soft); color: var(--study-easy-ink); border-color: var(--study-easy-line); }
.study-rate:hover { box-shadow: 0 0 0 3px var(--accent-ring); }
.study-card .editor-host { padding: 0 20px 20px; text-align: left; }

/* ── The success screen ── */
.study-done { max-width: 34rem; margin: 0 auto; padding: 8px 0 24px; text-align: center; }
.study-done-title {
    margin-top: 22px; font: 700 clamp(22px, 5vw, 30px)/1.2 var(--font-display);
    font-stretch: 88%; letter-spacing: -0.02em; color: var(--ink); text-wrap: balance;
}
.study-done-stats { margin-top: 12px; display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 8px; color: var(--ink-muted); font-size: 14px; }
.study-done-stats strong { color: var(--ink); font-weight: 650; font-variant-numeric: tabular-nums; }
.study-streak {
    display: inline-flex; align-items: center; gap: 6px; margin-top: 14px; padding: 4px 12px 4px 9px;
    border-radius: 999px; background: var(--study-hard-soft); color: var(--study-learn); font-weight: 600;
}
.study-streak .ui-icon { width: 16px; height: 16px; }
.study-next { margin-top: 16px; color: var(--ink-muted); }
.study-next strong { color: var(--ink); }
.study-done-actions { margin-top: 24px; display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
.study-other { max-width: 100%; }
.study-other-count { color: var(--study-due); font-weight: 650; }

/* Three index cards, dealt into a fan. The front one carries a tick in the
   app's pen; the blue header line is on all three, because they are cards. */
.study-fan { position: relative; height: 128px; display: flex; justify-content: center; align-items: flex-end; }
.study-fan-card {
    --turn: 0deg; --x: 0px;
    position: absolute; bottom: 0; left: 50%; width: 82px; height: 108px; margin-left: -41px;
    border-radius: 6px; background-color: var(--surface);
    background-image:
        linear-gradient(to bottom, transparent 24%, var(--card-margin) 24%, var(--card-margin) calc(24% + 2px), transparent calc(24% + 2px)),
        repeating-linear-gradient(to bottom, transparent 0 11px, var(--card-rule) 11px 12px);
    background-size: 100% 100%, 100% 68%; background-position: 0 0, 0 100%; background-repeat: no-repeat;
    box-shadow: var(--card-shadow);
    transform: translateX(var(--x)) rotate(var(--turn));
    transform-origin: 50% 100%;
}
.study-fan-1 { --turn: -14deg; --x: -26px; }
.study-fan-2 { --turn: 1deg; --x: 0px; z-index: 1; }
.study-fan-3 { --turn: 15deg; --x: 26px; z-index: 2; display: flex; align-items: center; justify-content: center; }
.study-fan-check { display: flex; margin-top: 18px; color: var(--accent); }
.study-fan-check .ui-icon { width: 34px; height: 34px; stroke-width: 2.6; }
.study-done-celebrate .study-fan-card { animation: study-deal 640ms cubic-bezier(.2, .9, .3, 1.25) both; }
.study-done-celebrate .study-fan-2 { animation-delay: 70ms; }
.study-done-celebrate .study-fan-3 { animation-delay: 140ms; }
.study-done-celebrate .study-fan-check { animation: study-pop 360ms 520ms cubic-bezier(.3, 1.6, .5, 1) both; }
@keyframes study-deal {
    from { transform: translateX(0) translateY(26px) rotate(0deg); opacity: 0; }
    to { transform: translateX(var(--x)) rotate(var(--turn)); opacity: 1; }
}
@keyframes study-pop { from { transform: scale(0); } to { transform: scale(1); } }

/* Confetti, once: tiny index cards in the project's tone and the app's pen.
   The tone class sets `color`, and every piece paints with it or the accent. */
.study-confetti { position: absolute; inset: -12px 0 auto; height: 0; background: none; pointer-events: none; }
.study-confetti i {
    --i: 1;
    position: absolute; top: 40px; left: 50%; width: 7px; height: 10px; border-radius: 1.5px;
    background: currentColor; opacity: 0;
    animation: study-confetti 1100ms calc(380ms + var(--i) * 25ms) cubic-bezier(.15, .7, .4, 1) both;
}
.study-confetti i:nth-child(3n) { background: var(--accent); }
.study-confetti i:nth-child(4n) { background: var(--card-margin); }
.study-confetti i:nth-child(1)  { --dx: -150px; --dy: -40px;  --r: 220deg; }
.study-confetti i:nth-child(2)  { --dx: -110px; --dy: -78px;  --r: -160deg; }
.study-confetti i:nth-child(3)  { --dx: -70px;  --dy: -96px;  --r: 300deg; }
.study-confetti i:nth-child(4)  { --dx: -34px;  --dy: -110px; --r: -240deg; }
.study-confetti i:nth-child(5)  { --dx: 8px;    --dy: -118px; --r: 180deg; }
.study-confetti i:nth-child(6)  { --dx: 46px;   --dy: -104px; --r: -300deg; }
.study-confetti i:nth-child(7)  { --dx: 84px;   --dy: -88px;  --r: 260deg; }
.study-confetti i:nth-child(8)  { --dx: 122px;  --dy: -62px;  --r: -200deg; }
.study-confetti i:nth-child(9)  { --dx: 156px;  --dy: -30px;  --r: 160deg; }
.study-confetti i:nth-child(10) { --dx: -128px; --dy: -14px;  --r: -120deg; }
.study-confetti i:nth-child(11) { --dx: 134px;  --dy: -8px;   --r: 140deg; }
.study-confetti i:nth-child(12) { --dx: -6px;   --dy: -64px;  --r: -90deg; }
@keyframes study-confetti {
    0% { opacity: 0; transform: translate(0, 0) rotate(0); }
    12% { opacity: 1; }
    60% { opacity: 1; transform: translate(var(--dx), var(--dy)) rotate(var(--r)); }
    100% { opacity: 0; transform: translate(calc(var(--dx) * 1.15), calc(var(--dy) + 90px)) rotate(calc(var(--r) * 1.6)); }
}
@media (prefers-reduced-motion: reduce) {
    .study-done-celebrate .study-fan-card, .study-done-celebrate .study-fan-check { animation: none; }
    .study-confetti { display: none; }
    .study-rate { transition: none; }
}
/* The screen's edges after an answer (study.js, flashEdges). A fixed layer
   that never takes a pointer, an inset shadow so nothing is covered but the
   rim, and opacity alone moves: a flash, not an animation to watch. */
.study-glow {
    --glow: var(--study-good-glow);
    position: fixed; inset: 0; z-index: 45; pointer-events: none; opacity: 0;
    box-shadow:
        inset 0 0 0 3px var(--glow),
        inset 0 0 56px 6px color-mix(in srgb, var(--glow) 45%, transparent);
}
.study-glow[data-rating="1"] { --glow: var(--study-again-glow); }
.study-glow[data-rating="2"] { --glow: var(--study-hard-glow); }
.study-glow[data-rating="3"] { --glow: var(--study-good-glow); }
.study-glow[data-rating="4"] { --glow: var(--study-easy-glow); }
.study-glow.is-on { animation: study-glow 620ms ease-out; }
@keyframes study-glow {
    0% { opacity: 0; }
    14% { opacity: 1; }
    100% { opacity: 0; }
}
@media (max-width: 479px) {
    .study-glow { box-shadow: inset 0 0 0 3px var(--glow), inset 0 0 32px 4px color-mix(in srgb, var(--glow) 45%, transparent); }
}

@media (max-width: 479px) {
    .study-body { padding-top: 22px; padding-bottom: 24px; }
    .study-card .study-q { font-size: 19px; }
    .study-card .study-a { font-size: 16px; }
    .study-ratings { gap: 6px; }
    .study-rate { min-height: 50px; border-radius: 14px; }
    .study-scroll { padding: 16px 12px; }
}
.study-custom summary { list-style: none; }
.study-custom summary::-webkit-details-marker { display: none; }
.study-custom summary::after { content: "+"; float: right; color: var(--ink-faint); font-weight: 400; }
.study-custom[open] summary::after { content: "–"; }
.study-custom-option { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.study-ahead-chip { background: var(--study-easy-soft); color: var(--study-easy-ink); }
/* Study all: the header's stand-in for a project icon, and the count on the
   button that starts it. */
.study-all-mark {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    width: 24px; height: 24px; border-radius: 7px; background: var(--accent-soft); color: var(--accent-strong);
}
.study-all-count {
    margin-left: 2px; padding: 0 7px; border-radius: 999px; font-size: 12px; font-weight: 650;
    background: rgba(255, 255, 255, .22); font-variant-numeric: tabular-nums;
}

/* ── Study help (docs/study-help.md) ── */
/* The stage and the help panel share the main area. On a wide screen the
   panel is a column and the card moves over for it; below 768px it is a sheet
   from the bottom over the card, with the card still peeking out above. */
.study-main { display: flex; height: 100%; min-height: 0; }
.study-stage-host { flex: 1; min-width: 0; height: 100%; }
.study-help-toggle { gap: 5px; }
.study-help {
    width: min(420px, 42vw); flex-shrink: 0; min-height: 0;
    display: flex; flex-direction: column;
    background: var(--surface); border-left: 1px solid var(--line);
}
.study-help.is-open { animation: study-help-in 180ms ease-out; }
@keyframes study-help-in { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } }
.study-help-backdrop { display: none; }
.study-help-head {
    display: flex; align-items: center; gap: 10px; flex-shrink: 0;
    padding: 10px 10px 10px 14px; border-bottom: 1px solid var(--line);
}
.study-help-mark {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    width: 30px; height: 30px; border-radius: 999px;
    background: var(--accent-soft); color: var(--accent-strong);
}
.study-help-mark .ui-icon { width: 16px; height: 16px; }
.study-help-title { font-size: 14px; font-weight: 650; line-height: 1.2; }
.study-help-sub { font-size: 12px; color: var(--ink-faint); line-height: 1.3; }
.study-help-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.study-help-thread {
    flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
    display: flex; flex-direction: column; gap: 10px; padding: 16px 14px;
}
.study-help-intro { margin: auto 0; padding: 8px 6px; text-align: center; color: var(--ink-muted); font-size: 13px; line-height: 1.5; }
.study-help-intro[hidden] { display: none; }
.study-help-hello { font-family: "Bricolage Grotesque", var(--font-sans, inherit); font-size: 17px; font-weight: 650; color: var(--ink); margin-bottom: 4px; }

.help-msg { display: flex; flex-direction: column; max-width: 100%; }
.help-msg-student { align-items: flex-end; }
.help-msg-tutor, .help-msg-error { align-items: flex-start; }
.help-pending { display: flex; flex-direction: column; gap: 10px; }
.help-bubble { max-width: 92%; padding: 9px 13px; font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }
.help-bubble-student {
    max-width: 85%; white-space: pre-wrap;
    background: var(--accent); color: var(--accent-ink); border-radius: 16px 16px 4px 16px;
}
.help-bubble-tutor { background: var(--surface-sunken); color: var(--ink); border-radius: 16px 16px 16px 4px; }
.help-bubble-tutor ul, .help-bubble-tutor ol { margin: 4px 0 4px 18px; }
.help-bubble-error { background: var(--danger-soft); color: var(--danger); border-radius: 12px; font-size: 13px; }
.help-typing { display: inline-flex; gap: 4px; padding: 13px 14px; }
.help-typing span, .study-help-loading span {
    width: 6px; height: 6px; border-radius: 999px; background: var(--ink-faint);
    animation: help-dot 1.1s ease-in-out infinite;
}
.help-typing span:nth-child(2), .study-help-loading span:nth-child(2) { animation-delay: .15s; }
.help-typing span:nth-child(3), .study-help-loading span:nth-child(3) { animation-delay: .3s; }
@keyframes help-dot { 0%, 60%, 100% { opacity: .3; transform: none; } 30% { opacity: 1; transform: translateY(-3px); } }
.study-help-loading { display: flex; gap: 5px; justify-content: center; margin: auto; padding: 24px; }

/* A suggested card: the card as it would read, and the one button that
   writes. Dashed while it is only a suggestion, solid once it is the card. */
.help-proposal {
    width: 92%; margin-top: 6px; padding: 10px 12px 12px;
    border: 1.5px dashed var(--line-strong); border-radius: 10px; background: var(--surface);
}
.help-proposal.is-applied { border-style: solid; border-color: var(--study-good-line); }
.help-proposal-label {
    display: flex; align-items: center; gap: 6px; margin-bottom: 8px;
    font-size: 11px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; color: var(--accent-strong);
}
.help-proposal-card {
    padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface);
    font-size: 14px; line-height: 1.5; box-shadow: 0 1px 0 var(--card-margin) inset;
}
.help-proposal-card .cloze { color: var(--accent-strong); font-weight: 650; }
.help-proposal-rule { margin: 8px 0; border: 0; border-top: 1px solid var(--line); }
.help-proposal-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 10px; }
.help-proposal-note { font-size: 12px; color: var(--ink-faint); }
.help-applied { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; color: var(--study-good-ink); }

.study-help-starters { flex-shrink: 0; padding: 4px 14px 6px; }
.study-help-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.study-help-chip {
    padding: 5px 11px; border: 1px solid var(--line-strong); border-radius: 999px;
    background: var(--surface); color: var(--ink); font-size: 12.5px; line-height: 1.3;
    transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.study-help-chip:hover:not([disabled]) { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-strong); }
.study-help-chip[disabled] { opacity: .5; cursor: default; }
.study-help-chip-write { border-style: dashed; }
.study-help-notice {
    margin: 0 14px 14px; padding: 10px 12px; border-radius: 10px;
    background: var(--study-hard-soft); color: var(--study-hard-ink); font-size: 13px; line-height: 1.45;
}

.study-help-form { display: flex; align-items: flex-end; gap: 8px; flex-shrink: 0; padding: 8px 12px 4px; }
body textarea#study-help-input {
    flex: 1; min-width: 0; min-height: 40px; max-height: 140px; resize: none;
    padding: 9px 14px; border-radius: 20px; font-size: 14px; line-height: 1.45;
}
.study-help-send {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    width: 40px; height: 40px; border-radius: 999px;
    background: var(--accent); color: var(--accent-ink); transition: background 120ms ease, opacity 120ms ease;
}
.study-help-send:hover:not([disabled]) { background: var(--accent-strong); }
.study-help-send[disabled] { opacity: .35; cursor: default; }
.study-help-send .ui-icon { width: 17px; height: 17px; }
.study-help-foot { flex-shrink: 0; padding: 2px 14px 10px; font-size: 11px; color: var(--ink-faint); text-align: center; }

@media (max-width: 767px) {
    .study-help {
        position: fixed; left: 0; right: 0; bottom: 0; z-index: 48;
        width: auto; height: min(85dvh, 640px);
        border-left: 0; border-radius: 18px 18px 0 0;
        box-shadow: 0 -12px 40px -12px rgba(22, 26, 44, .35);
        padding-bottom: env(safe-area-inset-bottom);
    }
    .study-help.is-open { animation-name: study-help-up; }
    .study-help-head { position: relative; padding-top: 16px; }
    /* The grabber, which says "sheet" without a word. */
    .study-help-head::before {
        content: ""; position: absolute; top: 6px; left: 50%; transform: translateX(-50%);
        width: 36px; height: 4px; border-radius: 999px; background: var(--line-strong);
    }
    .study-help-backdrop { display: block; position: fixed; inset: 0; z-index: 47; background: rgba(22, 26, 44, .32); }
}
@keyframes study-help-up { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
@media (max-width: 639px) {
    .study-help-word { display: none; }
}
@media (prefers-reduced-motion: reduce) {
    .study-help.is-open { animation: none; }
    .help-typing span, .study-help-loading span { animation: none; opacity: .6; }
}

/* ── The daily brief ── */
.brief-banner {
    display: flex; flex-wrap: wrap; align-items: center; gap: 12px 14px;
    padding: 14px 16px; border: 1px solid var(--line); border-radius: 14px;
    background: linear-gradient(100deg, var(--accent-soft), var(--surface) 70%);
}
.brief-banner-mark, .brief-num {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    width: 36px; height: 36px; border-radius: 999px; background: var(--surface); color: var(--accent-strong);
    box-shadow: 0 0 0 1px var(--accent-ring);
}
.brief-banner-mark .ui-icon { width: 18px; height: 18px; }
/* Wide enough to take a phone's whole first row, so the buttons wrap under
   it instead of squeezing it into a column of single words. */
.brief-banner-copy { flex: 1 1 14rem; min-width: 0; }
.brief-banner-title { font-size: 15px; font-weight: 650; }
.brief-banner-text { font-size: 13px; color: var(--ink-muted); }

.brief-page { max-width: 46rem; margin: 0 auto; }
.brief-notice {
    margin-top: 12px; padding: 9px 12px; border-radius: 10px; font-size: 13px;
    background: var(--study-hard-soft); color: var(--study-hard-ink);
}
.brief-list { display: flex; flex-direction: column; gap: 18px; margin-top: 22px; }
.brief-item {
    background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
    box-shadow: var(--card-shadow); overflow: hidden;
}
.brief-item-head {
    display: flex; align-items: center; gap: 10px; min-width: 0;
    padding: 10px 18px; border-bottom: 1.5px solid var(--card-margin);
}
.brief-num { width: 24px; height: 24px; font-size: 12px; font-weight: 650; background: var(--accent-soft); box-shadow: none; }
.brief-card { padding: 20px 22px 6px; text-align: center; }
.brief-card .study-rule { margin: 14px auto; }
.brief-card-q { font-size: 18px; line-height: 1.5; }
.brief-card-a { font-size: 16px; line-height: 1.55; }
.brief-card .cloze { color: var(--accent-strong); font-weight: 650; }
.brief-card ul, .brief-card ol { display: inline-block; text-align: left; }
.brief-card-img { display: block; max-width: 100%; max-height: 18rem; margin: 10px auto 0; border-radius: 4px; border: 1px solid var(--line); }
.brief-why { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; padding: 8px 18px 4px; }
.brief-why li {
    padding: 2px 9px; border-radius: 999px; font-size: 11.5px; font-weight: 600;
    background: var(--study-again-soft); color: var(--study-again-ink);
}
.brief-note { display: flex; flex-direction: column; gap: 14px; padding: 14px 22px 18px; font-size: 14.5px; line-height: 1.6; }
.brief-label {
    display: flex; align-items: center; gap: 6px; margin-bottom: 3px;
    font-size: 11px; font-weight: 650; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-faint);
}
.brief-hook {
    padding: 10px 14px; border-left: 3px solid var(--accent); border-radius: 0 8px 8px 0;
    background: var(--accent-soft); color: var(--accent-strong); font-weight: 550;
}
.brief-item > .help-proposal { width: auto; margin: 0 22px 16px; }
.brief-actions { display: flex; justify-content: flex-end; padding: 0 18px 16px; }
.brief-foot { margin: 26px 0 8px; font-size: 13px; color: var(--ink-muted); }
.brief-earlier { margin-top: 12px; }
.brief-earlier summary { cursor: pointer; font-weight: 600; color: var(--ink); }
.brief-earlier ul { margin-top: 6px; display: flex; flex-direction: column; gap: 4px; }
@media (max-width: 479px) {
    .brief-card { padding: 16px 14px 4px; }
    .brief-note { padding: 12px 14px 16px; }
    .brief-item > .help-proposal { margin: 0 14px 14px; }
}

/* ── Study statistics ── */
/* Card stages keep Anki's meaning of the colours: new blue, learning red,
   young the light green, mature the dark one. Validated as a categorical
   set (adjacent CVD ΔE ≥ 19); "young" sits under 3:1 on white, which is why
   the legend beside it always carries the number in ink. */
:root {
    --state-new: #2563c9; --state-learning: #c2410c; --state-young: #86c36a; --state-mature: #15803d;
    /* One hue, light to dark: the app's pen. */
    --heat-0: var(--surface-sunken); --heat-1: #d6dcf8; --heat-2: #9fadef; --heat-3: #5f74de; --heat-4: #2737ae;
}
.study-tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
@media (min-width: 768px) { .study-tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.study-tile {
    display: flex; flex-direction: column; gap: 2px; padding: 14px 16px;
    background: var(--surface); border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow-card);
}
.study-tile-label { font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-faint); }
.study-tile-value { font: 700 28px/1.1 var(--font-display); font-variant-numeric: tabular-nums; color: var(--ink); }
.study-tile-note { color: var(--ink-muted); font-size: 12px; }

.study-heat-scroll { overflow-x: auto; padding-bottom: 4px; }
.study-heat { --cell: 13px; --gap: 3px; width: max-content; }
.study-heat-months, .study-heat-grid {
    display: grid; grid-template-columns: repeat(var(--weeks), var(--cell)); column-gap: var(--gap);
}
.study-heat-months { height: 16px; font-size: 11px; color: var(--ink-faint); }
.study-heat-months span { white-space: nowrap; }
.study-heat-grid { grid-template-rows: repeat(7, var(--cell)); grid-auto-flow: column; row-gap: var(--gap); }
.study-heat-cell { display: inline-block; width: var(--cell, 11px); height: var(--cell, 11px); border-radius: 3px; background: var(--heat-0); }
.study-heat-cell.heat-0 { background: var(--heat-0); box-shadow: inset 0 0 0 1px var(--line); }
.study-heat-cell.heat-1 { background: var(--heat-1); }
.study-heat-cell.heat-2 { background: var(--heat-2); }
.study-heat-cell.heat-3 { background: var(--heat-3); }
.study-heat-cell.heat-4 { background: var(--heat-4); }
.study-heat-future { visibility: hidden; }
.study-heat-cell.tip:hover { outline: 2px solid var(--ink); outline-offset: 1px; }

.study-bars { display: flex; align-items: flex-end; gap: 2px; height: 140px; border-bottom: 1px solid var(--line-strong); }
.study-bar-slot { flex: 1; display: flex; align-items: flex-end; height: 100%; min-width: 0; }
.study-bar-slot:hover { background: var(--surface-sunken); }
.study-bar-fill { display: block; width: 100%; min-height: 2px; background: var(--accent); border-radius: 4px 4px 0 0; }
.study-bars-axis { display: flex; justify-content: space-between; margin-top: 4px; font-size: 11px; color: var(--ink-faint); }

.study-stack { display: flex; gap: 2px; height: 22px; }
.study-stack-part { min-width: 3px; }
.study-stack-part:first-child { border-radius: 4px 0 0 4px; }
.study-stack-part:last-child { border-radius: 0 4px 4px 0; }
.study-stack-part:only-child { border-radius: 4px; }
.state-new { background: var(--state-new); }
.state-learning { background: var(--state-learning); }
.state-young { background: var(--state-young); }
.state-mature { background: var(--state-mature); }
.study-legend { display: grid; gap: 6px; }
.study-legend li { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
.study-swatch { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }
.study-table { width: 100%; border-collapse: collapse; }
.study-table th, .study-table td { padding: 3px 6px; text-align: left; border-bottom: 1px solid var(--line); }
.study-table td:last-child, .study-table th:last-child { text-align: right; font-variant-numeric: tabular-nums; }
@media (min-width: 1024px) { .study-heat { --cell: 17px; --gap: 4px; } }

/* ── Study on a phone ── */
/* The confetti flies wider than a narrow card column; it must not open a
   sideways scroll inside the stage while it does. */
.study-scroll { overflow-x: hidden; }
/* Clear of the home indicator on a phone with no home button. A no-op until
   the viewport asks for `viewport-fit=cover`, and harmless after. */
.study-bar { padding-bottom: max(16px, env(safe-area-inset-bottom)); }
@media (hover: none) and (pointer: coarse) {
    /* Fingers, not cursors: every study control gets at least a 40px target.
       The header icons were 26px, the menu rows 30px, a deck name 16px. */
    .study-head .btn-icon, .study-head .btn-sm { min-width: 40px; min-height: 40px; justify-content: center; }
    .study-head .menu-item { min-height: 44px; }
    .study-name { display: block; padding: 10px 0; }
    .study-caret { width: 36px; height: 36px; }
    .study-caret-spacer { width: 36px; }
    .study-done-actions a, .study-overview .btn-primary, .study-overview .btn-secondary,
    .study-custom .btn-primary, .study-settings .btn-primary, .study-settings .btn-secondary { min-height: 44px; }
    .study-overview .btn-link, .study-local-note .btn-link { display: inline-block; padding: 10px 4px; }
    .study-custom summary, .study-settings summary { padding-top: 14px; padding-bottom: 14px; }
    .study-custom-option { min-height: 44px; }
    .study-custom-option input[type="radio"], .study-settings input[type="checkbox"] { width: 20px; height: 20px; }
    /* A tooltip follows hover, and a tap leaves hover stuck on whatever was
       touched: the "More" label sat on top of the open menu. The study pages
       name their icon buttons in aria-label already; the charts read out what
       was tapped in a line of their own (`.study-readout`). */
    .study-head .tip::after, .study-heat-cell.tip::after, .study-bar-slot.tip::after,
    .study-stack-part.tip::after { display: none !important; }
}
.study-readout { min-height: 1.4em; margin-top: 8px; font-size: 12px; color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.study-bar-slot[aria-pressed="true"], .study-heat-cell[aria-pressed="true"] { outline: 2px solid var(--ink); outline-offset: 1px; }
@media (hover: none) and (pointer: coarse) {
    body .study-stats-filter select { min-height: 40px; }
    .study-readout:empty::before { content: "Tap a day to see its number."; color: var(--ink-faint); }
}
@media (hover: none) and (pointer: coarse) {
    .study-slide-toggle .btn-link { display: inline-block; padding: 10px 6px; }
    .study-bar .btn-primary { min-height: 44px; }
}
/* The card menu hangs from a button near the middle of a narrow header, and
   right-aligned to it, half of it went off the left edge at 320px. On a phone
   it spans the width under the header instead. */
@media (max-width: 479px) {
    .study-head .menu-panel { position: fixed; top: 56px; left: 12px; right: 12px; min-width: 0; }
}
@media (hover: none) and (pointer: coarse) {
    .study-index-actions a { min-height: 44px; }
    .study-list .study-caret.btn-icon { width: 36px; height: 36px; min-width: 36px; padding: 0; flex-shrink: 0; }
    .study-table-toggle summary { padding: 12px 0; }
}
/* At 320px a project name broke over five lines beside its icon and three
   number columns; the icon gives way first, and the columns narrow. */
@media (max-width: 399px) {
    .study-project .project-icon { display: none; }
    .study-list-head, .study-row { grid-template-columns: minmax(0, 1fr) 2.1rem 2.4rem 2.1rem; gap: 2px; }
    .study-deck-row { padding-left: 46px; }
}

/* ── Progress from Anki ── */
.study-import-facts { list-style: disc; padding-left: 1.2em; display: grid; gap: 4px; color: var(--ink-muted); }
.study-import-facts strong { color: var(--ink); font-variant-numeric: tabular-nums; }
.study-steps { list-style: decimal; padding-left: 1.3em; display: grid; gap: 4px; color: var(--ink-muted); }
.study-import-result { border-color: var(--study-good-line); }
/* A file input is as wide as the browser's own button and filename text, which
   at 320px is wider than the form it sits in. */
body input.study-file { width: 100%; max-width: 100%; min-width: 0; }
