/* ── Fonts ────────────────────────────────────────────────────────────────────
   Three VARIABLE faces, not a pile of static instances. Each carries a live
   `wght` axis over 400..700, so 400/600/700 — and anything between — come out of
   one file instead of needing one download per weight.

   NB the naive version of this is a REGRESSION, not a saving: a variable font
   ships interpolation deltas for its whole design space, so the stock Noto Sans
   VF is 513 KB against 181 KB for a single static instance. Two things pay for
   it, and both are load-bearing:
     · the axes are cut to what we use — `wdth` pinned out entirely, `wght`
       clamped to 400..700 instead of 100..900;
     · the astral plane is subset away (mathematical alphanumerics, phonetic
       extensions). Every BMP codepoint the old fonts had survives — all Cyrillic
       and Greek, punctuation, arrows, math, and U+0301 COMBINING ACUTE, which is
       what ЧГК stress accents are made of. Dropping that would corrupt questions.
   Net: 416 KB for three faces vs 362 KB for the old two, and we gain real
   italics (previously the browser SYNTHESISED them by shearing the roman) and a
   real mono. Regenerate with the recipe in DESIGN.md if a face is ever rebuilt. */
@font-face {
  font-family: "Noto Sans";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/static/fonts/noto-sans-var.woff2") format("woff2");
}

@font-face {
  font-family: "Noto Sans";
  font-style: italic;
  font-weight: 400 700;
  font-display: swap;
  src: url("/static/fonts/noto-sans-var-italic.woff2") format("woff2");
}

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

:root {
  color-scheme: light;
  --grid: #e3e3e4;
  --grid-dark: #cececf;
  --text: #202124;
  --text-strong: #3c4043;
  --muted: #5f6368;
  --muted-light: #9aa0a6;
  --blue: #1a73e8;
  --blue-tint: #e8f0fe;
  --green: #69a882;
  --green-dark: #206836;
  --red: #ecc5c9;
  --red-dark: #9b3f48;
  --red-soft: #fff7f7;
  --success: #16a34a;
  --success-alt: #34a853;
  --success-soft: #edf8f1;
  --danger: #dc2626;
  --danger-soft: #fdf0f2;
  --warning-soft: #dfe9f9;
  /* Amber/warning cluster: the unnumbered-teams banner and fuzzy-match badge. */
  --amber-bg: #fde68a;
  --amber-border: #f59e0b;
  --amber-text-strong: #7c2d12;
  --amber-text: #b06a00;
  /* Diagnostic chip (download-log button): stays dark in every theme. */
  --diag-bg: rgba(30, 30, 30, 0.82);
  --diag-fg: #ffffff;
  --paper: #ffffff;
  --paper-transparent: rgba(255, 255, 255, 0);
  --structure: #f1f1f2;
  /* Transparent twins for gradient endpoints: a fade must end on the alpha-0
     version of ITS OWN color — `transparent` (black) or a hardcoded light value
     tints the ramp on the other themes. */
  --structure-transparent: rgba(241, 241, 242, 0);
  --accent-soft: #eef3fa;
  /* Semantic aliases onto existing theme-aware tokens (theme-aware through
     their referents, so no dark-block override needed). */
  --accent: var(--success);
  --text-muted: var(--muted);
  --orange-dark: var(--amber-text);
  --blue-strong: #174ea6;
  --blue-bright: #1d4ed8;
  --blue-soft: #d3ebff;
  --danger-text: #9f1239;

  /* THE NEUTRAL LADDER. Light keeps exactly four fill grays — paper (#fff),
     structure, the control fill, the control hover — plus the two line grays
     (--grid/--grid-dark). Every rung is NEUTRAL — the ghost wash's base
     rgb(32,33,36) over white — not blue-gray; a cool-tinted rung next to the
     neutral ghost read as two unrelated grays. Everything else aliases onto a rung: page,
     surface-tint and hover-bg all resolve to --structure. The old palette had
     nine near-identical fills (#f8fafc/#f6f7f9/#eef1f5/#eef1f4/#eaeef3/
     #e9eaec/#e3e8ef/…) whose differences carried no meaning and read as dirt.
     Dark keeps its own ladder below: there the steps ARE the elevation. */
  --page: var(--structure);
  --surface: var(--paper);
  --surface-tint: var(--structure);
  --hover-bg: var(--structure);

  /* ── Element outlines are OFF ──────────────────────────────────────────
     Buttons, menus, popovers, cards, modals and inputs separate themselves by
     FILL (+ shadow for floating layers), never by a contrast outline. The four
     outline tokens below are therefore `transparent` in the regular themes, and
     only the [data-contrast=high] blocks give them real colors — that mode
     exists precisely to carry separation on displays that cannot resolve the
     fills, so it is the one place an outline earns its keep.

     They stay `transparent` rather than being deleted for three reasons: the 1px
     box is still reserved, so nothing reflows; state rules that only set
     `border-color` (`.input:focus` → --blue, `.btn-danger`) still paint; and
     high contrast re-enables everything by re-pointing four variables.

     STRUCTURAL rules are a different thing and are NOT tokens: table cell grids,
     the topbar divider, pane splitters, dashed "add"/page-break affordances and
     dope's keypad key-seams all reference --grid / --grid-dark directly, and are
     unaffected by anything here. Do not route them back through --border. */
  --border: transparent;
  --border-soft: transparent;
  --action-border: transparent;
  --action-disabled-border: transparent;

  /* THE control fill. It carries the whole affordance now that the outlines
     are gone, so it must read against BOTH --surface (#fff) and --structure —
     the two backgrounds controls actually sit on. Buttons and fields share it
     in light (shape and typography already tell them apart; a 2% gray
     difference told nobody anything); dark keeps them split below, where
     recessed-field vs lifted-button is real elevation.
     NEUTRAL, not blue-gray: it must sit in the same hue family as the ghost
     wash below — a bluish filled button next to a neutral ghost read as two
     unrelated grays. These are the ghost's base rgb(32,33,36) over white at
     ~10% / ~15%. */
  --action-bg: #e9e9ea;
  --action-hover-bg: #dedee0;
  --action-fg: var(--text);
  --field-bg: var(--action-bg);

  /* Ghost = the low-emphasis button. It is TRANSLUCENT on purpose: a ghost sits
     on --surface, --structure and --page alike, and any opaque fill picked to
     read on one of them disappears into another. A wash of the text color works
     against all three. It must still read as a button — with the outlines gone,
     a ghost with no fill at all is just a word (which is what it had become). */
  --action-ghost-bg: rgba(32, 33, 36, 0.06);
  --action-ghost-hover-bg: rgba(32, 33, 36, 0.12);

  --action-disabled-bg: var(--structure);
  --action-disabled-fg: var(--muted-light);
  --action-disabled-opacity: 0.72;
  --cursor-action: pointer;
  --cursor-disabled: not-allowed;

  --space-0: 0;
  --space-0-5: 2px;
  --space-1: 4px;
  --space-1-5: 6px;
  --space-2: 8px;
  --space-2-5: 10px;
  --space-3: 12px;
  --space-3-5: 14px;
  --space-4: 16px;
  --space-5: 24px;
  --space-5-5: 28px;
  --space-6: 32px;
  --space-8: 48px;

  --radius-xs: 3px;
  --radius-sm: 4px;
  --radius-sm-plus: 5px;
  --radius-md: 6px;
  --radius-lg: 8px;
  --radius-pill: 24px;
  --radius-full: 50%;

  --text-2xs: 10px;
  --text-xs-tight: 11px;
  --text-xs: 12px;
  --text-xs-plus: 13px;
  --text-sm: 14px;
  --text-base: 15px;
  --text-md: 16px;
  --text-lg: 18px;
  --text-lg-xl: 21px;
  --text-xl: 22px;
  --text-xl-plus: 24px;
  --text-2xl: 30px;

  /* THE CONTROL HEIGHT SCALE. Everything that can share a row with a field —
     buttons, icon buttons, selects, the file field — resolves to one of these, so
     a row of mixed controls lines up by construction instead of by luck. It used
     to be luck, and it lost: a card header held a 38px select, a 30px button and
     a 34px icon button, three heights in one row, and nothing agreed with
     anything. Replaced elements (input[type=color], input[type=file]) can't be
     stretched by flexbox at all, so they MUST be told a height — hence a token
     rather than a guess at each call site. */
  --control-h: 38px;
  --control-h-sm: 30px;
  /* the micro tier: buttons that live INSIDE a table cell or a text line */
  --control-h-xs: 20px;

  --leading-tight: 1;
  --leading-snug: 1.25;
  --leading-relaxed: 1.55;

  --fw-regular: 400;
  --fw-semibold: 600;
  --fw-bold: 700;

  --shadow-sm: 0 1px 2px rgba(32, 33, 36, 0.12);
  --shadow-popover: 0 8px 20px rgba(32, 33, 36, 0.18);
  --shadow-popover-up: 0 -8px 20px rgba(32, 33, 36, 0.18);
  --shadow-card: 0 8px 24px rgba(32, 33, 36, 0.18);
  --shadow-modal: 0 18px 48px rgba(32, 33, 36, 0.22);
  --shadow-scroll-inset: inset -24px 0 18px -22px rgba(32, 33, 36, 0.45);
  --focus-ring-none: 0;
  --focus-outline-offset-tight: calc(var(--space-0-5) * -1);

  --backdrop: rgba(32, 33, 36, 0.35);

  --z-dropdown: 30;
  --z-popover: 35;
  --z-modal: 80;
  --z-toast: 120;
  --z-tooltip: 999;

  /* page chrome padding tiers */
  --page-pad-x: 24px;
  --page-pad-x-tight: 16px;
  --page-pad-y-tight: 8px;

  /* the ONE header height — every top bar (host, compact game/board, public)
     is this tall, and the mains/overlays that lay out against the header
     (calc(100dvh - …), sticky offsets) read the same token. */
  --header-h: 44px;

  /* scroll-fade utility */
  --scroll-fade-size: 24px;
  --scroll-fade-color: rgba(32, 33, 36, 0.18);
  --inline-fade-width: 34px;
  --inline-fade-bg: linear-gradient(to right, var(--paper-transparent), var(--paper));

  --font-sans: "Noto Sans", Arial, sans-serif;
  /* JetBrains Mono is shipped now (it was system-mono roulette before), but the
     stack keeps its fallbacks: the subset drops astral codepoints, so anything
     exotic in the 4s editor still lands on a system mono rather than tofu. */
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* ── scroll-fade utilities ────────────────────────────────
   Pure-CSS scroll shadows (Lea Verou trick): a soft fade appears
   at the edge the user CAN scroll toward, and disappears when
   they've reached that end. Background covers move with content
   (background-attachment: local) so they reveal the static
   shadows underneath as scrolling progresses.

   Two utilities on the overflow:auto element itself: .scroll-fade-y
   (vertical) and .scroll-fade-xy (all four edges). The kit's own scroll
   surfaces (.table-scroll, .sheet-frame) are in the groups below; an app
   container composes the utility class and overrides --scroll-fade-bg
   where its background isn't --surface. */
.scroll-fade-y,
.scroll-fade-xy,
.table-scroll,
.sheet-frame {
  --scroll-fade-bg: var(--surface);
}

.scroll-fade-y {
  background-image:
    linear-gradient(var(--scroll-fade-bg) 30%, transparent),
    linear-gradient(transparent, var(--scroll-fade-bg) 70%),
    radial-gradient(farthest-side at 50% 0, var(--scroll-fade-color), transparent),
    radial-gradient(farthest-side at 50% 100%, var(--scroll-fade-color), transparent);
  background-position: center top, center bottom, center top, center bottom;
  background-repeat: no-repeat;
  background-size:
    100% var(--scroll-fade-size),
    100% var(--scroll-fade-size),
    100% calc(var(--scroll-fade-size) * 0.5),
    100% calc(var(--scroll-fade-size) * 0.5);
  background-attachment: local, local, scroll, scroll;
}

.scroll-fade-xy,
.table-scroll,
.sheet-frame {
  background-image:
    linear-gradient(var(--scroll-fade-bg) 30%, transparent),
    linear-gradient(transparent, var(--scroll-fade-bg) 70%),
    linear-gradient(to right, var(--scroll-fade-bg) 30%, transparent),
    linear-gradient(to left, var(--scroll-fade-bg) 30%, transparent),
    radial-gradient(farthest-side at 50% 0, var(--scroll-fade-color), transparent),
    radial-gradient(farthest-side at 50% 100%, var(--scroll-fade-color), transparent),
    radial-gradient(farthest-side at 0 50%, var(--scroll-fade-color), transparent),
    radial-gradient(farthest-side at 100% 50%, var(--scroll-fade-color), transparent);
  background-position:
    center top, center bottom,
    left center, right center,
    center top, center bottom,
    left center, right center;
  background-repeat: no-repeat;
  background-size:
    100% var(--scroll-fade-size),
    100% var(--scroll-fade-size),
    var(--scroll-fade-size) 100%,
    var(--scroll-fade-size) 100%,
    100% calc(var(--scroll-fade-size) * 0.5),
    100% calc(var(--scroll-fade-size) * 0.5),
    calc(var(--scroll-fade-size) * 0.5) 100%,
    calc(var(--scroll-fade-size) * 0.5) 100%;
  background-attachment: local, local, local, local, scroll, scroll, scroll, scroll;
}

/* per-container background tint to keep covers matching the bg */
.sheet-frame { --scroll-fade-bg: var(--structure); }

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  min-height: 100%;
  font-family: var(--font-sans);
  color: var(--text);
  /* WebKit inflates text inside wide blocks on real iOS devices unless told
     not to — and it inflates the TEXT while CSS-px column widths stay put, so
     fixed-width tables overflow on a phone in ways no desktop device emulation
     reproduces (neither Chrome's nor Firefox's applies the autosizer). Our type
     scale is already sized for small screens, so opt out and render what the
     stylesheet says. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

button,
input,
select,
textarea {
  font: inherit;
}

[hidden] {
  display: none !important;
}

/* ── design system: surfaces ─────────────────────────────── */

.list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.list-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--surface);
  text-decoration: none;
  color: inherit;
}

