/* Control Center — a desk tool, not a kiosk. Dense tables, small controls,
   light by default with a dark variant that follows the operating system. */

:root {
  color-scheme: light dark;
  --bg: #f6f7f9;
  --surface: #ffffff;
  --border: #e2e5ea;
  --text: #14171c;
  --text-dim: #5b6472;
  --primary: #1f6feb;
  --danger: #d1392b;
  --ok: #1a7f4b;
  --warn: #9a6200;
  /* GEOMETRY.
   *
   * Three steps and a pill, which is what was already here once the values
   * were counted: 8px on every button, chip and note, 10px on every card and
   * panel, 999px on every status pill. What they replace is a 2px, a 3px, a
   * 5px, a 6px and a 7px, each invented at the moment it was needed — so two
   * controls side by side could differ by a pixel for no reason either of them
   * could state, and nobody could tell by reading whether that was deliberate.
   *
   * --radius-sm is for marks rather than controls: a 12px legend swatch, the
   * top of a chart bar, the grey bars in the branding preview.
   */
  --radius-sm: 4px;
  --radius-control: 8px;
  --radius-card: 10px;
  --radius-pill: 999px;

  /* CONTROL HEIGHT, as the one number that decides it.
   *
   * A button and the input beside it have to be the same height or every form
   * row is out of true. They were not: 8px of vertical padding on .btn and 9px
   * on an input, which at 15px/1.5 with a 1px border each is 40.5px against
   * 42.5px. Two pixels is small enough that nobody reports it and large enough
   * that every toolbar looks faintly wrong.
   *
   * It is the PADDING that is shared rather than a height, because a textarea
   * has to be able to grow and a fixed height would stop it. Both rules read
   * this token, so they cannot drift apart again without somebody editing the
   * one line that says what it is for.
   */
  --control-pad-y: 9px;

  /* RHYTHM.
   *
   * Six steps, each one a value that was already doing most of the work. What
   * they replace is fourteen different gaps and margins — 2, 3, 4, 5, 6, 7, 8,
   * 10, 12, 14, 16, 18, 20 and 24 — which is not a scale, it is fourteen
   * separate decisions. Every stray was within two pixels of one of these, so
   * nothing here moves by more than that.
   *
   * --gap-2xl exists for one rule, the gap between sections, and keeps its own
   * step rather than collapsing into --gap-xl: the outermost rhythm wants more
   * air than anything inside it, and flattening the two would tighten every
   * screen in the console by four pixels to save a line here.
   */
  --gap-xs: 4px;
  --gap-sm: 6px;
  --gap-md: 10px;
  --gap-lg: 14px;
  --gap-xl: 20px;
  --gap-2xl: 24px;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f1218; --surface: #161a22; --border: #262c38;
    --text: #e8eaee; --text-dim: #9aa3b2; --warn: #e0a83a;
  }
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 var(--font);
}

code { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.9em; }
a { color: var(--primary); }

/* --- Chrome -------------------------------------------------------------- */

.chrome {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--gap-lg); padding: 12px 24px;
  background: var(--surface); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 10;
}

.brand { font-weight: 700; cursor: pointer; }
.chrome-right { display: flex; align-items: center; gap: var(--gap-lg); }

.root { max-width: 1100px; margin: 0 auto; padding: 24px; }

/* --- Login --------------------------------------------------------------- */

.login-wrap { min-height: 70vh; display: grid; place-items: center; }
.login {
  width: min(380px, 92vw); padding: 28px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-card);
  display: flex; flex-direction: column; gap: var(--gap-lg);
}
.login h1 { margin: 0; font-size: 22px; }
.login p { margin: -8px 0 8px; }
/* The invitation note under the button. The rule above pulls a paragraph UP,
   which is right for the subtitle sitting under the heading and wrong for a
   paragraph at the other end of the form — it would ride up onto the button. */
.login .login-note { margin: 0; }

/* --- Layout blocks ------------------------------------------------------- */

.page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--gap-lg); margin-bottom: var(--gap-xl); flex-wrap: wrap;
}
.page-head h1, .page-head h2 { margin: 0 0 4px; font-size: 24px; }
.back { display: inline-block; margin-bottom: var(--gap-sm); font-size: 13px; text-decoration: none; }

.card {
  padding: 20px; margin-bottom: var(--gap-xl);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-card);
}
.card h3 { margin: 0 0 12px; font-size: 17px; }
.section { margin-top: var(--gap-2xl); }
.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 380px); gap: var(--gap-xl); align-items: start; }
@media (max-width: 900px) { .split { grid-template-columns: 1fr; } }

.muted { color: var(--text-dim); }
.small { font-size: 13px; }
.note { padding: 10px 12px; background: rgba(31,111,235,0.08); border-radius: var(--radius-control); font-size: 14px; }
.warn { color: var(--warn); font-weight: 600; }

/* NOTHING TO SHOW, in the two flavours it comes in.
 *
 * One rule, because they are the same shape of message — "there is nothing
 * here yet, and here is why" — and they appear in the same place, in the same
 * panel, often seconds apart as a fetch resolves. They did not look alike: the
 * empty state was centred with 28px of air and the loading line was a bare
 * paragraph hugging whatever was above it, so a panel visibly jumped as it
 * loaded. Same padding, same alignment, same colour; only the words differ. */
.empty,
.loading { padding: 28px; text-align: center; color: var(--text-dim); }

/* --- Forms --------------------------------------------------------------- */

.form { display: flex; flex-direction: column; gap: var(--gap-lg); max-width: 520px; }
.field { display: flex; flex-direction: column; gap: var(--gap-xs); }
.field-label { font-weight: 600; font-size: 14px; }
.field-hint { font-size: 12.5px; color: var(--text-dim); }

input, select, textarea {
  padding: var(--control-pad-y) 11px; font: inherit; color: var(--text);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-control);
}
input:focus, select:focus { outline: 2px solid var(--primary); outline-offset: 1px; }
input[type="color"] { padding: 2px; width: 46px; height: 36px; }
.inline-input { padding: 6px 9px; }
.color-row { display: flex; gap: var(--gap-md); align-items: center; }

.form-actions { display: flex; gap: var(--gap-md); margin-top: var(--gap-sm); flex-wrap: wrap; }

