/*
 * Moodboard Maker — application shell.
 *
 * The UI stays deliberately quiet: warm neutrals, one accent, no chrome that
 * competes with the board. A tool for composing images has to let the images
 * be the loudest thing on screen.
 */

:root {
  --bg: #EFEBE4;
  --surface: #FBF9F6;
  --surface-2: #F4F1EB;
  --line: #DDD6CB;
  --line-strong: #C6BDAF;
  --ink: #1E1B17;
  --ink-2: #5C554B;
  --ink-3: #8C8378;
  --accent: #2F5D50;
  --accent-hover: #24483E;
  --accent-soft: #E4EDE9;
  --danger: #A33A2E;

  /*
   * The three surfaces, in the order the eye should rank them: the board is
   * brightest, the panels sit just below it, the stage floor is darkest.
   *
   * They were the wrong way round. Panels were near-white, which made two
   * columns of controls the lightest thing in the window and left the board —
   * the only reason any of this exists — reading as the middle column.
   *
   * The floor and the board's shadow move together. A recessed well under a
   * flat board looks like a mistake, and a floating board over a flat floor
   * looks like a rendering bug.
   */
  --panel: #F1EDE6;
  --well: #E5E0D7;
  --board-shadow: 0 2px 4px rgba(30, 27, 23, .06), 0 12px 32px rgba(30, 27, 23, .16);

  --radius: 10px;
  --radius-sm: 7px;
  --shadow-sm: 0 1px 2px rgba(30, 27, 23, .06);
  --shadow: 0 8px 28px rgba(30, 27, 23, .10);
  --shadow-lg: 0 24px 60px rgba(30, 27, 23, .18);
  --panel-w: 300px;
  --topbar-h: 56px;
  --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --serif: "Fraunces", Georgia, serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #171614;
    --surface: #201E1B;
    --surface-2: #2A2724;
    --line: #35312C;
    --line-strong: #4A443D;
    --ink: #F0ECE5;
    --ink-2: #B5AEA4;
    --ink-3: #857E75;
    --accent: #7FBFA8;
    --accent-hover: #96CDB9;
    --accent-soft: #24352F;
    --danger: #E08476;
    /* Darker than the panels, not lighter: a well in a dark room is a hole,
       and a cream board dropped into it is unmistakably the subject. */
    --panel: #1D1B19;
    --well: #121110;
    --board-shadow: 0 2px 6px rgba(0, 0, 0, .5), 0 18px 48px rgba(0, 0, 0, .55);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);
    --shadow: 0 8px 28px rgba(0, 0, 0, .4);
    --shadow-lg: 0 24px 60px rgba(0, 0, 0, .55);
  }
}

* { box-sizing: border-box; }

/* ── scrollbars ──────────────────────────────────────────────────────
 *
 * Quiet enough to disappear against the panels, wide enough to actually
 * grab. 10px is the floor for a comfortable pointer target — the 4px
 * hairline that photographs well is genuinely worse to use.
 *
 * Firefox gets the two properties it supports; WebKit gets the pseudo
 * elements. Both are styled, so the app does not have one look on Chrome
 * and another on Firefox.
 */

* {
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }

::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 100px;
  /* Transparent border with background-clip insets the thumb without
     shrinking the hit area — the whole 10px stays draggable. */
  border: 3px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--ink-3);
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:active { background: var(--accent); background-clip: padding-box; }

/* Where both axes scroll, the corner would otherwise render as a grey square. */
::-webkit-scrollbar-corner { background: transparent; }

/*
 * The editor is a fixed full-height layout and must not scroll; the front page
 * is a document and must. Scoping this to a class on <html> rather than on
 * <body> is the whole point — a rule on the body cannot undo overflow:hidden
 * on its parent, which is why the front page would not scroll at all.
 */
html, body { margin: 0; }
html.app, html.app body { height: 100%; overflow: hidden; }

body {
  font: 14px/1.5 var(--sans);
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

/* ── top bar ─────────────────────────────────────────────────────── */

.topbar {
  height: var(--topbar-h);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  padding: 0 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

/* `justify-self` so the link is the width of its words: as a grid item filling
   the 1fr column it measured 558px on a laptop, making the whole left third of
   the bar a link home. The vertical padding is what makes it a tap target
   rather than a 21px line of text. */
.brand {
  display: flex; align-items: center; gap: 9px;
  justify-self: start; padding: 12px 0;
  font-weight: 600; color: var(--ink); text-decoration: none;
}

/* The mark is a miniature board with an arch hero — see web/assets/logo.svg.
   It replaces a plain gradient square that said nothing about the product. */
.brand-mark {
  width: 20px; height: 20px;
  background: url('/assets/logo.svg') center / contain no-repeat;
}

/* The bar has a fixed height, so a wrap does not make room — it overflows.
   Measured on a 390px phone, where "Moodboard Maker" broke over two lines and
   the second one sat outside the bar. */
.brand-name { letter-spacing: -0.01em; white-space: nowrap; }

.topbar-center { justify-self: center; }

.board-title {
  font: 500 15px var(--sans);
  color: var(--ink);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 6px 12px;
  min-width: 260px;
  text-align: center;
}
.board-title:hover { border-color: var(--line); }
.board-title:focus { outline: none; border-color: var(--accent); background: var(--bg); }

.topbar-actions { display: flex; align-items: center; gap: 8px; justify-self: end; }

.plan-badge {
  font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  padding: 4px 9px; border-radius: 100px;
  background: var(--accent-soft); color: var(--accent);
}
.plan-badge.is-warning { background: #F6E9D8; color: #8A5A1E; }

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

.btn {
  font: 500 13px var(--sans);
  padding: 7px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background .12s, border-color .12s, color .12s, transform .06s;
  white-space: nowrap;
  text-decoration: none;
  display: inline-block;
  text-align: center;
}
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { opacity: .42; cursor: not-allowed; }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); }
@media (prefers-color-scheme: dark) { .btn-primary { color: #14201C; } }

.btn-ghost { background: transparent; color: var(--ink-2); border-color: var(--line); }
.btn-ghost:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); border-color: var(--line-strong); }

.btn-danger { color: var(--danger); border-color: color-mix(in oklab, var(--danger) 35%, transparent); }
.btn-danger:hover:not(:disabled) { background: color-mix(in oklab, var(--danger) 10%, transparent); }

.btn-block { width: 100%; }
.btn-lg { padding: 11px 16px; font-size: 14px; }

.btn-icon {
  width: 26px; height: 26px; padding: 0; font-size: 15px; line-height: 1;
  background: transparent; color: var(--ink-2); border-color: var(--line);
}
.btn-icon:hover { background: var(--surface-2); color: var(--ink); }

.btn-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; }