a.list-row:hover { background: var(--hover-bg); }

.list-row-title { font-weight: var(--fw-semibold); }

.muted {
  color: var(--muted);
  font-size: var(--text-sm);
}

.table-scroll {
  overflow: auto;
}

.data-table {
  width: 100%;
  min-width: 420px;
  border-collapse: collapse;
  background: var(--surface);
  font-size: var(--text-sm);
}

.data-table th,
.data-table td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--grid);
  text-align: left;
  vertical-align: top;
}

.data-table th {
  color: var(--muted);
  font-weight: var(--fw-semibold);
}

.data-table input:not([type="checkbox"]),
.data-table select {
  width: 100%;
  min-height: 34px;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--field-bg);
  color: var(--text);
}

/* ── design system: form controls ────────────────────────── */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--text-sm);
}

/* Filled fields: --field-bg is what makes an input an input now that the outline
   is transparent — on --surface it reads as recessed, and it stays distinct from
   --structure too (forms sit on both). The transparent 1px border is kept so the
   :focus rule below still has a box to paint blue. */
.input,
.field > input,
.field > textarea,
.field > select {
  font-size: var(--text-base);
  min-height: var(--control-h);
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--field-bg);
  color: var(--text);
  outline: 0;
}

.input:focus,
.field > input:focus,
.field > textarea:focus,
.field > select:focus {
  border-color: var(--blue);
}

.field > textarea {
  resize: vertical;
  min-height: 96px;
  font-family: inherit;
}

/* A <select> sizes itself from its own UA box and lands 2px over --control-h on
   a bare min-height, which is exactly the 1px-off look in a header where it sits
   beside buttons. Pin the height; the padding has to go to 0 or the text clips. */
select.input,
.field > select {
  height: var(--control-h);
  padding-top: 0;
  padding-bottom: 0;
}

.checkbox {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

/* Native checkboxes/radios draw their own 1px box, which no token of ours can
   reach — appearance:none is the only way to stop them, and once it's off we owe
   them the whole widget (fill, tick, focus ring). The ring is --grid-dark and
   NOT the (transparent) --action-border: for an empty checkbox the outline IS
   the widget — an 18px pale circle with no edge reads as nothing at all on the
   page gray. A graphic, like the sync spinner's ring. */
input[type="checkbox"],
input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  display: inline-grid;
  place-content: center;
  width: 18px;
  height: 18px;
  margin: 0;
  padding: 0;
  border: 1px solid var(--grid-dark);
  border-radius: var(--radius-sm);
  background: var(--field-bg);
  cursor: var(--cursor-action);
}

input[type="radio"] { border-radius: var(--radius-full); }

input[type="checkbox"]::before,
input[type="radio"]::before {
  content: "";
  width: 10px;
  height: 10px;
  transform: scale(0);
  background: #fff;
  /* a tick, drawn rather than typed: a glyph would depend on the font stack */
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}

input[type="radio"]::before {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  clip-path: none;
}

input[type="checkbox"]:checked,
input[type="radio"]:checked {
  background: var(--blue);
  border-color: var(--blue);
}

input[type="checkbox"]:checked::before,
input[type="radio"]:checked::before { transform: scale(1); }

input[type="checkbox"]:disabled,
input[type="radio"]:disabled {
  background: var(--action-disabled-bg);
  opacity: var(--action-disabled-opacity);
  cursor: var(--cursor-disabled);
}

/* appearance:none also removes the UA focus ring — put a real one back, or the
   box becomes unreachable-looking for keyboard users. */
input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* The file field's "Browse…" is a UA-drawn button with its own border. Style the
   ::file-selector-button into an ordinary small button so the control stops
   looking like a stray piece of 1998 and matches --control-h. */
.input[type="file"] {
  height: var(--control-h);
  padding: 0;
  padding-right: var(--space-3);
  overflow: hidden;
}

.input[type="file"]::file-selector-button {
  height: 100%;
  margin-right: var(--space-3);
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-md) 0 0 var(--radius-md);
  /* one step down from the field fill (they share --action-bg now), so the
     button keeps a visible seam inside the field */
  background: var(--action-hover-bg);
  color: var(--text);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  cursor: var(--cursor-action);
}

.input[type="file"]:hover::file-selector-button { filter: brightness(0.96); }

/* The shadow carries the modal's elevation; the border paints only in high
   contrast (--border is transparent elsewhere). */
.modal-dialog {
  width: min(560px, calc(100vw - var(--space-6)));
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background-color: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-modal);
}

.modal-dialog::backdrop {
  background: var(--backdrop);
}

.modal-dialog h2 {
  margin: 0;
  font-size: var(--text-lg);
}

/* the right-aligned button row a dialog ends with */
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
}

/* ── design system: buttons & links ───────────────────────────────────────────
   THE BUTTON KINDS. The vocabulary has always declared four (primary / ghost /
   danger / secondary, see kit/vocab.json) but the stylesheet only ever painted
   two of them, so `secondary` and `primary` silently rendered as a plain .btn.
   All four are defined here now, and they separate by FILL WEIGHT — there are no
   outlines to separate them with:

     primary    strongest — an accent fill. One per view, the thing to do next.
     .btn       default (= secondary): a tonal fill. The everyday button.
     ghost      quietest — a translucent wash. Still a button, still clickable.
     danger     a red fill. Destructive, and it should look it.

   Peer actions must share a kind: a filled button next to a ghost one reads as
   "this one is real and that one is a label", which is a lie when both do the
   same class of thing. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* --control-h, so a button sitting in a row with a select or a field shares
     its height instead of being 2px short of it */
  min-height: var(--control-h);
  padding: 0 var(--space-4);
  border: 1px solid var(--action-border);
  border-radius: var(--radius-md);
  background: var(--action-bg);
  color: var(--text);
  /* semibold, not bold: every button on every page shouting at 700 is what made
     the UI feel heavy. Weight is for emphasis, and if all of it is emphasised
     none of it is. */
  font-weight: var(--fw-semibold);
  cursor: var(--cursor-action);
  text-decoration: none;
}

/* Hover is a fill step, not an outline. (It used to promote border-color to
   --text, which is exactly the "contrast border" this pass removes.) */
.btn:hover { background: var(--action-hover-bg); }

.btn.btn-primary {
  background: var(--blue);
  color: #fff;
}

.btn.btn-primary:hover { background: var(--blue); filter: brightness(1.08); }

/* Ghost: the quiet kind. It carries a faint wash rather than nothing at all —
   with the outlines gone, a fill-less ghost is indistinguishable from body text,
   and «Удалить карточку» / «Переместить» stopped reading as buttons entirely.
   Lives in the kit (not an app layer): it is one of the core button kinds. */
.btn.btn-ghost {
  background: var(--action-ghost-bg);
  color: var(--muted);
  font-weight: var(--fw-regular);
}

.btn.btn-ghost:hover {
  background: var(--action-ghost-hover-bg);
  color: var(--text);
}

/* Danger reads through its fill too. --red-soft was near-white and relied on the
   red outline to be legible as a danger button; --red is the actual tinted fill,
   and --red-dark is its designated on-color in every theme. */
.btn.btn-danger {
  color: var(--red-dark);
  background: var(--red);
}

.btn.btn-danger:hover { background: var(--red); filter: brightness(0.94); }

/* The micro button: composes .btn (fill, fill-step hover, disabled states) at
   in-cell scale. Peers of table content, not of fields — hence its own tier. */
.btn.btn-xs {
  min-height: var(--control-h-xs);
  padding: 0 var(--space-1-5);
  border-radius: var(--radius-sm-plus);
  font-size: var(--text-xs);
  font-weight: var(--fw-regular);
  line-height: var(--leading-tight);
  white-space: nowrap;
}

.btn:disabled,
.btn[aria-disabled="true"] {
  border-color: var(--action-disabled-border);
  background: var(--action-disabled-bg);
  color: var(--action-disabled-fg);
  cursor: var(--cursor-disabled);
  opacity: var(--action-disabled-opacity);
}

/* :hover also matches disabled buttons — re-assert the disabled fill after it. */
.btn:disabled:hover,
.btn[aria-disabled="true"]:hover {
  background: var(--action-disabled-bg);
}

.action-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* square, on the same scale as every other control — it was 34px against a
     38px select and a 30px button in the very same card header */
  width: var(--control-h);
  height: var(--control-h);
  padding: 0;
  border: 1px solid var(--action-border);
  border-radius: var(--radius-md);
  background: var(--action-bg);
  color: var(--text);
  font-size: 17px;
  font-weight: var(--fw-bold);
  cursor: pointer;
  text-decoration: none;
}

.action-icon:hover { background: var(--action-hover-bg); }

.disclosure > summary {
  width: max-content;
  list-style: none;
}

.disclosure > summary::-webkit-details-marker {
  display: none;
}

.disclosure > form {
  margin-top: var(--space-3);
}

/* ── public + host common page chrome ────────────────────── */

.host {
  overflow: hidden;
  background: var(--structure);
}

.host-top {
  position: relative;
  z-index: var(--z-dropdown);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  height: var(--header-h);
  padding: var(--space-1-5) var(--page-pad-x);
  border-bottom: 0;
  background: var(--structure);
}

/* Compact chrome — game/board pages where the content below the header is the
   point: tighter gaps and side padding (the height is the shared token). */
.host-compact .host-top {
  gap: var(--space-2-5);
  padding: var(--space-1-5) var(--page-pad-x-tight);
}

.host h1 {
  margin: 0;
  /* one title size on every top bar — sheet pages don't shout louder than
     board/game pages */
  font-size: var(--text-xl);
  line-height: var(--leading-tight);
  letter-spacing: 0;
}

/* Breadcrumbs — the shared header path (🏠 / фест / игра). Every crumb is a
   navigable prefix of the URL; the last is the page you are on, so it is plain
   text. The trail is one line that ellipsises rather than wrapping: the header
   is a fixed height, and a second row would push the content down. */
.host-brand {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  min-width: 0;
}