.btn {
  padding: var(--control-pad-y) 14px; font: inherit; font-weight: 600;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-control); cursor: pointer;
}
.btn:hover { border-color: var(--primary); }
.btn:disabled { opacity: 0.55; cursor: default; }
.btn--primary { background: var(--primary); border-color: var(--primary); color: #fff; }
.btn--danger { color: var(--danger); border-color: var(--danger); }
/* A .btn that is an <a>, which the invitation hand-off is: it opens a mail,
   WhatsApp or SMS app rather than doing something on this page, and that is
   what an anchor is for. Everything .btn sets applies except the two an
   anchor brings with it. */
a.btn { display: inline-block; text-decoration: none; }

/* --- Tables -------------------------------------------------------------- */

.table { width: 100%; border-collapse: collapse; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-card); overflow: hidden; }
.table th, .table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: middle; }
.table th { font-size: 13px; color: var(--text-dim); font-weight: 600; }
.table tr:last-child td { border-bottom: none; }
.table--tight th, .table--tight td { padding: 6px 9px; font-size: 13.5px; }
.tick-col { width: 34px; }
.row-actions { display: flex; gap: var(--gap-md); white-space: nowrap; }
.scroll-x { overflow-x: auto; }

.toolbar { display: flex; align-items: center; gap: var(--gap-lg); margin-bottom: var(--gap-lg); }
.search-input { flex: 1; max-width: 320px; }
.add-row { display: flex; align-items: center; gap: var(--gap-md); flex-wrap: wrap; }
.bulk {
  display: flex; align-items: center; gap: var(--gap-md); flex-wrap: wrap;
  padding: 10px 14px; margin-bottom: var(--gap-lg);
  background: rgba(31,111,235,0.1); border-radius: var(--radius-card);
}

.editable { display: inline-block; min-width: 60px; padding: 2px 4px; border-radius: var(--radius-sm); cursor: text; }
.editable:hover { background: rgba(127,127,127,0.14); }
.editable--empty { color: var(--text-dim); font-style: italic; }

.pill { padding: 2px 9px; border-radius: var(--radius-pill); font-size: 12px; font-weight: 600; background: rgba(127,127,127,0.16); }
.pill--active { background: rgba(26,127,75,0.16); color: var(--ok); }
.pill--archived { background: rgba(154,98,0,0.16); color: var(--warn); }

.archived { margin-top: var(--gap-xl); }
.archived summary { cursor: pointer; font-weight: 600; margin-bottom: var(--gap-md); }

/* --- Tabs ---------------------------------------------------------------- */

.tabs { display: flex; gap: var(--gap-xs); margin-bottom: var(--gap-xl); border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.tab {
  padding: var(--control-pad-y) 14px; font: inherit; font-weight: 600;
  background: none; border: none; border-bottom: 2px solid transparent;
  color: var(--text-dim); cursor: pointer;
}
.tab--active { color: var(--text); border-bottom-color: var(--primary); }

/* --- Wizard extras ------------------------------------------------------- */

.chips { display: flex; flex-wrap: wrap; gap: var(--gap-md); }
.chip { padding: 4px 10px; background: rgba(127,127,127,0.14); border-radius: var(--radius-pill); font-size: 13px; }
.chip--warn { background: rgba(154,98,0,0.18); color: var(--warn); }
.schema-row { display: flex; align-items: center; gap: var(--gap-lg); padding: 6px 0; flex-wrap: wrap; }

/* --- Capacity, in the guest editor --------------------------------------- */
/* The same chips the wizard uses, so one room reads the same on both screens.
   A tinted cell rather than an icon: the marker has to survive an inline edit
   committing, and it has to be visible while scanning a long list for the
   table somebody just asked about. */

.capacity { display: grid; gap: var(--gap-md); }
.capacity p { margin: 0; }
.cell--over { background: rgba(154,98,0,0.10); }
.url-row { display: flex; align-items: center; gap: var(--gap-md); flex-wrap: wrap; }

/* The walk-in QR code (§4). Sized in millimetres rather than pixels because
   the thing it becomes is a printed sign: 46mm is comfortably above the
   ~20mm a phone camera needs at arm's length, with the quiet zone included.
   The white plate is not decoration — a code on a dark surface does not
   read, and the console has a dark mode. */
.qr-figure {
  width: 46mm; max-width: 100%;
  margin: 0 0 var(--gap-lg);
  padding: var(--gap-md);
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
}
.qr-figure svg { width: 100%; height: auto; display: block; }

/* §21's metric tiles. auto-fit rather than a fixed column count: ten numbers
   wrap to two rows on a laptop and to a readable column on the phone
   somebody is actually holding at a door, without a media query per width. */
.metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
  gap: var(--gap-md);
}
.metric {
  padding: var(--gap-md) var(--gap-lg);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  /* The note lives in a title attribute, so the cursor should say there is
     something to read rather than leaving it to be discovered. */
  cursor: help;
}
.metric-value {
  font-size: 26px; font-weight: 700; line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.metric-label {
  margin-top: var(--gap-xs); font-size: 12.5px; color: var(--text-dim);
}
.url-row code { padding: 5px 8px; background: rgba(127,127,127,0.12); border-radius: var(--radius-sm); word-break: break-all; }

/* --- Branding preview ---------------------------------------------------- */
/* The frame carries the event's real aspect ratio, so choosing a screen shape
   shows the shape rather than describing it. */

.preview { position: sticky; top: 80px; }
.preview-tabs { display: flex; gap: var(--gap-xs); margin-bottom: var(--gap-md); border-bottom: 1px solid var(--border); }

.preview-frame {
  position: relative;
  width: 100%;
  max-height: 62vh;
  margin: 0 auto;
  border-radius: var(--radius-card);
  overflow: hidden;
  box-shadow: 0 8px 30px rgba(0,0,0,.25);
}

/* The ornament, mirrored into four corners the way the kiosk mirrors it. The
   percentage size keeps it in proportion however wide the preview column is. */
.preview-orn { position: absolute; width: 22%; height: auto; opacity: .85; pointer-events: none; }
.preview-orn--tl { top: 3%; left: 3%; }
.preview-orn--tr { top: 3%; right: 3%; transform: scaleX(-1); }
.preview-orn--bl { bottom: 3%; left: 3%; transform: scaleY(-1); }
.preview-orn--br { bottom: 3%; right: 3%; transform: scale(-1); }

.preview-body {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2.5%; padding: 12% 9%; text-align: center;
}

.preview-kicker { margin: 0; font-size: 10px; letter-spacing: .26em; text-transform: uppercase; opacity: .68; }
/* --display-scale comes off the frame, set from branding.heading_scale, exactly
   as the kiosk sets it on the document. The fallback keeps this readable in any
   other place a preview-display is rendered without one. */
.preview-display { margin: 0; font-family: Didot, Garamond, "Times New Roman", serif; font-size: calc(clamp(18px, 3.4vw, 30px) * var(--display-scale, 1)); font-weight: var(--display-weight, 400); line-height: 1.05; }
.preview-display--sm { font-size: calc(clamp(14px, 2.4vw, 21px) * var(--display-scale, 1)); }
.preview-script { margin: 0; font-family: "Snell Roundhand", "Apple Chancery", Didot, serif; font-size: clamp(16px, 2.8vw, 24px); }
.preview-rule { width: 42%; height: 1px; opacity: .55; }
/* Doubled alongside .brand-logo on the kiosk. The preview is only worth having
   if it lies about nothing, size included. */
.preview-logo { max-height: 68px; max-width: 74%; width: auto; }
/* On the reveal screen the logo stands where the monogram ring did, so it
   is sized to match it rather than to lead the attract screen. */
.preview-logo--sm { max-height: 44px; max-width: 52%; }
.preview-hero { width: 62%; aspect-ratio: 4/3; object-fit: cover; border: 1px solid currentColor; padding: 3px; }
.preview-cta { padding: 7px 16px; border-radius: var(--radius-pill); font-size: 9px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; }
.preview-field { width: 100%; padding: 9px; border: 1px solid currentColor; font-size: 10px; opacity: .6; }
.preview-result { width: 100%; display: flex; justify-content: space-between; padding: 7px 10px; background: rgba(127,127,127,.16); font-size: 10px; }
/* The kiosk centres its keys rather than pinning them to the floor, so the
   preview must not push them to the bottom either — it would promise a layout
   the room never gets. */
.preview-keys { display: flex; gap: var(--gap-xs); width: 100%; }
.preview-keys span { flex: 1; height: 12px; background: rgba(127,127,127,.22); border-radius: var(--radius-sm); }
.preview-crest {
  width: 52px; height: 52px; display: grid; place-items: center;
  border: 1px solid; border-radius: 50%;
  font-family: Didot, Garamond, serif; font-size: 16px;
}
.preview-table { margin: 0; font-family: Didot, Garamond, serif; font-size: clamp(26px, 5.5vw, 46px); line-height: 1; }

.field--row { flex-direction: row; align-items: center; gap: var(--gap-md); }
.stack { display: flex; flex-direction: column; gap: var(--gap-md); align-items: flex-start; }
h4.section { margin: 22px 0 10px; font-size: 14px; letter-spacing: .04em; text-transform: uppercase; color: var(--text-dim); }

/* --- Analytics ----------------------------------------------------------- */

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--gap-lg); }
.stat { padding: 16px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-card); }
.stat--warn { border-color: var(--warn); }
.stat-value { font-size: 28px; font-weight: 700; }
.stat-label { font-size: 13px; color: var(--text-dim); }