/* ── workspace ───────────────────────────────────────────────────── */

/*
 * `dvh`, with `vh` underneath it for anything too old to know the unit.
 *
 * On a phone `100vh` is the height the page would have if the browser's own
 * bars were gone — it does not shrink when the address bar is showing. So a
 * layout built on it is taller than the glass, and everything anchored to the
 * bottom of it sits below the fold. `dvh` is the height there actually is.
 */
.workspace {
  height: calc(100vh - var(--topbar-h));
  height: calc(100dvh - var(--topbar-h));
  display: grid;
  grid-template-columns: var(--panel-w) 1fr var(--panel-w);
}

.panel {
  background: var(--panel);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 16px;
  /* Reserves the scrollbar's width whether or not it is showing, so switching
     tabs does not shift every control sideways by ten pixels. */
  scrollbar-gutter: stable;
}
.panel-left { border-right: 1px solid var(--line); padding-top: 0; }
.panel-right { border-left: 1px solid var(--line); }

/* ── tabs ────────────────────────────────────────────────────────── */

.tabs {
  display: flex;
  gap: 2px;
  position: sticky;
  top: 0;
  background: var(--panel);
  padding: 12px 0 10px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--line);
  z-index: 2;
}

.tab {
  flex: 1;
  font: 500 12.5px var(--sans);
  padding: 7px 4px;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
}
.tab:hover { color: var(--ink); background: var(--surface-2); }
.tab.is-active { color: var(--accent); background: var(--accent-soft); }

.tabpanel { display: none; padding-top: 12px; }
.tabpanel.is-active { display: block; }

/* ── form controls ───────────────────────────────────────────────── */

.field { display: block; margin-bottom: 14px; border: none; padding: 0; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }

.field-label {
  display: block;
  font-size: 11.5px; font-weight: 600; letter-spacing: .03em; text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 6px;
}
.field-label em { font-style: normal; font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--ink-2); }

.field-hint { display: block; font-size: 12px; color: var(--ink-3); margin-top: 5px; line-height: 1.4; }

.input {
  width: 100%;
  font: 400 13.5px var(--sans);
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
}
.input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

.textarea { resize: vertical; min-height: 62px; line-height: 1.45; }

.select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-3) 50%),
                    linear-gradient(135deg, var(--ink-3) 50%, transparent 50%);
  background-position: calc(100% - 15px) 52%, calc(100% - 10px) 52%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 30px;
}

.check {
  display: flex; align-items: center; gap: 8px;
  font-size: 13px; color: var(--ink-2);
  margin-bottom: 8px; cursor: pointer;
}
.check input { accent-color: var(--accent); width: 15px; height: 15px; }

.color-input {
  width: 100%; height: 34px; padding: 3px;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); cursor: pointer;
}

/*
 * The accent belongs to every range and every checkbox, not to the ones that
 * happen to sit in the right wrapper. Scoping it to `.slider-field` meant two
 * sliders added later came out in the browser's default blue, next to a column
 * of green ones — the kind of inconsistency nobody writes on purpose and
 * everybody sees.
 */
input[type="range"] { accent-color: var(--accent); }

.slider-field { display: block; margin-bottom: 14px; }
.slider-field input[type="range"] { width: 100%; }
.slider-field output { float: right; font-weight: 500; color: var(--ink-2); text-transform: none; letter-spacing: 0; }

.search-row { display: grid; grid-template-columns: 1fr auto; gap: 6px; }

.rule { border: none; border-top: 1px solid var(--line); margin: 18px 0; }

/* ── chips ───────────────────────────────────────────────────────── */

/*
 * Canvas picker.
 *
 * Each card draws the preset at its real proportion inside a fixed box, so a
 * 9:16 story and a 3:1 banner are told apart at a glance rather than read off
 * a dropdown line and imagined.
 */
.canvas-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; }

.canvas-card {
  display: grid;
  gap: 4px;
  justify-items: center;
  padding: 6px 3px 5px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}
.canvas-card:hover { border-color: var(--line-strong); }
.canvas-card.is-active { border-color: var(--accent); background: var(--accent-soft); }
.canvas-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.canvas-proportion { display: grid; place-items: center; height: 36px; }
.canvas-proportion i {
  display: block;
  background: var(--ink-3);
  border-radius: 1px;
  opacity: 0.5;
}
.canvas-card.is-active .canvas-proportion i { background: var(--accent); opacity: 1; }

.canvas-name {
  font: 500 9px var(--sans);
  color: var(--ink-3);
  line-height: 1.15;
  text-align: center;
  /* Two lines' worth whether the name needs them or not. "A4 Landscape" wraps
     and "Story" does not, which left the second row of cards taller than the
     first for no reason a reader could see. */
  min-height: calc(9px * 1.15 * 2);
}
.canvas-card.is-active .canvas-name { color: var(--accent); }