.crumbs {
  display: flex;
  flex: 1 1 auto;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  font-size: var(--text-xl);
  line-height: var(--leading-snug);
}

.crumb {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  color: var(--muted);
  text-decoration: none;
  text-overflow: ellipsis;
  /* The overflow:hidden that yields the ellipsis clips at the padding edge, so
     a line-height bump alone still shaved bold Cyrillic descenders (у, щ, р).
     Give the clip box explicit bottom room so descenders survive. */
  padding-bottom: 0.2em;
}

.crumb:hover {
  text-decoration: underline;
}

/* The home crumb is an icon: it should not shrink, ellipsise, or dim. */
.crumb-home {
  flex: 0 0 auto;
  overflow: visible;
  font-size: 18px;
  line-height: 1;
}

/* The page you are on is the one that should read as the title. */
.crumb-current {
  flex: 1 1 auto;
  color: var(--text-strong);
  font-weight: var(--fw-bold);
}

.crumb-current:hover {
  text-decoration: none;
}

.crumb-sep {
  flex: 0 0 auto;
  color: var(--muted);
}

/* Ancestors are the first to give up room; the current page keeps its share. */
@media (max-width: 640px) {
  .crumbs {
    font-size: var(--text-base);
    gap: var(--space-1-5);
  }

  .crumbs .crumb:not(.crumb-current):not(.crumb-home) {
    max-width: 6ch;
  }
}

.host-actions {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: var(--space-2);
  position: relative; /* anchors .notif-panel under the 🔔 bell */
}


.sync-status {
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 28px;
  color: var(--blue);
}

.sync-status::before {
  content: "";
  display: block;
  box-sizing: border-box;
}

/* The spinner ring and the offline/pending dots below are DRAWN out of their
   border — it is the graphic, not an outline — so they take --grid-dark
   directly and must never be routed through the (transparent) --action-border. */
.sync-status:not([data-state="saved"])::before {
  width: 16px;
  height: 16px;
  border: 2px solid var(--grid-dark);
  border-top-color: currentColor;
  border-radius: var(--radius-full);
  animation: sync-spin 700ms linear infinite;
}

.sync-status[data-state="error"] {
  color: var(--red-dark);
}

.sync-status[data-state="saved"]::before {
  width: 14px;
  height: 8px;
  border-left: 2.5px solid var(--green-dark);
  border-bottom: 2.5px solid var(--green-dark);
  transform: translateY(-2px) rotate(-45deg);
}

/* Offline (no connection) and pending (queued offline edits) are steady states,
   not spinners — override the generic non-saved spinner. */
.sync-status[data-state="offline"]::before,
.sync-status[data-state="pending"]::before {
  width: 12px;
  height: 12px;
  border: 2px solid var(--grid-dark);
  border-radius: var(--radius-full);
  animation: none;
  transform: none;
}

.sync-status[data-state="offline"] {
  color: var(--text-muted);
}

.sync-status[data-state="pending"] {
  color: var(--orange-dark);
}

.sync-status[data-state="pending"]::before {
  background: currentColor;
  border-color: currentColor;
}

@keyframes sync-spin {
  to {
    transform: rotate(360deg);
  }
}

.match-main {
  height: calc(100vh - var(--header-h));
  height: calc(100dvh - var(--header-h));
  min-height: 360px;
  padding: var(--space-3-5);
  overflow: hidden;
  background: var(--structure);
}

.sheet-frame {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: auto;
  border-top: 0;
  background-color: var(--structure);
}

.import-frame {
  padding: var(--space-4);
}

/* the message primitive: a status/result line that preserves line breaks. The
   min-height reserves the line so text appearing doesn't shift the layout; when
   empty it collapses instead of leaving a dead gap at the end of a dialog. */
.message {
  min-height: 24px;
  margin: 0;
  color: var(--muted);
  white-space: pre-wrap;
}

.message:empty {
  min-height: 0;
  display: none;
}

/* the section primitive: a titled column of page content.
   Inputs use .input, submit buttons use .btn — see design system above. */
.section {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* on sheet pages (login, register, profile stages) a section is a form/step
   column and keeps the narrow column measure */
.sheet-frame .section {
  max-width: 420px;
}

/* stacked sections must not glue together (two one-button sections otherwise
   render as one visual block) */
.section + .section {
  margin-top: var(--space-4);
}

/* the hint primitive: muted helper text under a field or section */
.hint {
  margin: 0;
  color: var(--muted);
  font-size: var(--text-sm);
}

/* danger variant: a warning that must not read as quiet helper text */
.hint-danger {
  color: var(--danger-text);
}

/* the empty primitive: an "nothing here yet" placeholder line */
.empty {
  margin: 0;
  color: var(--muted);
}

@media (max-width: 760px) {
  :root {
    --header-h: 48px;
  }

  .host-top {
    gap: 8px;
    padding: 7px 10px;
  }

  .host h1 {
    font-size: var(--text-lg-xl);
  }

  .match-main {
    min-height: 0;
    padding: 4px;
  }

  .import-frame {
    padding: 6px;
  }
}

/* ════════════════════════════════════════════════════════════════════════
   APPEARANCE: dark theme + high-contrast mode
   Two independent axes set by menu.js on <html>:
     data-theme    = light | dark
     data-contrast = regular | high
   Default (no overrides) is the light/regular palette in :root above. Each
   block below only re-points existing color variables, so the whole app
   re-themes through the variables it already uses. The high-contrast blocks
   are scoped per theme ([data-theme=light/dark][data-contrast=high]) so they
   layer on top of the active theme with higher specificity than the bare
   dark block — order-independent.
   Palette rationale (Material dark theme + WCAG): no pure black; depth via
   lighter surfaces ~3-4 L apart, not shadows; off-white (not #fff) text;
   lightened/desaturated accents; high-contrast separates adjacent surfaces
   with stronger, text-colored borders and darker grays.
   ════════════════════════════════════════════════════════════════════════ */

/* ── Dark theme ─────────────────────────────────────────────────────────── */
:root[data-theme="dark"] {
  /* let native controls (select dropdown arrows, scrollbars, the date picker)
     render with the dark UA palette — otherwise the <select> arrows stay black
     and vanish against dark fills */
  color-scheme: dark;
  /* neutral surfaces — base (page) darkest, paper/cards lifted, structure/
     header/hover progressively lighter to read as elevation */
  --page: #14161b;
  --paper: #1e2128;
  --paper-transparent: rgba(30, 33, 40, 0);
  --surface: var(--paper);
  --structure: #262a31;
  --structure-transparent: rgba(38, 42, 49, 0);
  --hover-bg: #2c313a;
  --accent-soft: #1d2532;
  --grid: #353b45;
  --grid-dark: #474e5a;
  /* outlines off (see the :root block) — high contrast turns them back on */
  --border: transparent;
  --border-soft: transparent;
  --action-border: transparent;
  --action-disabled-border: transparent;
  /* lifted clear of --structure (#262a31) and --paper (#1e2128): with no outline
     the fill is the only thing that makes a control a control */
  --action-bg: #333944;
  --action-hover-bg: #3e4552;
  --field-bg: #2b303a;
  --action-ghost-bg: rgba(255, 255, 255, 0.07);
  --action-ghost-hover-bg: rgba(255, 255, 255, 0.14);
  --action-disabled-bg: #20242b;

  /* text — off-white high-emphasis, dimmer for muted (avoids halation) */
  --text: #e4e6ea;
  --text-strong: #f2f4f6;
  --muted: #a6acb5;
  --muted-light: #7a818b;

  /* accents — lighter + a touch desaturated so they don't vibrate on dark */
  --blue: #5b9bf5;
  --blue-tint: #1b2942;
  --blue-soft: #213450;
  --blue-strong: #8fbaf8;
  --blue-bright: #5e8ff2;
  --warning-soft: #243349;

  /* status — re-picked for dark, not desaturated light values */
  --green: #33814b;
  /* --green-dark is a *foreground* on dark: green text on dark paper, and the
     question-number digit over the --green cell fill. #2b8446 sat too close to
     both surfaces in luminance to read — lift it to a legible light green. */
  --green-dark: #7fd29b;
  --red: #9a353c;
  --red-dark: #f0a0aa;
  --red-soft: #2a1c1e;
  --success: #2ecc6f;
  --success-alt: #3ed47e;
  --success-soft: #15291d;
  --danger: #f25555;
  --danger-soft: #2c1518;
  --danger-text: #f7a6b4;

  /* amber/warning — shifted warm so it stays visible on dark */
  --amber-bg: #463713;
  --amber-border: #c4881d;
  --amber-text-strong: #f6ddc0;
  --amber-text: #e3aa55;

  /* overlays — deepen black-based scrims; shadows are a weak cue on dark */
  --backdrop: rgba(0, 0, 0, 0.6);
  --scroll-fade-color: rgba(0, 0, 0, 0.5);
}

:root[data-theme="dark"] body {
  background: var(--page);
}

/* The detailed team number is muted (secondary) in light, but on dark a muted
   gray drops below the names/scores and disappears on poor displays — the very
   monitors this feature targets. Promote it to the primary text color. */
:root[data-theme="dark"] .match-table.od-detailed .od-detailed-team-number {
  color: var(--text);
}

/* ── High contrast, light theme ─────────────────────────────────────────────
   Motivating case: a monitor that can't separate white from light gray.
   Darken the structural grays well clear of white and strengthen borders so
   cell/row boundaries carry separation regardless of fill. Push text toward
   AAA. */
:root[data-theme="light"][data-contrast="high"] {
  --grid: #9aa1ab;
  --grid-dark: #5f6772;
  --border: var(--grid-dark);
  --border-soft: #b5bcc6;
  --structure: #d2d8e0;
  --structure-transparent: rgba(210, 216, 224, 0);
  --page: #e6e9ee;
  --hover-bg: #ccd2da;
  --accent-soft: #dbe5f4;
  --surface-tint: #e9edf2;
  /* Outlines back ON — the fills alone are what a bad display cannot resolve, so
     this mode re-points every outline token the regular themes zero out. */
  --action-bg: #e9edf2;
  --action-hover-bg: #ccd2da;
  --field-bg: #ffffff;
  --action-border: #8b929c;
  --action-disabled-border: var(--grid-dark);

  --text: #16181b;
  --text-strong: #000000;
  --muted: #3c4043;
  --muted-light: #5f6368;

  --blue-strong: #103d86;
  --green-dark: #1f6e44;
  --red-dark: #8a2e37;
  --success-text: #0f5132;
  --danger-text: #7a0c2e;

  /* answer-cell fills (right/wrong). The default pastel 'wrong' reads as gray
     on the high-contrast gray page. Saturate both, and separate them in
     LUMINANCE as well as hue (dark green vs light coral) so they stay
     distinguishable for red-green colour-blindness, not by hue alone. */
  --green: #3d8f5b;
  --red: #ec8585;
}

/* ── High contrast, dark theme ─────────────────────────────────────────────
   Brighten borders and text, and widen the surface lightness gaps so adjacent
   panels stay distinct on a poor display. */
:root[data-theme="dark"][data-contrast="high"] {
  --page: #0e1014;
  --paper: #1f232b;
  --paper-transparent: rgba(31, 35, 43, 0);
  --structure: #2f3540;
  --structure-transparent: rgba(47, 53, 64, 0);
  --header: #2b313b;
  --toolbar: #282d36;
  --hover-bg: #363d49;
  --grid: #5a626e;
  --grid-dark: #828b98;
  /* Outlines back ON — see the light high-contrast block. */
  --border: var(--grid-dark);
  --border-soft: #4a515c;
  --action-border: #5a626e;
  --action-disabled-border: var(--grid-dark);
  --field-bg: #171a20;

  --text: #f6f8fa;
  --text-strong: #ffffff;
  --muted: #c4cad2;
  --muted-light: #9aa1ab;

  --blue: #7fb0f8;
  --green-dark: #9be0b3;
  --red-dark: #f6bcc3;
}

/* ── ☰ menu (Appearance item, edit/view jump, downloads, account) ───────── */
.menu {
  position: relative;
  display: inline-flex;
}

.menu-floating {
  position: fixed;
  top: var(--space-2);
  right: var(--space-2);
  z-index: var(--z-dropdown);
}

.menu-trigger svg {
  display: block;
}

/* In a .public-top bar, pin the menu to the right edge. */
.menu-public {
  margin-left: auto;
}

.menu-floating .menu-trigger {
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-popover);
}

