/* Pocket Language Guide. Custom properties and scale follow the root site's
   style.css so the app does not look bolted on. */

:root {
  color-scheme: light;

  --ink: #111820;
  --muted: #3f4a55;
  --line: #d7dee5;
  --line-soft: #e8edf1;
  --paper: #ffffff;
  --surface: #f4f6f8;
  --surface-2: #eaeff3;
  --accent: #0b67a3;
  --accent-soft: #e6f0f7;
  /* The same accent lifted for use on a dimmed overlay -- the gallery lightbox
     puts controls over an 82%-black backdrop, where #0b67a3 is barely there.
     Holds 7.4:1 against that backdrop. */
  --accent-lift: #79c2ef;
  --alert: #b3261e;
  --ok: #237547;

  --radius: 8px;
  --radius-sm: 5px;
  --space-1: 0.35rem;
  --space-2: 0.7rem;
  --space-3: 1.1rem;
  --space-4: 1.8rem;

  --fs--1: 0.78rem;
  --fs-0: 0.9rem;
  --fs-1: 1.05rem;
  --fs-2: clamp(1.2rem, 1rem + 0.8vw, 1.5rem);
  --fs-3: clamp(1.5rem, 1.15rem + 1.6vw, 2.1rem);

  --ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--ui);
  font-size: var(--fs-0);
  line-height: 1.45;
  color: var(--ink);
  background: var(--paper);
  -webkit-text-size-adjust: 100%;
}

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

h1 { font-size: var(--fs-3); margin: 0 0 var(--space-1); line-height: 1.15; }
h2 { font-size: var(--fs-2); margin: 0 0 var(--space-2); }
h3 { font-size: var(--fs-1); margin: 0 0 var(--space-1); }

.container { width: min(100% - 2rem, 1180px); margin-inline: auto; }
/* The studio is full-bleed, so the header above it must be too: capped, the brand
   sat 250px inboard of the panel title it labels. Matches `.studio > section`
   padding so the two line up. */
.container-wide { width: 100%; padding-inline: var(--space-2); }
.lede { color: var(--muted); max-width: 62ch; margin: 0 0 var(--space-3); }
.row { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }
.spacer { flex: 1; }
.muted { color: var(--muted); }
.small { font-size: var(--fs--1); }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* --- controls ------------------------------------------------------------- */

button, .btn {
  font: inherit;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 0.4em 0.8em;
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
}
button:hover, .btn:hover { background: var(--surface); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
button.primary, .btn.primary {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600;
}
button.primary:hover, .btn.primary:hover { filter: brightness(1.08); }
/* Subdued, but still a button: a transparent border and no fill read as plain
   text, so "All on / All off / Balance columns" looked like a sentence. */
button.ghost { border-color: var(--line-soft); background: var(--surface); color: var(--muted); }
button.ghost:hover { background: var(--surface-2); border-color: var(--line); color: var(--ink); }

select, input[type="text"], input[type="number"] {
  font: inherit; color: var(--ink); background: var(--paper);
  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.35em 0.5em;
}
label { display: block; }
.field { margin-bottom: var(--space-2); }
.field > span { display: block; font-size: var(--fs--1); color: var(--muted); margin-bottom: 2px; }
.field select, .field input { width: 100%; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- site header --------------------------------------------------------- */

.site-header {
  border-bottom: 1px solid var(--line);
  padding: var(--space-2) 0;
  position: sticky;
  top: 0;
  background: var(--paper);
  z-index: 20;
}
/* Wraps rather than pushing the picker -- or, in the studio, the export buttons --
   off the side of a phone. */
.site-header .container { display: flex; align-items: center; gap: var(--space-1) var(--space-3); flex-wrap: wrap; }
.brand { font-weight: 700; text-decoration: none; color: var(--ink); white-space: nowrap; }
.reader-picker { display: flex; align-items: center; gap: var(--space-1); font-size: var(--fs--1); min-width: 0; }
/* A long endonym must not set the header's width. */
.reader-picker select { min-width: 0; max-width: 100%; text-overflow: ellipsis; }

/* Out of flow and off the top until focused, so it costs no layout and never
   widens the page; z-index clears the sticky header. */
.skip-link {
  position: absolute; top: var(--space-1); left: var(--space-1); z-index: 30;
  transform: translateY(calc(-100% - var(--space-3)));
  background: var(--accent); color: #fff; text-decoration: none;
  padding: 0.35em 0.8em; border-radius: var(--radius-sm); font-size: var(--fs--1);
}
.skip-link:focus { transform: none; }

/* --- gallery ------------------------------------------------------------- */

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  /* Equal rows: only some cards carry a coverage line, and rows sized to their own
     content left the first row standing 25px taller than the rest. */
  grid-auto-rows: 1fr;
  gap: var(--space-3);
  padding-block: var(--space-3) var(--space-4);
}
.card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background: var(--paper);
}
.card-head {
  display: flex; align-items: flex-start; gap: var(--space-2); padding: var(--space-2);
}
.card-badge {
  font-size: 1.25rem; line-height: 1; min-width: 1.6em; text-align: center; color: var(--accent);
}
.card-name { font-weight: 600; }
/* The thumbnail is a button so it can open the preview; it must not look like one. */
.card-thumb-button {
  display: block; width: 100%; padding: 0; border: 0; background: none; cursor: zoom-in;
}
.card-thumb-button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.card-thumb-button:hover .card-thumb { filter: brightness(0.97); }
.card-thumb {
  display: block; width: 100%; aspect-ratio: 7 / 5; object-fit: cover; object-position: top left;
  background: var(--surface); border-block: 1px solid var(--line-soft);
}
.card-thumb.placeholder {
  display: grid; place-items: center; color: var(--muted); font-size: var(--fs--1); text-align: center;
  padding: var(--space-2);
}
/* One row, always: three buttons that wrap make the cards different heights. */
.card-actions {
  display: flex; gap: 4px; padding: var(--space-2); flex-wrap: nowrap;
  align-items: stretch;
}
.card-actions > * { min-width: 0; }
.card-actions .btn, .card-actions button {
  padding: 0.35em 0.5em; font-size: var(--fs--1); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; justify-content: center; flex: 1 1 auto;
}
.card-actions .tag { flex: 0 0 auto; align-self: center; white-space: nowrap; }
.tag {
  font-size: var(--fs--1); border: 1px solid var(--line); border-radius: 999px;
  padding: 0.05em 0.6em; color: var(--muted);
}
.tag.draft { border-color: #d8a24a; color: #8a5a06; }
.tag.planned { border-style: dashed; }

/* --- studio -------------------------------------------------------------- */

/* The studio takes whatever the header and banner actually leave. A calc() against
   a guessed chrome height overflowed by 46px with the banner up and left a 3px gap
   with it hidden, and the overflow read as 900px of blank page below the fold. The
   height must be definite, not a minimum: against an indefinite one `.studio`'s
   `flex-basis: 0%` falls back to its 9,000px content and contains nothing. */
body:has(> .studio) { display: flex; flex-direction: column; height: 100dvh; }

.studio {
  display: grid;
  /* Ratios, not percentages, so the format column can hold its 250px floor: at
     1101px a flat 20% gave it 220px and every control wrapped an orphan row.
     The two side columns become explicit widths once a resizer is dragged, and
     the canvas keeps whatever is left. */
  grid-template-columns:
    var(--col-format, minmax(250px, 1fr)) auto
    minmax(0, 2.5fr)
    auto var(--col-content, minmax(0, 1.5fr));
  /* One row, exactly the studio's height. An auto row grew to the 9,000px content
     tree, so the panel's own `overflow: auto` never had anything to scroll. */
  grid-template-rows: minmax(0, 1fr);
  flex: 1;
  min-height: 0;
}
/* Positioned so the panel is the containing block for its own absolutely
   positioned children: a `.visually-hidden` input 1,900px down the format panel
   was otherwise measured against the page and added 1,100px of blank scroll. */
.studio > section {
  min-height: 0; overflow: auto; padding: var(--space-2); position: relative;
}
/* No `border-inline-end` here: at this width the seam between panels is the
   draggable `.col-resizer` between them, which draws its own rule. The stacked
   layouts below hide the resizers and put the borders back. */
.panel-title {
  font-size: var(--fs--1); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); margin: 0 0 var(--space-2);
}