/*
 * Template picker.
 *
 * The thumbnails are real layouts rendered by the engine, so the grid's job is
 * to get out of their way: a light card, a consistent well for the picture, and
 * the aspect ratio left visible — a 9:16 story and a 3:1 banner should not be
 * squeezed into the same box, because their proportion *is* the difference.
 */
/*
 * Sixteen cards is around 950px — more than the whole panel. So the picker
 * scrolls inside itself at about four rows, which keeps every template one
 * flick away instead of pushing tone and canvas off the bottom of the page.
 */
.template-grid {
  display: grid;
  gap: 12px;
  max-height: 336px;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-right: 4px;
  margin-right: -4px;
}

/*
 * Fade the edge a scroll continues past.
 *
 * A fixed-height scroll area cuts whatever happens to be at the boundary, and
 * a card sliced cleanly in half reads as broken rather than as "there is more
 * below" — the hint sitting underneath then looks like it belongs to the
 * severed row. The mask is applied per edge from JavaScript, so the last row
 * is not left permanently faded once you have scrolled to the end.
 */
.scroll-edge { --fade: 26px; }
.scroll-edge.can-up.can-down {
  mask-image: linear-gradient(to bottom, transparent, black var(--fade), black calc(100% - var(--fade)), transparent);
}
.scroll-edge.can-down:not(.can-up) {
  mask-image: linear-gradient(to bottom, black calc(100% - var(--fade)), transparent);
}
.scroll-edge.can-up:not(.can-down) {
  mask-image: linear-gradient(to bottom, transparent, black var(--fade));
}

.template-group { display: grid; gap: 6px; }
.template-group-label {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 2px 0 3px;
  background: var(--panel);
  font: 600 9.5px var(--sans);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.template-cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; }

.template-card {
  display: grid;
  gap: 5px;
  padding: 5px 5px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
  cursor: pointer;
  text-align: left;
  transition: border-color 0.12s ease, background 0.12s ease;
}
.template-card:hover { border-color: var(--line-strong); }
.template-card.is-active { border-color: var(--accent); background: var(--accent-soft); }
.template-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.template-thumb {
  display: grid;
  place-items: center;
  height: 54px;
  border-radius: 2px;
  background:
    linear-gradient(45deg, rgba(0, 0, 0, 0.028) 25%, transparent 25% 75%, rgba(0, 0, 0, 0.028) 75%) 0 0 / 8px 8px,
    linear-gradient(45deg, rgba(0, 0, 0, 0.028) 25%, transparent 25% 75%, rgba(0, 0, 0, 0.028) 75%) 4px 4px / 8px 8px;
}
.template-thumb svg {
  max-width: 100%;
  max-height: 54px;
  display: block;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.14);
}

.template-name {
  font: 500 10.5px var(--sans);
  color: var(--ink-2);
  line-height: 1.25;
}
.template-card.is-active .template-name { color: var(--accent); }

.tone-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 5px; }

.tone-btn {
  display: flex; flex-direction: column; gap: 5px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
  cursor: pointer;
  text-align: left;
}
.tone-btn:hover { border-color: var(--line-strong); }
.tone-btn.is-active { border-color: var(--accent); background: var(--accent-soft); }
.tone-swatches { display: flex; height: 16px; border-radius: 3px; overflow: hidden; }
.tone-swatches i { flex: 1; }
.tone-label { font: 500 11px var(--sans); color: var(--ink-2); }
.tone-btn.is-active .tone-label { color: var(--accent); }

/* Custom palettes sit in the same grid — a home-made direction is not a
   second-class one. The pencil only appears on hover, so the row of swatches
   stays the thing you read. */
.tone-btn { position: relative; }
.tone-edit {
  position: absolute; top: 3px; right: 3px;
  width: 15px; height: 15px; line-height: 15px; text-align: center;
  border-radius: 3px; font-size: 9px;
  background: var(--surface); color: var(--ink-3);
  opacity: 0; transition: opacity 0.12s ease;
}
.tone-btn:hover .tone-edit, .tone-btn:focus-within .tone-edit { opacity: 1; }
.tone-edit:hover { color: var(--accent); }

.tone-new {
  align-items: center; justify-content: center; gap: 2px;
  border-style: dashed; color: var(--ink-3); font-size: 13px;
}
.tone-new:hover { color: var(--accent); border-color: var(--accent); }

/* ── palette editor ──────────────────────────────────────────────── */

.dialog-note {
  font: 400 12px/1.5 var(--sans);
  color: var(--ink-2);
  margin: 0 0 14px;
  max-width: 46ch;
}

.palette-slots { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }

.palette-slot {
  position: relative;
  display: grid; gap: 4px; justify-items: center;
  padding: 6px 4px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.palette-slot input[type="color"] {
  width: 100%; height: 34px; padding: 0;
  border: none; border-radius: 3px; background: none; cursor: pointer;
}
.palette-slot input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.palette-slot input[type="color"]::-webkit-color-swatch { border: none; border-radius: 3px; }
.palette-hex { font: 500 9.5px ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--ink-2); }

/* An empty slot has to look empty rather than grey, or "no colour" and
   "the colour grey" are the same thing. */
.palette-slot.is-empty { border-style: dashed; }
.palette-slot.is-empty input[type="color"] { opacity: 0.18; }
.palette-slot.is-empty .palette-hex { color: var(--ink-3); }

.palette-clear {
  position: absolute; top: 2px; right: 2px;
  width: 14px; height: 14px; line-height: 12px;
  border: none; border-radius: 3px; padding: 0;
  background: none; color: var(--ink-3); cursor: pointer;
  font-size: 12px; opacity: 0;
}
.palette-slot:hover .palette-clear { opacity: 1; }
.palette-slot.is-empty .palette-clear { display: none; }