/* ── popover base ─────────────────────────────────────────────────────────
   Every floating surface — the ☰ dropdown, hover name popovers, suggestion
   lists, notification panels — shares one silhouette: paper + the popover
   shadow (outlines are OFF; the 1px --border box paints only in high
   contrast). App emitters compose by adding this class next to their
   positioning class; .menu-dropdown gets it here (it is emitted in too many
   places to retrofit). */
.popover,
.menu-dropdown {
  background-color: var(--paper);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-popover);
  z-index: var(--z-dropdown);
}

/* The dropdown sizes itself to its longest item (width: max-content) instead of
   forcing every label to wrap inside a fixed 184px column — menu labels are
   single actions and must each read on ONE line. min-width keeps short menus
   from collapsing to a sliver; max-width keeps a long label from running off a
   narrow screen (it ellipsises there rather than wrapping). */
.menu-dropdown {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  width: max-content;
  min-width: 184px;
  max-width: min(360px, calc(100vw - var(--space-4)));
  display: flex;
  flex-direction: column;
  padding: var(--space-1);
}

/* flex + gap gives the emoji a real column of its own, so the labels align down
   the menu instead of starting wherever the previous glyph happened to end. */
.menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  text-align: left;
  padding: var(--space-2) var(--space-2-5);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--text-base);
  line-height: var(--leading-snug);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-decoration: none;
  cursor: var(--cursor-action);
}

.menu-item:hover,
.menu-item:focus-visible {
  background: var(--hover-bg);
}

/* ── Appearance modal ───────────────────────────────────────────────────── */
.appearance-modal-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: var(--backdrop);
  z-index: var(--z-modal);
}

.appearance-modal {
  width: min(420px, 100%);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-5);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-modal);
}

/* modal variant="wide": for dialogs that show content rather than a few
   controls — the 360px default turns prose and images into a column of scraps. */
.modal-wide { width: min(720px, 100%); text-align: left; }

.appearance-modal-title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--fw-semibold);
}

.appearance-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.appearance-row-label {
  font-size: var(--text-sm);
  color: var(--muted);
}

.appearance-modal-done {
  align-self: flex-end;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--action-border);
  border-radius: var(--radius-md);
  background: var(--action-bg);
  color: var(--action-fg);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  cursor: var(--cursor-action);
}

/* === ui layout utilities (internal/ui render.go) ============================
   The closed set of flex utilities the v2 UI primitives (col/row/spacer)
   compile to. Gap tokens map onto the existing --space-* scale, calibrated so
   form layouts keep their rhythm (auth-form/auth-actions used --space-2 = sm).
   Do not extend ad hoc: add a token here only when a primitive emits it. */
.u-col { display: flex; flex-direction: column; }

.u-row { display: flex; flex-direction: row; }

.u-row.u-wrap { flex-wrap: wrap; }

.u-grow { flex: 1 1 auto; min-width: 0; }

.u-spacer { flex: 1 1 auto; }

.u-gap-xs { gap: var(--space-1); }

.u-gap-sm { gap: var(--space-2); }

.u-gap-md { gap: var(--space-3); }

.u-gap-lg { gap: var(--space-4); }

.u-gap-xl { gap: var(--space-6); }

.u-align-start { align-items: flex-start; }

.u-align-center { align-items: center; }

.u-align-end { align-items: flex-end; }

.u-justify-center { justify-content: center; }

.u-justify-end { justify-content: flex-end; }

.u-justify-between { justify-content: space-between; }

/* === core widget rules promoted from the xy layer ==========================
   Base styling for classes emitted by core primitives (unreaddot, label,
   bigcode, subhead, tabs/tab/tabpanel, sliderrow) — they belong to the shared
   design system so any app using these primitives is styled. App-specific
   overrides (e.g. xy's mobile .section-label margin) stay in the app layer,
   which is concatenated after this file. */
.unread-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--blue); vertical-align: middle; }

/* iconbtn badgeid: the dot pinned to the button's corner, ringed with the
   button's own fill so it reads as a cutout */
.action-icon.has-badge { position: relative; }

.unread-dot-badge {
  position: absolute;
  top: 3px;
  right: 3px;
  border: 1.5px solid var(--action-bg);
}

/* the floating variant: pinned to the top-right of a positioned card/tile */
.unread-dot-corner {
  position: absolute;
  top: var(--space-1-5);
  right: var(--space-1-5);
}

/* Section labels read in the case they were authored in ("Метки", not "МЕТКИ").
   The uppercase + tracked-out + 700 treatment is three separate emphasis devices
   stacked on a word whose whole job is to be quiet scaffolding; muted color and
   a small size already say "label". Same reasoning for .fld-label in xy.

   PROXIMITY: a label must sit much closer to the thing it labels than to the
   block above it, or it reads as floating between the two and you cannot tell
   what it refers to. It was 20px above / 16px below — near-equidistant, so it
   bound to nothing. The margin-bottom is NEGATIVE on purpose: these labels live
   in flex columns with a `gap`, which applies equally on both sides, and pulling
   the bottom back in is the only way to beat it. Ratio is now ~32 above / ~4-12
   below. */
.section-label {
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--muted);
  margin-top: var(--space-5);
  margin-bottom: calc(var(--space-1) * -1);
}

/* The dash IS the affordance here (a tear-off code box), so it takes --grid-dark
   directly rather than the transparent --border. */
.bigcode { font-family: var(--font-mono); font-size: var(--text-2xl); font-weight: var(--fw-bold); letter-spacing: 0.08em; text-align: center; padding: var(--space-3); border: 1px dashed var(--grid-dark); border-radius: var(--radius-md); }

/* Click-to-copy inline token (login: bot handle + code). The dashed underline
   marks it actionable; .copied (set by JS for ~1s) floats a confirmation. */
.copyable { font-family: var(--font-mono); font-weight: var(--fw-bold); cursor: pointer; border-bottom: 1px dashed var(--grid-dark); position: relative; }
.copyable::after { content: "скопировано"; position: absolute; left: 50%; bottom: calc(100% + var(--space-1)); transform: translateX(-50%); padding: var(--space-0-5) var(--space-1); font-family: var(--font-sans); font-size: var(--text-xs); font-weight: var(--fw-regular); white-space: nowrap; color: var(--surface); background: var(--text-strong); border-radius: var(--radius-sm); opacity: 0; pointer-events: none; z-index: var(--z-tooltip); }
.copyable.copied::after { opacity: 1; }

.subhead { margin: 0; font-size: var(--text-md); font-weight: var(--fw-semibold); color: var(--text-strong); }

/* ── segmented control ────────────────────────────────────────────────────
   THE either/or switch: the tabs primitive (the card-detail view switch), the
   appearance modal's theme/contrast rows, xy's editor toggles. A recessed
   track holding flat buttons; the active one lifts to a surface card
   (.active, toggled by the page's JS). .seg-grow splits the row equally. */
.seg {
  display: inline-flex;
  gap: var(--space-0-5);
  padding: var(--space-0-5);
  background: var(--structure);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.seg-grow { display: flex; }

.seg-grow .seg-btn { flex: 1 1 0; }

.seg-btn {
  padding: var(--space-1-5) var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--text-sm);
  cursor: var(--cursor-action);
}

.seg-btn:hover { color: var(--text); }

.seg-btn.active {
  background: var(--surface);
  color: var(--text-strong);
  font-weight: var(--fw-semibold);
  box-shadow: var(--shadow-sm);
}

.tabpanel { margin-top: var(--space-2); }

.sizes-row { display: flex; flex-direction: column; gap: var(--space-1); }

.sizes-row-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }

.sizes-value { font-size: var(--text-sm); color: var(--muted); font-variant-numeric: tabular-nums; }

.sizes-hint { margin: 0; font-size: var(--text-xs); color: var(--muted); }

.sizes-row input[type="range"] { width: 100%; accent-color: var(--blue-strong); cursor: var(--cursor-action); }

/* ============================================================
   xy — board / card / kanban (built on the shared design tokens)
   ============================================================ */

/* Kanban depth is carried by fill, not borders: the canvas, the column and the
   card are three luminance steps. Light: pure white canvas, gray column panels,
   white cards back on top (canvas == card is fine — they never touch, a card
   always sits on a column). The kit's structure canvas made all three grays sit
   within a couple of L of each other and the board read as dirty. Dark keeps
   the kit canvas: panel/card luminance is remapped below for the same reason. */
:root { --canvas: var(--paper); --kanban-col: var(--structure); --kanban-card: var(--surface); --board-tile: var(--structure); }
:root[data-theme="dark"] { --canvas: var(--structure); --kanban-col: var(--surface); --kanban-card: var(--structure); --board-tile: var(--surface); }

/* The app shell (header + the section under it) sits on the canvas, not on the
   kit's structure gray. */
.host,
.host-top,
.match-main,
.sheet-frame { background: var(--canvas); }

.sheet-frame { --scroll-fade-bg: var(--canvas); }

/* .btn-ghost now lives in the kit (core.css) — it is one of the core button
   kinds, not an xy invention, and it needs a fill there to read as a button. */
.btn-small { min-height: var(--control-h-sm); padding: 0 var(--space-3); }

/* board list. .match-main is a fixed-height app surface and the page can't
   scroll (see .board-main below), so the grid is the scroller — otherwise an
   account with more boards than fit one screen can't reach the rest. */
.list-page .match-main { display: flex; flex-direction: column; }
.list-page .board-grid { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.board-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  /* the grid is also the page's flex-filling scroller (above); without this,
     align-content:stretch inflates a lone row of tiles to the viewport height */
  align-content: start;
  gap: var(--space-4);
  padding: var(--space-4) 0;
}
/* Board tiles carry depth by fill, not shadow — the same two-luminance-step
   scheme as the kanban (--board-tile flips against --canvas in dark). Hover is
   --action-bg: the one fill picked to read against structure and paper alike. */
.board-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-height: 90px;
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background: var(--board-tile);
  text-decoration: none;
  color: var(--text);
  position: relative; /* anchors the unread dot and the title popover */
}
.board-card:hover { background: var(--action-bg); }
/* dope's truncated-cell fade+popover: a long title stays on one line, clipped
   (no ellipsis) with a right-edge fade; the .board-card-name-truncated flag (set
   by index.js only when it overflows) turns the fade on, and index.js floats the
   full name below on hover/focus. The fade is a mask on the text so it needs no
   bg-matched gradient and survives the tile/hover/dark fills paper rows never see. */
.board-card-name-wrap { position: relative; display: block; min-width: 0; }
.board-card-name {
  display: block;
  font-weight: var(--fw-semibold);
  font-size: var(--text-lg);
  overflow: hidden;
  text-overflow: clip;
  white-space: nowrap;
}
.board-card-name-truncated .board-card-name {
  -webkit-mask-image: linear-gradient(to right, var(--paper) calc(100% - var(--inline-fade-width)), transparent);
  mask-image: linear-gradient(to right, var(--paper) calc(100% - var(--inline-fade-width)), transparent);
}
/* One shared node, appended to <body> and positioned by index.js (position:fixed),
   so the grid's scroll clip and neighbouring tiles never crop it. */
.board-card-name-popover {
  position: fixed;
  z-index: var(--z-tooltip);
  display: none;
  max-width: min(420px, calc(100vw - var(--space-4)));
  padding: var(--space-1) var(--space-2);
  font-weight: var(--fw-semibold);
  white-space: normal;
  overflow-wrap: anywhere;
  pointer-events: none;
}
.board-card-name-popover.visible { display: block; }
.board-card-role { font-size: var(--text-xs); color: var(--muted); }
/* blue "unread" dot — a board has changes by another member the user hasn't
   read. The kit's .unread-dot(-corner); a touch bigger on the large tile. */