.banner {
  display: flex; align-items: center; gap: var(--space-2);
  background: var(--accent-soft); border-bottom: 1px solid var(--line);
  padding: var(--space-1) var(--space-2); font-size: var(--fs--1);
}
.banner[hidden] { display: none; }

.canvas { background: var(--surface-2); display: flex; flex-direction: column; gap: var(--space-2); }
.face-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-2); }
.face {
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius-sm);
  box-shadow: 0 1px 3px rgb(17 24 32 / 8%); position: relative; cursor: zoom-in; overflow: hidden;
}
.face svg { display: block; width: 100%; height: auto; }
/* The focused face is exactly the drawn page and nothing else.
   Both overlays place themselves as percentages of the page's units against this
   box, so it has to *be* the page: it was the panel's full width with the SVG
   centred inside at a fixed 672px, which offset every row highlight and every drag
   handle from the ink they refer to. The aspect comes from the plan's own geometry
   (`ui/preview.js`), the width takes what the panel offers, and the height cap is
   what keeps a portrait card on screen -- the phone preset is 2.2 times as tall as
   it is wide, so fitting it to the width alone made it twice the canvas's height. */
/* The face and the strip live in here, so this is what has to take the panel's
   height for their auto margins to have anything to share out. */
#face-area {
  flex: 1; min-block-size: 0; display: flex; flex-direction: column; gap: var(--space-2);
}

/* `margin: auto` on both axes, and on the strip below it, because whose space the
   leftover is depends on which axis binds. A 7x5 card is limited by the panel's
   width, so there is height to spare and it used to pile up under the strip; a phone
   card is limited by the height and the spare is at the sides. Three auto margins
   share whatever is going, so the card sits in the middle of the room above the
   strip and the strip stays on the floor, whichever case it is. The way to give the
   card more room is to widen the panel, which the seams either side of it now do. */
.face.focused {
  display: block; cursor: default; margin-inline: auto; margin-block: auto;
  aspect-ratio: var(--face-aspect, 1.4);
  inline-size: min(100%, calc(74vh * var(--face-aspect, 1.4)));
}
.face.focused svg { inline-size: 100%; block-size: 100%; }
.face-strip { display: flex; gap: var(--space-1); margin-top: auto; }
.face-strip .face { flex: 1; cursor: pointer; }
.face.current { outline: 2px solid var(--accent); }
/* Colour alone should not say "you are here": the tick survives greyscale. */
.face.current::after {
  content: '\2713'; position: absolute; inset-block-start: 0; inset-inline-end: 0;
  font-size: 0.75rem; line-height: 1.1; padding: 0 0.25em;
  color: var(--paper); background: var(--accent); border-end-start-radius: var(--radius-sm);
}
/* Focus must not be mistaken for "current": current hugs the edge and carries the
   tick, focus stands a thicker ring off the page, and a focused current face shows
   the ring and the tick together. Ordered after `.current` so focus wins the
   outline the two rules share. */
.face:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

.hit-layer { position: absolute; inset: 0; }
.hit {
  position: absolute; cursor: pointer; border-radius: 2px;
}
.hit:hover { background: rgb(11 103 163 / 12%); box-shadow: inset 0 0 0 1px var(--accent); }
/* Drawn inside its own box: a row is a few pixels tall and its face clips, so an
   offset ring would be cut off or land on the neighbouring row. */