.chart { display: flex; align-items: flex-end; gap: var(--gap-xs); height: 140px; padding: 10px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-card); }
.chart-col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; height: 100%; gap: var(--gap-xs); }
.chart-bar { width: 100%; background: var(--primary); border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
.chart-label { font-size: 11px; color: var(--text-dim); }

/* --- Toast --------------------------------------------------------------- */

.toast {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  z-index: 50; max-width: min(560px, 92vw);
  padding: 11px 18px; border-radius: var(--radius-card);
  background: #22262e; color: #fff; box-shadow: 0 6px 24px rgba(0,0,0,0.28);
}
.toast--ok { background: var(--ok); }
.toast--error { background: var(--danger); }

/* --- Background work ----------------------------------------------------- */

.chrome-nav { display: flex; gap: var(--gap-xs); margin-left: 18px; }
.navlink {
  padding: 5px 11px; border: 0; border-radius: var(--radius-pill);
  background: transparent; color: var(--text-dim);
  font: inherit; font-size: 13.5px; cursor: pointer;
}
.navlink:hover { color: var(--text); background: rgba(127,127,127,0.12); }
.navlink--on { color: var(--text); background: rgba(127,127,127,0.16); font-weight: 600; }

/* The filter chips are buttons, so the base .chip style needs its borders and
   cursor reset rather than inheriting the browser's button chrome. */
button.chip {
  display: inline-flex; align-items: center; gap: var(--gap-sm);
  border: 1px solid transparent; color: var(--text-dim);
  font: inherit; font-size: 13px; cursor: pointer;
}
button.chip:hover { color: var(--text); }
.chip--on { border-color: var(--primary); color: var(--text); font-weight: 600; }
/* Only ever set when something actually needs a person. */
.chip--alert { background: rgba(209,57,43,0.14); color: var(--danger); }
.chip-count { font-variant-numeric: tabular-nums; opacity: .7; font-weight: 400; }

.pill--queued    { background: rgba(127,127,127,0.16); color: var(--text-dim); }
.pill--running   { background: rgba(31,111,235,0.16); color: var(--primary); }
.pill--succeeded { background: rgba(26,127,75,0.16); color: var(--ok); }
.pill--failed    { background: rgba(154,98,0,0.18); color: var(--warn); }
.pill--dead      { background: rgba(209,57,43,0.16); color: var(--danger); }

.banner {
  padding: 11px 14px; margin: 14px 0;
  border: 1px solid var(--border); border-left: 3px solid var(--text-dim);
  border-radius: var(--radius-card); background: var(--surface); font-size: 14px;
}
.banner--warn { border-left-color: var(--danger); color: var(--danger); }

/* A long error message must scroll in its own box, never widen the page. */
.table-wrap { overflow-x: auto; margin-top: var(--gap-lg); }
.table .num { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* --- Photos ---------------------------------------------------------------
   A contact sheet. Tiles are sized so a 500-photo event scrolls as a grid
   rather than a list, and every tile carries its own state: the batch panel
   above is transient, but a photo that failed has to stay findable. */

.photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--gap-lg);
  margin-top: var(--gap-xl);
}

.photo-tile {
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  overflow: hidden;
  background: var(--surface);
  display: flex;
  flex-direction: column;
}
.photo-tile--failed { border-color: var(--danger); }

.photo-thumb {
  aspect-ratio: 4 / 3;
  background: var(--bg);
  display: grid;
  place-items: center;
}
.photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Until a signed URL arrives the img has no src, which browsers render as a
   broken-image glyph. Hide it and let the tile background stand in. */