.board-card-unread {
  top: var(--space-3);
  right: var(--space-3);
  width: 10px;
  height: 10px;
}

/* trello import */
.import-form { max-width: 760px; margin: 0 auto; }
.import-form .card-desc { min-height: 320px; }

/* kanban — the board is an SPA: the header is fixed and never scrolls, the
   lower section (.board-main) fills the rest of the viewport and only scrolls
   *inside* itself (the kanban scrolls horizontally, each list vertically), so
   the page as a whole never scrolls or rubber-bands. */
.board-page .board-main {
  height: calc(100dvh - var(--header-h));
  overflow: hidden;
  padding: var(--space-4) var(--page-pad-x);
}
/* The board page uses the kit's compact chrome (44px, .host-compact on body).
   z-index: the card modal (--z-card-overlay) covers the lower section, but the
   fixed header — and its ☰ dropdown — must stay above it (the settings/unlock
   modals at z-modal still win over both). */
.board-page .host-top { z-index: var(--z-board-header); }
/* The header breadcrumb (.crumbs) is the kit's now — both apps share it. */

/* Blue "unread" dots are the kit's .unread-dot: .unread-dot-corner on kanban
   cards and board tiles, .unread-dot-badge on the 🔔 bell, plain in the card
   detail head / timeline label / bell panel rows. */
.notif-panel {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  width: min(320px, 90vw);
  max-height: 60vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}
.notif-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2-5);
  border-bottom: 1px solid var(--border);
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
}
.notif-empty { padding: var(--space-3); color: var(--muted); font-size: var(--text-sm); text-align: center; }
.notif-row {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  padding: var(--space-2);
  cursor: pointer;
  text-align: left;
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
}
.notif-row:hover { background: var(--action-bg); }
.notif-row .unread-dot { margin-top: 6px; flex: 0 0 auto; }
.notif-row-body { min-width: 0; flex: 1 1 auto; }
.notif-row-meta { font-size: var(--text-xs); color: var(--muted); }
.notif-row-preview { font-size: var(--text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* import & other .board-main pages: the main area is their scroller. It can't be
   the page — body.host is overflow:hidden, and per CSS's viewport-propagation
   rule that value lands on the viewport itself, so the document never scrolls.
   (.board-page overrides this back to overflow:hidden above: the kanban scrolls
   its columns instead.) */
.board-main {
  height: calc(100dvh - var(--header-h));
  overflow-y: auto;
  padding: var(--space-4) var(--page-pad-x) var(--space-6);
}
.kanban {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  height: 100%;
  overflow-x: auto;
  padding-bottom: var(--space-2);
  /* On a wide monitor a full-bleed board strands the reader at the screen edge,
     so the workspace is a centred column with a user-set cap («Изменить размеры»).
     max-width, not width: the box still scrolls internally once the lists
     outgrow it. none = full bleed, the old behaviour. */
  max-width: var(--kanban-max-w, none);
  margin-inline: auto;
}
/* Workspace width, list width and card line count are user-tunable («Изменить
   размеры» in the ☰ menu, board.js) — the modal overrides these vars on <html>. */
:root {
  --kanban-max-w: none;   /* none = board spans the full viewport width */
  /* board stacking: card overlay < fixed header < --z-modal; the timer floats
     above ALL of it (modals and toasts included) — it is dragged anywhere on
     screen and must stay visible while a question is being played. */
  --z-card-overlay: 50;
  --z-timer: 130;
  --z-board-header: 60;
  --klist-w: 280px;
  --kcard-lines: none;    /* none = card shows the whole question, unclamped */
  --kcard-font: 14px;     /* card text size — smaller packs more questions on screen */
}
.klist {
  flex: 0 0 var(--klist-w);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-height: 100%;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background: var(--kanban-col);
}
.klist.dragging { opacity: 0.5; }
.klist-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-2); padding: var(--space-1) var(--space-2); }
.klist-title { font-weight: var(--fw-semibold); }
/* title + question count share a text BASELINE (centring would drop the smaller
   count's baseline below the title's), and the count wraps to its own line when
   a long title leaves it no room; the ⋯ menu stays pinned top-right. */
.klist-headmain { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--space-2); flex: 1 1 auto; min-width: 0; }
.klist-count { font-size: var(--text-xs); color: var(--muted); white-space: nowrap; }
.kadd {
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: var(--text-lg);
  line-height: 1;
  cursor: var(--cursor-action);
}
.kadd:hover { color: var(--text); }
/* the SVG glyphs (app.js strokeIcon: +, ✓) size with the surrounding text and
   inherit their color via currentColor — the reason they replaced the emoji */
.stroke-ico { width: 1em; height: 1em; display: inline-block; vertical-align: -0.125em; }
.kcards { display: flex; flex-direction: column; gap: var(--space-2); overflow-y: auto; min-height: var(--space-6); padding: var(--space-1); }
.kcard {
  position: relative; /* anchors .kcard-unread */
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--kanban-card);
  cursor: pointer;
}
.kcard:hover { background: var(--hover-bg); }
.kcard.dragging { opacity: 0.4; }
/* The node holds the card's whole text; -webkit-line-clamp cuts it at N lines
   *with an ellipsis* (a plain max-height would slice a line in half). `none`
   clamps nothing, so the card grows to fit the question. The -webkit- prefixed
   pair is still the only thing that works cross-browser; `line-clamp` is the
   standard alias, harmless where unsupported. */
.kcard-title {
  font-size: var(--kcard-font);
  white-space: pre-wrap;
  word-break: break-word;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--kcard-lines);
  line-clamp: var(--kcard-lines);
  overflow: hidden;
}
/* The alias sits below the view panels, between Метки and Вложения: it belongs
   to the card rather than to any one view, so it stays put as the tabs change. */
#cardAliasRow { margin-bottom: var(--space-2); gap: 0; }
/* The alias input and its save button are one fused control — a visual cue that
   the alias saves on its own, apart from the card's «Сохранить». Both are
   control-h, so they meet flush; the shared edge is a single divider. */
#cardAlias { border-top-right-radius: 0; border-bottom-right-radius: 0; }
#cardAliasSave {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  border-left: 1px solid var(--border);
  white-space: nowrap;
}

/* An alias is the card's name; it reads like any other card title (no special
   weight). It's already short, so the --kcard-lines clamp has nothing to do. */
.kcard-title-alias {
  -webkit-line-clamp: none;
  line-clamp: none;
}
/* auto/directive question number reads as scaffolding, not content */
.kcard-num { color: var(--muted); }
.kcard-labels { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-bottom: var(--space-1); }
/* A card that isn't a question isn't a tile: no fill, so the list shows through
   and the eye can tell scaffolding (headings, meta) from the questions. */
.kcard-heading { background: transparent; padding-bottom: var(--space-1); }
.kcard-heading:hover { background: transparent; }
.kcard-heading .kcard-title {
  font-weight: var(--fw-bold);
  letter-spacing: 0.02em;
  color: var(--muted);
}
.kcard-meta .kcard-title { color: var(--muted); font-style: italic; }
/* No fill, so no visible box: zero the vertical padding to line the input's
   top edge up with the list panels' top edge. */
.klist-add { background: transparent; padding-block: 0; }
/* the ghost "new list" tile is an input, but it stands in the row of list
   columns — take their fill, not the form-field one (a subtly different grey) */
.klist-add .input { background: var(--kanban-col); }
.klist-add .kadd-form { color: var(--muted); }
.klist-add:hover .kadd-form { color: var(--text); }
.kadd-form { width: 100%; }
/* the ✓ that creates the list (the Android soft keyboard has no Enter): a
   control-height touch target in the topbar tick's green, not the muted
   hover-reveal of the other .kadd buttons — on touch there is no hover */
.kadd-ok {
  min-width: var(--control-h);
  min-height: var(--control-h);
  font-size: var(--text-xl);
}
.klist-add .kadd-form .kadd-ok, .klist-add:hover .kadd-form .kadd-ok { color: var(--green-dark); }
/* The Тип row under the new-list field: breathe away from the field, and the
   select shrinks to the row's xs type scale instead of full .input chrome.
   min-height too — .input's --control-h floor is what kept the select
   full-size in Firefox/Chromium (Safari's native select ignores it). */
.kadd-form .attach-lossless { margin-top: var(--space-2); }
.kadd-form select.input {
  width: auto;
  height: auto;
  min-height: 0;
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-xs);
}

/* linked lists (list_of_lists) leave the board layout alone: each member just
   carries a small tag naming its group, right under the title (the negative
   margin eats most of the column's gap so the tag reads as a subtitle). */
.klist-group-tag {
  /* overflow:hidden zeroes the automatic min-height, letting a full column's
     flex squash the tag under the first card — pin it */
  flex: none;
  font-size: var(--text-xs);
  color: var(--muted);
  padding: 0 var(--space-2);
  margin-top: calc(-1 * var(--space-1-5));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* labels */
.label-chip {
  display: inline-block;
  width: 34px;
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--muted);
}

/* card detail overlay — occupies the whole lower section (below the fixed
   header), fully obscuring the kanban; the panel itself scrolls internally so
   the page never scrolls. */
.card-overlay {
  position: fixed;
  top: var(--header-h);
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: stretch;
  justify-content: center;
  background: var(--backdrop);
  overflow: hidden;
  z-index: var(--z-card-overlay);
}
.card-detail {
  width: min(680px, 100%);
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-5);
  overflow-y: auto;
  background: var(--surface);
  box-shadow: var(--shadow-modal);
}
/* The detail is a fixed-height scrolling flex column; pin every child at its
   natural size (flex: none) so nothing is compressed — iOS WebKit otherwise
   shrinks native <select>s below their min-height and clips the value text. */
.card-detail > * { flex: none; }
/* type selector, copy button and close × share one line */
.card-detail-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.card-kind-select { flex: 0 1 auto; width: auto; min-width: 0; }
/* Test-card session heading (shown instead of the kind selector). */
.card-detail-title { flex: 1 1 auto; min-width: 0; margin: 0; font-size: var(--text-base); font-weight: var(--fw-semibold); color: var(--text-strong); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-head-actions { display: flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; margin-left: auto; }
/* Copy-for-test feedback sits right under the button (top-right of the panel). */
.card-copy-msg { align-self: flex-end; margin-top: calc(var(--space-1) * -1); font-size: var(--text-xs); color: var(--green); }
.card-copy-msg[data-err] { color: var(--red); }
/* The card view no longer needs to flatten anything by hand: borders are off
   system-wide and fields carry --field-bg from the kit. The old override here
   (border-color: transparent + background: --page) was written back when fields
   were boxed, and it now CONTRADICTS the design system — it made every field in
   the card RECESSED (--page is darker than --surface in dark) while every field
   everywhere else is a step LIGHTER. Same widget, opposite direction, one screen
   apart. Deleted; the editor just picks up the shared field fill below. */
.card-desc {
  width: 100%;
  min-height: 260px;
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--field-bg);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  resize: vertical;
}
.card-desc:focus { border-color: var(--blue); }
.comment-input { min-height: 52px; }

/* label picker */
.label-picker { display: flex; flex-wrap: wrap; gap: var(--space-2); }
/* Assigned labels render as filled colour pills (the colour is set via the CSSOM
   in paintLabels); the trailing "×" stays as the remove affordance. */
.label-pick {
  padding: var(--space-0-5) var(--space-2);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: var(--muted);
  color: #fff;
  font-size: var(--text-xs);
  cursor: var(--cursor-action);
}
.label-pick.is-on { font-weight: var(--fw-bold); }
/* No horizontal padding: this sits at the left edge of the labels row and its
   text has to line up with the block above and below it, not sit indented 8px
   into open space. */
.label-empty { font-size: var(--text-xs); color: var(--muted); padding: var(--space-1) 0; }
/* position:relative anchors the custom add-label popup (.menu-dropdown) below
   the trigger button. */
.label-add-row { position: relative; display: flex; gap: var(--space-2); align-items: center; }
.label-add-row .input { flex: 1; min-width: 0; }