/* The existing rule right-aligns these; the spacer is what lets "Take from
   board" and "Delete" sit apart from the confirm pair. */
.dialog-actions .spacer { flex: 1; }

/* ── shared board, read only ─────────────────────────────────────── */

/*
 * The viewer is the same application with everything that edits it removed,
 * rather than a second page — a separate viewer would be a second
 * implementation of the one thing this app does, and the two would drift.
 */
.is-viewer .panel-left,
.is-viewer .panel-right,
.is-viewer .panel-scrim,
.is-viewer #btn-save,
.is-viewer #btn-share,
.is-viewer #btn-export,
.is-viewer #btn-boards,
.is-viewer #plan-badge,
.is-viewer .stage-toolbar { display: none; }
.is-viewer .topbar-actions .btn-primary { display: inline-block; }

.is-viewer .workspace { grid-template-columns: 1fr; }
.is-viewer .board-title { pointer-events: none; }

/* Signing in is the owner's business. Someone opening a link has one thing to
   do next, and a second button beside it only splits the decision. */
.is-viewer #btn-auth { display: none; }

/*
 * A shared board is a page, not a workspace.
 *
 * The editor is a fixed-height application: the stage is a well that scrolls
 * inside itself. The viewer inherited that, so on a phone the board sat in the
 * top half and the remaining 55% of the screen was well — while the colours
 * and the photographers' names, which the editor keeps in its side panels,
 * were hidden along with the panels. Both facts were measured; see
 * scripts/inspect-viewer.mjs.
 */
html.is-viewer, html.is-viewer body { height: auto; overflow: visible; }
/* The page scrolls now, and the one thing a visitor might want to do next
   should not scroll away with the board. */
.is-viewer .topbar { position: sticky; top: 0; z-index: 30; }
.is-viewer .workspace { height: auto; min-height: calc(100vh - var(--topbar-h)); }
.is-viewer .stage {
  flex: none;
  overflow: visible;
  padding: clamp(14px, 4vw, 40px);
  box-shadow: none;
}
.is-viewer .mb-item { cursor: zoom-in; }
.is-viewer .mb-item.is-openable:hover { filter: brightness(1.04); }

.viewer-sheet { background: var(--bg); border-top: 1px solid var(--line); }

.vs-inner {
  max-width: 640px;
  margin: 0 auto;
  padding: clamp(28px, 7vw, 56px) clamp(18px, 5vw, 32px) clamp(48px, 12vw, 80px);
  text-align: center;
}

.vs-title { font: 400 clamp(24px, 6vw, 32px) var(--serif); margin: 0; letter-spacing: -0.015em; }
.vs-brief { font: 400 15px/1.6 var(--sans); color: var(--ink-2); margin: 10px 0 0; }

.vs-block { margin-top: 36px; }
.vs-block h2 {
  font: 600 11px var(--sans); letter-spacing: .09em; text-transform: uppercase;
  color: var(--ink-2); margin: 0 0 14px;
}

.vs-palette { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; list-style: none; margin: 0; padding: 0; }
.vs-swatch { display: grid; gap: 6px; justify-items: center; cursor: copy; }
.vs-swatch span { width: 52px; height: 52px; border-radius: 10px; box-shadow: inset 0 0 0 1px rgba(0,0,0,.08); }
.vs-swatch code { font: 400 10px ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--ink-2); letter-spacing: .03em; }

.vs-credits { font: 400 14px/1.6 var(--sans); color: var(--ink-2); margin: 0; }
/* Inline-block with real padding: these are the attribution links Unsplash's
   terms require to be followable, and as bare inline text they measured 17px
   tall — under half a fingertip. */
.vs-credits a {
  display: inline-block; padding: 8px 0;
  color: var(--ink); text-decoration: underline; text-underline-offset: 3px;
}

.vs-note { font: 400 12px/1.6 var(--sans); color: var(--ink-2); margin: 10px 0 0; }

.vs-cta {
  display: inline-block; margin-top: 40px;
  padding: 14px 26px; font-size: 15px; text-decoration: none;
}

/* One photograph, full size. */
.dialog-photo {
  width: min(880px, 94vw);
  padding: 14px;
  background: var(--surface);
}
.dialog-photo img { display: block; width: 100%; max-height: 76vh; object-fit: contain; border-radius: 8px; }
.photo-credit { font: 400 13px/1.6 var(--sans); color: var(--ink-2); margin: 12px 4px 2px; text-align: center; }
.photo-credit a { color: var(--ink); }
.photo-close {
  position: absolute; top: 10px; right: 10px; z-index: 2;
  width: 40px; height: 40px; border: none; border-radius: 50%;
  background: rgba(20, 18, 16, .58); color: #fff;
  font-size: 22px; line-height: 1; cursor: pointer;
}

/* After the rules above, or these lose on source order at equal specificity. */
@media (max-width: 720px) {
  /* The one thing a visitor might do next, at a size a thumb can hit. */
  .is-viewer .topbar-actions .btn-primary {
    display: inline-flex; align-items: center;
    min-height: 44px; padding-inline: 18px;
  }
  .vs-swatch span { width: 58px; height: 58px; }
  .dialog-photo { width: 100vw; max-width: none; border-radius: 0; padding: 10px; }
  .dialog-photo img { max-height: 74vh; }
}

.viewer-status {
  font: 400 14px var(--sans);
  color: var(--ink-2);
  text-align: center;
  margin: auto;
  padding: 40px 24px;
  max-width: 34ch;
}

