/* LIFIRIK — all styling. Design tokens as CSS custom properties (§10.1).
   NOTE: the violet accent is historically stored in --teal — do not "fix" the name. */

:root {
  --teal: #6558e6;             /* violet accent (historic name) */
  --teal-soft: rgba(101, 88, 230, 0.12);
  --ink: #232a35;
  --ink-soft: #5a6474;
  --sky-top: #ccd7f6;
  --sky-bot: #fdebd8;
  --paper: #ffffff;
  --paper-dim: #f4f6fb;
  --line: #dfe4ef;
  --danger: #e05555;
  --gold: #d4a017;
  --radius: 10px;
  --shadow: 0 6px 24px rgba(35, 42, 53, 0.12);
  --font: system-ui, 'Segoe UI', Roboto, sans-serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  font-family: var(--font);
  color: var(--ink);
  background: linear-gradient(var(--sky-top), var(--sky-bot)) fixed;
}

#app { min-height: 100%; display: flex; flex-direction: column; }

/* ---------- splash ---------- */

.splash {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  min-height: 100vh;
}
.splash-mark svg { filter: drop-shadow(0 4px 10px rgba(35,42,53,.18)); }

/* ---------- nav ---------- */

.nav {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 10px 22px;
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 40;
}
.brand { display: flex; align-items: center; text-decoration: none; }
.nav-link {
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
  font-size: 14px;
  padding: 6px 10px;
  border-radius: 8px;
}
.nav-link:hover { background: var(--teal-soft); color: var(--teal); }
.nav-link.points { color: var(--gold); }
.nav-user { display: flex; align-items: center; gap: 8px; }
.spacer { flex: 1; }

.main { flex: 1; width: min(1100px, 100% - 40px); margin: 0 auto; padding: 24px 0 60px; }
.main.full { width: 100%; margin: 0; padding: 0; display: flex; }

/* ---------- buttons & inputs ---------- */

.btn {
  font: inherit;
  font-weight: 600;
  font-size: 14px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 8px 14px;
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: transform 0.06s ease, box-shadow 0.12s ease;
}
.btn:hover { box-shadow: 0 2px 10px rgba(35,42,53,.12); }
.btn:active { transform: translateY(1px); }
.btn.primary { background: var(--teal); border-color: var(--teal); color: #fff; }
.btn.primary:hover { background: #5748d9; }
.btn.ghost { background: transparent; border-color: transparent; }
.btn.ghost:hover { background: var(--teal-soft); }
.btn.danger { color: var(--danger); }
.btn.tiny { font-size: 12px; padding: 4px 9px; border-radius: 7px; }
.btn.big { font-size: 16px; padding: 12px 22px; border-radius: 12px; }
.btn.active { outline: 2px solid var(--teal); }
.btn:disabled { opacity: 0.5; cursor: default; }

.input {
  font: inherit;
  font-size: 14px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
  color: var(--ink);
  width: 100%;
}
.input:focus { outline: 2px solid var(--teal); border-color: transparent; }
select.input { width: auto; }
/* A select sizes itself to its widest OPTION. Inside a fixed-width modal that
   pushes the whole dialog sideways — the save destinations are full sentences,
   and the note under the field repeats the chosen one anyway. */
.field select.input { width: 100%; min-width: 0; }
.field { display: block; font-size: 13px; font-weight: 600; margin: 10px 0 4px; }
.field .input { margin-top: 4px; font-weight: 400; }
/* the "which of these two boxes is which" note, on the label itself rather than
   under the field — it has to be read BEFORE the box is filled in */
.field-note { font-weight: 400; color: var(--ink-soft); font-size: 12px; }
.check { display: flex; align-items: center; gap: 8px; margin: 12px 0; font-size: 14px; }
.form-error { color: var(--danger); font-size: 13px; min-height: 18px; margin: 6px 0 0; }
.muted { color: var(--ink-soft); font-size: 13px; }

/* ---------- home ---------- */

.hero { text-align: center; padding: 60px 20px 30px; }
.hero-mark { display: flex; justify-content: center; margin-bottom: 18px; }
.hero-tag { font-size: 17px; font-weight: 600; color: var(--ink); max-width: 620px; margin: 0 auto; }
.hero-sub { color: var(--ink-soft); max-width: 560px; margin: 10px auto 26px; }
.hero-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

.page-title { margin: 8px 0 20px; }
.section-title { margin: 34px 0 14px; font-size: 20px; }

/* ---------- cards ---------- */

.cards-grid, .cards-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 16px;
}
.card {
  position: relative;
  display: block;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  text-decoration: none;
  color: var(--ink);
  transition: transform 0.1s ease, box-shadow 0.15s ease;
}
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.card-thumb { width: 100%; height: auto; display: block; background: linear-gradient(var(--sky-top), var(--sky-bot)); }
.card-body { padding: 10px 12px 12px; }
.card-title { font-weight: 700; font-size: 14px; }
.card-sub { margin-top: 3px; }
/* The author's own line about the level, two lines of it. Clamped rather than
   truncated in JS so the whole string is still in the DOM — the details panel,
   search and a screen reader all still get it — and so a card with no
   description costs no height at all. */
.card-desc {
  margin: 5px 0 0;
  font-size: 12px; line-height: 1.35;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
.card-badges { display: flex; flex-wrap: nowrap; margin-top: 5px; font-size: 13px; }
.card-done {
  position: absolute;
  top: 8px; right: 8px;
  font-size: 20px;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,.4));
}
/* account-gated challenge: visible, dimmed, padlocked — the card sells the
   sign-up, the server refuses the data either way */