/* Custom add-label popup: filter field, scrollable sorted label list, and the
   create-new-label form at the foot. It hangs off the ➕ button (a ~180px anchor)
   rather than a full-width row now, so it sizes itself instead of stretching to
   the anchor — it has to hold a name field + colour + submit. */
.label-add-popup {
  left: 0;
  right: auto;
  width: max-content;
  min-width: 280px;
  max-width: min(420px, calc(100vw - var(--space-4)));
  gap: var(--space-1);
}
.label-add-filter { margin-bottom: var(--space-1); }
.label-add-list { display: flex; flex-direction: column; max-height: 240px; overflow-y: auto; }
.label-add-item { display: flex; align-items: center; gap: var(--space-2); }
.label-swatch { flex: none; width: 12px; height: 12px; border-radius: var(--radius-sm); background: var(--muted); }
.label-add-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* --control-h, not a hardcoded 34px and not `align-self: stretch`: the swatch is
   an input[type=color], a REPLACED element, so flex stretch is silently ignored
   on it (verified — align-self computes to `stretch` and the height stays at its
   intrinsic 27px). It has to be told the field height explicitly to share a
   baseline with the name field beside it. */
.label-color-input { width: 38px; height: var(--control-h); padding: 0; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); }

/* timeline */
.timeline { display: flex; flex-direction: column; gap: var(--space-2); max-height: 320px; overflow-y: auto; }
.tl-event { padding: var(--space-2) var(--space-3); border: 1px solid var(--border-soft); border-radius: var(--radius-sm); background: var(--page); }
/* wraps: on a phone the author·time, the «в ответ» link and the actions cannot
   share a line, and unwrapped the pieces stretched apart around a stranded «·» */
.tl-meta { font-size: var(--text-2xs); color: var(--muted); display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }
.tl-comment { white-space: pre-wrap; font-size: var(--text-sm); margin-top: var(--space-1); }
/* copy-link affordance on a comment; revealed on hover/focus to stay quiet */
.tl-link { border: none; background: none; cursor: pointer; padding: 0; font-size: var(--text-2xs); line-height: 1; opacity: 0; transition: opacity 0.1s; }
.tl-event:hover .tl-link, .tl-link:focus-visible { opacity: 0.7; }
.tl-link:hover { opacity: 1; }
/* touch devices have no hover — keep the link visible there */
@media (hover: none) { .tl-link { opacity: 0.55; } }
/* Comment controls, pinned to the top-right corner of the event: the 🔗 stays
   quiet (hover-revealed), but ⋯ is the entry point to edit/delete/выписка and is
   always visible, at a real touch-sized target — hunting for a hover-only sliver
   after a variable-width timestamp is exactly the friction to avoid. */
.tl-actions { margin-left: auto; display: flex; align-items: center; gap: var(--space-1); }
.tl-menu {
  border: none; background: none; cursor: var(--cursor-action); color: var(--muted);
  padding: 0 var(--space-1); min-width: 24px; min-height: 24px;
  font-size: var(--text-lg); line-height: 1;
}
.tl-menu:hover { color: var(--text); }
/* a comment flagged as a выписка — tinted so it reads as reference material
   rather than discussion while scanning the лента */
.tl-event.tl-excerpt { background: var(--structure); border-color: var(--border); }
.tl-badge { font-size: var(--text-2xs); color: var(--muted); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0 var(--space-1); }
/* in-place comment editor (board.js#startCommentEdit) */
.tl-editbox { display: flex; flex-direction: column; gap: var(--space-2); margin-top: var(--space-1); }
.tl-editrow { display: flex; gap: var(--space-2); }
/* Replies stay in the flat лента, visually identical to any other comment — the
   «↳ в ответ X» mark in the meta line carries it. No indent, no rule: the лента
   is a flat history, and decorating replies made it read as a tree it is not. */
.tl-sep { color: var(--muted); }
.tl-replyto {
  border: none; background: none; padding: 0; cursor: var(--cursor-action);
  font: inherit; font-size: var(--text-2xs); color: var(--blue);
}
.tl-replyto:hover { text-decoration: underline; }
/* «карточка создана» — the лента's floor, quieter than the events above it */
.tl-event.tl-born { background: transparent; border-style: dashed; }
/* a comment whose text was deleted but whose thread survives */
.tl-event.tl-deleted { color: var(--muted); font-style: italic; }
.tl-thread {
  margin-top: var(--space-1); border: none; background: none; padding: 0;
  cursor: var(--cursor-action); font: inherit; font-size: var(--text-2xs); color: var(--blue);
}
.tl-thread:hover { text-decoration: underline; }
/* thread modal: one exchange, oldest first — the root set off from its replies */
.thread { display: flex; flex-direction: column; gap: var(--space-2); max-height: 55vh; overflow-y: auto; }
.thread-item { padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); background: var(--page); }
.thread-item.thread-root { background: var(--structure); }
/* flash a comment when arrived at via a direct link */
.tl-event.tl-highlight { animation: tl-flash 2.5s ease-out; }
@keyframes tl-flash { 0%, 40% { background: var(--success-soft); border-color: var(--success); } 100% { background: var(--page); } }

/* members / sharing modal */
.members-list { display: flex; flex-direction: column; gap: var(--space-1); max-height: 320px; overflow-y: auto; }
.member-row { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1) 0; }
.member-name { flex: 1; min-width: 0; font-size: var(--text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.member-role { font-size: var(--text-2xs); color: var(--muted); }
.member-del { font-size: var(--text-lg); }

/* attachments (card detail) */
.attachments { display: flex; flex-direction: column; gap: var(--space-1); }

/* A mount with nothing in it is still a flex ITEM, and a flex item still eats a
   `gap` on each side — so an empty .attachments/.timeline (0px tall, invisible)
   silently pushed its section 8px apart, which is why «Вложения» and «Лента» sat
   12px off the thing they label while «Метки» (whose mount is never empty) sat
   at 4px. Collapse them out of the layout when they hold nothing. */
.attachments:empty,
.timeline:empty { display: none; }
.attach-row { display: flex; align-items: center; gap: var(--space-2); }
.attach-name { flex: 1; text-align: left; border: none; background: transparent; color: var(--blue); cursor: var(--cursor-action); font-size: var(--text-sm); }
.attach-name:hover { text-decoration: underline; }
.attach-size { font-size: var(--text-2xs); color: var(--muted); }
.attach-del { border: none; background: transparent; color: var(--muted); cursor: var(--cursor-action); font-size: var(--text-lg); line-height: 1; }
.attach-del:hover { color: var(--red-dark); }
.attach-lossless { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); color: var(--muted); }

/* выписки modal: the card's excerpts in one scrollable column. Images are shown
   inline at readable size (click → the lightbox below) rather than as links —
   they are screenshots of a source, and the point is to glance at them. */
.excerpt-count { font-size: var(--text-sm); color: var(--muted); }
.excerpts { display: flex; flex-direction: column; gap: var(--space-3); max-height: 65vh; overflow-y: auto; }
.excerpt { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-2) var(--space-3); border: 1px solid var(--border-soft); border-radius: var(--radius-sm); background: var(--page); }
.excerpt-meta { font-size: var(--text-2xs); color: var(--muted); }
.excerpt-text { white-space: pre-wrap; font-size: var(--text-sm); }
/* .excerpt is a flex column, so the default align-items:stretch was forcing the
   image to the container's width and squashing it — a replaced element with an
   intrinsic size still stretches on the cross axis. align-self + height:auto
   keep the aspect ratio. */
.excerpt-img { align-self: flex-start; max-width: 100%; height: auto; border-radius: var(--radius-sm); cursor: zoom-in; }

/* Image lightbox: a full-screen viewer for image attachments. Above the modals
   it may be opened over, below the timer (which must stay usable). The <img> is
   a real element on the blob URL, so right-click → «Сохранить как…» still works. */
.img-lb {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: var(--backdrop);
  z-index: 125;
  overflow: hidden;
  touch-action: none;
}
.img-lb[hidden] { display: none; }
.img-lb-img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  cursor: zoom-in;
  will-change: transform;
  user-select: none;
  -webkit-user-drag: none;
}
/* Keep the max-width/height fit cap even when zoomed: the transform scales the
   fit-sized layout box, so the zoom math (lbBaseW/H) stays consistent. */
.img-lb-img.img-lb-zoomed { cursor: grab; }
.img-lb-close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  width: 2rem;
  height: 2rem;
  border: none;
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-size: var(--text-xl);
  line-height: 1;
  cursor: var(--cursor-action);
}
.img-lb-close:hover { filter: brightness(0.96); }
/* selects must never push past their container on narrow screens */
.card-detail select.input { max-width: 100%; }

/* per-list "⋯" menu: the shared .menu-dropdown floats on <body> at a fixed,
   viewport-clamped spot (board.js popupMenu) — anchored inside the kanban
   scroll container it was clipped at the work area's edge. The wrap only
   keeps the trigger button inline. */
.klist-menu-wrap { position: relative; display: inline-flex; }
/* A body-mounted fixed popup must outrank whatever it was opened from, and the
   comment/attachment ⋯ menus are opened from INSIDE the card overlay — at the
   inherited --z-dropdown (30) they painted under it (50) and read as a dead
   button. Above the overlay and the board header, still below --z-modal. */
.menu-dropdown.menu-fixed { position: fixed; top: auto; right: auto; z-index: 70; }
/* a toggle row in a popup menu: .menu-item is already the flex row, this only
   makes the <label> behave like the <button> siblings it sits among */
.menu-item-check { cursor: var(--cursor-action); user-select: none; }

/* краткий diff: one flowing line, old and new inline, the untouched bulk of the
   question replaced by … — the whole point is that it costs a couple of lines */
.tl-brief {
  margin-top: var(--space-1); font-size: var(--text-sm);
  white-space: pre-wrap; word-break: break-word; line-height: 1.5;
}
.tl-gap { color: var(--muted); }
/* The two-pane view colours a change by which PANE it is in; inline there are no
   panes, so the tokens carry the colour themselves — red struck-through for what
   went, green for what came. */
.tl-brief del.tl-chg { background: var(--red); text-decoration: line-through; }
.tl-brief ins.tl-chg { background: var(--green); text-decoration: none; }
/* the whitespace that used to separate them was absorbed into the cluster
   (clusterChanges), so the gap between the old chunk and the new one is drawn */
.tl-brief .tl-chg + .tl-chg { margin-left: 0.35em; }

/* the expanded лента: the same events flowed into columns. The container has a
   definite height, so the columns fill top-to-bottom and continue sideways —
   hence the horizontal scroll rather than a vertical one. */
/* full width, unlike the 820px reading column the other doc overlays want —
   the whole point is to spend the screen on columns */
/* both classes: .preview-doc's own width rule is later in this file and would
   otherwise win on equal specificity */
.preview-doc.feed-doc { width: 100%; }
/* .section-label is a heading for the block BENEATH it and carries a top margin
   to prove it; used as an inline caption beside a control it drags itself off
   the shared centre line. Same fix as .handouts-doc's. */
.card-detail .u-row > .section-label { margin: 0; }
/* Feed head: both labelled selects on ONE line, close button pushed to the far
   right. No «Лента» title — after «Развернуть» the surface is unmistakable.
   Compact padding: this is chrome above the content, not content. */
.feed-doc .preview-head {
  align-items: center;
  padding: var(--space-3) var(--space-4);
}
.feed-doc .preview-head-actions {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
}
.feed-doc .preview-head-actions .section-label { margin: 0; }
.feed-doc #feedClose { margin-left: auto; } /* far right, on the first line */
/* On a phone these dropdowns dwarfed the content — bigger font than the feed
   itself. Shrink them to roughly half (smaller type, ~26px tall — note the kit
   hard-sets select HEIGHT, not min-height, so it must be overridden) so they
   read as chrome. The × pins to the top-right corner instead of wrapping onto a
   row of its own; the controls wrap in the space left of it. */