/* What the parser made of the brief, shown while it is typed. */
.brief-read {
  margin-top: 8px;
  padding: 8px 9px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  animation: read-in .16s ease;
}
@keyframes read-in { from { opacity: 0; transform: translateY(-2px); } }
@media (prefers-reduced-motion: reduce) { .brief-read { animation: none; } }

.read-chips { display: flex; flex-wrap: wrap; gap: 4px; }

.read-chip {
  display: inline-flex; align-items: center; gap: 4px;
  font: 500 10.5px var(--sans);
  padding: 3px 7px;
  border-radius: 100px;
  background: var(--surface-2);
  color: var(--ink-2);
}
.read-chip i { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.read-cat { background: var(--accent-soft); color: var(--accent); }
.read-weak { background: transparent; color: var(--ink-3); padding-left: 0; font-weight: 400; }

.read-sample {
  margin: 6px 0 0;
  font: 400 10.5px var(--sans);
  color: var(--ink-3);
  line-height: 1.4;
  word-break: break-word;
}

.chips { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 14px; }

.chip {
  font: 400 12px var(--sans);
  padding: 4px 9px;
  border-radius: 100px;
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--ink-2);
  cursor: pointer;
}
.chip:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }

/* ── stage ───────────────────────────────────────────────────────── */

.stage-wrap { display: flex; flex-direction: column; min-width: 0; background: var(--bg); }

.stage-toolbar {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--line);
}
.spacer { flex: 1; }
.zoom { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--ink-2); }
#zoom-level { min-width: 42px; text-align: center; font-variant-numeric: tabular-nums; }

/*
 * The stage is a well, not another panel.
 *
 * The file opens by claiming that a tool for composing images has to let the
 * images be the loudest thing on screen. It was not true: the panels and the
 * canvas sat within a few percent of the same value, so the board — the only
 * reason any of this exists — read as the middle column rather than as the
 * subject. Recessing the stage and lifting the board apart is the whole fix;
 * nothing moves and no colour changes.
 */
.stage {
  flex: 1;
  display: grid;
  place-items: center;
  padding: 32px;
  overflow: auto;
  position: relative;
  background: var(--well);
  /* An inner shadow along the top and sides reads as depth without drawing a
     border, which at this size would only add another line to look at. */
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.14), inset 0 0 40px rgba(0, 0, 0, 0.05);
}
.stage:focus { outline: none; }

/* Sized inline by renderToDom to the scaled footprint of the board — a CSS
   transform leaves no trace in normal flow, so without this the stage cannot
   centre or scroll correctly. */
.mb-stage {
  box-shadow: var(--board-shadow);
  border-radius: 2px;
}

.mb-item { cursor: grab; }
.mb-item:active { cursor: grabbing; }
.mb-item.is-selected { outline: 2px solid var(--accent); outline-offset: 3px; }
.mb-item.is-locked { cursor: default; }

/*
 * What a release would do.
 *
 * The tile you are about to trade with is marked, and the one in your hand
 * goes translucent so you can see it. A swap that only announced itself after
 * the fact would rearrange someone's board without warning, which is a bug
 * however carefully it was written.
 */
.mb-item.is-swap-target {
  outline: 3px dashed var(--accent);
  outline-offset: 3px;
  filter: brightness(1.08);
}
.mb-item.is-swapping { opacity: 0.55; }

/* The tile a dragged photograph would land in. Solid rather than dashed, to
   read as "this one is being replaced" rather than "these two trade". */
.mb-item.is-drop-target {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  filter: brightness(1.12);
}

/* While a photograph is in the air, the board says it will take it. */
body.is-dragging-photo .mb-stage { outline: 2px dashed var(--line-strong); outline-offset: 6px; }
.thumb[draggable="true"] { cursor: grab; }
.thumb[draggable="true"]:active { cursor: grabbing; }

/* Tiles deal in after a generation, in z-order. */
.mb-item.is-revealing {
  animation: tile-in .42s cubic-bezier(.22, .9, .3, 1) backwards;
}
@keyframes tile-in {
  from { opacity: 0; transform: translateY(10px) scale(.975); }
}
@media (prefers-reduced-motion: reduce) {
  .mb-item.is-revealing { animation: none; }
}

/* Resize handles. Counter-scaled inline so they stay one size at any zoom. */
.mb-handle {
  position: absolute;
  width: 11px; height: 11px;
  background: var(--surface);
  border: 1.5px solid var(--accent);
  border-radius: 2px;
  transform: scale(var(--handle-scale, 1));
  z-index: 10;
}
.mb-handle-nw { left: 0; top: 0; transform-origin: top left; cursor: nwse-resize; margin: -6px 0 0 -6px; }
.mb-handle-ne { right: 0; top: 0; transform-origin: top right; cursor: nesw-resize; margin: -6px -6px 0 0; }
.mb-handle-sw { left: 0; bottom: 0; transform-origin: bottom left; cursor: nesw-resize; margin: 0 0 -6px -6px; }
.mb-handle-se { right: 0; bottom: 0; transform-origin: bottom right; cursor: nwse-resize; margin: 0 -6px -6px 0; }

/* Alignment guides shown while dragging. */
.mb-guide {
  position: absolute;
  background: var(--accent);
  opacity: .8;
  pointer-events: none;
  z-index: 9998;
}

.empty-state { max-width: 560px; text-align: center; color: var(--ink-2); padding: 8px; }
.empty-state h1 {
  font: 400 38px/1.1 var(--serif);
  color: var(--ink);
  margin: 0 0 14px;
  letter-spacing: -0.02em;
}
.empty-state p { margin: 0 auto; line-height: 1.6; max-width: 44ch; }

.starter-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin: 28px 0 12px;
}