.photo-thumb img:not([src]) { visibility: hidden; }

.photo-meta { padding: 10px 12px; display: flex; flex-direction: column; gap: var(--gap-sm); }
.photo-name {
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.photo-meta .warn.small { font-size: 12px; margin: 0; }

.pill--uploading  { background: rgba(127,127,127,0.16); color: var(--text-dim); }
.pill--stored     { background: rgba(127,127,127,0.16); color: var(--text-dim); }
.pill--processing { background: rgba(31,111,235,0.16); color: var(--primary); }
.pill--ready      { background: rgba(26,127,75,0.16); color: var(--ok); }

/* The upload batch: one row per file, because "12 failed" is not actionable. */
.batch {
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  padding: 14px 16px;
  margin-top: var(--gap-xl);
  background: var(--surface);
}
.batch-list { max-height: 260px; overflow-y: auto; margin-top: var(--gap-md); }
.batch-row {
  display: flex;
  justify-content: space-between;
  gap: var(--gap-lg);
  padding: 5px 0;
  border-bottom: 1px solid var(--line);
  font-size: 13px;
}
.batch-row:last-child { border-bottom: 0; }
.batch-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.batch-state { color: var(--text-dim); white-space: nowrap; }
.batch-row[data-state="failed"] .batch-state { color: var(--danger); }
.batch-row[data-state="done"] .batch-state { color: var(--ok); }
.batch-row[data-state="duplicate"] .batch-state { color: var(--warn); }

/* The file input is driven by a styled label; it must stay reachable by
   keyboard and to assistive tech, so it is moved off-screen rather than
   hidden with display:none. */
#photo-input {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0);
  white-space: nowrap; border: 0;
}

/* --- Attendees ------------------------------------------------------------ */

.notice-card { border-left: 3px solid var(--warn); }

.chip--ok { background: rgba(26,127,75,0.16); color: var(--ok); }

.pill--not_invited { background: rgba(127,127,127,0.16); color: var(--text-dim); }
.pill--invited     { background: rgba(31,111,235,0.16); color: var(--primary); }
.pill--enrolled    { background: rgba(26,127,75,0.16); color: var(--ok); }
.pill--declined    { background: rgba(127,127,127,0.16); color: var(--text-dim); }
/* A withdrawal is not a failure, but it is a state somebody must be able to
   find at a glance when answering a request about their own data. */
.pill--withdrawn   { background: rgba(154,98,0,0.18); color: var(--warn); }

/* --- Review queue ---------------------------------------------------------
   One card per match, worst score first. The frame shows the crop the provider
   actually matched on: asking someone to pick a face out of a group shot and
   decide whether the score belongs to it is asking them to guess. */

.review-list { display: flex; flex-direction: column; gap: var(--gap-lg); margin-top: var(--gap-lg); }

.review-card {
  display: flex;
  gap: var(--gap-lg);
  align-items: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  padding: 12px 14px;
  background: var(--surface);
}

.review-face {
  flex: none;
  width: 96px;
  height: 96px;
  border-radius: var(--radius-control);
  overflow: hidden;
  background: var(--bg);
  position: relative;
}
.review-face img { display: block; max-width: none; }
.review-face img:not([src]) { visibility: hidden; }

.review-body { flex: 1; display: flex; flex-direction: column; gap: var(--gap-sm); }
.review-body .row-actions { margin-top: var(--gap-xs); }

.pill--confirmed { background: rgba(26,127,75,0.16); color: var(--ok); }
.pill--review    { background: rgba(154,98,0,0.18); color: var(--warn); }
.pill--rejected  { background: rgba(127,127,127,0.16); color: var(--text-dim); }
.pill--withdrawn { background: rgba(209,57,43,0.16); color: var(--danger); }

/* --- RSVP console --------------------------------------------------------- */

/* A checkbox and its label on one line. The stacked .field puts the box above
   its own text, which for a single toggle reads as two unrelated controls. */
.field--inline { display: flex; align-items: center; gap: var(--gap-md); }
.field--inline .field-label { margin: 0; }
.field--inline input[type="checkbox"] { width: 20px; height: 20px; flex: none; }

/* Invitation state. Draft is deliberately the quietest: it is a row that
   cannot be replied to yet, which is a fact rather than a problem. */
.pill--draft     { background: rgba(127,127,127,0.16); color: var(--text-dim); }
.pill--invited   { background: rgba(31,111,235,0.16); color: var(--primary); }
.pill--responded { background: rgba(26,127,75,0.16); color: var(--ok); }

/* One person's answer. Pending is warm rather than red — nobody has done
   anything wrong by not having replied yet. */
.pill--attending    { background: rgba(26,127,75,0.16); color: var(--ok); }
.pill--declined     { background: rgba(127,127,127,0.16); color: var(--text-dim); }
.pill--pending      { background: rgba(154,98,0,0.18); color: var(--warn); }
.pill--not-invited  { background: rgba(209,57,43,0.16); color: var(--danger); }

/* Message delivery, shown against a household. The five that matter are the
   ones a planner asks about; the rest inherit the neutral pill. */
.pill--sent      { background: rgba(31,111,235,0.16); color: var(--primary); }
.pill--delivered { background: rgba(26,127,75,0.16); color: var(--ok); }
.pill--opened    { background: rgba(26,127,75,0.16); color: var(--ok); }
.pill--bounced   { background: rgba(209,57,43,0.16); color: var(--danger); }
.pill--complained { background: rgba(209,57,43,0.16); color: var(--danger); }
.pill--suppressed { background: rgba(154,98,0,0.18); color: var(--warn); }

/* --- Seating chart --------------------------------------------------------- */

.plan-toolbar { display: flex; align-items: center; gap: var(--gap-md); flex-wrap: wrap; }
.plan-zoom { min-width: 3.5em; text-align: center; }

/* The save state, which is the only honest signal that anything reached the
   database. Sized for its longest word so the toolbar does not jump as it
   changes, and coloured only when it is bad news: a "Saved" that lights up
   green every second and a half is a flashing light nobody reads. */
.plan-status { min-width: 9.5em; }
.plan-status[data-state="failed"] { color: var(--danger); font-weight: 600; }

/* A card that explains why the screen is doing less than usual. */
.card--notice { border-left: 3px solid var(--warn); }

.plan-versions { list-style: none; margin: 12px 0 0; padding: 0; font-size: 13px; }
.plan-versions li {
  display: flex; align-items: baseline; gap: var(--gap-sm); flex-wrap: wrap;
  padding: 6px 0; border-top: 1px solid var(--border);
}