@media (max-width: 560px) {
  .feed-doc .preview-head {
    position: relative;
    padding: var(--space-2) var(--space-3);
    padding-right: 42px;
  }
  .feed-doc .preview-head-actions .section-label { font-size: var(--text-2xs); }
  .feed-doc .preview-head-actions select {
    font-size: var(--text-2xs);
    height: 26px;
    min-height: 0;
    padding: 0 16px 0 6px;
  }
  .feed-doc #feedClose {
    position: absolute;
    top: var(--space-2);
    right: var(--space-3);
    margin: 0;
    height: 26px; min-height: 26px; width: 26px; min-width: 26px;
    padding: 0;
    font-size: var(--text-sm);
  }
}
.feed-grid {
  flex: 1; min-height: 0; overflow: auto;
  column-width: 340px; column-gap: var(--space-4);
  padding: var(--space-3) var(--space-5);
}
.feed-grid .tl-event { break-inside: avoid; margin-bottom: var(--space-2); }
/* one column on a phone: 340px columns in a 375px viewport leave no margin, and
   scrolling a feed sideways is worse than scrolling it down */
@media (max-width: 560px) {
  .feed-grid { column-width: auto; column-count: 1; padding: var(--space-3); }
}
/* a column-broken .tl-diff turns into two unreadable slivers */
.feed-grid .tl-diff { grid-template-columns: 1fr; }

/* two-pane before/after diff (timeline desc_edit), with the word-level changes
   highlighted inside each pane */
.tl-diff { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); margin-top: var(--space-1); font-family: var(--font-mono); font-size: var(--text-2xs); }
.tl-before, .tl-after { padding: var(--space-2); border-radius: var(--radius-xs); white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
.tl-before { background: var(--red-soft); }
.tl-after { background: var(--success-soft); }
.tl-chg { border-radius: var(--radius-xs); color: #fff; padding: 0 1px; }
.tl-before .tl-chg { background: var(--red); text-decoration: line-through; }
.tl-after .tl-chg { background: var(--green); text-decoration: none; }


/* card create mode: show only the type chooser + description editor */
.card-detail.creating [data-edit-only] { display: none !important; }
.card-detail:not(.creating) [data-create-only] { display: none !important; }

/* list preview ("à la docx export", rendered client-side) — a wide, scrollable
   reading view layered over the board like the card detail. */
.preview-doc {
  width: min(820px, 100%);
  height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  box-shadow: var(--shadow-modal);
}
.preview-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  flex: none;
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
}
.preview-title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--fw-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.preview-head-actions { display: flex; align-items: center; gap: var(--space-3); flex: none; }
.preview-screen-toggle { white-space: nowrap; cursor: var(--cursor-action); }
.preview-body {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-5) var(--space-6);
  font-size: var(--text-base);
  line-height: 1.55;
  color: var(--text);
}
/* Cap the reading column to a comfortable measure (~66 characters) and centre it
   — full-width lines on a wide modal are tiring to read. */
.preview-body > * { margin: 0 auto var(--space-3); max-width: 66ch; }
/* docx-like rhythm: the gap below each question matches the preview's own top
   padding (space-5) so the spacing reads even top-to-bottom; a small gap between
   the question text and its answer block; and no gap between the answer/zachet/
   comment/source/author lines (they read as one tight block). */
.pv-q { margin-bottom: var(--space-5); }
/* per-card ✏️ jump-to-editor button, rendered inline as a small bordered pill
   right before the block's leading label ("✏️ Вопрос 1.") so it stays clearly
   separated from the text and never covers it — including on mobile, where an
   overlaid pencil used to obscure the question. */
.pv-edit {
  display: inline-block;
  border: 1px solid var(--action-border);
  background: var(--action-bg);
  border-radius: var(--radius-sm);
  padding: 0.05em 0.35em;
  margin-right: 0.4em;
  font-size: 0.8em;
  line-height: 1.4;
  cursor: var(--cursor-action);
  vertical-align: middle;
}
.pv-edit:hover, .pv-edit:focus-visible { background: var(--action-hover-bg); }
/* preserve authored line breaks inside multi-line question/answer/meta text */
.pv-q-text, .pv-field, .pv-meta, .pv-list-body { white-space: pre-wrap; }
.pv-q-text { margin-bottom: var(--space-2); }
.pv-field { margin-bottom: 0; }
.pv-label { font-weight: var(--fw-bold); }
/* numbered list (sources, blitz/duplet questions & answers): the "N." marker and
   the item body are separate boxes so wrapping a long unbreakable value (e.g. a
   bare URL) inside the body can never orphan the marker onto its own line. The
   numbers are plain weight (only field labels are bold). */
.pv-list { margin-top: var(--space-1); }
.pv-list-item { display: flex; gap: 0.4em; margin-bottom: var(--space-0-5); }
.pv-list-num { flex: 0 0 auto; font-weight: var(--fw-regular); }
.pv-list-body { min-width: 0; flex: 1; overflow-wrap: anywhere; }
.pv-handout { color: var(--muted); }
/* sources/authors set smaller, matching the exports' 12pt → 10pt. The block after
   a body-size one starts one BODY line below, not one small line: the lost height
   is 1.55 × (2/12) body-em = 0.31 of the small block's own em. */
.pv-small { font-size: calc(10em / 12); }
:not(.pv-small) + .pv-small { margin-top: 0.31em; }
.pv-heading { font-size: var(--text-xl); font-weight: var(--fw-bold); margin: var(--space-5) 0 var(--space-3); }
.pv-section { font-size: var(--text-lg); font-weight: var(--fw-semibold); margin: var(--space-4) 0 var(--space-2); }
.pv-empty { color: var(--muted); }
/* break long URLs at any character so they never force horizontal scroll on
   narrow screens (links are often unbreakable runs with no spaces). */
.pv-link { color: var(--blue); overflow-wrap: anywhere; word-break: break-word; }
.pv-sc { font-variant: small-caps; }
/* the dash IS the page-break mark, so it takes --grid-dark, not --border */
.pv-pagebreak { border: 0; border-top: 1px dashed var(--grid-dark); margin: var(--space-4) 0; }
.pv-img {
  display: block;
  max-width: min(100%, 480px);
  max-height: 12em;
  width: auto;
  height: auto;
  margin: var(--space-2) 0;
  border-radius: var(--radius-sm);
  cursor: zoom-in; /* click opens the lightbox */
}
/* block, like the image it stands in for — renderRich swallows the source
   newline after an (img …), so an inline placeholder would glue to the text */
.pv-img-missing { display: block; color: var(--muted); font-style: italic; }

/* Touch / small screens: shrink the type to fit more questions on screen (the
   same densification ~/dope applies on mobile). */
@media (max-width: 760px) {
  /* tighter board header so more of the board is visible on phones */
  .board-page .host-top { padding: 6px 10px; gap: 6px; }
  .board-page h1 { font-size: var(--text-lg); }
  .board-page .action-icon { width: 30px; height: 30px; font-size: 15px; }

  .board-main { padding: var(--space-2) var(--space-2); }
  .klist { flex-basis: 86vw; max-width: 320px; }
  .klist-title { font-size: var(--text-sm); }
  .kcard { padding: var(--space-1) var(--space-2); }
  /* phones already densify to 12px; a smaller user-chosen card font still wins */
  .kcard-title { font-size: min(var(--kcard-font), var(--text-xs)); }
  /* keep the label pills off the card's top edge */
  .kcard-labels { margin-top: var(--space-1); }
  .card-detail { padding: var(--space-4) var(--space-3); }
  .preview-head { padding: var(--space-3) var(--space-4); }
  .preview-body { padding: var(--space-4) var(--space-4); font-size: var(--text-sm); }
  .card-desc { font-size: var(--text-xs-plus); min-height: 200px; }
  .section-label { margin-top: var(--space-2); }
  .tl-comment { font-size: var(--text-xs-plus); }
  .label-pick { font-size: var(--text-2xs); }
}

/* ЧГК play timer — floats bottom-right by default, above everything
   (--z-timer). Toggled by the ⏰ header button; draggable anywhere on screen
   (timer.js sets left/top and remembers them in localStorage). */
.timer-overlay {
  position: fixed;
  right: var(--space-4);
  bottom: var(--space-4);
  z-index: var(--z-timer);
  cursor: grab;
  touch-action: none;
  user-select: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: 240px;
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-card);
}
.timer-overlay[hidden] { display: none; }
/* controls stay fully interactive inside the draggable surface */
.timer-overlay :is(input, select, button) { touch-action: auto; user-select: auto; -webkit-user-select: auto; }
.timer-overlay.timer-moved { right: auto; bottom: auto; }
.timer-overlay.timer-dragging { cursor: grabbing; }
.timer-row, .timer-custom { display: flex; }
.timer-preset, .timer-custom-input { width: 100%; }
.timer-display {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-2) 0;
}
.timer-time {
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-bold);
  font-size: 56px;
  line-height: 1;
  color: var(--text-strong);
}
.timer-time.timer-urgent { color: var(--danger); }
.timer-time.timer-answer { color: var(--accent); }
.timer-label {
  font-size: var(--text-xs);
  color: var(--muted);
  min-height: 1em;
}
.timer-actions { display: flex; gap: var(--space-2); justify-content: center; }
.timer-actions .btn { flex: 1; display: flex; align-items: center; justify-content: center; }
.timer-ico { width: 16px; height: 16px; display: block; }

@media (max-width: 760px) {
  .timer-overlay { right: var(--space-2); bottom: var(--space-2); width: min(220px, calc(100vw - 2 * var(--space-2))); padding: var(--space-2); }
  .timer-time { font-size: 44px; }
  .timer-display { padding: var(--space-1) 0; }
  /* let the three control buttons shrink to share the row instead of overflowing
     the box (flex items default to min-width:auto = their text width) */
  .timer-actions { gap: var(--space-1); }
  .timer-actions .btn { min-width: 0; padding-left: var(--space-2); padding-right: var(--space-2); }
}

/* API tokens page (tokens.html) — built on the auth/sheet layout above.
   .subhead (the subhead primitive) lives in core.css. */
.token-value {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  word-break: break-all;
  user-select: all;
}
.token-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.token-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}
.token-row-inactive { opacity: 0.55; }
.token-row-main {
  grid-column: 1;
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.token-row-label { font-weight: var(--fw-semibold); color: var(--text-strong); }
.token-row-status { font-size: var(--text-xs); color: var(--muted); }
.token-row-meta {
  grid-column: 1;
  font-size: var(--text-xs);
  color: var(--muted);
}
.token-revoke {
  grid-column: 2;
  grid-row: 1 / span 2;
  white-space: nowrap;
}

/* card detail: 3-view switch (Просмотр / Поля / Текст) — the tabs/tab/tabpanel
   primitives; the .seg/.seg-btn/.tabpanel base lives in core.css. */
/* preview view inside the card: a bounded, scrollable docx-style render */
.card-preview-screen { display: inline-flex; align-items: center; gap: var(--space-1); cursor: var(--cursor-action); margin-bottom: var(--space-2); }
.card-preview-body {
  padding: var(--space-3) var(--space-4);
  background: var(--page);
  border-radius: var(--radius-md);
  max-height: 52vh;
  overflow-y: auto;
  cursor: text;
}
.card-preview-body > * { max-width: none; }
/* The card-detail preview shows a single question, so the inter-question
   bottom margin (.pv-q) is just a trailing gap — drop it; the container's own
   padding already gives symmetric top/bottom breathing room. */
.card-preview-body .pv-q { margin-bottom: 0; }

/* ── structured fields (Поля) ─────────────────────────────────────────────── */
/* Density (user request): absent fields are pills that FLOW TOGETHER on shared
   wrap rows, and a present field spans the full row with its label BESIDE the
   control, not above it — the stacked label/input made any filled card scroll. */
.card-fields { display: flex; flex-flow: row wrap; gap: var(--space-2); }
.fld { display: flex; flex-direction: column; }
.fld-wide, .fld-present { flex: 1 1 100%; }
.fld-present {
  display: grid;
  /* as narrow as the longest unbreakable label («Комментарий» at text-xs)
     allows — multi-word labels wrap instead of widening the column */
  grid-template-columns: 5.5em 1fr;
  column-gap: var(--space-2);
  align-items: start;
}
/* label column: × next to the label (not flushed to the far edge of the card),
   label dropped onto the input's first text line */
.fld-present > .fld-head { justify-content: flex-start; padding-top: 7px; gap: var(--space-1); }
.fld-present .fld-label { font-size: var(--text-xs); }
.fld-present .fld-rm { padding: 0 2px; }
.fld-present > .fld-body { margin-top: 0; }
/* the dashed ring is this button's whole identity ("add a field") — --grid-dark,
   not the transparent --border */
.fld-add {
  align-self: flex-start;
  border: 1px dashed var(--grid-dark);
  background: var(--page);
  color: var(--muted);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--fw-regular);
  padding: var(--space-1) var(--space-3);
  border-radius: 999px;
  cursor: var(--cursor-action);
}
.fld-add:hover { color: var(--text); border-color: var(--muted); }
.fld-muted .fld-add { opacity: 0.75; }
.fld-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.fld-label { font-size: var(--text-sm); font-weight: var(--fw-semibold); color: var(--muted); }
.fld-rm, .fld-row-rm, .fld-tag-rm {
  border: none; background: transparent; color: var(--muted);
  font-size: var(--text-base); line-height: 1; cursor: var(--cursor-action);
  padding: 0 var(--space-1);
}
.fld-rm:hover, .fld-row-rm:hover, .fld-tag-rm:hover { color: var(--red); }
.fld-body { margin-top: var(--space-1); }
.fld-input { width: 100%; }
/* rows=1 + no floor: a one-line answer takes one line; autoGrow adds the rest */
textarea.fld-input { min-height: 0; padding-block: var(--space-2); }
/* desktop: shave the 38px control floor inside Поля — it reads airy repeated
   over nine fields; phones keep the full touch target */