.starter {
  display: flex; flex-direction: column; gap: 8px;
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  text-align: left;
  transition: border-color .14s, transform .14s, box-shadow .14s;
}
.starter:hover {
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: var(--shadow);
}
.starter-swatches { display: flex; height: 26px; border-radius: 4px; overflow: hidden; }
.starter-swatches i { flex: 1; }
.starter-label { font-size: 12px; font-weight: 500; color: var(--ink); }
.starter-hint { font-size: 12px; color: var(--ink-3); }

.tool-divider { width: 1px; height: 18px; background: var(--line); margin: 0 2px; }

.shortcuts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 9px 16px;
  margin: 0;
  align-items: baseline;
}
.shortcuts dt { display: flex; gap: 3px; align-items: center; }
.shortcuts dd { margin: 0; font-size: 13px; color: var(--ink-2); }
kbd {
  font: 500 11px var(--sans);
  padding: 3px 6px;
  border: 1px solid var(--line-strong);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: var(--bg);
  color: var(--ink-2);
  white-space: nowrap;
}

/* ── right panel ─────────────────────────────────────────────────── */

.prop-group { padding-bottom: 18px; margin-bottom: 18px; border-bottom: 1px solid var(--line); }
.prop-group:last-child { border-bottom: none; }

.prop-title {
  font: 600 11.5px var(--sans);
  letter-spacing: .05em; text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 12px;
}

.engine-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 5px; }

.engine-btn {
  font: 500 12px var(--sans);
  padding: 7px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--ink-2);
  cursor: pointer;
  text-align: center;
}
.engine-btn:hover { border-color: var(--line-strong); color: var(--ink); }
.engine-btn.is-active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }

.palette-strip {
  display: flex;
  height: 42px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  border: 1px solid var(--line);
  margin-bottom: 8px;
}
.palette-strip:empty { display: none; }

.swatch { flex: 1; position: relative; cursor: pointer; }
.swatch::after {
  content: attr(data-hex);
  position: absolute; inset: auto 0 0 0;
  font: 500 9px var(--sans); text-align: center; padding: 2px 0;
  background: rgba(0, 0, 0, .55); color: #fff;
  opacity: 0; transition: opacity .12s;
}
.swatch:hover::after { opacity: 1; }

/* ── thumbnails ──────────────────────────────────────────────────── */

/*
 * Two columns, not three.
 *
 * Three put the thumbnails at 92px, which is too small to judge a photograph
 * before adding it, and left the credit line clamped mid-name — "Photo by
 * Collov Home Design on…", with the word Unsplash cut off. The credit is a
 * condition of using the API, not a caption, so it has to fit.
 */
.thumb-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px 6px; }

/* Present and clickable wherever a photograph is: flat text would look like
   attribution while sending the photographer nothing. */
.thumb-cell { display: grid; gap: 4px; align-content: start; }

.thumb-credit {
  margin: 0;
  font: 400 9.5px/1.35 var(--sans);
  color: var(--ink-3);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}
.thumb-credit a { color: var(--ink-2); text-decoration: underline; text-underline-offset: 1px; }
.thumb-credit a:hover { color: var(--accent); }

#selection-credit a { color: var(--ink-2); text-decoration: underline; text-underline-offset: 2px; }
#selection-credit a:hover { color: var(--accent); }

.thumb {
  position: relative;
  aspect-ratio: 1;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-2);
  border: none; padding: 0; cursor: pointer;
}
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.thumb::after {
  content: "+";
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-size: 22px; color: #fff;
  background: rgba(30, 27, 23, .5);
  opacity: 0; transition: opacity .12s;
}
.thumb:hover::after { opacity: 1; }

.dropzone {
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--radius);
  padding: 24px 16px;
  text-align: center;
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color .12s, background .12s;
}
.dropzone:hover, .dropzone.is-over { border-color: var(--accent); background: var(--accent-soft); }
.dropzone p { margin: 0; line-height: 1.5; }

/* ── status & toasts ─────────────────────────────────────────────── */

.status {
  margin-top: 12px;
  padding: 10px 12px;
  background: var(--surface-2);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  color: var(--ink-2);
}
.status-bar { height: 3px; background: var(--line); border-radius: 100px; margin-top: 8px; overflow: hidden; }
.status-bar span { display: block; height: 100%; background: var(--accent); transition: width .3s ease; }

.notice {
  margin-top: 10px; padding: 9px 11px;
  background: #F6E9D8; color: #7A4E14;
  border-radius: var(--radius-sm); font-size: 12.5px; line-height: 1.45;
}
@media (prefers-color-scheme: dark) { .notice { background: #37291A; color: #E0BE8A; } }

.toast-host {
  position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; gap: 8px; align-items: center;
  z-index: 100; pointer-events: none;
}

.toast {
  background: var(--ink); color: var(--surface);
  padding: 10px 16px; border-radius: 100px;
  font-size: 13px; box-shadow: var(--shadow);
  animation: toast-in .2s ease;
  max-width: 90vw;
}
.toast.is-error { background: var(--danger); color: #fff; }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } }

.muted { color: var(--ink-3); }
.small { font-size: 12px; line-height: 1.5; }

/* ── dialogs ─────────────────────────────────────────────────────── */

.dialog {
  border: none;
  border-radius: 14px;
  padding: 24px;
  width: min(420px, 92vw);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
}
.dialog-wide { width: min(720px, 92vw); }
/* Four actions and a five-column grid of colours need more than 420px. */
.dialog-palette { width: min(500px, 94vw); }
.dialog::backdrop { background: rgba(30, 27, 23, .45); backdrop-filter: blur(3px); }
.dialog h2 { font: 400 21px var(--serif); margin: 0 0 16px; letter-spacing: -0.01em; }
/*
 * Wrapping, because `justify-content: flex-end` on a row that does not fit
 * puts the overflow off the *left* edge, where a dialog has no scrollbar and
 * nothing says anything is missing. Editing a saved palette shows a fourth
 * button — Delete — and the row then wanted 414px in 372px of room: "Take
 * from board" was sliced down its first two letters.
 */
.dialog-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 20px; }

/* A helper, not one of the form's answers: it sits away from the other three,
   and takes its own line first when there is not room for one. */
.btn-aside { margin-right: auto; }

.upgrade-reason {
  background: var(--accent-soft); color: var(--accent);
  padding: 11px 13px; border-radius: var(--radius-sm);
  font-size: 13px; line-height: 1.5; margin: 0 0 16px;
}

.plan-features { margin: 0; padding-left: 18px; color: var(--ink-2); font-size: 13.5px; line-height: 1.9; }

.boards-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; max-height: 60vh; overflow-y: auto; }

.board-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg);
  cursor: pointer;
  text-align: left;
  padding: 0;
}
.board-card:hover { border-color: var(--accent); }
.board-card img { width: 100%; aspect-ratio: 4/5; object-fit: cover; display: block; background: var(--surface-2); }
.board-card-body { padding: 9px 10px; }
.board-card-title { font-size: 12.5px; font-weight: 500; margin: 0 0 3px; }
.board-card-meta { font-size: 11px; color: var(--ink-3); margin: 0; }
.board-card-palette { display: flex; height: 4px; }
.board-card-palette i { flex: 1; }