.plan-layout { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: var(--gap-lg); align-items: start; }
@media (max-width: 900px) { .plan-layout { grid-template-columns: minmax(0, 1fr); } }

/* On a phone, on the day (§29 step 10).
   The plan and the guest list are what somebody standing in the room needs;
   the palette and the properties panel are a desktop job and only push the
   two useful things below the fold. The guest list goes FIRST — the question
   in the room is "where does this person sit", not "what is on the plan". */
@media (max-width: 600px) {
  .plan-layout { gap: var(--gap-lg); }
  .plan-guests-panel { order: -1; }
  /* Tall enough to read at arm's length rather than 4:3 letterboxed into a
     strip. The canvas pans, so it does not have to show the whole room. */
  .plan-canvas { aspect-ratio: 1 / 1; }
  .plan-toolbar { gap: var(--gap-sm); font-size: 13px; }
  .plan-guests { max-height: none; }
}

.plan-canvas {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  overflow: hidden;
  /* A fixed ratio rather than a height: the plan is 4:3 in its own units, and
     letting the box follow the content would make every redraw resize it. */
  aspect-ratio: 4 / 3;
}
.plan-svg { display: block; width: 100%; height: 100%; touch-action: none; }

/* The wrapper exists so the name box can be positioned against the canvas
   rather than against the page, which scrolls. */
.plan-canvas-wrap { position: relative; }

/* The layer is inert until it holds a box: a full-bleed element over the
   plan would otherwise swallow every click meant for a table. */
.plan-edit-layer { position: absolute; inset: 0; pointer-events: none; }
.plan-edit-layer > * { pointer-events: auto; }

/* Typed on the plan itself, over the shape being named. Centred on the
   shape by translating back half its own size, so it covers the label it is
   replacing rather than hanging off the corner. */
.plan-edit-input {
  position: absolute;
  transform: translate(-50%, -50%);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  text-align: center;
  padding: var(--gap-sm);
  border: 2px solid var(--primary);
  border-radius: var(--radius-control);
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .28);
}

.plan-floor { fill: var(--bg); cursor: grab; }
.plan-grid line { stroke: var(--border); stroke-width: .5; opacity: .5; }

.plan-table { cursor: move; }
.plan-table:focus { outline: none; }
.plan-top { fill: var(--surface); stroke: var(--border); stroke-width: 1.5; }
.plan-seat { fill: var(--bg); stroke: var(--border); stroke-width: 1.5; }
.plan-table-name { font-size: 15px; font-weight: 600; fill: var(--text); pointer-events: none; }

.plan-table.is-selected .plan-top,
.plan-object.is-selected .plan-object-body { stroke: var(--primary); stroke-width: 2.5; }
/* Focus has to be visible on its own: a keyboard user never sees the pointer
   hover that would otherwise tell them where they are. */
.plan-table:focus-visible .plan-top,
.plan-object:focus-visible .plan-object-body { stroke: var(--primary); stroke-width: 3; }
.plan-table.is-locked, .plan-object.is-locked { cursor: not-allowed; }
.plan-table.is-locked .plan-top,
.plan-object.is-locked .plan-object-body { stroke-dasharray: 4 3; }

/* --- The grips on the selected shape --------------------------------------
   Four corners to resize by and one grip above to turn by, so a room can be
   laid out by reaching for the shape instead of typing into the panel.

   The cursors are the point of the corner rules. A square white box on a
   corner is ambiguous — it could be a thing to drag, a thing to click or a
   decoration — and `nwse-resize` under the pointer is what says "pull this
   and the shape changes size" without a word of instruction. They are set
   per corner because they are directional: the north-west and south-east
   corners move along one diagonal, the other two along the other. Both
   cursors are mirrored by the browser when the shape is rotated past 45
   degrees, which is wrong by a quarter turn at worst and still far better
   than `move`.

   The stroke widths are in plan units, like the grips themselves, so they
   are divided by the zoom the same way: vector-effect keeps the outline a
   hairline on screen at every zoom instead of a slab at 4x and invisible at
   0.25x. */