.hit:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -1px;
  background: rgb(11 103 163 / 12%);
}

/* --- content tree -------------------------------------------------------- */

.tree { list-style: none; margin: 0; padding: 0; }
.tree > li { border-bottom: 1px solid var(--line-soft); }
.tree summary {
  display: flex; align-items: center; gap: var(--space-1);
  padding: 0.3em 0.2em; cursor: pointer; font-size: var(--fs--1);
}
.tree summary::-webkit-details-marker { display: none; }
.tree summary .count { color: var(--muted); font-size: 0.9em; margin-inline-start: auto; }
.tree .dot { width: 8px; height: 8px; border-radius: 2px; flex: none; }
.items { list-style: none; margin: 0; padding: 0 0 var(--space-1) var(--space-2); }
.items li { display: flex; gap: var(--space-1); align-items: baseline; font-size: var(--fs--1); }
.items label { display: flex; gap: 0.4em; align-items: baseline; cursor: pointer; }
.items li.flash { background: var(--accent-soft); }
/* What the last balance did, until the reader touches anything else. A bar down the
   inside edge rather than a fill, so it does not fight the row's own text or the
   hover state -- and a word as well as a colour, because roughly one man in twelve
   cannot use the colour on its own. */
.items li.added, .items li.removed { position: relative; padding-inline-start: 6px; }
.items li.added::before, .items li.removed::before {
  content: ''; position: absolute; inset-block: 1px; inset-inline-start: 0;
  inline-size: 3px; border-radius: 2px;
}
.items li.added::before { background: #B3261E; }
.items li.removed::before { background: #0B67A3; }
.items li.added::after, .items li.removed::after {
  font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.04em;
  flex: none; align-self: center;
}
.items li.added::after { content: 'added'; color: #B3261E; }
.items li.removed::after { content: 'off'; color: #0B67A3; }
.items .target { font-weight: 600; }
.items .gloss { color: var(--muted); }
/* Right against the panel edge, so the numbers line up as a column to scan rather
   than sitting where the gloss happens to end. Tabular figures for the same
   reason. */
.items .weight {
  margin-inline-start: auto; color: var(--muted); font-size: 0.85em;
  font-variant-numeric: tabular-nums; flex: none; padding-inline-start: var(--space-1);
}
/* The swatch is a button inside a <summary>, so it needs to look like the dot it
   replaced until the pointer is on it. */
.tree-color {
  padding: 0; border: 0; background: none; display: inline-flex; align-items: center;
  cursor: pointer; border-radius: 3px; flex: none;
}
.tree-color:hover, .tree-color:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}

.warnings { list-style: none; margin: 0 0 var(--space-2); padding: 0; font-size: var(--fs--1); }
.warnings li { padding: 0.3em 0.5em; border-radius: var(--radius-sm); margin-bottom: 2px; }
.warnings li.error { background: #fdecea; color: #7d1b15; }
.warnings li.warn { background: #fdf4e3; color: #7a5209; }
.warnings li.info { background: var(--surface); color: var(--muted); }

.status { font-size: var(--fs--1); color: var(--muted); }
.status[data-busy="1"]::after { content: " solving…"; color: var(--accent); }

/* Tablet to small laptop: the sheet on top at full width, the two control panels
   side by side under it. Three 1099px-wide bands left ~850px of dead space beside
   every control group. */
/* A grabbable seam between panels. 6px wide with a 1px rule down the middle, so it
   reads as the border it replaces until the pointer is on it. */
.col-resizer {
  inline-size: 6px; cursor: col-resize; background: none; border: 0; padding: 0;
  position: relative; touch-action: none;
}
.col-resizer::before {
  content: ''; position: absolute; inset-block: 0; inset-inline-start: 2px;
  inline-size: 1px; background: var(--line);
}
.col-resizer:hover::before, .col-resizer:focus-visible::before, .col-resizer.dragging::before {
  inset-inline-start: 1px; inline-size: 3px; background: var(--accent);
}
.col-resizer:focus-visible { outline: none; }

@media (min-width: 701px) and (max-width: 1100px) {
  .studio {
    grid-template-columns: minmax(250px, 22rem) minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
  }
  /* Two columns and a stacked canvas: there is no vertical seam to drag, so the
     panels carry their own borders again. */
  .studio > .col-resizer { display: none; }
  .studio > section:first-of-type { border-inline-end: 1px solid var(--line); }
  .studio > .canvas {
    grid-area: 1 / 1 / 2 / -1;
    border-inline-end: none; border-bottom: 1px solid var(--line);
  }
  .studio > section:first-of-type { grid-area: 2 / 1; }
  .studio > section:last-of-type { grid-area: 2 / 2; }
}

/* A phone scrolls the page as one -- three panels locked into one viewport would
   each be too short to read -- but the sheet comes first, because it is the thing
   being edited, and the two long panels scroll inside themselves rather than
   running the page to 11,000px. */
@media (max-width: 700px) {
  .studio > .col-resizer { display: none; }
  body:has(> .studio) { display: block; height: auto; }
  .studio { grid-template-columns: 1fr; grid-template-rows: none; }
  .studio > section { border-inline-end: none; border-bottom: 1px solid var(--line); }
  .studio > .canvas { order: -1; }
  .studio > section:first-of-type, .studio > section:last-of-type { max-height: 70vh; }
  .face-grid { grid-template-columns: 1fr; }
}

.hit.lit { background: rgb(11 103 163 / 18%); box-shadow: inset 0 0 0 1px var(--accent); }

/* --- balance proposals --------------------------------------------------- */

.diff {
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  padding: var(--space-2);
  margin-bottom: var(--space-2);
  font-size: var(--fs--1);
}
.diff[hidden] { display: none; }
.diff ul { list-style: none; margin: var(--space-1) 0; padding: 0; max-height: 32vh; overflow: auto; }
.diff li { display: flex; gap: 0.4em; align-items: baseline; padding: 0.15em 0; }
.diff .why { color: var(--muted); font-size: 0.92em; }

/* --- drag handles -------------------------------------------------------- */

.handle-layer { position: absolute; inset: 0; pointer-events: none; }
.handle {
  position: absolute; pointer-events: auto; background: transparent;
  transition: background 120ms;
}
.handle.vertical {
  top: 0; bottom: 0; width: 9px; margin-left: -4px; cursor: ew-resize;
}
.handle.horizontal {
  left: 0; right: 0; height: 9px; margin-top: -4px; cursor: ns-resize;
}
.handle:hover, .handle.dragging { background: rgb(11 103 163 / 22%); }
.handle.vertical:hover::after, .handle.vertical.dragging::after {
  content: ''; position: absolute; inset: 0 4px; background: var(--accent);
}
.handle.horizontal:hover::after, .handle.horizontal.dragging::after {
  content: ''; position: absolute; inset: 4px 0; background: var(--accent);
}
.handle-readout {
  position: absolute; left: 50%; top: 6px; transform: translateX(-50%);
  background: var(--ink); color: #fff; font-size: var(--fs--1);
  padding: 0.15em 0.5em; border-radius: var(--radius-sm); pointer-events: none;
  white-space: nowrap; z-index: 2;
}

/* --- quiz ---------------------------------------------------------------- */

dialog.quiz {
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--space-3); max-width: 34rem; width: calc(100% - 2rem);
  font: inherit; color: var(--ink);
}
dialog.quiz::backdrop { background: rgb(17 24 32 / 45%); }
dialog.quiz fieldset {
  border: 1px solid var(--line-soft); border-radius: var(--radius-sm);
  padding: var(--space-2); margin: 0 0 var(--space-2);
}
dialog.quiz legend { font-size: var(--fs--1); color: var(--muted); padding-inline: 0.3em; }
.quiz-option {
  display: flex; gap: 0.5em; align-items: baseline; padding: 0.2em 0; cursor: pointer;
}

.warnings .fixes { margin-top: 0.4em; gap: var(--space-1); }
button.fix {
  font-size: inherit; padding: 0.2em 0.6em; background: var(--paper);
  border-color: currentColor; color: inherit; font-weight: 600;
}
button.fix:hover { background: rgb(255 255 255 / 70%); }

/* --- flags and the multilingual picker label ------------------------------ */

.card-titles { min-width: 0; flex: 1 1 auto; }

/* A grid on the right of the header rather than a row underneath it: the header is
   already two lines tall for the name and endonym, so the flags cost nothing. */
.flags {
  /* `--flag-cols` is set per row by ui/flags.js so the block is two rows tall for
     any number of countries -- one column for a language spoken in two, three for
     one spoken in six. Two rows is the target because the title beside it is two
     lines, and a third row would make one card taller than its neighbours. */
  display: grid; grid-template-columns: repeat(var(--flag-cols, 2), auto); gap: 1px 3px;
  justify-content: end; align-content: start; margin-inline-start: auto;
  flex: 0 0 auto; line-height: 1.05;
}
.flag { font-size: 0.85rem; }
/* 0.66rem was 10.56px; 0.7rem is the 11px floor. */
.flags.as-codes .flag {
  font-size: 0.7rem; font-weight: 600; color: var(--muted); text-align: center;
  border: 1px solid var(--line); border-radius: 3px; padding: 0 0.2em; line-height: 1.3;
}
.flag.more {
  font-size: 0.7rem; color: var(--muted); align-self: center; text-align: center;
  position: relative; cursor: default;
}
.flag.more:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 3px; }
/* The flags "+3" stands for, revealed on the spot. Absolutely positioned so showing
   them cannot reflow the card underneath. */
.flag-rest {
  position: absolute; inset-inline-end: 0; top: calc(100% + 3px); z-index: 5;
  display: none; gap: 3px; padding: 0.25em 0.4em; white-space: nowrap;
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius-sm);
  box-shadow: 0 2px 8px rgb(17 24 32 / 12%);
}
.flag.more:hover .flag-rest, .flag.more:focus-visible .flag-rest { display: flex; }

/* The gallery lightbox.
   The card is the only opaque thing on screen: the dialog itself has no surface,
   no border and no title, so what floats on the dimmed page is the sheet. Every
   control sits over that dim -- the pair above, the paging carets on the card's
   own edges, the thumbnails and the two buttons below. Nothing that floats is
   drawn in a box: a panel over the card is a panel over the words. */
dialog.lightbox {
  /* The sheet's own proportion, 504 x 360pt, and how much room a caret takes.
     Both live here so the paper, the thumbnails and the width arithmetic all read
     the same numbers. */
  --card-aspect: 1.4;
  --caret: 4rem;
  border: 0; padding: 0; background: none; max-inline-size: none; max-block-size: none;
  inline-size: 100vw; block-size: 100vh; font: inherit; color: var(--paper);
  display: grid; grid-template-rows: auto minmax(0, 1fr); justify-items: center;
  align-content: center; gap: var(--space-2); padding-block: var(--space-3);
}
dialog.lightbox::backdrop { background: rgb(10 14 19 / 82%); }

/* The card, the strip below it and the pair above it share one width, so their
   left and right edges are the same edges.
   Only the card can state that width, and it cannot state it in CSS: it is the
   card's *rendered* height times the sheet's aspect, and on a short window the
   height is what binds. So `ui/lightbox.js` measures the face and publishes
   `--card-w`, and everything else takes it from there. The fallback is the width a
   tall window gives, which is what the card used to assume unilaterally -- the pair
   was hard-coded to it and the foot was sized by its own contents, so on a short
   window the language names and the thumbnails both sat outboard of the paper. */
.lightbox-stack {
  display: grid; grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--space-2); min-block-size: 0; block-size: 100%;
  justify-items: stretch;
  inline-size: min(94vw, 1600px);
}

/* The card, sized to whatever the window leaves once the pair and the strip have
   their rows. `aspect-ratio` is the sheet's own, so the carets sit on the paper's
   edge and not on the box around it. */
.lightbox-card {
  position: relative; min-block-size: 0;
  /* Three shrink-wrapped columns, centred as a group -- so each caret sits against
     the paper's edge rather than against the window's. With `minmax(0, 1fr)` in the
     middle the paper was centred inside a track far wider than itself, which left
     the carets a hundred pixels out at the margins. */
  display: grid; grid-template-columns: auto auto auto;
  align-items: center; justify-content: center; column-gap: 0.4rem;
}
.lightbox-face { grid-column: 2; grid-row: 1; }
.lightbox-face {
  /* The sheet's own proportion, 504 x 360pt. Both dimensions are capped, and the
     height is written as the smaller of the row and what the width allows: with
     `aspect-ratio` alone an explicit height wins, so on a narrow window the box
     stayed tall, the SVG letterboxed inside it, and the carets sat on the box's
     edge rather than on the paper's. */
  position: relative;
  inline-size: auto;
  block-size: min(100%, calc((min(94vw, 1600px) - 2 * var(--caret)) / var(--card-aspect)));
  aspect-ratio: var(--card-aspect);
  background: var(--paper); border-radius: var(--radius-sm);
  box-shadow: 0 1.4rem 3.5rem rgb(0 0 0 / 45%);
  overflow: hidden;
}
.lightbox-face > svg, .lightbox-face > img {
  display: block; inline-size: 100%; block-size: 100%;
}
/* The thumbnail holding the frame while a pair typesets. It is a raster of the
   first face at gallery size, so it is blurred at card size -- which is the honest
   signal that the real thing has not arrived yet. */
.lightbox-holding { object-fit: cover; opacity: 0.55; }
.lightbox-failed {
  position: absolute; inset-block-end: 0; inset-inline: 0; margin: 0;
  padding: var(--space-2); background: var(--surface); color: var(--ink);
  font-size: var(--fs--1);
}

/* Carets on the card's edges, shown once the pointer is on the card -- or once
   anything in it has the keyboard, so they are never unreachable.
   No box: they sit over the sheet's own rows, and a panel there hides two of them.
   So they are ink rather than paper, semi-transparent, and large enough to read
   as a control at that weight. */
/* Always faintly there, so the card reads as one of a series before the pointer
   arrives -- and `background: none` unconditionally, on a selector that beats the
   sheet's generic `button:hover`.
   That specificity was the whole of the left/right asymmetry: the old override was
   guarded `:not(:disabled)`, so on a *disabled* caret `button:hover` won and
   painted `--surface` into a 5px-radius box. `prev` is disabled on the first face,
   which is where the lightbox opens -- so the box looked like a property of the
   left caret when it was a property of being at the end of the series. */
/* Beside the card rather than on top of it, and twice the size.
   On the paper they had to carry a halo of `--paper` to stay legible over the type;
   outside it they sit on the backdrop, so the halo goes and the colour flips to
   paper-on-dark. `.lightbox-card` is a three-column grid, so the card keeps its own
   width and the carets take the gutters -- which is why the caret column is part of
   `--card-w`'s siblings rather than something the card has to make room for. */
.lightbox-step {
  grid-row: 1; align-self: center; justify-self: center;
  padding: 0; border: 0; border-radius: 0; background: none; backdrop-filter: none;
  display: flex; align-items: center; justify-content: center;
  font-size: 10rem; line-height: 0.6; cursor: pointer;
  color: rgb(255 255 255 / 38%);
}
.lightbox-step.prev { grid-column: 1; }
.lightbox-step.next { grid-column: 3; }
.lightbox-step:hover, .lightbox-step:focus-visible {
  color: rgb(255 255 255 / 70%); outline: none; background: none;
}
/* Brighter on hover, and outlined -- but the outline is on the glyph, drawn as a
   halo of paper, so it separates the caret from the type under it without covering
   any. Only where there is a card to go to: the ends of the series stay faint. */
.lightbox-step:not(:disabled):hover, .lightbox-step:not(:disabled):focus-visible {
  color: var(--paper); outline: none; background: none;
}
.lightbox-step:disabled { opacity: 0.34; cursor: default; }

/* The strip, and the two things to do with the card, on one row exactly as tall as
   a thumbnail: the buttons divide that height rather than setting their own. */
.lightbox-foot {
  /* Aligned to the *paper*, not to the stack -- the stack is wider because it holds
     a caret on each side. `--card-w` is the paper's measured width, published by
     `ui/lightbox.js` because it is the card's rendered height times the sheet's
     aspect and CSS cannot state it. */
  inline-size: var(--card-w, 100%); margin-inline: auto;
  min-inline-size: 0;
  display: flex; align-items: stretch; justify-content: space-between;
  gap: var(--space-3);
}
/* Every thumbnail fits, rather than eight of them scrolling.
   A grid of equal fractions, so the row is exactly as wide as the space the buttons
   leave and each thumb takes 1/n of it -- ten faces give narrower thumbs, not a
   scrollbar. `minmax(0, 1fr)` is what lets a track go below its content's width.
   The thumbs keep the sheet's aspect, so their height follows and the foot's own
   height follows that. */
.lightbox-strip {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  gap: 0.4rem; min-inline-size: 0; flex: 1 1 0;
}
.lightbox-thumb {
  padding: 0; border: 1px solid rgb(255 255 255 / 25%); border-radius: 3px;
  background: var(--paper); cursor: pointer; overflow: hidden;
  /* No width of its own: the grid track decides, capped so two faces do not become
     two enormous thumbnails. */
  inline-size: 100%; max-inline-size: 8rem; aspect-ratio: var(--card-aspect, 1.4);
}
.lightbox-thumb > svg { display: block; inline-size: 100%; block-size: 100%; }
.lightbox-thumb:hover { border-color: rgb(255 255 255 / 70%); }
/* The one you are looking at, said twice: a ring the width of the gap so the row
   does not shift, and `aria-selected` for anyone not seeing the ring. */
.lightbox-thumb[aria-selected='true'] {
  border-color: var(--paper); outline: 2px solid var(--accent-lift); outline-offset: 1px;
}
/* Sized to the strip beside it: `flex: 0 1 auto` so a narrow window shrinks the
   button column too rather than squeezing the thumbnails alone, with a floor so the
   labels stay readable. */
.lightbox-do {
  display: flex; flex-direction: column; gap: 0.4rem;
  flex: 0 1 auto; min-inline-size: 5.5rem;
}
.lightbox-do .btn {
  flex: 1 1 0; min-block-size: 0; padding-block: 0;
  display: flex; align-items: center; justify-content: center; text-align: center;
}

/* The pair, with the mark between the languages on the centre line -- the two names
   are different lengths, so centring the group as a whole put the mark off centre.
   Three tracks, the outer two equal, and each name pushed against the middle. */
.lightbox-pair {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: var(--space-2); font-size: var(--fs-1);
  inline-size: var(--card-w, min(94vw, 1600px));
  margin-inline: auto;
}
.pair-side { display: flex; min-inline-size: 0; }
.pair-side.start { justify-content: flex-end; }
.pair-side.end { justify-content: flex-start; }

/* Both languages are controls and the mark is a third, and none of them draws a
   box on hover: these sit right above the sheet and a box there reads as chrome
   over the thing you came to look at. Colour, and an underline on the names. */
/* The pair's own override has to undo `min-width` and `text-align` too, or the
   arrow looks off-centre while sitting exactly on the grid's centre line: the
   button keeps a 7rem floor with its text at the left edge, so the *left* name
   ends up to 3rem short of the arrow while the right one hugs it -- and the gap
   changes with the language, since it is `7rem - width(endonym)`. */
.lightbox-pair .lang-picker-button {
  min-width: 0; text-align: center;
}
.lightbox-pair .lang-picker-button {
  background: none; border: 0; color: var(--paper);
  font-size: inherit; font-weight: 600; cursor: pointer; padding: 0.15em 0.4em;
  text-decoration: 1px solid underline transparent; text-underline-offset: 0.25em;
}
.lightbox-pair .lang-picker-button:hover,
.lightbox-pair .lang-picker-button:focus-visible {
  /* The picker's own `:hover` fills a surface in, and it comes later at the same
     specificity, so this has to say so rather than rely on the base rule. */
  color: var(--accent-lift); outline: none; background: none;
  text-decoration-color: currentColor;
}
.lightbox-swap {
  display: grid; place-items: center; cursor: pointer; padding: 0.2rem;
  background: none; border: 0; color: rgb(255 255 255 / 65%);
}
.lightbox-swap svg { inline-size: 1.9rem; block-size: 1.2rem; }
/* The carets either side of the card need no help -- U+2039 is Bidi_Mirrored, so it
   draws the other way round on its own, and `grid-column: 1` is already the side a
   reader of Arabic starts from. An SVG path is not text and gets neither, so the
   one arrow drawn rather than typed has to be turned over here. */
[dir="rtl"] .lightbox-swap svg { transform: scaleX(-1); }
.lightbox-swap path { fill: none; stroke: currentColor; stroke-width: 1.6; }
.lightbox-swap .swap-two { display: none; }
/* One head normally, because that is what the pair means; two on hover, because
   that is what pressing it does. */
.lightbox-swap:hover, .lightbox-swap:focus-visible {
  color: var(--paper); outline: none; background: none;
}
.lightbox-swap:hover .swap-one, .lightbox-swap:focus-visible .swap-one { display: none; }
.lightbox-swap:hover .swap-two, .lightbox-swap:focus-visible .swap-two { display: block; }

/* Grid rather than a text button, so the multiplication sign is centred on both
   axes instead of sitting on a baseline. */
.lightbox-close {
  position: fixed; inset-inline-end: var(--space-3); top: var(--space-3);
  display: grid; place-items: center;
  border: 1px solid rgb(255 255 255 / 25%); background: rgb(17 24 32 / 45%);
  color: rgb(255 255 255 / 75%); border-radius: var(--radius-sm);
  inline-size: 2.6rem; block-size: 2.6rem; padding: 0;
  font-size: 1.7rem; line-height: 1; cursor: pointer;
}
.lightbox-close:hover { background: rgb(17 24 32 / 80%); color: var(--paper); }

/* Narrow: the strip wraps under the buttons, and the carets stay on the card
   rather than moving out of it -- there is no room beside it to move to. */
@media (max-width: 640px) {
  .lightbox-foot { flex-direction: column; align-items: center; }
  .lightbox-do { flex-direction: row; }
  .lightbox-do .btn { padding-block: 0.35rem; }
  .lightbox-thumb { max-inline-size: 4.4rem; }
  .lightbox-step { font-size: 5rem; }
  dialog.lightbox { --caret: 2rem; }
}

/* The Custom row a numeric setting opens: a slider to feel the range, a box to type
   or step an exact value. Hidden until Custom is chosen, so the ladder stays the
   simple thing you see first. */
.numeric-custom {
  display: flex; align-items: center; gap: var(--space-2);
  margin-top: var(--space-1); padding: var(--space-1) 0;
}
/* `display: flex` above outranks the user agent's `[hidden] { display: none }`, so
   without this the slider sat under every ladder whether Custom was chosen or not,
   showing a value that was not in effect. Same shape as `.banner[hidden]`. */
.numeric-custom[hidden] { display: none; }
.numeric-custom input[type="range"] { flex: 1 1 auto; min-width: 0; accent-color: var(--accent); }
.numeric-box {
  flex: 0 0 4.6rem; font: inherit; font-size: var(--fs--1); color: var(--ink);
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 0.15em 0.3em;
}
.numeric-box:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.numeric-unit { font-size: var(--fs--1); color: var(--muted); flex: 0 0 auto; }

/* The Custom row for colours: one swatch per section role, plus the ink. Native
   colour inputs, because every platform already has a picker better than one drawn
   here and this is the one setting where the reader needs to see the result. */
.swatches { flex-wrap: wrap; gap: 5px; }
.swatch {
  inline-size: 2rem; block-size: 1.7rem; padding: 0; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: none;
}
.swatch::-webkit-color-swatch-wrapper { padding: 2px; }
.swatch::-webkit-color-swatch { border: none; border-radius: 2px; }
.swatch::-moz-color-swatch { border: none; border-radius: 2px; }
.swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Back to the gallery. Rightmost in the header, but quiet: the export button beside
   it is the loud one. */
.back-link { white-space: nowrap; }

/* The "I speak" control. Collapsed it is just the endonym; the list adds the name
   in the reader's own language, greyed and pushed to the trailing edge so the two
   read as a name and a gloss rather than as one long string. */
.lang-picker { position: relative; }
.lang-picker-button {
  font: inherit; color: var(--ink); background: var(--paper);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 0.25em 1.6em 0.25em 0.6em; cursor: pointer; min-width: 7rem; text-align: start;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: right 0.7em top 55%, right 0.42em top 55%;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
}
.lang-picker-button:hover { background-color: var(--surface); }
.lang-picker-list {
  position: absolute; inset-inline-end: 0; top: calc(100% + 4px); z-index: 20;
  margin: 0; padding: 0.25rem; list-style: none; min-width: 100%; max-height: 60vh;
  overflow-y: auto; background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius-sm); box-shadow: 0 4px 16px rgb(17 24 32 / 14%);
}
.lang-picker-option {
  display: flex; gap: var(--space-3); align-items: baseline; justify-content: space-between;
  padding: 0.3em 0.6em; border-radius: var(--radius-sm); cursor: pointer; white-space: nowrap;
}
.lang-picker-option:hover { background: var(--surface-2); }
.lang-picker-option:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.lang-picker-option.current { font-weight: 600; }
.lang-picker-aside { color: var(--muted); font-size: var(--fs--1); text-align: end; }