@media (min-width: 761px) {
  .card-fields .input { min-height: 32px; padding-block: 5px; }
  .card-fields select.input { height: 32px; }
}
/* phones have no width to spare for a label column — stack label above input
   again (this block must FOLLOW the .fld-present grid rules to win the tie) */
@media (max-width: 760px) {
  /* align-items too: the grid rule's `start` would shrink the stacked children
     to content width */
  .fld-present { display: flex; flex-direction: column; align-items: stretch; }
  .fld-present > .fld-head { justify-content: space-between; padding-top: 0; }
  .fld-present > .fld-body { margin-top: var(--space-1); }
}
.fld-rows { display: flex; flex-direction: column; gap: var(--space-1); }
.fld-row { display: flex; align-items: center; gap: var(--space-1); }
.fld-row-input { flex: 1; }
.fld-add-row { align-self: flex-start; font-size: var(--text-sm); margin-top: var(--space-1); cursor: var(--cursor-action); padding: var(--space-1) var(--space-2); width: auto; }
/* hand-drawn autocomplete (suggestWrap in board.js — <datalist> never opens on
   iOS Safari, so the dropdown is ours) */
.suggest-wrap { position: relative; flex: 1 1 auto; min-width: 0; }
.suggest-wrap > .input { width: 100%; }
.suggest-menu {
  position: absolute;
  top: calc(100% + 2px);
  left: 0;
  right: 0;
  z-index: var(--z-dropdown);
  display: flex;
  flex-direction: column;
  max-height: 40vh;
  overflow-y: auto;
  background: var(--surface);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-popover);
}
.suggest-item {
  border: none;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--text-sm);
  text-align: left;
  padding: var(--space-1-5) var(--space-2-5);
  cursor: var(--cursor-action);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.suggest-item:hover, .suggest-item.active { background: var(--hover-bg); }

/* handout text/image toggle (.seg lives in the kit) */
.fld-body > .seg { margin-bottom: var(--space-1); }
/* test-card tester rows: игрок/команда toggle sits inline with the name input */
.tester-seg { flex: none; }
/* author tags */
.fld-tags { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-bottom: var(--space-1); }
.fld-tag {
  display: inline-flex; align-items: center; gap: var(--space-1);
  background: var(--page); border: 1px solid var(--border); border-radius: 999px;
  padding: 2px var(--space-2); font-size: var(--text-sm);
}

/* ── handouts generation modal ────────────────────────────────────────────── */
.handouts-doc { width: min(1100px, 100%); }
.handouts-body { flex: 1; display: flex; min-height: 0; }
.handouts-pane { flex: 1 1 50%; display: flex; flex-direction: column; min-width: 0; padding: var(--space-4) var(--space-5); overflow-y: auto; }
/* pane splitter — a structural rule, so --grid (like the table cell grids) */
.handouts-src { border-right: 1px solid var(--grid); }
.handouts-textarea { flex: 1; min-height: 320px; width: 100%; }
/* The two panes are mirrored columns: header row, then the big area, spaced by
   the pane's gap (not by per-widget margins, which is what glued the label /
   buttons to the textarea). The section-label's own margins are for flowing
   forms, not headers — zeroed here; the header rows reserve a small-button
   height so «Скачать» appearing doesn't push the PDF down out of line. */
.handouts-doc .handouts-pane { gap: var(--space-3); }
.handouts-doc .section-label { margin: 0; }
.handouts-doc .handouts-pane > .u-row:first-child { min-height: var(--control-h-sm); }
/* The frame fills its box via absolute inset, not flex + height:100% — a
   percentage height inside a flex-sized box resolves against an indefinite
   height in some engines, which cropped the preview to a fraction of the box. */
.handouts-pdf { flex: 1; background: var(--field-bg); border-radius: var(--radius-md); min-height: 320px; position: relative; }
.handouts-pdf-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: none; border-radius: var(--radius-md); }
/* Safari-standalone/iOS fallback: no inline PDF there, offer a real navigation */
.handouts-pdf-fallback { position: absolute; inset: 0; display: flex; flex-direction: column; gap: var(--space-3); align-items: center; justify-content: center; padding: var(--space-4); text-align: center; }
.handouts-pdf-note { font-size: var(--text-xs); color: var(--muted); }

@media (max-width: 760px) {
  .handouts-body { flex-direction: column; }
  .handouts-src { border-right: none; border-bottom: 1px solid var(--grid); }
  .card-preview-body { max-height: 40vh; }
}

/* ── import verification modal ─────────────────────────────────────────────────
   Reuses the handouts split-pane shell: parsed 4s source on the left, the same
   preview the list preview renders on the right. */
.import-doc { width: min(1100px, 100%); }
.import-textarea { font-family: var(--font-mono); font-size: var(--text-sm); line-height: 1.5; }
.import-count { color: var(--muted); font-size: var(--text-sm); white-space: nowrap; }
.import-preview > * { margin: 0 0 var(--space-3); }

/* ── sizes modal (list width / card height) ───────────────────────────────── */
.sizes-modal { width: min(420px, 100%); text-align: left; }
/* The .sizes-row/.sizes-value/.sizes-hint slider rows are the sliderrow
   primitive; their base rules live in core.css. */

/* The pseudo-board preview in the sizes dialog (profile.js#renderPreview):
   a to-scale wireframe of a monitor — the board column centres inside the
   "screen", lists are --pv-list-w wide, cards are stacks of "text line" bars
   cut by the card-height clamp. Proportions only, hence bars, not text. */
.sizes-preview {
  height: 150px;
  border-radius: var(--radius-md);
  overflow: hidden;
}
.pvb-screen {
  display: flex;
  justify-content: center;
  height: 100%;
  background: var(--structure);
  overflow: hidden;
}
.pvb-board {
  display: flex;
  align-items: flex-start;
  gap: 3px;
  width: 100%;
  max-width: var(--pv-board-w, none);
  padding: 5px;
  overflow: hidden;
}
.pvb-list {
  flex: 0 0 var(--pv-list-w, 70px);
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 3px;
  border-radius: 3px;
  background: var(--action-bg);
}
.pvb-title { height: 4px; width: 55%; border-radius: 2px; background: var(--grid-dark); margin: 1px 0 2px; }
.pvb-card {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 3px;
  border-radius: 2px;
  background: var(--surface);
}
.pvb-line { height: var(--pvb-line-h, 3px); border-radius: 1.5px; background: var(--grid-dark); }
.pvb-line-last { width: 60%; }

/* ── lists management modal ────────────────────────────────────────────────── */
.lists-manage { width: min(560px, 100%); text-align: left; }
.lists-move-pos { width: 4.5rem; }
.lists-manage-rows {
  display: flex; flex-direction: column; gap: var(--space-2);
  max-height: 55vh; overflow-y: auto; padding: var(--space-1);
}
.lm-unit { border-radius: var(--radius-md); background: var(--structure); }
.lm-unit.dragging { opacity: 0.5; }
.lm-row { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-2); }
.lm-check { display: inline-flex; align-items: center; }
.lm-pos { color: var(--muted); font-size: var(--text-sm); min-width: 2.2rem; }
.lm-handle { color: var(--muted); cursor: grab; font-size: var(--text-lg); line-height: 1; }
.lm-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lm-group-title { font-weight: var(--fw-semibold); color: var(--blue-strong); white-space: normal; }
.lm-icon {
  border: none; background: transparent; color: var(--muted);
  cursor: var(--cursor-action); font-size: var(--text-sm); padding: 0 var(--space-1);
}
.lm-icon:hover { color: var(--text); }
.lm-move { display: flex; align-items: center; gap: var(--space-1); }
.lm-move-pos { width: 3.5rem; }
.lm-members {
  display: flex; flex-direction: column; gap: var(--space-1);
  padding: 0 var(--space-2) var(--space-2) calc(var(--space-5) + var(--space-2));
}
.lm-member { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-0-5) var(--space-2); border-left: 2px solid var(--blue-strong); }
.lm-member.dragging { opacity: 0.5; }

/* PWA html/body overrides — xy layer only (dope must NOT inherit these).
   iOS: don't bump font sizes on rotate, don't rubber-band/zoom; the app is a
   fixed shell whose inner sections scroll (see menu.js gesture guards). */
html,
body {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  overscroll-behavior: none;
  touch-action: manipulation;
}

/* ---- test sessions: the Тесты panel, the label editor, the mark template ----
   All three are list-shaped modals, so they share one row skeleton. */
.sess-row {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  align-items: center; justify-content: space-between;
  padding: var(--space-2) 0; border-bottom: 1px solid var(--border-soft);
}
.sess-row:last-child { border-bottom: 0; }
.sess-head { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: baseline; min-width: 0; flex: 1 1 16rem; }
.sess-title { font-weight: var(--fw-semibold); }
.sess-meta { color: var(--muted); font-size: var(--text-sm); }
/* A copied session says how stale it might be: its testers froze at transfer
   time, and that list is what «Видели» reads (ADR-0003). */
.sess-origin { color: var(--muted); font-size: var(--text-sm); font-style: italic; }
.sess-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.sess-cities { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.sess-invite-box { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin: var(--space-3) 0; }
.sess-invite { flex: 1 1 18rem; margin: 0; font-size: var(--text-base); }

/* A test label is drawn apart from a topic label whatever colour it landed on,
   so an imported Trello tag can never read as «взяли» (issue #25). */
.label-chip.is-test, .label-pick.is-test { border: 1px dashed var(--grid-dark); }

/* «Видели»: derived from the sessions a card is tagged with, stored nowhere. */
#cardSeen { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: baseline; }
.seen-label { color: var(--muted); font-size: var(--text-sm); }
.seen-names { flex: 1 1 12rem; }
.seen-copy { flex: 0 0 auto; }

/* The person directory's dropdown: this board's names first and unlabelled,
   then names from other unlocked boards tagged with where they came from. */
.suggest-pop { position: absolute; z-index: 30; max-height: 12rem; overflow-y: auto; }
.suggest-board { color: var(--muted); font-size: var(--text-sm); margin-left: var(--space-2); }

/* A tester row is seg + name + ×, which does not fit a 393px phone. Let it wrap
   and give the name input its own line: the seg and the × stay paired above it. */
#sessionForm .tester-row { flex-wrap: wrap; }
#sessionForm .tester-row .fld-row-input { flex: 1 1 100%; min-width: 0; }

/* The session's debrief: what was said about any question at this test. */
.sess-notes { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-2); }
.sess-note { margin: 0; }