/* ── responsive ──────────────────────────────────────────────────── */

/* The properties panel is permanent on a wide screen, so its toggle is not. */
.btn-props { display: none; }
.panel-scrim { display: none; }

/*
 * Below this width the right panel becomes a drawer rather than disappearing.
 *
 * It used to be `display: none`, which silently removed the layout engines,
 * finish controls, palette and colour tones — half the tool — from anyone on a
 * laptop or a narrow window, with nothing on screen to suggest they existed.
 * Hiding controls is not a responsive strategy; moving them is.
 */
@media (max-width: 1080px) {
  .workspace { grid-template-columns: 260px 1fr; }

  .btn-props { display: inline-block; }

  .panel-right {
    position: fixed;
    top: var(--topbar-h);
    right: 0;
    bottom: 0;
    width: min(320px, 88vw);
    z-index: 60;
    transform: translateX(101%); /* 101% clears the box shadow too */
    transition: transform .22s cubic-bezier(.22, .9, .3, 1);
    box-shadow: var(--shadow-lg);
    border-left: 1px solid var(--line);
  }
  .panel-right.is-open { transform: translateX(0); }

  /*
   * `:not([hidden])`, not a bare `.panel-scrim`.
   *
   * `[hidden] { display: none }` comes from the browser's own stylesheet, and
   * any author rule beats the browser's whatever its specificity. So a plain
   * `.panel-scrim { display: block }` here quietly overrode the attribute that
   * `togglePropsPanel` sets, and the scrim was permanently on: a transparent
   * fixed layer from the top bar down, at z-index 55, over the whole tool.
   * Below 1080px nothing outside the top bar could be tapped or clicked.
   * Measured by scripts/inspect-hit.mjs — 20 controls on a phone, 37 in a
   * narrow laptop window. Making the attribute part of the selector means it
   * is the switch rather than a competitor.
   */
  .panel-scrim:not([hidden]) {
    display: block;
    position: fixed;
    inset: var(--topbar-h) 0 0 0;
    background: rgba(30, 27, 23, .35);
    z-index: 55;
    animation: scrim-in .18s ease;
  }
  @keyframes scrim-in { from { opacity: 0; } }

  @media (prefers-reduced-motion: reduce) {
    .panel-right { transition: none; }
    .panel-scrim { animation: none; }
  }
}

/*
 * Docked so the button that makes a board is always on screen.
 *
 * The Generate panel runs to about 1750px of options — canvas, template, tone,
 * count, engine, both shape controls, sources, two checkboxes — and Generate
 * sits after all of them. Measured on a laptop as well as a phone: it was
 * below the fold of the column holding it everywhere.
 */
.generate-dock {
  position: sticky;
  bottom: 0;
  margin-top: 18px;
  padding-top: 12px;
  background: linear-gradient(to bottom, transparent, var(--panel) 14px);
}

/* The handle belongs to the phone sheet; on a column there is nothing to
   expand, so it is not shown and takes no space. */
.sheet-handle { display: none; }
.btn-more { display: none; }