/* A static collage: the reader's own label leads, the rest recede. No motion. */
#reader-label {
  display: flex; align-items: baseline; gap: 0.45em; flex-wrap: wrap;
  justify-content: flex-end; row-gap: 0;
}
.speak { white-space: nowrap; color: var(--muted); }
.speak.lead { color: var(--ink); font-weight: 600; }

@media (max-width: 760px) {
  /* On a narrow screen the collage would push the picker off; the lead is enough. */
  .speak:not(.lead) { display: none; }
}

/* --- glyph settings controls ---------------------------------------------- */

.panel-field { margin-bottom: var(--space-3); }
.panel-field-title {
  display: block; font-size: var(--fs--1); color: var(--muted); margin-bottom: 0.25em;
}
.panel-note { margin: 0.35em 0 0; }

/* Segments hang from the top of their row, so a caption that wraps to two lines
   ("Credit-card size") cannot push its row-mates' glyphs down. */
.segmented { display: flex; flex-wrap: wrap; align-items: start; gap: 4px; }
.segment {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 4px 5px 3px; min-width: 44px; border: 1px solid var(--line);
  border-radius: var(--radius-sm); background: var(--paper); cursor: pointer;
}
.segment:hover { background: var(--surface); }
.segment.current {
  border-color: var(--accent); background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.segment-caption {
  /* 11px floor; two reserved lines so the segment boxes in a row stay even. */
  font-size: 0.7rem; line-height: 1.1; min-height: 2.2em;
  color: var(--muted); text-align: center;
  /* Wide enough for the longest caption in any catalogue to sit on one line, and
     the button grows with it -- a row of three becomes a row of two where it has
     to. The cap used to be 6.5em with `overflow-wrap: anywhere`, which broke
     "Romanisation" after the "o": a mid-syllable break reads as a rendering fault,
     and it is worse than one fewer segment per row. `break-word` is the last
     resort for a word longer than the panel. */
  max-width: 9em; overflow-wrap: break-word;
}
.segment.current .segment-caption { color: var(--accent); font-weight: 600; }
/* A fact rather than a name, so it is set apart from the caption and stays a fact
   when the caption goes bold on selection. Tabular figures because these are read
   against each other -- 2.17:1 and 2.22:1 differ in one digit. */
.segment-sub {
  font-size: 0.65rem; line-height: 1; margin-top: -0.35em;
  color: var(--muted); text-align: center;
  font-variant-numeric: tabular-nums;
}

svg.glyph { display: block; }
svg.glyph .g-page { fill: #fff; stroke: var(--muted); stroke-width: 0.8; }
svg.glyph .g-col { fill: #ccd6de; }
svg.glyph .g-ink { fill: #55606b; }
svg.glyph .g-ink.faint { fill: #aeb8c1; }
svg.glyph .g-shade { fill: #eef2f5; }
svg.glyph .g-accent { fill: var(--accent); }
svg.glyph .g-cut { stroke: var(--accent); stroke-width: 1; stroke-dasharray: 2 2; fill: none; }
svg.glyph .g-mark { fill: var(--accent); }
/* A column toggle draws the real cell from the current pair, so the text is the
   glyph rather than a label beside it. The dashed rule is the other case: a column
   the corpus has nothing for in this language. */
svg.glyph .g-accent-text { fill: var(--accent); }
svg.glyph .g-accent-line { stroke: var(--accent); stroke-width: 1.1; opacity: 0.55; }
/* The count now has a reserved strip along the bottom of the 30-unit box rather
   than being tucked into the corner on top of the drawing, so it is centred and
   needs no halo to stay legible. */
svg.glyph .g-count {
  font-size: 7.5px; fill: var(--muted); text-anchor: middle;
}
.segment.current svg.glyph .g-col { fill: #a9c6da; }

svg.glyph .g-letter { fill: #55606b; font-weight: 700; text-anchor: middle; font-family: var(--ui); }
svg.glyph .g-bracket { stroke: var(--accent); stroke-width: 1; fill: none; }

.tree-icon { width: 13px; height: 13px; flex: none; }

/* --- add a term ---------------------------------------------------------- */

details.add-term {
  margin-top: var(--space-3); border-top: 1px solid var(--line); padding-top: var(--space-2);
}
details.add-term > summary {
  cursor: pointer; font-size: var(--fs--1); font-weight: 600; color: var(--accent);
}
details.add-term .field { margin-bottom: var(--space-1); }
details.add-term input[type="text"] { width: 100%; }
.tag.mine { border-color: var(--accent); color: var(--accent); font-size: 0.7rem; }
svg.glyph .g-sample { fill: var(--ink); text-anchor: middle; }

/* --- duplex check -------------------------------------------------------- */

.duplex { display: flex; flex-direction: column; gap: var(--space-2); }
.duplex-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-2);
}
.duplex-card {
  position: relative; background: #fff; border: 1px solid var(--line);
  border-radius: var(--radius-sm); overflow: hidden;
}
.duplex-side { position: relative; }
.duplex-side svg { display: block; width: 100%; height: auto; }
/* The back is drawn as red words only: its shading and rules are dropped so the
   front stays readable underneath. Presentation attributes on the SVG lose to
   these rules, so no !important is needed. */
.duplex-side.back { position: absolute; inset: 0; }
.duplex-side.back svg rect { fill: none; }
.duplex-side.back svg text { fill: #c0392b; }
.duplex-side.back svg g { stroke: #c0392b; }
.duplex-label {
  position: absolute; inset-block-start: 2px; inset-inline-start: 4px;
  font-size: var(--fs--1); font-weight: 600; color: var(--accent);
  background: rgb(255 255 255 / 75%); padding: 0 0.3em; border-radius: 3px;
}

/* --- the quick export page ------------------------------------------------ */

.sheet-page { padding-block: var(--space-3) var(--space-4); }
.sheet-head { margin-bottom: var(--space-3); }
.sheet-head .lede { margin-bottom: var(--space-1); }
/* Where the language is spoken, under its name rather than adrift at the far edge. */
.sheet-where { margin: 0.15rem 0 var(--space-2); }
.sheet-where .flags { justify-content: start; margin-inline-start: 0; grid-auto-flow: column; grid-template-columns: none; }
.sheet-where .flag { font-size: 1.05rem; }
.sheet-body {
  display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: var(--space-4);
  align-items: start;
}
/* No `position: sticky` here: the options are taller than the viewport at every
   width, and `align-items: start` makes their grid area exactly their own height,
   so the rule had no room to slide in and only looked like it did something. */
.export-block {
  display: flex; flex-direction: column; gap: var(--space-2);
  border-top: 1px solid var(--line); padding-top: var(--space-3); margin-top: var(--space-3);
}
button.big { justify-content: center; padding: 0.6em 1em; font-size: var(--fs-1); }
.export-block .row > button { flex: 1; justify-content: center; }
/* Two across, so a four-face sheet reads as the two sheets you will print. */
.sheet-preview .face-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }

@media (max-width: 860px) {
  .sheet-body { grid-template-columns: 1fr; }
  .sheet-preview .face-grid { grid-template-columns: 1fr; }
}

/* A long printer name should wrap in its own menu rather than being cut off. */
.field select { text-overflow: ellipsis; }
/* The printer presets are the one control whose text is *data* rather than interface
   copy: the names come from `data/registry/paper.csv` and are English regardless of
   who is reading, so in an Arabic interface a name like `Epson ET-8550 · 5x7 photo,
   bordered` was laid out right-to-left and ellipsised from its own beginning, which
   cut the brand off and left `...on ET-8550`. `plaintext` lets each option settle its
   direction from its own first strong character, which is the same rule the sheet's
   own runs use and for the same reason. */
#paper { unicode-bidi: plaintext; }
.sheet-options .field select { font-size: var(--fs--1); }

/* --- the donate link ------------------------------------------------------ */

/* On the two working pages, not the gallery: the gallery is the front door and has
   its own business. Named for GiveWell in the label rather than left as a bare
   "Donate", because a donate button on a personal site reads as "pay the author"
   and this one does not do that -- it goes to a charity evaluator. Saying so is the
   whole point of it, so it belongs in the text and not only in the href. */
.page-footer {
  display: flex; justify-content: center;
  padding: var(--space-2) var(--space-3) var(--space-3);
  /* The studio is a flex column of exactly the viewport height, so this must not
     be allowed to shrink -- and must not grow either, or it takes the canvas's
     room. */
  flex: 0 0 auto;
}
.donate {
  display: inline-flex; align-items: center; gap: 0.4em;
  font-size: var(--fs--1); color: var(--muted); text-decoration: none;
  border: 1px solid var(--line); border-radius: 999px;
  padding: 0.3em 0.85em; background: var(--paper);
}
.donate:hover, .donate:focus-visible {
  color: var(--ink); border-color: var(--muted); outline: none;
}
/* The heart is the only coloured thing in the bar, which is what keeps the link
   subtle while still reading as a donate button. */
.donate-mark { color: #b3261e; font-size: 1.05em; line-height: 1; }