.plan-handle {
  fill: var(--surface);
  stroke: var(--primary);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
}
.plan-handle--corner[data-handle="nw"],
.plan-handle--corner[data-handle="se"] { cursor: nwse-resize; }
.plan-handle--corner[data-handle="ne"],
.plan-handle--corner[data-handle="sw"] { cursor: nesw-resize; }
.plan-handle--rotate { cursor: grab; fill: var(--primary); }
.plan-handle--rotate:active { cursor: grabbing; }
.plan-handle:hover { fill: var(--primary); }
.plan-handle-stem {
  stroke: var(--primary);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

.plan-object { cursor: move; }
.plan-object:focus { outline: none; }
.plan-object-body { fill: var(--bg); stroke: var(--border); stroke-width: 1.5; }
.plan-object-label { font-size: 13px; fill: var(--text); pointer-events: none; }

/* FLOOR OBJECTS ARE COLOURED BY CATEGORY, NOT BY KIND.
 *
 * There are fifty-eight kinds. Fifty-eight colours a person can tell apart
 * do not exist: past about a dozen they collapse into a palette of browns,
 * and a plan with a different colour for the buffet and the dessert table is
 * harder to read than one where both are simply "food". Eight categories is
 * a legend somebody can hold in their head while looking at the room.
 *
 * THE FILL IS TRANSLUCENT AND THE STROKE IS SOLID, and that is what makes
 * one set of colours work in both themes. An 18% tint over #f6f7f9 is a pale
 * wash; the same tint over #0f1218 is a dark one. The hue survives either
 * way because the stroke carries it at full strength. An opaque fill would
 * have needed a second palette and a second chance to get it wrong.
 *
 * THE LABEL IS var(--text), NOT var(--text-dim), and that is measured rather
 * than chosen. A dimmed label on a tinted fill came to 4.40:1 on Venue &
 * Safety, 4.43 on Special Event and 4.50 on Guest Services — three of the
 * eight under the 4.5:1 WCAG asks for, which is the kind of near-miss that
 * passes a glance and fails a person. The full-strength ink is 12.38:1 at
 * worst in light and 13.22:1 in dark, which also bought the room to take the
 * tint from 14% to 18% and make the colours themselves easier to tell
 * apart.
 *
 * These come BEFORE the per-kind rules below on purpose. They have the same
 * specificity, so source order decides, and the kind rules are the ones that
 * must win: they say an exit is an outline and a label has no box, which is
 * shape rather than colour and is true whatever category it sits in. */
.plan-object--cat-entertainment .plan-object-body {
  fill: rgba(124, 58, 237, .18); stroke: #7c3aed;
}
.plan-object--cat-food-beverage .plan-object-body {
  fill: rgba(180, 83, 9, .18); stroke: #b45309;
}
.plan-object--cat-media-production .plan-object-body {
  fill: rgba(14, 116, 144, .18); stroke: #0e7490;
}
.plan-object--cat-guest-services .plan-object-body {
  fill: rgba(29, 78, 216, .18); stroke: #1d4ed8;
}
.plan-object--cat-venue-safety .plan-object-body {
  fill: rgba(185, 28, 28, .18); stroke: #b91c1c;
}
.plan-object--cat-exhibitions-vendors .plan-object-body {
  fill: rgba(21, 128, 61, .18); stroke: #15803d;
}
.plan-object--cat-special-event .plan-object-body {
  fill: rgba(190, 24, 93, .18); stroke: #be185d;
}
.plan-object--cat-shapes-lines .plan-object-body {
  fill: rgba(100, 116, 139, .18); stroke: #64748b;
}
.plan-object--cat-custom .plan-object-body {
  fill: rgba(71, 85, 105, .18); stroke: #475569;
}

/* Dark mode moves the STROKE up the scale and leaves the fills alone. The
   mid-tones above are chosen against a near-white page; on #0f1218 they read
   as dark outlines on a dark floor. The tints need no change, because a 14%
   wash is relative to whatever is behind it. */
@media (prefers-color-scheme: dark) {
  .plan-object--cat-entertainment .plan-object-body { stroke: #a78bfa; }
  .plan-object--cat-food-beverage .plan-object-body { stroke: #f59e0b; }
  .plan-object--cat-media-production .plan-object-body { stroke: #22d3ee; }
  .plan-object--cat-guest-services .plan-object-body { stroke: #60a5fa; }
  .plan-object--cat-venue-safety .plan-object-body { stroke: #f87171; }
  .plan-object--cat-exhibitions-vendors .plan-object-body { stroke: #4ade80; }
  .plan-object--cat-special-event .plan-object-body { stroke: #f472b6; }
  .plan-object--cat-shapes-lines .plan-object-body { stroke: #cbd5e1; }
  .plan-object--cat-custom .plan-object-body { stroke: #94a3b8; }
}

/* The glyph. Stroked, never filled, and inheriting the body's stroke so it
   is always the category's own colour — which is what stops it from ever
   being invisible against the fill behind it. vector-effect keeps the line
   one weight however far the drawing has been scaled down. */
.plan-object-icon { pointer-events: none; }
.plan-object-icon path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.plan-object--cat-entertainment .plan-object-icon       { color: #7c3aed; }
.plan-object--cat-food-beverage .plan-object-icon       { color: #b45309; }
.plan-object--cat-media-production .plan-object-icon    { color: #0e7490; }
.plan-object--cat-guest-services .plan-object-icon      { color: #1d4ed8; }
.plan-object--cat-venue-safety .plan-object-icon        { color: #b91c1c; }
.plan-object--cat-exhibitions-vendors .plan-object-icon { color: #15803d; }
.plan-object--cat-special-event .plan-object-icon       { color: #be185d; }
.plan-object--cat-shapes-lines .plan-object-icon        { color: #64748b; }
.plan-object--cat-custom .plan-object-icon              { color: #475569; }

@media (prefers-color-scheme: dark) {
  .plan-object--cat-entertainment .plan-object-icon       { color: #a78bfa; }
  .plan-object--cat-food-beverage .plan-object-icon       { color: #f59e0b; }
  .plan-object--cat-media-production .plan-object-icon    { color: #22d3ee; }
  .plan-object--cat-guest-services .plan-object-icon      { color: #60a5fa; }
  .plan-object--cat-venue-safety .plan-object-icon        { color: #f87171; }
  .plan-object--cat-exhibitions-vendors .plan-object-icon { color: #4ade80; }
  .plan-object--cat-special-event .plan-object-icon       { color: #f472b6; }
  .plan-object--cat-shapes-lines .plan-object-icon        { color: #cbd5e1; }
  .plan-object--cat-custom .plan-object-icon              { color: #94a3b8; }
}

/* The Add panel: a glyph beside every name, so the list is scanned by shape
   rather than read word by word. */
.plan-palette .btn-icon { width: 18px; height: 18px; flex: none; }
.plan-palette .btn-icon path {
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}
.plan-palette button {
  display: inline-flex; align-items: center; gap: var(--gap-sm); text-align: left;
}
.catalog-edit .plan-palette button { opacity: 1; }
.catalog-edit .plan-palette button.is-off { opacity: .45; }
.catalog-hidden-note { margin-top: var(--gap-sm); }

/* SHAPES ARE DRAWN AS THEMSELVES.
 *
 * A line is a line and a circle is an ellipse; neither is a rounded box with
 * a word in the middle. The renderer gives these kinds a different body
 * element, and these rules give them the weight that makes a demarcation
 * read as one from across a room rather than as a thin empty rectangle. */
/* Invisible, but it is what the pointer actually lands on. */
.plan-object-hit { fill: transparent; stroke: none; }

.plan-object--line .plan-object-body,
.plan-object--dashed_line .plan-object-body {
  stroke-width: 3; stroke-linecap: round;
}
.plan-object--dashed_line .plan-object-body { stroke-dasharray: 10 8; }
.plan-object--circle .plan-object-body { fill: rgba(100, 116, 139, .10); stroke-width: 2.5; }

/* A line carries no label: there is no inside to put one in, and a word
   floating on a divider reads as a thing rather than as a boundary. */
.plan-object--line .plan-object-label,
.plan-object--dashed_line .plan-object-label { display: none; }

/* Per-kind rules: SHAPE, not colour. Each one deliberately leaves the stroke
   alone so the category still identifies the object. */
.plan-object--dance_floor .plan-object-body { stroke-dasharray: 6 4; }
.plan-object--entrance .plan-object-body,
.plan-object--exit .plan-object-body { fill: none; stroke-width: 3; }
.plan-object--text .plan-object-body { fill: none; stroke: none; }
.plan-object--text .plan-object-label { font-size: 16px; fill: var(--text); }

.plan-palette { display: flex; flex-wrap: wrap; gap: var(--gap-sm); }
.plan-palette .btn { flex: 1 1 auto; }
.plan-pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-md); }

/* --- Seating: guests, drops and checks ------------------------------------ */

.plan-layout { grid-template-columns: 250px minmax(0, 1fr) 280px; }
@media (max-width: 1200px) { .plan-layout { grid-template-columns: 230px minmax(0, 1fr); } }
@media (max-width: 900px) { .plan-layout { grid-template-columns: minmax(0, 1fr); } }

.plan-counts { margin: 0 0 10px; }
.plan-filters { display: flex; flex-wrap: wrap; gap: var(--gap-xs); margin: 10px 0; }
.chip--on { background: var(--primary); color: #fff; }
.chip--vip { background: rgba(154,98,0,0.18); color: var(--warn); }
.plan-filters .chip { border: none; font: inherit; font-size: 13px; cursor: pointer; }

.plan-guests { list-style: none; margin: 0; padding: 0; max-height: 46vh; overflow-y: auto; }
.plan-guests li { display: flex; align-items: center; gap: var(--gap-sm); }
.plan-guests .btn { padding: 3px 8px; font-size: 12px; }

.plan-guest {
  flex: 1; display: flex; align-items: center; gap: var(--gap-sm); flex-wrap: wrap;
  padding: 6px 8px; margin: 2px 0; text-align: left;
  font: inherit; color: var(--text);
  background: none; border: 1px solid transparent; border-radius: var(--radius-control); cursor: grab;
}
.plan-guest:hover { background: rgba(127,127,127,0.12); }
/* Nobody can be picked up on a locked plan, and it has to look that way
   rather than simply not responding to the click. */
.plan-guest[disabled] { cursor: default; opacity: .55; }
.plan-guest[disabled]:hover { background: none; }
.plan-guest-name { font-weight: 600; font-size: 14px; }
.plan-guest.is-seated .plan-guest-name { font-weight: 500; }
/* Held has to be unmistakable: the next click puts this person somewhere. */
.plan-guest.is-held { background: rgba(31,111,235,0.16); border-color: var(--primary); cursor: grabbing; }

/* While somebody is in hand, every place they could go says so. */
.plan-table.is-droppable .plan-top { stroke-dasharray: 5 3; }
.plan-seat.is-droppable { stroke: var(--primary); }
.plan-seat.is-taken { fill: var(--primary); stroke: var(--primary); }
.plan-seat-initials { font-size: 11px; font-weight: 700; fill: #fff; pointer-events: none; }

.plan-table-count { font-size: 12px; fill: var(--text-dim); pointer-events: none; }
.plan-table-count.is-over { fill: var(--warn); font-weight: 700; }

.plan-findings { margin: 0; padding-left: 18px; }
.plan-findings li { margin-bottom: var(--gap-sm); }

/* --- Stages ---------------------------------------------------------------
   The eleven screens of an event, grouped into the five stages of running one.
   A flat row of eleven equal buttons said nothing about order or progress; the
   group label is what turns a list into a sequence. */

.stages {
  display: flex; gap: 0; margin-bottom: var(--gap-xl);
  border-bottom: 1px solid var(--border);
  /* Narrow screens scroll rather than wrap: a wrapped group reads as two
     stages, which is exactly the confusion this replaced. */
  overflow-x: auto; scrollbar-width: thin;
}

.stage-group {
  display: flex; flex-direction: column; gap: var(--gap-xs);
  padding: 0 14px 0 0; margin-right: 14px;
  border-right: 1px solid var(--border);
  flex: 0 0 auto;
}
.stage-group:last-child { border-right: none; margin-right: 0; padding-right: 0; }

.stage-group-label {
  font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-dim); padding: 8px 8px 0;
}
.stage-group--on .stage-group-label { color: var(--primary); }

.stage-group-tabs { display: flex; gap: var(--gap-xs); }

/* --- Home ----------------------------------------------------------------- */

.section-head {
  margin: 26px 0 12px; font-size: 13px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--text-dim);
}

.all-clear { margin: 18px 0 0; color: var(--text-dim); }

.attention { border-left: 3px solid var(--warn); }

/* The two answers to "where is this guest sitting" have drifted apart, so the
 * numbers on the rest of the screen are wrong. A heavier edge than
 * .attention's on purpose: a worklist is a job to do, and this is a reason to
 * distrust everything under it. */
.drift { border-left: 3px solid var(--danger); }
.attention-list { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: var(--gap-sm); }
.attention-item { line-height: 1.45; }
.attention-item--warn::marker { color: var(--danger); }
.attention-item--todo::marker { color: var(--warn); }

.home-grid {
  display: grid; gap: var(--gap-lg);
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}

.event-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-card); padding: 16px;
  display: flex; flex-direction: column; gap: var(--gap-md);
}
.event-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--gap-md); }
.event-card-head h3 { margin: 0; font-size: 17px; }
.event-card-sub, .event-card-figures { margin: 0; }
.event-card-figures { font-variant-numeric: tabular-nums; }
.fig-warn { color: var(--warn); font-weight: 600; }

/* The stage strip: the same five stages, in the same order, as the nav above.
   Each carries a MARK as well as a colour — these are the states of a
   checklist, and roughly one man in twelve cannot tell the amber from the
   green. */
.stage-strip {
  list-style: none; margin: 2px 0; padding: 0;
  display: flex; gap: var(--gap-xs); flex-wrap: wrap;
}
.stage-dot {
  display: flex; align-items: center; gap: var(--gap-xs);
  padding: 3px 8px; border-radius: var(--radius-pill); font-size: 12px;
  background: rgba(127,127,127,0.12); color: var(--text-dim);
}
.stage-dot-mark { font-weight: 700; }
.stage-dot--done { background: rgba(26,127,75,0.16); color: var(--ok); }
.stage-dot--warn { background: rgba(209,57,43,0.16); color: var(--danger); font-weight: 600; }
.stage-dot--todo { background: rgba(154,98,0,0.18); color: var(--warn); }
.stage-dot--skip { opacity: .55; }

@media (max-width: 560px) {
  .stage-dot-label { display: none; }
  .stage-dot { padding: 3px 7px; }
}

/* --- When ----------------------------------------------------------------
   The date, which every other screen is relative to: a seating chart three
   days out and one three months out are different kinds of unfinished. */

.event-card-when { margin: 0; display: flex; flex-wrap: wrap; gap: var(--gap-sm); align-items: baseline; }
.event-card-when strong { font-size: 15px; }

/* Colour by how close it is, not by whether it is "good". A past event is not
   a failure, so it recedes rather than going red. */
.event-card-when--now  strong { color: var(--danger); }
.event-card-when--soon strong { color: var(--warn); }
.event-card-when--past { opacity: .6; }

.event-when { display: flex; align-items: center; gap: var(--gap-md); flex-wrap: wrap; margin: 6px 0 0; }
.event-when .field-label { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }
.event-when input { max-width: 230px; }
.event-when-said--now  { color: var(--danger); font-weight: 600; }
.event-when-said--soon { color: var(--warn); font-weight: 600; }
.event-when-said--past { opacity: .6; }

/* --- Live seating map (spec §18, §19) ------------------------------------ */

.page-head--tight { margin-bottom: var(--gap-md); }
.page-head--tight h3, .page-head--tight h4 { margin: 0 0 2px; }

.card--nested {
  margin-top: var(--gap-lg);
  background: var(--bg);
  border: 1px solid var(--border);
}
.card--nested h4 { margin: 0 0 var(--gap-md); font-size: 15px; }

/* The one-time invitation link. It takes the whole row because it is long,
   and a monospace face because somebody reading it aloud over a counter needs
   to be able to tell an l from a 1. */
.link-field {
  flex: 1 1 18rem;
  min-width: 0;
  font-family: ui-monospace, Menlo, Consolas, monospace;
}

/* The six state colours. Each is paired with a word and a mark everywhere it
   is used — §18 says not to rely on colour alone, and these are chosen to
   stay distinguishable in greyscale as well, lightest to darkest in the order
   available, nearly full, full. */
.tbl--available  { --tbl: var(--ok); }
.tbl--nearly_full { --tbl: var(--warn); }
.tbl--full       { --tbl: var(--danger); }
.tbl--reserved   { --tbl: #6b4fcf; }
.tbl--locked     { --tbl: var(--text-dim); }

.tbl-map-wrap {
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  background: var(--bg);
  overflow: auto;
  max-height: 60vh;
}
.tbl-map { display: block; width: 100%; height: auto; }

.tbl-floor { fill: var(--surface); stroke: var(--border); stroke-width: 2; }

.tbl-object { fill: var(--bg); stroke: var(--border); stroke-width: 1.5; }
.tbl-object--door { stroke: var(--primary); stroke-dasharray: 6 4; }

/* The demarcation shapes, drawn here as they are drawn in the editor.
 *
 * A line needs its own weight: at the 1.5 above it is a hairline on a
 * screen somebody is reading from across a room, and the whole point of
 * the mark is to be seen at a glance. The dashes are longer than the
 * door's for the same reason. */
.tbl-object--line,
.tbl-object--dashed_line { stroke: var(--text-dim); stroke-width: 3; }
.tbl-object--dashed_line { stroke-dasharray: 10 8; }
.tbl-object--circle { fill: none; stroke: var(--text-dim); stroke-width: 2.5; }
.tbl-object-text {
  fill: var(--text-dim); text-transform: uppercase; letter-spacing: .08em;
  pointer-events: none;
}

.tbl { cursor: pointer; }
.tbl-shape {
  fill: color-mix(in srgb, var(--tbl) 14%, var(--surface));
  stroke: var(--tbl);
  stroke-width: 2.5;
}
.tbl:hover .tbl-shape { stroke-width: 4; }
.tbl:focus { outline: none; }
.tbl:focus-visible .tbl-shape, .tbl.tbl--on .tbl-shape {
  stroke-width: 5;
  stroke: var(--text);
}

/* The three things on every table: its name, how full it is, and a mark. The
   fraction is the one an operator reads first, so it is the biggest. */
.tbl-name  { fill: var(--text); font-weight: 600; pointer-events: none; }
.tbl-count { fill: var(--text); font-weight: 700; pointer-events: none; }
.tbl-mark  {
  fill: var(--tbl); font-weight: 700; letter-spacing: .08em;
  pointer-events: none;
}
.tbl-new {
  fill: var(--primary); font-weight: 700; letter-spacing: .14em;
  pointer-events: none;
}

.tbl-legend {
  display: flex; flex-wrap: wrap; gap: var(--gap-sm) var(--gap-xl);
  margin: 0 0 var(--gap-lg); padding: 0;
}
.tbl-key { display: inline-flex; align-items: center; gap: var(--gap-sm); font-size: 13px; }
.tbl-key-swatch {
  width: 12px; height: 12px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--tbl) 20%, var(--surface));
  border: 2px solid var(--tbl);
  flex: none;
}
.tbl-key-mark {
  font-size: 10px; font-weight: 700; letter-spacing: .06em;
  color: var(--tbl);
}
.tbl-key-label { font-weight: 600; }
.tbl-key-count {
  min-width: 20px; padding: 0 5px; border-radius: var(--radius-pill);
  background: var(--border); text-align: center;
  font-size: 12px; font-weight: 700;
}

.tbl-badge {
  padding: 3px 10px; border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--tbl) 16%, var(--surface));
  border: 1px solid var(--tbl);
  color: var(--tbl);
  font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; white-space: nowrap;
}

.tbl-figures {
  display: flex; flex-wrap: wrap; gap: var(--gap-md) var(--gap-2xl);
  margin: var(--gap-xs) 0 var(--gap-lg);
}
.tbl-figure { display: flex; flex-direction: column; }
.tbl-figure-value { font-size: 22px; font-weight: 700; line-height: 1.1; }
.tbl-figure-label {
  font-size: 11px; color: var(--text-dim);
  text-transform: uppercase; letter-spacing: .08em;
}

.tbl-detail:empty { display: none; }
.tbl-detail h5 { margin: 16px 0 6px; font-size: 13px; text-transform: uppercase;
  letter-spacing: .06em; color: var(--text-dim); }

.tbl-seats { display: flex; flex-wrap: wrap; gap: var(--gap-sm); }
.tbl-seat {
  display: inline-flex; align-items: baseline; gap: var(--gap-sm);
  padding: 4px 9px; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: var(--surface);
  font-size: 12px;
}
.tbl-seat b { font-variant-numeric: tabular-nums; }
.tbl-seat--taken { border-color: var(--primary); }
/* A chair the planner took out of play. Dashed as well as dim, because "this
   one is not for walk-ins" and "nobody is in it" must not look the same. */
.tbl-seat--held { border-style: dashed; color: var(--text-dim); }