.card-lock {
  position: absolute;
  top: 8px; right: 8px;
  font-size: 20px;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,.4));
}
.card.locked .card-thumb { filter: saturate(0.45) brightness(0.82); }
.card.locked:hover .card-thumb { filter: saturate(0.7) brightness(0.9); }
.card-edit-btn {
  position: absolute;
  top: 8px; left: 8px;
  width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px;
  background: rgba(255,255,255,.9);
  border: 1px solid var(--line);
  border-radius: 50%;
  cursor: pointer;
}
.card-edit-btn:hover { background: #fff; }
/* the curator's star — same corner treatment as the edit pencil, opposite side
   of it so both can sit on one card */
.card-feature-btn {
  position: absolute;
  top: 8px; left: 40px;
  width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; line-height: 1;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: rgba(255,255,255,.85);
  cursor: pointer;
  padding: 0;
}
.card-feature-btn:hover { background: #fff; }
.card-feature-btn.on { color: var(--gold); border-color: var(--gold); background: #fff; }
.featured-row { margin-bottom: 26px; }
.featured-row.hidden { display: none; }
.featured-row .section-title { margin-top: 0; color: var(--gold); }
.featured-row > .muted { margin: -8px 0 12px; }
.chip-official { color: var(--gold); font-weight: 800; }

/* ---------- challenges (§11.8) ---------- */

.challenge-row { margin-bottom: 26px; }
.challenge-row.hidden { display: none; }
.challenge-row .section-title { margin-top: 0; color: var(--teal); }
.challenge-row > .muted { margin: -8px 0 12px; }
/* a sealed race is a card you can look at but not open — the cursor says so
   before the click does */
.card.sealed { cursor: default; }
.card.sealed .card-thumb { filter: saturate(0.4); }
.chal-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px; }
.chal-chip {
  font-size: 11px;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--teal-soft);
  color: var(--ink);
  white-space: nowrap;
}
.chal-chip.race { background: #fde9c9; }
.chal-panel {
  margin: 10px 0;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--teal);
  border-radius: 8px;
  background: var(--paper-dim);
}
.chal-row { font-size: 13px; }
.chal-row + .chal-row { margin-top: 8px; border-top: 1px solid var(--line); padding-top: 8px; }
/* search + sort above a profile list */
/* The challenge action on your OWN level rows is a plain `.btn.tiny`, exactly
   like the delete beside it — see levelChallengeAction(). Two hover-reveal
   rules used to live here, `.card-chal-btn` (revealed by `.card:hover`) and
   `.row-chal-btn` (by `.solve-row:hover`), and BOTH named an ancestor the
   profile table does not have: the button rendered at opacity 0 forever, and
   absolutely positioned into the corner of the page if it ever showed. A
   sealed race keeps its own colour, because that one is a STATE rather than an
   action and should read at a glance down a list. */
/* A secret shown exactly once (the admin password reset, §13). Big, monospace
   and selectable, because it is going to be read aloud or retyped by hand — and
   boxed, so it reads as a thing to take away rather than as body text. */
/* "Press Space to start" on a machine opened from a link (§10.4). Sits over the
   canvas, out of the way of the toolbar and the dock, and is clickable itself
   because a visitor who followed a link may well be on a touchscreen. */
.start-prompt {
  left: 50%;
  top: 96px;
  transform: translateX(-50%);
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 14px;
  padding: 10px 16px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(255,255,255,.95);
  box-shadow: var(--shadow);
  cursor: pointer;
  animation: start-pulse 2.2s ease-in-out infinite;
}
.start-prompt:hover { background: #fff; }
@keyframes start-pulse { 0%, 100% { opacity: .92; } 50% { opacity: 1; } }

.reveal-row { display: flex; align-items: center; gap: 8px; margin: 8px 0; }
.reveal-code {
  flex: 1;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: .04em;
  user-select: all;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--paper);
  overflow-wrap: anywhere;
}
.chal-sealed { background: #fde9c9; border-color: #e0b062; }
.chal-sealed:hover { background: #fbdcae; }
/* the countdown on a sealed race's own page — the whole point of the screen,
   so it gets the size to match */
.race-clock {
  font-size: 44px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--teal);
  margin: 10px 0;
}
/* Icon only — the name lives in the tooltip (§11.4). Fixed width so the full
   set lines up column-for-column across stacked rows. */
.badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  background: var(--paper-dim);
  border: 1px solid var(--line);
  border-radius: 50%;
  font-size: 13px;
  line-height: 1;
  margin: 2px 3px 0 0;
  cursor: help;
}
.badge.tiny { width: 21px; height: 21px; font-size: 11px; }
/* Un-won: the slot is still there, just hollowed out — a missing piece rather
   than an absent one. Drained of colour and mostly transparent, so a row of
   ghosts never competes with the ones actually earned. */
.badge.ghost {
  background: transparent;
  border-style: dashed;
  border-color: color-mix(in srgb, var(--line) 70%, transparent);
  opacity: 0.32;
  filter: grayscale(1);
}
.badge.ghost:hover { opacity: 0.6; }

.challenge-page { margin-bottom: 34px; }
.challenge-page h2 { margin-bottom: 2px; }
.challenge-page > .muted { margin: 0 0 12px; }

/* Search, author, badges — one row, because they are one question. The text
   boxes are deliberately modest: a search field wide enough for a paragraph
   suggests you should type one, and the badge filter earns the space more
   (nine taps vs. remembering what a level was called). */
.browse-search-row { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
/* Sized so search + author + both selects + the badge filter land on ONE line at
   the 1100 px content width. The badges are the fixed cost (they can't shrink
   and stay tappable) and the filter never breaks internally — it wraps as a
   unit — so the text boxes absorb the difference, which is the right way round
   anyway. Adding a badge widens the filter by ~34 px; check this row still fits. */
.browse-search-row .search { flex: 0 1 220px; min-width: 130px; }
.browse-search-row .author { flex: 0 1 150px; min-width: 110px; }
.browse-search-row select.input { flex: 0 0 auto; }
.badge-filter { display: inline-flex; flex-wrap: nowrap; gap: 4px; align-items: center; }
.badge-btn { width: 30px; padding: 4px 0; font-size: 14px; cursor: help; }
.badge-btn:not(.active) { filter: grayscale(1); opacity: 0.55; }
.badge-btn.active { outline: 2px solid var(--teal); background: var(--teal-soft); }

.center-msg { text-align: center; padding: 80px 20px; width: 100%; }

/* ---------- profile ---------- */

.profile-head { margin-bottom: 6px; }
.points-disclaimer { font-size: 12px; color: var(--ink-soft); }
.profile-diff { margin: 2px 0 0; }
/* the figure itself is the point of the line, so it isn't muted with the label */
.diff-strong { color: var(--ink); font-weight: 600; cursor: help; }
.profile-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
.points-log { margin-top: 6px; }
.points-row { padding: 2px 0; }

/* ---------- import screen ---------- */

.import-lede { max-width: 78ch; margin: -10px 0 18px; }
.import-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 18px;
  align-items: start;
}
.import-panel {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px 16px;
}
.import-panel h2 { font-size: 16px; margin: 0 0 10px; }
.import-panel-head { display: flex; align-items: center; gap: 8px; }
.import-panel-head h2 { flex: 1; }
.import-text {
  width: 100%;
  font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
  font-size: 12px;
  line-height: 1.45;
  resize: vertical;
  /* level text is usually one very long line — wrap it so the whole paste is
     readable instead of hiding it behind a horizontal scrollbar */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.import-opts { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0 12px; }
/* the global select.input is width:auto (it lives in flex rows elsewhere);
   in this grid each control owns its column, so fill it */
.import-opts select.input { display: block; width: 100%; }
.import-scale { display: flex; gap: 6px; align-items: center; margin-top: 4px; }
.import-scale .input { flex: 1; min-width: 0; margin-top: 0; }
.import-codes { border-collapse: collapse; font-size: 13px; margin: 4px 0 10px; }
.import-codes td { padding: 2px 12px 2px 0; vertical-align: top; }
.import-codes td:first-child { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-weight: 700; white-space: nowrap; }
.import-preview {
  width: 100%;
  height: auto;
  display: block;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: linear-gradient(var(--sky-top), var(--sky-bot));
}
.import-stats { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 10px; font-size: 13px; color: var(--ink-soft); }
.import-stat b { color: var(--ink); font-weight: 700; }
.import-warnings { margin: 10px 0 0; padding-left: 18px; font-size: 13px; color: var(--ink-soft); }
.import-warnings li { margin: 4px 0; }
.import-warnings:empty { display: none; }
.import-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 14px 0 4px; }

/* ---------- game screen ---------- */

.screen-holder { position: relative; flex: 1; display: flex; min-height: calc(100vh - 53px); }
.game-host { position: relative; flex: 1; }
.game-root { position: absolute; inset: 0; overflow: hidden; user-select: none; }
.game-canvas { position: absolute; inset: 0; touch-action: none; cursor: crosshair; }

.hud { position: absolute; z-index: 10; }

.hud-top {
  top: 0; left: 0; right: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: linear-gradient(rgba(255,255,255,.9), rgba(255,255,255,.55) 70%, transparent);
  pointer-events: none;
}
.hud-top > * { pointer-events: auto; }
.level-name { font-weight: 800; font-size: 15px; }
.tabs { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: #fff; }
.tab { font: inherit; font-size: 13px; font-weight: 600; border: 0; background: transparent; padding: 6px 12px; cursor: pointer; color: var(--ink-soft); }
.tab.active { background: var(--teal); color: #fff; }

.hint-box {
  position: absolute;
  top: 48px; left: 50%;
  transform: translateX(-50%);
  z-index: 12;
  background: #fff8dc;
  border: 1px solid #e8d48b;
  padding: 10px 16px;
  border-radius: 10px;
  max-width: 480px;
  box-shadow: var(--shadow);
  font-size: 14px;
}

/* Toolbar: relocatable (drag the grip) + orientable (horizontal/vertical).
   Position is set inline (left/top) by JS; only chrome lives here. */
.toolbar-wrap {
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: rgba(255,255,255,.92);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 6px;
  box-shadow: var(--shadow);
  max-width: min(90vw, 480px);   /* vertical: one ~60px icon column, plenty */
}
.toolbar-wrap.horizontal {
  flex-direction: row;
  align-items: flex-start;
  /* must fit the header column + the full 13-icon row (~632px content) —
     the vertical cap above is far too narrow and made the panel's own
     background stop short of its icons */
  max-width: min(96vw, 760px);
}
.toolbar-header {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: space-between;
  gap: 2px;
}
.toolbar-wrap.horizontal .toolbar-header { flex-direction: column; }
.toolbar-grip, .toolbar-orient {
  font: inherit;
  border: 0;
  background: transparent;
  color: var(--ink-soft);
  border-radius: 6px;
  padding: 3px 5px;
  cursor: pointer;
}
.toolbar-grip { cursor: grab; font-size: 14px; touch-action: none; }
.toolbar-grip:active { cursor: grabbing; background: var(--teal-soft); }
.toolbar-orient { font-size: 13px; }
.toolbar-orient:hover, .toolbar-grip:hover { background: var(--teal-soft); color: var(--teal); }
/* Single column/row + scroll, NOT flex-wrap: column-wrap with only
   max-height (no definite height) wraps unreliably across browsers/viewport
   sizes — it silently clipped tools rather than reflowing them. A scrollbar
   guarantees every tool stays reachable at any window size. */
.toolbar-tools {
  display: flex;
  flex-direction: column;
  /* NOT flex-shrink:0 here — the container must be allowed to shrink below
     its content's natural size so ITS OWN overflow:auto can kick in when
     the wrap is tight; flex-shrink:0 belongs on .tool (below) so the ICONS
     never shrink, not on the container (that made the container refuse to
     shrink and spill past the wrap's edge instead of scrolling). min-height
     is required alongside — flex items default to min-height:auto, which
     silently blocks shrinking-to-scroll the same way flex-shrink:0 would. */
  min-height: 0;
  gap: 5px;
  /* A STARTING cap only — _fitBarInner replaces it with an inline max-height
     measured against the canvas on every layout pass, because the bar is
     draggable and the Create tab's 14 tools (625 px) exceed any figure that
     also fits a short window. The flat `min(600px, 100vh - 160px)` this
     replaces made Create scroll always, on every screen. */
  max-height: calc(100vh - 90px);
  overflow-y: auto;
  overflow-x: hidden;
  padding-right: 2px;
  scrollbar-width: thin;
}
.toolbar-wrap.horizontal .toolbar-tools {
  flex-direction: row;
  min-height: 0;
  min-width: 0;              /* same reasoning as min-height, for the row axis */
  max-height: none;
  /* generously exceeds the ~632px true content width (13 × 44px + 12 × 5px
     gap) so a normal desktop window shows the whole row with no scrollbar —
     it should only appear on genuinely narrow viewports */
  max-width: min(94vw, 700px);
  overflow-x: auto;
  overflow-y: hidden;
  padding-right: 0;
  padding-bottom: 2px;
}
.tool {
  position: relative;
  flex-shrink: 0;           /* never squash — overflow must scroll, not shrink icons unreadable */
  width: 44px; height: 40px;
  font: inherit;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.tool:hover { background: var(--teal-soft); }
.tool.active { background: var(--teal); border-color: var(--teal); }
.tool.active .tool-label { color: #fff; }
.tool-label { font-size: 17px; font-weight: 800; color: var(--tool-tint, var(--ink)); }
.tool-label svg { display: block; }
.icon-btn { padding: 3px 6px; }
.icon-btn svg { display: block; }
.corner-toggle { display: inline-flex; gap: 2px; }
.corner-btn { color: var(--ink-soft); padding: 3px 5px; }
.corner-btn.active { color: #fff; background: var(--teal); border-color: var(--teal); }
.tool-key {
  position: absolute;
  right: 3px; bottom: 1px;
  font-size: 9px;
  color: var(--ink-soft);
}
.tool.active .tool-key { color: rgba(255,255,255,.8); }

/* Play dock — shares .toolbar-wrap chrome (drag grip + orient toggle) with
   the piece toolbar; only the content layout (.dock-inner) is dock-specific.
   Default orientation is horizontal, opposite of the piece toolbar, so the
   row/column swap is inverted relative to .toolbar-tools. */
.dock-inner {
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-shrink: 0;
  min-height: 0;
  gap: 8px;
  max-width: min(90vw, 700px);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
}
.toolbar-wrap.vertical .dock-inner {
  flex-direction: column;
  align-items: stretch;
  min-width: 0;
  max-width: none;
  max-height: min(600px, calc(100vh - 160px));
  overflow-y: auto;
  overflow-x: hidden;
}
.dock-inner .btn { flex-shrink: 0; }
.play-btn { min-width: 40px; justify-content: center; background: var(--teal); border-color: var(--teal); color: #fff; font-size: 17px; }
.play-btn.playing { background: var(--danger); border-color: var(--danger); }
.dock-stat {
  flex-shrink: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-soft);
  padding: 0 4px;
  white-space: nowrap;
}
.toolbar-wrap.horizontal .dock-stat:not(:last-child)::after { content: ' · '; color: var(--line); margin-left: 4px; }
.toolbar-wrap.vertical .dock-stat { text-align: center; }
.dock-time { min-width: 48px; text-align: right; }
.toolbar-wrap.vertical .dock-time { min-width: 0; text-align: center; }
.dock-speed { width: 70px; flex-shrink: 0; }
.dock-speed-label { min-width: 32px; }

.chip-grip { cursor: grab; color: var(--ink-soft); padding: 4px 2px; letter-spacing: -2px; }
.chip-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 2px 0; width: 100%; }
.chip-label { font-size: 12px; font-weight: 700; color: var(--ink-soft); min-width: 52px; }
.chip-note { font-size: 12px; color: var(--ink-soft); }
.chip-input { width: 58px; font-size: 12px; padding: 2px 6px; border: 1px solid var(--line); border-radius: 6px; }
.chip-slider { width: 90px; }
.swatch {
  width: 20px; height: 20px;
  border-radius: 6px;
  border: 1px solid rgba(0,0,0,.25);
  cursor: pointer;
  padding: 0;
}
.swatch.wide { width: 34px; }
.swatch.active { outline: 2px solid var(--teal); }

/* ONE LINE. It is a toolbar, and a toolbar that reflows to two rows every
   time the Group button appears moves every other button under the cursor —
   the ops are picked by position, so the row has to stay put. `nowrap` alone
   would let it run off a narrow screen (it is pinned to the right edge and
   the full set is ~550 px), so it is bounded by its pane and scrolls
   horizontally in the case that cannot fit rather than growing a second row. */
.align-chip {
  right: 12px;
  bottom: 14px;
  display: flex;
  align-items: center;
  gap: 5px;
  flex-wrap: nowrap;
  max-width: calc(100% - 24px);
  overflow-x: auto;
  scrollbar-width: thin;
  background: rgba(255,255,255,.95);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 7px 10px;
  box-shadow: var(--shadow);
}
/* nothing in the row may be squeezed thinner than its content: flex items
   default to shrinking, which would turn the glyph buttons into slivers
   before the scroll ever engaged */
.align-chip > * { flex: 0 0 auto; }
.align-chip .chip-label { min-width: 0; white-space: nowrap; }
.info-chip {
  left: 12px;
  bottom: 14px;
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-soft);
  background: rgba(255,255,255,.85);
  border-radius: 8px;
  padding: 4px 10px;
  min-height: 14px;
  pointer-events: none;   /* click-through, except its own grip below */
}
.info-chip .chip-grip { pointer-events: auto; }

.win-banner {
  top: 56px; right: 14px;
  width: 250px;
  background: rgba(255,255,255,.97);
  border: 2px solid var(--gold);
  border-radius: 14px;
  padding: 12px 14px;
  box-shadow: var(--shadow);
}
/* The scenery-edit banner (§10.5): centred, persistent, and a BUTTON — the
   sign that says which layer you are in is also the way out of it. */
.back-banner {
  top: 12px; left: 50%; transform: translateX(-50%);
  background: rgba(43, 38, 66, .92);
  color: #fff;
  border: 1px solid rgba(255,255,255,.25);
  border-radius: 999px;
  padding: 7px 16px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  box-shadow: var(--shadow);
}
.back-banner:hover { background: rgba(43, 38, 66, 1); }
.win-title { font-weight: 800; font-size: 17px; }
.win-stats { font-size: 13px; color: var(--ink-soft); margin: 4px 0 6px; }
/* The full set has to read as ONE row — a wrapped badge looks like an
   afterthought rather than part of the haul. Eight 26 px badges don't fit the
   250 px banner, so they lose 2 px each here (still bigger than the 21 px
   `tiny` used on cards). Wrap stays on as the safe failure mode if the set
   ever outgrows even this. */
.win-badges { display: flex; flex-wrap: nowrap; margin-bottom: 8px; }
.win-badges .badge { width: 24px; height: 24px; margin-right: 2px; }
.win-note { font-size: 12px; color: var(--ink-soft); margin-bottom: 8px; }
.win-actions { display: flex; gap: 6px; flex-wrap: wrap; }
/* Nailed It fuse: a burning rope shrinking over the 5s grace window,
   ending at a dimmed target that _renderWinBanner lights up (or doesn't)
   once grace resolves and the real badge row replaces this whole banner. */
.fuse-row { display: flex; align-items: center; gap: 8px; margin: 6px 0 2px; }
.fuse-track {
  position: relative;
  flex: 1;
  height: 7px;
  background: rgba(35,42,53,.1);
  border-radius: 4px;
  overflow: visible;
}
.fuse-remaining {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 100%;
  background: linear-gradient(90deg, #a87b4f, #7d5a38);
  border-radius: 4px;
}
.fuse-spark {
  position: absolute;
  right: -9px; top: 50%;
  transform: translate(50%, -50%);
  font-size: 14px;
  filter: drop-shadow(0 0 3px rgba(255,140,0,.85));
}
.fuse-target { font-size: 15px; opacity: .5; }
.fuse-label { font-size: 11px; color: var(--ink-soft); margin-bottom: 8px; }

.replay-bar {
  position: absolute;
  top: 56px; left: 50%;
  transform: translateX(-50%);
  z-index: 11;
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(35,42,53,.92);
  color: #fff;
  border-radius: 12px;
  padding: 8px 14px;
  font-size: 13px;
  font-weight: 600;
}

.toast {
  position: absolute;
  left: 50%;
  bottom: 74px;
  transform: translateX(-50%);
  z-index: 30;
  background: rgba(35,42,53,.94);
  color: #fff;
  font-size: 13px;
  padding: 9px 16px;
  border-radius: 10px;
  max-width: 420px;
  text-align: center;
  transition: opacity 0.2s;
}
.toast.hidden { opacity: 0; pointer-events: none; }
.hidden { display: none !important; }
.toast.hidden { display: block !important; }

.ctx-menu {
  position: absolute;
  z-index: 50;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 5px;
  min-width: 150px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: min(78vh, 640px);
  overflow-y: auto;
}
.ctx-item {
  font: inherit;
  font-size: 13px;
  text-align: left;
  background: transparent;
  border: 0;
  border-radius: 7px;
  padding: 7px 10px;
  cursor: pointer;
}
.ctx-item:hover { background: var(--teal-soft); }
.ctx-item.danger { color: var(--danger); }
.ctx-item.active { background: var(--teal-soft); }
.ctx-title { font-size: 11px; font-weight: 700; color: var(--ink-soft); padding: 4px 10px 2px; }
.ctx-swatches { display: flex; gap: 5px; padding: 4px 10px 6px; flex-wrap: wrap; }
/* a stack of rows inside a menu: a toggle, the dial it reveals, and a note
   about what it does to the rest of the level (the planet panel, §5.10) */
.chip-col { display: flex; flex-direction: column; gap: 2px; padding: 2px 10px 4px; }
.chip-col .ctx-item { padding-left: 0; padding-right: 0; }
.ctx-note { font-size: 11px; color: var(--ink-soft); line-height: 1.35; max-width: 230px; }

/* terrain texture + surface panel. The 4×4 grid sets the menu's width, and
   the terrain menu is the tallest one in the app (panel + corners + z-order +
   path controls), so this is also where the menu gets a scroll ceiling. */
.tex-panel { display: flex; flex-direction: column; }
.tex-grid { display: grid; grid-template-columns: repeat(4, 44px); gap: 4px; padding: 2px 10px 6px; }
.tex-swatch {
  width: 44px; height: 44px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 6px;
  background-size: cover;
  background-position: center;
  cursor: pointer;
}
.tex-swatch:hover { border-color: var(--teal); }
.tex-swatch.active { outline: 2px solid var(--teal); outline-offset: 1px; border-color: var(--teal); }
.tex-head { display: flex; justify-content: space-between; align-items: baseline; }
.tex-reset {
  font: inherit; font-size: 10px; font-weight: 600;
  background: transparent; border: 0; padding: 0;
  color: var(--teal); cursor: pointer;
}
.tex-reset:hover { text-decoration: underline; }
.tex-row {
  display: grid;
  grid-template-columns: 40px 1fr 50px;
  align-items: center;
  gap: 6px;
  padding: 1px 10px;
  font-size: 11px;
  cursor: pointer;
}
.tex-row input[type=range] { width: 100%; margin: 0; accent-color: var(--teal); }
.tex-name { color: var(--ink-soft); font-weight: 600; }
.tex-val { text-align: right; font-size: 10px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
/* mini toolbar at the top of the context menu — the piece toolbar at 18 px */
.ctx-tools {
  display: grid;
  grid-template-columns: repeat(var(--ctx-cols, 7), 1fr);
  gap: 3px;
  padding: 2px 2px 5px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 3px;
}
.ctx-tool {
  display: flex; align-items: center; justify-content: center;
  padding: 3px;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 6px;
  cursor: pointer;
}
.ctx-tool:hover { background: var(--teal-soft); }
.ctx-tool.active { background: var(--teal); border-color: var(--teal); }
.ctx-tool svg { display: block; }
/* The background picker rides the same lattice as the tool grid above it —
   same columns, same gap, same cell — because the two sit in one menu and any
   other geometry reads as two widgets that happened to land in one box. It is
   the LAST thing in the menu, so it drops the divider the tool grid uses to
   separate itself from the rows below. Its cells carry a gradient rather than
   an icon, so they need their own height (an icon cell is sized by its 18 px
   SVG) and must keep their fill when active — hence the tint moves to the
   outline instead of the background. */
.ctx-bg-grid { border-bottom: 0; margin-bottom: 0; }
.ctx-bg { height: 26px; padding: 0; }
.ctx-bg:hover { outline: 2px solid var(--teal-soft); }
.ctx-bg.active { outline: 2px solid var(--teal); border-color: var(--teal); }
/* z-order ladder: four chevrons in one row, never wrapping to a second */
.z-row { flex-wrap: nowrap; gap: 4px; }
.z-btn { color: var(--ink-soft); }
.z-btn:hover { color: var(--teal); }

/* ---------- profile tables (same sheet as the Solves screen) ---------- */

.profile-table { margin-bottom: 26px; }
.profile-table .info-filters { margin-bottom: 8px; }
.solve-table td.where, .solve-table th.where { white-space: nowrap; }
.solve-table td.vis, .solve-table th.vis { white-space: nowrap; font-size: 12px; }
/* the tag says where a thing lives; local is the one that needs noticing,
   because it exists in this browser and nowhere else */
.where-tag { font-size: 11px; padding: 1px 6px; border-radius: 999px; white-space: nowrap; }
.where-tag.local { background: var(--gold); color: #fff; }
.where-tag.server { background: var(--paper-dim); color: var(--ink-soft); border: 1px solid var(--line); }
.solve-table tr.local td.name a { color: var(--ink); }
.btn.tiny.danger { color: var(--danger); }
.btn.tiny.danger:hover { background: var(--danger); color: #fff; }

/* Bulk delete on the levels table (§13). The select-all header must NOT look
   like a sortable column — every other `th` in this table is a click target
   that re-sorts, and one that re-sorted under the hand about to tick it would
   be a trap. */
.solve-table th.pick, .solve-table td.pick { width: 30px; padding-right: 0; text-align: center; }
.solve-table th.pick { cursor: default; }
.solve-table th.pick .sort-mark { display: none; }
.row-pick { width: 15px; height: 15px; accent-color: var(--teal); cursor: pointer; margin: 0; }
.row-pick:disabled { cursor: default; opacity: .4; }
/* The bar only exists while something is selected, so it can be loud: it is
   holding a destructive action and the count is the thing to read. */
.bulk-bar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0 0 8px;
  padding: 6px 10px;
  background: var(--teal-soft);
  border: 1px solid var(--teal);
  border-radius: 8px;
  font-size: 13px; font-weight: 600;
}
.bulk-bar.hidden { display: none; }

/* ---------- stale-tab notice ----------
   Deliberately quiet: bottom-left, small, translucent until you look at it.
   It is a note for whoever is editing the code, not an announcement — it must
   never read as an error, and it must never sit over anything you're using. */

.build-note {
  position: fixed;
  left: 12px; bottom: 12px;
  z-index: 400;                    /* above the Solves screen, which is 200 */
  display: flex; align-items: center; gap: 8px;
  padding: 5px 6px 5px 11px;
  font-size: 12px;
  color: var(--ink-soft);
  background: rgba(255,255,255,.82);
  border: 1px solid var(--line);
  border-radius: 999px;
  box-shadow: var(--shadow);
  opacity: 0.55;
  transition: opacity .15s ease;
}
.build-note:hover { opacity: 1; }
/* The "you left the Maker" offer shares the build note's chrome but not its
   shyness: it is answering an accident that just happened, so it reads at full
   strength, and it sits on the other side so the two can never overlap. */
.return-note { left: auto; right: 12px; opacity: 1; }
.build-note .btn { font-size: 11px; padding: 2px 8px; }
.build-note-x { padding: 2px 6px; color: var(--ink-soft); }

/* ---------- Solves screen (level details, records, solves, comments) ----------
   Fixed to the viewport and above the site nav: it replaced a 340 px slide-out
   aside, and "takes over the screen" has to include the nav or it isn't one. */

.info-screen {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  background: linear-gradient(var(--sky-top), var(--sky-bot)) fixed;
}
/* sticky header so the way out is always on screen, however far down the
   solve table you have scrolled */
.info-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 18px;
  background: rgba(255,255,255,.97);
  border-bottom: 1px solid var(--line);
  box-shadow: var(--shadow);
}
.info-bar h2 { margin: 0; font-size: 20px; }
.info-close { font-size: 18px; padding: 2px 10px; }
.info-body {
  flex: 1;
  overflow-y: auto;
  padding: 18px;
  /* the content column is capped, not the panel: a full-width table of solves
     on a 27" monitor is a row of numbers a foot apart */
  max-width: 1180px;
  width: 100%;
  margin: 0 auto;
}
.info-section { margin-bottom: 26px; }
.info-section h3 { margin: 0 0 10px; font-size: 16px; }

/* 1. details + thumbnail */
.info-details {
  display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px;
}
.info-thumb {
  width: 384px; max-width: 100%; height: auto;
  border-radius: 8px; border: 1px solid var(--line);
  background: var(--paper-dim);
}
.info-meta { flex: 1 1 320px; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.info-meta p { margin: 0; }
.info-desc { white-space: pre-wrap; }
.info-scales { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.info-badges { display: flex; flex-wrap: nowrap; font-size: 16px; }

/* the link people paste into Discord — full, selectable, one-click copy */
.info-link-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.info-link-row label { flex: 0 0 auto; font-size: 12px; }
.info-url {
  flex: 1 1 260px; min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  background: var(--paper-dim);
}

/* 2. records — each tile now names its holder underneath */
.record-by { font-size: 12px; margin-top: 2px; }
.record-by a { color: var(--teal); text-decoration: none; }
.record-by a:hover { text-decoration: underline; }

/* 3. the solve sheet */
/* Same shape as the Workshop's row, same reasoning — see .browse-search-row. */
.info-filters { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; flex-wrap: wrap; }
.info-filters .search { flex: 0 1 240px; min-width: 140px; }
.info-filters .author { flex: 0 1 170px; min-width: 120px; }
.info-filters select { flex: 0 0 auto; }
.info-count { margin-left: auto; font-size: 12px; }
/* the table scrolls sideways inside its own box — the page never does */
.solve-table-wrap {
  overflow-x: auto;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.solve-table { border-collapse: collapse; width: 100%; font-size: 13px; }
.solve-table th, .solve-table td {
  padding: 6px 9px;
  text-align: left;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.solve-table thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--paper-dim);
  font-size: 12px; font-weight: 700; white-space: nowrap;
  cursor: pointer;
  user-select: none;
}
.solve-table thead th:hover { color: var(--teal); }
.solve-table th.n, .solve-table td.n { text-align: right; white-space: nowrap; }
.solve-table th.num, .solve-table td.num { white-space: nowrap; }
.solve-table td.who { font-weight: 700; white-space: nowrap; }
.solve-table td.who a { color: var(--teal); text-decoration: none; }
.solve-table td.name { min-width: 160px; }
.solve-table td.act { white-space: nowrap; text-align: right; }
.solve-table td.act > * { margin-left: 4px; }
.solve-table tr.attempt td { opacity: 0.6; }
.solve-table tbody tr:hover td { background: var(--teal-soft); }
.sort-mark { margin-left: 4px; opacity: 0.35; font-size: 10px; }
.sort-mark::after { content: '↕'; }
.sorted-up .sort-mark, .sorted-down .sort-mark { opacity: 1; color: var(--teal); }
.sorted-up .sort-mark::after { content: '↑'; }
.sorted-down .sort-mark::after { content: '↓'; }
.solve-table .solve-note { font-size: 12px; margin-top: 2px; white-space: normal; }
.solve-table .solve-visibility { font-size: 11px; padding: 1px 4px; width: auto; display: inline-block; }
.info-empty { text-align: center; padding: 18px; }

/* title and rating share a line, and wrap together rather than the stars
   dangling on their own when the panel is narrow */
.level-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.level-head h2 { margin: 0; }

.rating-row { display: inline-flex; align-items: center; gap: 1px; cursor: help; }
/* square box so the "yours" ring is a circle and not an oval */
.star {
  font-size: 20px;
  width: 1.25em; height: 1.25em;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; padding: 0; line-height: 1;
  color: #cfd6e4;
  cursor: pointer;
  border-radius: 50%;
}
.star:hover { color: var(--gold); }
/* filled = the global average */
.star.on { color: var(--gold); }
/* ringed = the star YOU picked. An outline rather than a border so nothing
   reflows when it appears, and it follows the radius into a circle. */
.star.mine { outline: 2px solid var(--gold); outline-offset: -2px; }
.star.mine:not(.on) { outline-color: color-mix(in srgb, var(--gold) 60%, var(--line)); }
.rating-row.readonly .star { cursor: inherit; }
.rating-row.readonly .star:hover { color: #cfd6e4; }
.rating-row.readonly .star.on:hover { color: var(--gold); }

/* difficulty 1–10: a ten-pip bar, not ten stars. Length answers "how hard",
   the green→red ramp answers it again, and neither can be mistaken for the
   gold quality stars sitting next to it. */
.diff-row { display: inline-flex; align-items: center; gap: 2px; cursor: help; }
.pip {
  width: 6px; height: 15px;
  background: #d8dde8;
  border: 0; padding: 0; margin: 0;
  border-radius: 2px;
  cursor: pointer;
  transition: background .1s;
}
.pip:first-child { border-radius: 4px 2px 2px 4px; }
.pip:last-child { border-radius: 2px 4px 4px 2px; }
/* filled = the crowd average, each pip in its own place on the ramp */
.pip.on { background: var(--pip); }
/* hover fills 1..hovered like a slider. :has() reaches the EARLIER siblings,
   which no plain combinator can; without it you just get the one pip lit. */
.diff-row:not(.readonly) .pip:hover,
.diff-row:not(.readonly) .pip:has(~ .pip:hover) { background: var(--pip); }
/* ringed = the number YOU picked. Outline, so nothing reflows when it lands;
   zero offset, or at 2px gaps the ring sits on top of its neighbours. */
.pip.mine { outline: 2px solid var(--ink); outline-offset: 0; }
.diff-row.readonly .pip { cursor: inherit; }

/* nobody has voted: an empty row of cells otherwise reads as a half-loaded
   progress bar, which is a louder statement than "no ratings" ever was */
.rating-row.empty, .diff-row.empty { opacity: .4; }
.rating-row.empty:hover, .diff-row.empty:hover { opacity: 1; }

.rating-note { font-size: 12px; white-space: nowrap; }
/* beside a title the stars shouldn't outweigh it */
.level-head .star, .hud-top .star { font-size: 15px; }
.hud-top .rating-row { margin-left: 2px; }
.level-head .pip, .hud-top .pip { height: 13px; width: 5px; }
.hud-top .diff-row { margin-left: 8px; }
/* on a card the two scales sit inside the subtitle line, so they shrink — and
   they need real air between them or they read as one strange strip */
.card-scales { display: inline-flex; align-items: center; gap: 7px; vertical-align: middle; }
.card-sub .star { font-size: 11px; width: 1em; height: 1em; }
.card-sub .pip { height: 8px; width: 3px; border-radius: 1px; }
.card-sub .diff-row { gap: 1.5px; }
.solve-list { display: flex; flex-direction: column; gap: 6px; }
.solve-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  font-size: 13px;
  background: var(--paper-dim);
  border-radius: 8px;
  padding: 6px 9px;
}
.solve-row.attempt { opacity: 0.65; }
.solve-who { font-weight: 700; }
.solve-who a { color: var(--teal); text-decoration: none; }
.solve-badges { display: inline-flex; flex-wrap: nowrap; font-size: 13px; }
.comments { display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px; }
.comment { background: var(--paper-dim); border-radius: 8px; padding: 7px 10px; font-size: 13px; }
.comment p { margin: 3px 0 0; }
.comment-remove { float: right; padding: 0 5px; line-height: 1.4; }
.comment-input { display: flex; gap: 6px; }

/* ---------- modal ---------- */

.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(35,42,53,.45);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.modal {
  background: #fff;
  border-radius: 14px;
  box-shadow: var(--shadow);
  width: min(480px, 100%);
  max-height: 86vh;
  overflow-y: auto;
  padding: 18px 20px;
}
/* The save dialog trades a dropdown for a row of destination buttons, and five
   buttons plus their labels don't fit the standard box. A modifier rather than
   a wider .modal everywhere: every other dialog is a name and two actions, and
   600px of white space around those reads as a mistake. */
.modal.wide { width: min(600px, 100%); }
.modal h3 { margin: 0 0 10px; }

/* The label editor (§10.6). There is no preview pane: the FIELD is the preview,
   rendered in the chosen face, colour and style as you type — a truer answer
   than a thumbnail beside it, and one fewer thing to read. Size is absent on
   purpose too; it is the corner handles and the scroll wheel. */
.text-edit-area {
  font-size: 20px;              /* big enough that a typeface is identifiable */
  line-height: 1.25;
  resize: vertical;
  min-height: 74px;
  transition: background .12s ease;
}
/* Rows of controls, label on the left, in the modal AND in the context menu —
   one builder feeds both (`_textControls`), so one rule dresses both. */
.text-controls { gap: 6px; padding: 6px 0 2px; }
.text-controls .ctx-swatch-row {
  display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
}
.text-controls .btn.tiny { min-width: 26px; }
/* the dropdown prints each option in its own face — the only honest preview of
   a typeface is the typeface */
.text-font-select { flex: 1 1 150px; min-width: 120px; max-width: 240px; padding: 5px 8px; }
/* The custom-colour swatch: a colour wheel until one is picked, then the picked
   colour itself, with a pencil over it either way so it reads as "open the
   picker" rather than as a fifteenth palette entry. */
.swatch-custom { position: relative; }
.swatch-custom-mark {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; color: #fff;
  text-shadow: 0 0 3px rgba(35,42,53,.9);
  pointer-events: none;
}
.swatch-custom.open { outline: 2px solid var(--teal); }

/* The picker itself (§10.6) — HSV square, hue strip, R/G/B and hex. Built
   rather than `<input type=color>`, which opens the OS dialog: a different
   window, a different look on every machine, and no live preview of the label
   underneath it. */
.cp {
  display: flex; flex-direction: column; gap: 6px;
  padding: 8px; margin: 2px 0 4px;
  background: var(--paper-dim);
  border: 1px solid var(--line); border-radius: 10px;
  width: max-content;
}
.cp-square-wrap, .cp-hue-wrap { position: relative; line-height: 0; }
.cp-square, .cp-hue { display: block; border-radius: 6px; cursor: crosshair; touch-action: none; }
.cp-dot, .cp-hue-dot {
  position: absolute;
  border: 2px solid #fff;
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(35,42,53,.6);
  pointer-events: none;
}
.cp-dot { width: 12px; height: 12px; margin: -6px 0 0 -6px; }
.cp-hue-dot { width: 10px; height: 18px; margin: -2px 0 0 -5px; border-radius: 4px; top: 0; }
.cp-fields { display: flex; align-items: center; gap: 5px; }
.cp-preview {
  width: 26px; height: 26px; flex: 0 0 auto;
  border: 1px solid var(--line); border-radius: 6px;
}
.cp-field { display: flex; align-items: center; gap: 2px; font-size: 11px; color: var(--ink-soft); }
.cp-num { width: 46px; padding: 3px 4px; font-size: 12px; }
/* the spinners are noise at this size — the value is typed or dragged */
.cp-num::-webkit-outer-spin-button, .cp-num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.cp-num { -moz-appearance: textfield; }
.cp-hex {
  width: 78px; padding: 3px 5px; font-size: 12px;
  font-family: ui-monospace, Consolas, monospace;
}
.text-controls .chip-note { font-family: ui-monospace, Consolas, monospace; font-size: 11px; }
.ctx-menu .text-controls { padding: 4px 10px 6px; max-width: 268px; }
.modal-actions { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
/* Cancel-side left, commit-side right. The gap between the two groups is the
   point: the destructive-to-the-gesture button can't sit next to the one that
   publishes to the world. */
.modal-actions.split { justify-content: space-between; align-items: center; }
.modal-actions.split .action-group { display: flex; gap: 8px; flex-wrap: wrap; }

/* Where a save goes (§11.6): four ordered stops from "this browser" out to
   "the whole Workshop", resting on the safe end. The labels are buttons too —
   a four-stop range is fiddly to hit exactly, and clicking the word you want
   is what everyone tries first. */
.vis-picker { margin-top: 14px; }
.vis-ticks { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.vis-tick {
  font: inherit; font-size: 12px; font-weight: 700;
  color: var(--ink-soft);
  background: transparent;
  border: 0;
  padding: 4px 2px;
  border-radius: 6px;
  cursor: pointer;
}
.vis-tick:hover { background: var(--teal-soft); }
.vis-tick.on { color: var(--teal); }

/* The track is built rather than borrowed so its stops can sit exactly under
   the WORDS above it — at (i + 0.5)/n, the centre of each label's column. A
   native range thumb sweeps its own left edge to its own right edge instead,
   which is why it could never line up (see _visSlider). Every position here is
   a percentage of the same box, so the fill, the dots and the thumb agree by
   construction. */
.vis-track {
  position: relative;
  height: 22px;
  margin: 2px 0 0;
  cursor: pointer;
  touch-action: none;
}
.vis-track:focus-visible { outline: 2px solid var(--teal); outline-offset: 3px; border-radius: 6px; }
.vis-rail, .vis-fill {
  position: absolute; top: 50%; left: 0;
  height: 6px; margin-top: -3px;
  border-radius: 999px;
}
.vis-rail { right: 0; background: var(--line); }
.vis-fill { background: var(--teal); }
.vis-dot {
  position: absolute; top: 50%;
  width: 8px; height: 8px; margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: var(--paper); border: 2px solid var(--line);
  pointer-events: none;
}
.vis-dot.on { background: var(--teal); border-color: var(--teal); }
.vis-thumb {
  position: absolute; top: 50%;
  width: 18px; height: 18px; margin: -9px 0 0 -9px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--teal);
  box-shadow: 0 1px 3px rgba(35,42,53,.3);
  pointer-events: none;
  /* No transition on `left`. Four discrete stops do not need easing, and a
     transition is a promise the page will keep producing frames — which is not
     true everywhere this runs (a non-compositing embed froze the thumb at its
     first value while the fill, which has none, tracked correctly). The
     cheapest way not to depend on animation is not to animate. */
}
.vis-note { font-size: 12px; line-height: 1.4; min-height: 34px; margin-top: 4px; }

/* ---------- admin ---------- */

.admin-stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 10px;
  margin-bottom: 10px;
}
.admin-stat { background: var(--paper-dim); border-radius: 8px; padding: 10px 12px; text-align: center; }
.admin-stat-val { font-size: 22px; font-weight: 700; }
.admin-user-table { display: flex; flex-direction: column; gap: 6px; margin-bottom: 30px; }
.admin-user-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  background: var(--paper-dim); border-radius: 8px; padding: 8px 12px;
}
.admin-user-name { flex: 1; font-weight: 600; }
.admin-user-row.banned { background: rgba(224, 85, 85, 0.13); }
.admin-user-row.held { background: rgba(212, 160, 23, 0.13); }

/* Active now. The dot is the only ornament: a live list wants one glance to
   answer "is anyone here", and a green pip does that faster than a timestamp. */
.admin-active .active-dot { color: var(--teal); font-size: 10px; margin-right: 5px; vertical-align: 1px; }
.admin-active-path { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; opacity: .75; }

/* Admin users spreadsheet. A grid rather than a flex list so every column
   lines up down the page — the whole point is comparing accounts against each
   other and against their limits. Scrolls horizontally on its own so a narrow
   window never makes the page itself scroll sideways. */
.admin-grid {
  display: grid;
  grid-template-columns: minmax(150px, 1.4fr) minmax(140px, 1.2fr) 92px 72px
                         104px 64px 104px 68px 84px 110px 70px 92px;
  gap: 1px 0;
  margin-bottom: 30px;
  overflow-x: auto;
  font-size: 13px;
}
.admin-grid-row { display: contents; }
.admin-grid-c, .admin-grid-h {
  padding: 7px 8px;
  background: var(--paper-dim);
  display: flex; align-items: center; gap: 2px;
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.admin-grid-c.num { justify-content: flex-end; }
.admin-grid-h {
  position: sticky; top: 0; z-index: 1;
  background: var(--paper);
  border-bottom: 2px solid var(--line);
  font-weight: 700; font-size: 12px; color: var(--ink-soft);
  cursor: pointer; text-align: left;
}
.admin-grid-h:hover { color: var(--ink); }
.admin-grid-h.sorted { color: var(--teal); }
.admin-grid-row.banned .admin-grid-c { background: rgba(224, 85, 85, 0.13); }
.admin-grid-row.held .admin-grid-c { background: rgba(212, 160, 23, 0.13); }
.admin-grid-c.at-limit .admin-used { color: var(--danger); font-weight: 700; }
.admin-limit {
  width: 52px; min-width: 0;
  font: inherit; text-align: right;
  border: 1px solid var(--line); border-radius: 5px;
  padding: 1px 3px; background: var(--paper);
}
.admin-limit:focus { outline: 2px solid var(--teal); border-color: var(--teal); }
.admin-status { font: inherit; border: 1px solid var(--line); border-radius: 5px; padding: 1px 3px; background: var(--paper); }
.admin-status.hold { color: var(--gold); font-weight: 700; }
.admin-status.banned { color: var(--danger); font-weight: 700; }
.role-tag { color: var(--teal); font-weight: 800; font-size: 11px; }

.level-records { display: flex; gap: 8px; margin-bottom: 10px; }
.level-records .record {
  flex: 1; text-align: center;
  background: var(--paper-dim); border-radius: 8px; padding: 6px 4px;
  font-size: 12px;
}
.level-records .record-val { font-size: 15px; font-weight: 700; }

/* solve star ratings + comment thumbs */
.solve-stars { display: inline-flex; align-items: center; }
.solve-stars .star.tiny { font-size: 13px; padding: 0; }
.solve-name { font-weight: 600; }
.comment-votes { display: inline-flex; align-items: center; gap: 1px; margin-left: 4px; }
.comment-votes .vote { padding: 0 3px; font-size: 12px; opacity: 0.5; }
.comment-votes .vote:hover { opacity: 1; }
.comment-votes .vote.on { opacity: 1; background: var(--teal-soft); }
.flagged-row { align-items: flex-start; }
.flagged-body { flex: 1; min-width: 220px; }
.flagged-body a.admin-user-name { color: var(--teal); text-decoration: none; }
.flagged-text { margin: 4px 0; font-size: 13px; }

/* ---------- tooltips ---------- */

/* One fixed node reused for every [data-tip] (util.js installTooltips), so it
   can't be clipped by the overflow:hidden on level cards. */
.tip {
  position: fixed;
  left: 0; top: 0;
  z-index: 200; /* above the modal overlay (100) — it's pointer-events:none */
  max-width: 230px;
  background: var(--ink);
  color: #fff;
  font-size: 12px;
  line-height: 1.35;
  border-radius: 8px;
  padding: 6px 9px;
  box-shadow: var(--shadow);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s ease;
}
.tip.on { opacity: 1; }
/* one-line tips (data-tip-1line): short enough to read as a single sentence,
   and wrapping them orphans the last word. Still bounded by the viewport, and
   showTip clamps the x position, so it can't run off the edge. */
.tip.one-line { max-width: min(96vw, 520px); white-space: nowrap; }

/* ---------- misc ---------- */

@media (max-width: 860px) {
  .import-grid { grid-template-columns: 1fr; }
}

@media (max-width: 760px) {
  .main { width: calc(100% - 24px); }
  .info-body { padding: 12px; }
  .info-details { padding: 10px; }
  .info-count { margin-left: 0; }
  .nav { gap: 8px; padding: 8px 12px; }
  .nav-link { padding: 5px 7px; font-size: 13px; }
}

/* ---------- sound settings (§17) ---------- */
.nav-icon { font-size: 17px; line-height: 1; padding: 0 2px; }
.settings { max-width: 720px; }
.settings h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-soft); margin: 22px 0 8px; }
.set-row { display: flex; align-items: center; gap: 9px; padding: 6px 0; cursor: pointer; }
.set-row input[type=range] { flex: 1; max-width: 260px; accent-color: var(--teal); }
.set-label { font-weight: 600; font-size: 13px; }
.set-hint { font-size: 12px; }
.set-val { font-size: 12px; font-variant-numeric: tabular-nums; color: var(--ink-soft); min-width: 42px; text-align: right; }
.set-master { border-bottom: 1px solid var(--line); padding-bottom: 12px; }
/* three side-by-side cards: the hint is the whole point, so they need room to
   breathe rather than being a radio group of one-word labels */
.set-themes { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 8px; }
.set-theme {
  display: flex; flex-direction: column; gap: 5px;
  text-align: left; font: inherit; cursor: pointer;
  padding: 11px 13px;
  background: #fff;
  border: 1px solid var(--line); border-radius: 10px;
}
.set-theme:hover:not(:disabled) { border-color: var(--teal); }
.set-theme.active { border-color: var(--teal); outline: 2px solid var(--teal); outline-offset: -1px; }
.set-theme:disabled { opacity: .5; cursor: default; }
.set-theme-name { font-weight: 700; font-size: 13px; }
.set-theme-hint { font-size: 11.5px; color: var(--ink-soft); line-height: 1.45; }
.set-section { display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--line); }
.set-section:last-child { border-bottom: 0; }
.set-section .set-row { flex: 1; }

/* ---------- controls reference + tutorial (§18) ---------- */
.keys h2 { margin: 26px 0 8px; }
.key-stories { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px; margin: 14px 0 6px; }
.key-story {
  background: #fff;
  border: 1px solid var(--line); border-radius: 10px;
  padding: 11px 13px;
}
.key-story h3 { margin: 0 0 5px; font-size: 13px; }
.key-story p { margin: 0; font-size: 12.5px; color: var(--ink-soft); line-height: 1.5; }
.keys-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.keys-table td { padding: 6px 10px 6px 0; border-bottom: 1px solid var(--line); vertical-align: top; line-height: 1.5; }
.keys-table tr:last-child td { border-bottom: 0; }
.keys-table td:first-child { white-space: nowrap; width: 180px; }
.keycol { display: inline-flex; align-items: center; gap: 3px; flex-wrap: wrap; }
kbd {
  font: 600 11.5px var(--font);
  background: var(--paper-dim);
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 2px 6px;
}
/* The snap button has three states, so it has to read as three things at a
   glance and not as one thing that is sometimes lit (§8.1). ON is the lit
   toolbar button every other toggle uses; REVERSED is an ordinary button with
   the ⇧ prefix saying which key wakes it; OFF is dimmed, because "nothing will
   move under your cursor" should look like nothing. */
.snap-btn { min-width: 34px; font-variant-emoji: text; }
.snap-btn.snap-off { opacity: .55; }

/* The FC retraining page (§18): one difference per block, the thing running
   above the words, same show-then-say order the tutorial uses. */
.nav-fc { font-size: 12px; font-weight: 700; letter-spacing: .04em; }
.fc { max-width: 820px; }
.fc-part {
  margin: 34px 0 4px; padding-top: 16px; border-top: 2px solid var(--line);
  font-size: 15px; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft);
}
.fc-diff { margin: 22px 0 0; }
.fc-diff h2 {
  margin: 0 0 12px; font-size: 20px; line-height: 1.3;
  /* `.settings h2` uppercases every heading in a settings panel, which is right
     for a settings panel and wrong for a sentence: these headings ARE sentences
     and read as shouting in caps. */
  text-transform: none; letter-spacing: normal; color: var(--ink);
}
.fc-show { margin-bottom: 12px; }
.fc-show .demo-canvas { margin: 0 auto; max-width: 100%; }
.fc-static canvas { width: 100%; height: auto; display: block; }
.fc-say p { margin: 0 0 9px; line-height: 1.62; }
.fc-say p:last-child { margin-bottom: 0; }
.fc-say .learn-steps { margin-top: 4px; }

.learn-section { margin-top: 30px; }
.learn-section h2 { margin: 0 0 10px; }
.demo-canvas { width: 100%; max-width: 560px; height: auto; border-radius: 10px; border: 1px solid var(--line); display: block; margin: 10px 0; }

/* ---------- the guided tour (§18) ----------
   One step on screen at a time, the picture above the words. The layout is the
   argument: if the demo is not the first thing under the heading, the step has
   been written the wrong way round. */
.tour { max-width: 760px; }
.tour-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.tour-count { margin: 0; font-variant-numeric: tabular-nums; }
.tour-skip { font-size: 12.5px; text-decoration: none; }
.tour-skip:hover { text-decoration: underline; }
.tour-rail { display: flex; gap: 6px; margin: 10px 0 18px; flex-wrap: wrap; }
.tour-dot {
  flex: 1 1 0; min-width: 14px; height: 6px; padding: 0;
  border: 0; border-radius: 3px; background: var(--line); cursor: pointer;
  transition: background .18s ease;
}
.tour-dot:hover { background: #c3cadb; }
.tour-dot.done { background: rgba(101, 88, 230, 0.4); }
.tour-dot.on { background: var(--teal); }
.tour-title { margin: 0 0 14px; font-size: 26px; line-height: 1.25; }
.tour-show { margin-bottom: 16px; }
.tour-show .demo-canvas { margin: 0 auto; max-width: 100%; }
.tour-say p { margin: 0 0 10px; line-height: 1.65; }
.tour-say p:last-child { margin-bottom: 0; }
.tour-keys { display: inline-flex; gap: 3px; vertical-align: baseline; }
.tour-nav { display: flex; gap: 10px; justify-content: space-between; margin-top: 22px; }
.tour-nav .btn:only-child { margin-left: auto; }

/* Contrast pairs and trios: the caption under each panel is the lesson, so it
   is styled as a label rather than as a sentence. */
.tour-pair, .tour-trio { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.tour-pair .tour-panel { flex: 1 1 240px; }
.tour-trio .tour-panel { flex: 1 1 190px; }
.tour-panel { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.tour-panel .demo-canvas { margin: 0; width: 100%; }
.tour-panel-label {
  font-size: 12px; color: var(--ink-soft); text-align: center;
  padding: 3px 6px; border-radius: 6px; background: #f4f6fb;
}
.tour-panel.good .tour-panel-label { background: rgba(46, 160, 96, 0.12); color: #1f7a48; }
.tour-panel.bad .tour-panel-label { background: rgba(201, 67, 127, 0.12); color: #a3376a; }

.tour-buttons { display: flex; flex-direction: column; gap: 8px; }
.tour-button {
  display: flex; gap: 12px; align-items: baseline;
  background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: 11px 13px;
}
.tour-button kbd { flex: 0 0 62px; text-align: center; }
.tour-button span { font-size: 13.5px; line-height: 1.55; }
.tour-badges { margin: 0; }

/* A picture of the real dialog, made of the real parts — not interactive, and
   not a screenshot that would rot the first time the dialog changed. */
.tour-mock {
  background: #fff; border: 1px solid var(--line); border-radius: 12px;
  padding: 14px 16px; max-width: 420px; margin: 0 auto;
  box-shadow: 0 6px 22px rgba(35, 42, 53, 0.07);
}
.tour-mock-title { font-weight: 600; margin-bottom: 10px; }
.tour-mock .chip-row { margin-bottom: 7px; }
.tour-mock .input { max-width: 90px; }
.tour-mock-actions { margin-top: 12px; text-align: right; }
.tour-mock-actions .btn { cursor: default; }
.tour-out { display: flex; gap: 9px; flex-wrap: wrap; margin: 14px 0; }

@media (max-width: 620px) {
  .tour-title { font-size: 21px; }
  .tour-trio .tour-panel { flex: 1 1 100%; }
}
.learn-steps { margin: 10px 0; padding-left: 22px; font-size: 13.5px; line-height: 1.7; }
.piece-row { display: flex; flex-wrap: wrap; gap: 10px; margin: 10px 0; }
.piece-fig {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  background: #fff; border: 1px solid var(--line); border-radius: 10px;
  padding: 8px 10px 7px; width: 118px; text-align: center;
}
.piece-fig canvas { width: 92px; height: 78px; }
.piece-fig span { font-size: 11px; color: var(--ink-soft); line-height: 1.35; }
.badge-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 9px; }
.badge-card {
  display: flex; gap: 9px; align-items: flex-start;
  background: #fff; border: 1px solid var(--line); border-radius: 10px;
  padding: 9px 11px; font-size: 12.5px; line-height: 1.45;
}
.badge-emoji { font-size: 21px; line-height: 1; }