@media (max-width: 720px) {
  /*
   * The board first, the controls in a sheet under it.
   *
   * Before this the panel was a 42vh drawer above the board holding 1750px of
   * options, which put Generate 1454px below its fold, and the board — the
   * only reason the tool exists — got 40% of the screen. Both measured; see
   * scripts/inspect-phone.mjs.
   */
  /* Handle, tabs, the brief and the docked button — measured, not guessed. At
     208px the docked button sat on top of the last line of the textarea. */
  :root { --sheet-collapsed: 244px; --sheet-handle-h: 46px; }

  .workspace { grid-template-columns: 1fr; grid-template-rows: 1fr; }

  /* The bar holds the brand and a menu. Everything else moves into the menu. */
  .topbar { grid-template-columns: 1fr auto; position: relative; }
  .topbar-center { display: none; }
  .board-title { min-width: 0; }
  .btn-more { display: inline-flex; align-items: center; justify-content: center; font-size: 20px; }

  .topbar-actions {
    display: none;
    position: absolute;
    top: calc(var(--topbar-h) - 4px);
    right: 10px;
    z-index: 70;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    min-width: 216px;
    padding: 8px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 14px;
    box-shadow: var(--shadow-lg);
  }
  body.menu-open .topbar-actions { display: flex; }
  .topbar-actions .btn { justify-content: flex-start; text-align: left; }
  .topbar-actions .plan-badge { align-self: flex-start; margin: 2px 0 6px 10px; }

  /*
   * A shared board has one action, and it lives in this same container. Left
   * as a menu it would be a ⋯ button opening a panel with a single item in it,
   * and the item — the only way from a board someone has just been sent to the
   * tool that made it — would start the visit hidden behind a tap.
   */
  body.is-viewer .btn-more { display: none; }
  body.is-viewer .topbar-actions {
    display: flex; flex-direction: row; position: static;
    min-width: 0; padding: 0; gap: 8px;
    background: none; border: none; box-shadow: none;
  }

  /* The stage keeps everything the sheet is not covering. */
  .stage-wrap { padding-bottom: var(--sheet-collapsed); }
  .stage { padding: 14px; }

  /*
   * No keyboard, so no shortcut sheet; pinch is the zoom. Add text needs
   * typing and then placing a box by hand, which is a desktop job — it is
   * still there on a wider screen. Removing these three is what lets the rest
   * fit: they were what pushed Adjust off the right edge.
   */
  #btn-shortcuts, .stage-toolbar .zoom, #btn-text { display: none; }
  .stage-toolbar { padding: 8px 10px; gap: 6px; overflow-x: auto; scrollbar-width: none; }
  .stage-toolbar::-webkit-scrollbar { display: none; }

  /* ── the sheet ─────────────────────────────────────────────────── */

  .panel-left {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 45;
    height: var(--sheet-collapsed);
    max-height: none;
    padding: 0;
    border: none;
    border-top: 1px solid var(--line);
    border-radius: 16px 16px 0 0;
    box-shadow: 0 -10px 30px rgba(0, 0, 0, .18);
    transition: height .24s cubic-bezier(.22, .9, .3, 1);
    /* Vertical drags are this panel's to scroll, and nothing else's to claim. */
    touch-action: pan-y;
  }
  /* The handle drags the sheet, so the browser must not read it as a scroll. */
  .sheet-handle { touch-action: none; }
  /*
   * A tile is dragged, not scrolled past. Without this the browser treats a
   * finger moving on one as a possible scroll and cancels the drag mid-way —
   * and on a phone the board is fitted to the stage, so there is nothing under
   * it to scroll to anyway.
   */
  .mb-item { touch-action: none; }
  /*
   * Clamped so the handle can never leave the screen.
   *
   * A flat 82vh is 82% of a viewport that pretends the address bar is not
   * there. With it showing, the sheet came out taller than the glass: its top
   * — and the handle, the only way to close it — went off the top edge, and
   * its foot went under the browser's own bar. Nothing could be closed and the
   * last of the options could not be reached. The second value keeps it inside
   * what is really visible whatever the browser is doing with its bars.
   */
  body.sheet-open .panel-left {
    height: 82vh;
    height: min(82dvh, calc(100dvh - var(--topbar-h) - 8px));
  }


  /* Below the handle, not on top of it. Both were pinned to `top: 0` — the
     tab bar is sticky in the base stylesheet and the handle was added at the
     same offset, so scrolling the sheet slid the tabs under the handle. */
  .panel-left .tabs { padding: 0 14px; top: var(--sheet-handle-h); z-index: 2; }
  .panel-left .tabpanel { padding: 0 14px 14px; }

  .sheet-handle {
    display: grid;
    justify-items: center;
    align-content: center;
    gap: 4px;
    position: sticky;
    top: 0;
    z-index: 3;
    width: 100%;
    /* Declared rather than derived from padding, because the tab bar sticks
       directly below it and needs the number. */
    height: var(--sheet-handle-h);
    padding: 0;
    border: none;
    background: var(--panel);
    cursor: pointer;
    font: 500 11px var(--sans);
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ink-2);
  }
  .sheet-grip { width: 38px; height: 4px; border-radius: 100px; background: var(--line-strong, var(--line)); }

  /* Clear of the home indicator, so the docked button is not half a bar. */
  .generate-dock { padding-bottom: max(10px, env(safe-area-inset-bottom)); }

  /* ── things a finger has to hit ────────────────────────────────── */

  /*
   * 54 of 103 controls measured under 44px. The panel gets taller for this and
   * scrolls; that is the correct trade on a touch screen.
   */
  .btn, .tab {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .btn-block { display: flex; }
  /* `.btn-icon` sets an explicit 26px box, so a minimum cannot reach it. */
  .btn-icon { width: 44px; height: 44px; }
  .chip { min-height: 38px; display: inline-flex; align-items: center; }
  /* The label is the tap target — clicking it toggles the box — but a 15px
     box next to it still reads as the thing to aim at, so it grows too. */
  .check { min-height: 44px; display: flex; align-items: center; gap: 10px; }
  .check input[type="checkbox"], .check input[type="radio"] { width: 20px; height: 20px; }
  .input, .select { min-height: 44px; }
  .textarea { min-height: 68px; }

  /* Clear of the sheet, or every message lands behind it. */
  .toast-host { bottom: calc(var(--sheet-collapsed) + 14px); }

  .dialog { width: min(420px, 94vw); padding: 18px; }
  /* Its own line, rather than dragging one of the answers down with it. */
  .btn-aside { flex: 1 0 100%; margin: 0 0 4px; }
  .boards-list { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
}

/*
 * A short screen, which is what a phone becomes once the browser's bars are
 * showing. 244px of sheet is 29% of a tall phone and 44% of this one, and what
 * it takes comes out of the board. The label goes because the placeholder
 * already says the same thing.
 */
@media (max-width: 720px) and (max-height: 680px) {
  :root { --sheet-collapsed: 212px; }
  .panel-left .field-brief > .field-label { display: none; }
  .textarea { min-height: 52px; }
  .stage { padding: 10px; }
}
