/* EventPlanza — The Atelier design system, ported from the approved artifact. */

@font-face {
  font-family: 'Fraunces Var';
  src: url('/assets/fonts/Fraunces-Var.ttf') format('truetype-variations');
  font-weight: 100 900;
  font-stretch: 75% 125%;
  font-display: swap;
}
@font-face {
  font-family: 'Archivo Var';
  src: url('/assets/fonts/Archivo-Var.ttf') format('truetype-variations');
  font-weight: 100 900;
  font-stretch: 75% 125%;
  font-display: swap;
}

:root {
  --ink: #14231c;
  --ink-raised: #1c3125;
  --pine: #24402f;
  --pine-line: #3a5844;
  --brass: #c89b3c;
  --brass-soft: #e4c97a;
  --brass-dim: #8a6c2c;
  --paper: #f3efe4;
  --paper-dim: #e8e1d0;
  --paper-line: #d8cfb8;
  --ink-text: #1c2a22;
  --ink-text-soft: #52604f;
  --on-dark: #f3efe4;
  --on-dark-soft: #b9c2b2;
  --sage: #5b8a6b;
  --clay: #b5613f;
  --danger: #b5613f;
  --danger-rgb: 181, 97, 63;

  --display: 'Fraunces Var', ui-serif, Georgia, serif;
  --body: 'Archivo Var', ui-sans-serif, system-ui, sans-serif;
  --mono: ui-monospace, 'SF Mono', 'Cascadia Mono', Consolas, monospace;

  --surface: var(--paper);
  --surface-raised: #ffffff;
  --surface-line: var(--paper-line);
  --surface-dim: var(--paper-dim);
  --text: var(--ink-text);
  --text-soft: var(--ink-text-soft);
  --accent: var(--brass-dim);
  --accent-ink: var(--ink);
  --accent-soft: #f4e6bf;
}

@media (prefers-color-scheme: dark) {
  :root {
    --surface: #101c16; --surface-raised: #182a20; --surface-line: #2c4534; --surface-dim: #1a2e22;
    --text: #ece6d6; --text-soft: #a9b6a1; --accent: var(--brass-soft); --accent-soft: #2e2712;
  }
}
:root[data-theme="dark"] {
  --surface: #101c16; --surface-raised: #182a20; --surface-line: #2c4534; --surface-dim: #1a2e22;
  --text: #ece6d6; --text-soft: #a9b6a1; --accent: var(--brass-soft); --accent-soft: #2e2712;
}
:root[data-theme="light"] {
  --surface: var(--paper); --surface-raised: #ffffff; --surface-line: var(--paper-line); --surface-dim: var(--paper-dim);
  --text: var(--ink-text); --text-soft: var(--ink-text-soft); --accent: var(--brass-dim); --accent-soft: #f4e6bf;
}

* { box-sizing: border-box; }
/* The `hidden` attribute is only a UA-stylesheet `display: none`, so ANY rule
   that sets display beats it — .btn-secondary and .pill both do. That made
   `el.hidden = true` silently do nothing for those elements, which is not
   something you notice until a button you "hid" is still on screen. */
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
/* `clip` rather than `hidden`, with hidden left first as the fallback for
   browsers that don't know it. Both stop sideways scrolling, but `hidden`
   makes the element a scroll container, and that silently breaks
   `position: sticky` anywhere inside it — which is what pinned the sidebar to
   the page instead of the viewport. `clip` has no such side effect. */
html { overflow-x: hidden; overflow-x: clip; }
body {
  font-family: var(--body);
  color: var(--text);
  background: var(--surface);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  overflow-x: clip;
}
h1, h2, h3 { font-family: var(--display); text-wrap: balance; margin: 0; }
p { margin: 0; }
a { color: inherit; }
button { font-family: inherit; cursor: pointer; }
img { max-width: 100%; }
/* Theme-aware — was hardcoded to the golden-hour brass token, so every
   focus ring showed gold regardless of the active theme (or the sidebar's
   own always-dark accent). */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}

/* ---------- page shells ---------- */
.page-dark { background: var(--ink); color: var(--on-dark); min-height: 100vh; }
.page-paper { background: var(--surface); color: var(--text); min-height: 100vh; }

.wordmark { font-family: var(--display); font-weight: 480; font-size: 20px; letter-spacing: 0.01em; text-decoration: none; }
.wordmark em { font-style: italic; font-weight: 380; opacity: 0.8; }
.eyebrow { font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.75; }

/* ---------- buttons ---------- */
.cta {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  background: var(--brass); color: var(--ink); border: none; border-radius: 999px;
  padding: 16px 30px; font-size: 15px; font-weight: 600; letter-spacing: 0.01em;
  text-decoration: none; transition: background 0.2s ease, transform 0.2s ease;
}
.cta:hover { background: var(--brass-soft); transform: translateY(-1px); }
.cta:active { transform: translateY(0); }
.cta[disabled] { opacity: 0.5; pointer-events: none; }

.btn-primary {
  display: block; width: 100%; text-align: center; text-decoration: none;
  background: var(--accent); color: var(--accent-ink); border: none; border-radius: 9px;
  padding: 14px; font-size: 14.5px; font-weight: 600; margin-top: 6px;
  cursor: pointer;
  /* Same lift-then-settle language as .btn-add and .btn-secondary — this was
     the one button in the family with no hover at all, which made the biggest,
     most important button on a page feel like the least responsive. The
     easing overshoots slightly on the way up and lands flatter on the press,
     so a click reads as landing rather than as a class toggling. */
  transition: transform 0.18s cubic-bezier(0.34, 1.4, 0.64, 1), box-shadow 0.18s ease, filter 0.18s ease;
}
/* A plain black-alpha shadow rather than one mixed from --accent: this button
   also renders on the standalone onboarding page, which sets no --accent-rgb,
   and an unresolvable color drops the whole declaration. */
.btn-primary:hover:not([disabled]) {
  transform: translateY(-2px);
  filter: brightness(1.06);
  box-shadow: 0 10px 22px -12px rgba(0, 0, 0, 0.45);
}
.btn-primary:active:not([disabled]) {
  transform: translateY(0) scale(0.985);
  box-shadow: none;
  transition-duration: 0.08s;
}
.btn-primary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.btn-primary[disabled] { opacity: 0.5; cursor: default; }
@media (hover: none) {
  /* :hover latches after a tap on touch, which would leave the button stuck
     raised until something else is tapped. */
  .btn-primary:hover:not([disabled]) { transform: none; filter: none; box-shadow: none; }
}

.btn-secondary {
  display: inline-flex; align-items: center; justify-content: center; text-decoration: none; text-align: center;
  background: transparent; color: var(--text); border: 1px solid var(--surface-line);
  border-radius: 9px; padding: 10px 16px; font-size: 13.5px; font-weight: 600;
  transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.btn-secondary:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); border-color: var(--accent); transform: translateY(-1px); }
.btn-secondary:active { transform: translateY(0); }
.btn-secondary.danger { color: var(--clay); border-color: color-mix(in srgb, var(--clay) 45%, var(--surface-line)); }
.btn-secondary.danger:hover { background: color-mix(in srgb, var(--clay) 12%, transparent); border-color: var(--clay); }
/* Follows the same lift-on-hover, settle-on-press language as .btn-secondary
   above, so the two read as one family. The easing carries a slight overshoot
   on the way up and a faster, flatter curve on the press, which is what makes
   the click feel like it lands rather than merely toggling a class. */
.btn-add {
  background: var(--accent); color: var(--accent-ink); border: none; border-radius: 8px;
  padding: 10px 16px; font-size: 13px; font-weight: 600; white-space: nowrap;
  cursor: pointer;
  transition: transform 0.18s cubic-bezier(0.34, 1.4, 0.64, 1), box-shadow 0.18s ease, filter 0.18s ease;
}
.btn-add:hover {
  transform: translateY(-2px);
  filter: brightness(1.06);
  box-shadow: 0 6px 16px -8px color-mix(in srgb, var(--accent) 90%, transparent);
}
.btn-add:active {
  transform: translateY(0) scale(0.97);
  box-shadow: none;
  transition-duration: 0.08s;
}
/* The button carries no border, so the browser's default focus outline is easy
   to lose against the accent fill — an offset ring keeps it obvious for anyone
   tabbing through the add-forms. */
.btn-add:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .btn-add { transition: filter 0.15s ease, box-shadow 0.15s ease; }
  .btn-add:hover, .btn-add:active { transform: none; }
}
.btn-danger-link {
  background: none; border: none; color: var(--clay); cursor: pointer; padding: 0;
  font-size: 12.5px; font-family: inherit;
}

/* In-theme replacement for window.confirm() — see confirmDialog() in
   seating/index.php. The browser's native dialog (grey box, "site says")
   breaks out of the app's look entirely; this reuses the same card/button
   styling as everything else instead. */
.confirm-modal-overlay {
  position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center;
  background: rgba(20, 20, 16, 0.45); padding: 20px;
}
.confirm-modal-overlay[hidden] { display: none; }
.confirm-modal {
  background: var(--surface-raised); border: 1px solid var(--surface-line); border-radius: 14px;
  padding: 22px 24px; max-width: 360px; width: 100%; box-shadow: 0 30px 60px -25px rgba(0,0,0,0.45);
}
.confirm-modal-message { font-size: 14.5px; color: var(--text); line-height: 1.5; margin: 0 0 18px; }
.confirm-modal-actions { display: flex; justify-content: flex-end; gap: 10px; }

/* ---------- card / form ---------- */
.card {
  background: var(--surface-raised); border: 1px solid var(--surface-line);
  border-radius: 16px; padding: 32px; box-shadow: 0 20px 50px -30px rgba(20, 35, 28, 0.35);
}
.card + .card { margin-top: 24px; }

/* Bottom-edge grip for resizing the seating canvas card — see the
   pointerdown/move/up handler on #canvas-resize-handle in
   seating/index.php, which persists the chosen height on layout.canvasHeight
   so it survives a reload instead of resetting to the 480px default. */
.canvas-resize-handle {
  position: absolute; left: 0; right: 0; bottom: 0; height: 10px; cursor: ns-resize; touch-action: none; z-index: 6;
}
.canvas-resize-handle::after {
  content: ""; position: absolute; left: 50%; bottom: 3px; transform: translateX(-50%);
  width: 36px; height: 4px; border-radius: 999px; background: var(--surface-line);
}
.canvas-resize-handle:hover::after { background: var(--accent); }
.field { margin-bottom: 18px; }
.field label { display: block; font-size: 12.5px; font-weight: 600; margin-bottom: 8px; color: var(--text); }
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }
.field input, .field select, .field textarea {
  width: 100%; border: 1px solid var(--surface-line); background: var(--surface-dim);
  border-radius: 9px; padding: 12px 14px; font-size: 14.5px; font-family: var(--body); color: var(--text);
}
/* Date and time inputs draw an internal editing widget that's taller than a
   plain text line box — at identical padding and font-size they measured
   46.1px and 46.8px against a text input's 41.6px, so any row mixing them
   (the add-task, add-schedule and settings forms all do) sat visibly uneven.
   Pinning an explicit height makes every control the same box no matter what
   the browser chooses to draw inside it. Textareas are excluded: their height
   is meant to grow with the rows attribute. */
.field input:not([type="checkbox"]):not([type="radio"]),
.field select {
  height: 44px; line-height: 1.2;
}
.field select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  cursor: pointer; padding-right: 40px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' viewBox='0 0 11 7' fill='none'%3E%3Cpath d='M1 1L5.5 5.5L10 1' stroke='%23888888' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
}
.field select::-ms-expand { display: none; }
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--accent); background: var(--surface-raised); }
/* The browser draws its own calendar/clock glyph for these, oblivious to the
   event's theme — giving it a small accent-colored swatch (instead of trying
   to recolor the glyph itself, which isn't reliably stylable cross-browser)
   is enough to make it read as "part of this theme" rather than a bare OS
   control dropped on top of it. */
.field input[type="date"]::-webkit-calendar-picker-indicator,
.field input[type="time"]::-webkit-calendar-picker-indicator {
  cursor: pointer; border-radius: 5px; padding: 3px; background: var(--accent);
}
/* iOS Safari sizes date/time inputs from their internal widget and ignores an
   author height unless the native appearance is switched off — so the 44px
   above held on desktop but not on an iPhone, leaving a time field visibly
   taller than the text field beside it. */
.field input[type="date"], .field input[type="time"] {
  cursor: pointer; -webkit-appearance: none; appearance: none; box-sizing: border-box;
}
.field input[type="date"]:read-only, .field input[type="time"]:read-only { background: var(--surface-dim); }

/* ---------- planner locked (its day has passed — see PlannerAccess) ---------- */
/* Anything still interactive inside a locked planner is greyed and inert.
   The templates already swap their add-forms and row actions out for plain
   text, so this is the belt-and-braces layer for whatever slips through —
   and the visual cue that the page is frozen rather than broken. */
.planner-locked input:not([type="hidden"]),
.planner-locked select,
.planner-locked textarea,
.planner-locked button,
.planner-locked .btn-add,
.planner-locked .editable-text,
.planner-locked .rsvp-trigger,
.planner-locked .relationship-trigger,
/* The theme swatches are <label>s wrapping a hidden radio — a label still
   forwards its click to that radio even when the radio itself is inert, so
   the card has to be neutralised in its own right. */
.planner-locked .theme-card {
  opacity: 0.55; pointer-events: none; cursor: not-allowed; filter: grayscale(0.6);
}
/* Locking a planner freezes *editing*, not looking. Anything marked
   .lock-exempt — browsing the photo gallery, opening a photo, favouriting
   one for the download bundle — keeps working for the whole gallery window,
   which is exactly what the buyer is still here to do. */
.planner-locked .lock-exempt,
.planner-locked .lock-exempt button,
.planner-locked .lock-exempt a,
.planner-locked .lock-exempt input {
  opacity: 1; pointer-events: auto; cursor: pointer; filter: none;
}

/* The panel is followed by a <form> on Settings and by the .dash-grid on the
   Dashboard, so app.css's usual `.card + .card` sibling gap never applies —
   it needs its own bottom margin to sit apart from what comes next. */
.planner-ended { border-color: var(--accent); margin-bottom: 24px; }
.planner-ended-head { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 16px; }
.planner-ended-mark {
  flex-shrink: 0; width: 40px; height: 40px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent);
}
.planner-ended-mark svg { width: 22px; height: 22px; }
.planner-ended-title { font-family: var(--display); font-size: 20px; font-weight: 500; margin: 0 0 4px; }
.planner-ended-sub { color: var(--text-soft); font-size: 14px; margin: 0; }
.planner-ended-thanks { font-size: 14.5px; line-height: 1.6; margin: 0 0 18px; }
.planner-ended-gallery {
  display: flex; align-items: center; gap: 18px; padding: 16px 18px; border-radius: 12px;
  background: var(--surface-dim); border: 1px solid var(--surface-line); margin-bottom: 16px;
}
.planner-ended-countdown { text-align: center; flex-shrink: 0; min-width: 62px; }
.planner-ended-countdown b { display: block; font-family: var(--display); font-size: 30px; line-height: 1; color: var(--accent); }
.planner-ended-countdown span { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-soft); }
.planner-ended-gallery-copy p { margin: 0; font-size: 14px; line-height: 1.55; }
.planner-ended-hint { color: var(--text-soft); font-size: 13px; line-height: 1.55; margin-top: 4px; }
.planner-ended-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 18px; }
.planner-ended-btn { width: auto; display: inline-flex; align-items: center; text-decoration: none; padding-left: 22px; padding-right: 22px; }
.planner-ended-footer {
  border-top: 1px solid var(--surface-line); padding-top: 16px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
}
.planner-ended-footer p { margin: 0; font-size: 13.5px; color: var(--text-soft); max-width: 46ch; }
.planner-ended-buy { color: var(--accent); font-weight: 600; font-size: 13.5px; text-decoration: none; white-space: nowrap; }
.planner-ended-buy:hover { text-decoration: underline; }
@media (max-width: 560px) {
  .planner-ended-gallery { flex-direction: column; align-items: flex-start; gap: 10px; }
  .planner-ended-actions .planner-ended-btn { flex: 1; justify-content: center; }
}

/* ---------- themed date/time picker (assets/js/theme-datepicker.js) ---------- */
.theme-picker-panel {
  position: absolute; z-index: 300; background: var(--surface-raised); border: 1px solid var(--surface-line);
  border-radius: 12px; box-shadow: 0 20px 45px -20px rgba(0,0,0,0.4); padding: 10px;
  animation: dash-fade-in 0.12s ease both;
}
.theme-time-panel { display: flex; gap: 4px; }
.theme-picker-col {
  display: flex; flex-direction: column; gap: 2px; width: 56px; max-height: 220px; overflow-y: auto;
  scroll-behavior: smooth;
}
.theme-picker-opt {
  border: none; background: none; padding: 7px 0; border-radius: 7px; cursor: pointer;
  font-family: var(--mono); font-size: 13.5px; color: var(--text); text-align: center;
}
.theme-picker-opt:hover { background: var(--surface-dim); }
.theme-picker-opt.is-selected { background: var(--accent); color: var(--accent-ink); font-weight: 600; }

.theme-date-panel { width: 264px; }
.theme-picker-header { display: flex; align-items: center; justify-content: space-between; padding: 4px 4px 8px; }
.theme-picker-label { font-family: var(--display); font-size: 14.5px; font-weight: 500; text-transform: capitalize; }
.theme-picker-nav {
  border: none; background: none; cursor: pointer; color: var(--text-soft); font-size: 15px;
  width: 26px; height: 26px; border-radius: 7px; display: flex; align-items: center; justify-content: center;
}
.theme-picker-nav:hover { background: var(--surface-dim); color: var(--text); }
.theme-picker-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; margin-bottom: 2px; }
.theme-picker-weekdays span { font-size: 10.5px; font-weight: 600; text-transform: uppercase; color: var(--text-soft); }
.theme-picker-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.theme-picker-day {
  border: none; background: none; cursor: pointer; padding: 7px 0; border-radius: 7px;
  font-size: 13px; color: var(--text); text-align: center;
}
.theme-picker-day:hover { background: var(--surface-dim); }
.theme-picker-day.is-today { color: var(--accent); font-weight: 700; }
.theme-picker-day.is-selected { background: var(--accent); color: var(--accent-ink); font-weight: 600; }
.theme-picker-day-outside { color: var(--text-soft); opacity: 0.4; text-align: center; padding: 7px 0; font-size: 13px; }
.theme-picker-footer { display: flex; justify-content: space-between; margin-top: 6px; padding: 6px 4px 2px; border-top: 1px solid var(--surface-line); }
.theme-picker-text-btn { border: none; background: none; cursor: pointer; color: var(--accent); font-size: 12.5px; font-weight: 600; padding: 4px; }
.theme-picker-text-btn:hover { text-decoration: underline; }
.field-hint { margin-top: 6px; font-size: 12px; color: var(--text-soft); }
.form-error {
  background: color-mix(in srgb, var(--danger) 14%, transparent); border: 1px solid var(--danger);
  color: var(--danger); padding: 11px 15px; border-radius: 9px; font-size: 13.5px; margin-bottom: 18px;
}
.form-success {
  background: var(--accent-soft); border: 1px solid var(--brass-dim);
  color: var(--brass-dim); padding: 11px 15px; border-radius: 9px; font-size: 13.5px; margin-bottom: 18px;
}
:root[data-theme="dark"] .form-success, :root:not([data-theme="light"]) .form-success { color: var(--brass-soft); }
.legal { margin-top: 14px; font-size: 11.5px; color: var(--text-soft); line-height: 1.6; }

/* ---------- ticket (redemption claim) ---------- */
.ticket {
  display: inline-flex; align-items: center; gap: 14px; background: var(--ink-raised);
  border: 1px solid var(--pine-line); border-radius: 10px; padding: 14px 22px; position: relative;
}
.ticket::before, .ticket::after {
  content: ""; position: absolute; width: 18px; height: 18px; background: var(--ink);
  border-radius: 50%; top: 50%; transform: translateY(-50%);
}
.ticket::before { left: -9px; }
.ticket::after { right: -9px; }
.ticket-label { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; color: var(--on-dark-soft); text-transform: uppercase; }
.ticket-code { font-family: var(--mono); font-size: 20px; color: var(--brass-soft); font-variant-numeric: tabular-nums; }

/* ---------- theme grid ---------- */
/* The one theme in use, shown above the (collapsed) grid of all of them. */
.theme-current {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: var(--surface-raised); border: 1px solid var(--surface-line);
  border-radius: 14px; padding: 14px 16px; margin-bottom: 22px;
}
.theme-current-swatch {
  width: 46px; height: 46px; border-radius: 10px; flex-shrink: 0;
  border: 1px solid rgba(0, 0, 0, 0.08);
}
.theme-current-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.theme-current-eyebrow {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--text-soft);
}
.theme-current-name { font-size: 16px; font-weight: 600; }
/* Pushed to the far edge, so the row reads "this is your theme … and here is
   how to change it" rather than as two competing controls. */
.theme-toggle { margin-left: auto; flex-shrink: 0; }

.theme-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 22px; }
.theme-card {
  border-radius: 14px; overflow: hidden; border: 1px solid var(--surface-line); background: var(--surface-raised);
  cursor: pointer; text-align: left; padding: 0; transition: transform 0.2s ease, box-shadow 0.2s ease; display: block;
}
.theme-card:hover { transform: translateY(-3px); box-shadow: 0 16px 30px -20px rgba(20,35,28,0.4); }
.theme-card[data-selected="true"] { box-shadow: 0 0 0 2px var(--brass-dim), 0 16px 30px -20px rgba(20,35,28,0.4); }
:root[data-theme="dark"] .theme-card[data-selected="true"] { box-shadow: 0 0 0 2px var(--brass-soft), 0 16px 30px -20px rgba(0,0,0,0.5); }
.theme-card input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
.theme-swatch { height: 110px; position: relative; display: flex; align-items: flex-end; padding: 14px; }
.theme-swatch-dots { display: flex; gap: 6px; }
.theme-swatch-dots span { width: 14px; height: 14px; border-radius: 50%; display: block; box-shadow: 0 0 0 2px rgba(255,255,255,0.35); }
.theme-check {
  position: absolute; top: 12px; right: 12px; width: 22px; height: 22px; border-radius: 50%;
  background: var(--brass); color: var(--ink); display: none; align-items: center; justify-content: center; font-size: 13px;
}
.theme-card[data-selected="true"] .theme-check { display: flex; }
.theme-body { padding: 16px 18px 20px; }
.theme-name { font-family: var(--display); font-size: 19px; font-weight: 500; }
.theme-desc { margin-top: 6px; font-size: 12.5px; color: var(--text-soft); line-height: 1.5; }

/* ---------- builder shell ---------- */
.builder-shell { display: flex; min-height: 100vh; }
.builder-side { width: 290px; flex-shrink: 0; padding: 28px 20px; }
/* The "My Planners" nav entry is desktop-redundant: the account switcher at
   the foot of the sidebar already lists every planner and links to the same
   page. Hidden only from 1025px up, which is exactly where that switcher is
   on screen — below this the sidebar gives way to the dock and top bar, where
   the switcher isn't visible and the nav entry is the only way through. */
@media (min-width: 1025px) {
  .planners-nav-item { display: none; }
}
.builder-side-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
.builder-mobile-account { display: none; font-size: 13px; color: var(--text-soft); }
.builder-mobile-account a { color: var(--text-soft); text-decoration: none; }
.builder-theme-tag {
  display: inline-flex; align-items: center; gap: 8px; font-family: var(--mono); font-size: 11px;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-soft); margin-bottom: 22px;
}
.builder-theme-tag span { width: 9px; height: 9px; border-radius: 50%; background: var(--accent); }
.module-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
/* The lighter background lives on the whole <li> (not just the nav link)
   so it wraps a page's own injected submenu too (see $sidebarExtra in
   app_header.php) — a page like Seating Chart with its Guests/Tables
   submenus reads as one active group, not just a highlighted link with
   unrelated content hanging below it. */
.module-list-item[data-active="true"] { background: var(--surface-dim); border-radius: 8px; padding-bottom: 4px; }
.module-item {
  display: flex; align-items: center; justify-content: space-between; padding: 11px 12px; border-radius: 8px;
  font-size: 14px; color: var(--text-soft); background: transparent; text-decoration: none;
}
.module-item[data-active="true"] { color: var(--text); font-weight: 600; }
.module-item .count {
  /* Tinted from the theme's own --text instead of a fixed light color —
     the fixed rgba(230,227,216) read fine on light themes but was nearly
     invisible against --text-soft on a dark theme like Charcoal & Silver
     (light-gray text on a pill that also blended toward light-gray). */
  font-family: var(--mono); font-size: 12.5px; font-variant-numeric: tabular-nums;
  background: color-mix(in srgb, var(--text) 14%, transparent); color: var(--text);
  border-radius: 999px; padding: 3px 10px; flex-shrink: 0;
}
.module-item-main { display: flex; align-items: center; gap: 10px; min-width: 0; }
.module-item-icon { width: 17px; height: 17px; flex-shrink: 0; opacity: 0.85; }
.module-item[data-active="true"] .module-item-icon { opacity: 1; }
/* ---- Sidebar account block ------------------------------------------------
   Inside the sidebar, --accent/--surface/--text are already remapped to the
   dark-sidebar palette (see app_header.php), so these rules stay theme-aware
   without naming a single colour of their own. */
.builder-account { margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--surface-line); }
.builder-account a { color: var(--text-soft); text-decoration: none; font-size: 13px; }

.account-event { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }
.account-event-name {
  font-size: 13px; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.account-edit {
  font-size: 11px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--accent) !important; flex-shrink: 0;
  border-bottom: 1px solid transparent; transition: border-color 0.15s ease;
}
.account-edit:hover { border-bottom-color: var(--accent); }

.account-toggle {
  display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 8px;
  border: none; border-radius: 8px; background: none; cursor: pointer;
  font-family: inherit; font-size: 13px; color: var(--text-soft);
  transition: background 0.15s ease, color 0.15s ease;
}
.account-toggle:hover { background: var(--surface-dim); color: var(--text); }
.account-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.account-chevron { width: 14px; height: 14px; flex-shrink: 0; transition: transform 0.2s ease; }
.account-toggle[aria-expanded="false"] .account-chevron { transform: rotate(-90deg); }
.account-toggle-label { flex: 1; text-align: left; }
.account-count {
  font-family: var(--mono); font-size: 11px; line-height: 1; padding: 3px 7px;
  border-radius: 999px; background: var(--surface-dim); color: var(--text-soft);
}

.account-planners { list-style: none; margin: 2px 0 6px; padding: 0 0 0 10px; }
.account-planners[hidden] { display: none; }
.account-planner, .account-buy {
  display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 7px;
  font-size: 12.5px; transition: background 0.15s ease, color 0.15s ease;
}
.account-planner:hover, .account-buy:hover { background: var(--surface-dim); color: var(--text) !important; }
.account-planner-dot {
  width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
  background: var(--surface-line);
}
.account-planner.is-current { color: var(--text) !important; font-weight: 600; }
.account-planner.is-current .account-planner-dot { background: var(--accent); }
.account-planner-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-buy { color: var(--accent) !important; font-weight: 600; margin-top: 2px; }
.account-buy-plus {
  width: 6px; text-align: center; font-size: 14px; line-height: 1; flex-shrink: 0;
}

.account-logout {
  display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 8px;
  transition: background 0.15s ease, color 0.15s ease;
}
.account-logout:hover { background: var(--surface-dim); color: var(--text) !important; }
.account-logout svg { width: 15px; height: 15px; flex-shrink: 0; }

.account-footer {
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--surface-line);
  display: flex; flex-direction: column; gap: 3px;
  font-size: 10.5px; line-height: 1.5; color: var(--text-soft); opacity: 0.75;
}
.account-footer-brand { font-size: 10.5px !important; font-weight: 600; color: var(--text-soft) !important; }
.account-footer-brand:hover { color: var(--accent) !important; }
.account-footer-made { display: inline-flex; align-items: center; gap: 4px; }
.account-heart { color: var(--accent); }

/* Page-injected sidebar content (see app_header.php's $sidebarExtra hook) —
   the seating chart's Guests/Tables submenus, and the Vendors module's three
   section links. */
.sidebar-extra { margin: 4px 0 10px; padding-left: 6px; }

/* A flat list of a module's own sub-pages, shown under its nav item while that
   module is active. No <details> wrapper, unlike the seating submenus: there
   are only three short links and they *are* the navigation, so hiding them
   behind a disclosure would put the destination two clicks away. */
.sidebar-sublinks { list-style: none; margin: 0; padding: 0 0 0 6px; }
.sidebar-sublinks a {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 6px 10px; border-radius: 8px;
  font-size: 13px; color: var(--text-soft); text-decoration: none;
  /* The rail marks which sub-page is open without needing a filled pill,
     which at this size competed with the module item above it. */
  border-left: 2px solid transparent;
  transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
}
.sidebar-sublinks a:hover { color: var(--text); background: rgba(255, 255, 255, 0.05); }
.sidebar-sublinks a[data-active="true"] {
  color: var(--text); font-weight: 600;
  border-left-color: var(--accent);
  background: rgba(255, 255, 255, 0.06);
}
/* Deliberately plainer than the module .count pill above it — a filled pill on
   each of three sub-rows turned the block into a wall of badges. */
.sidebar-sublink-count { font-size: 11.5px; font-variant-numeric: tabular-nums; opacity: 0.65; }

/* ---- Section tabs (see assets/js/section-tabs.js) ----
   Modules that are several lists on one page — vendors, registry — show one
   section at a time rather than stacking into a very long scroll. Desktop
   switches from the sidebar submenu above; the pills are the phone's stand-in
   for it, since the sidebar is a drawer there. */
.section-panel { display: none; }
.section-panel.is-active { display: block; }
.panel-tabs { display: none; }
@media (max-width: 640px) {
  .panel-tabs {
    display: flex; gap: 6px; margin-bottom: 18px;
    overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
  }
  .panel-tabs::-webkit-scrollbar { display: none; }
  .panel-tab {
    flex: 0 0 auto; cursor: pointer; white-space: nowrap;
    display: inline-flex; align-items: center;
    background: var(--surface-raised); color: var(--text-soft);
    border: 1px solid var(--surface-line); border-radius: 999px;
    padding: 8px 16px; font-size: 13px; font-weight: 600; font-family: var(--body);
    transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
  }
  .panel-tab.is-active {
    background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
  }
  .panel-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
}
.sidebar-submenu { margin-bottom: 4px; }
.sidebar-submenu summary {
  cursor: pointer; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-soft); padding: 8px 6px; list-style: none;
}
.sidebar-submenu summary::-webkit-details-marker { display: none; }
.sidebar-submenu summary::before { content: '▸'; display: inline-block; width: 12px; font-size: 10px; }
.sidebar-submenu[open] summary::before { content: '▾'; }
.sidebar-submenu > *:not(summary) { padding: 0 6px; }
.sidebar-submenu-summary {
  /* Was a hardcoded brass/gold rgba() left over from the Golden Hour-only
     days — same fix as the nav .count pills: tint from the sidebar's own
     --accent instead of a fixed color, so it matches whichever theme (and
     the sidebar's own adjusted accent) is actually active. */
  display: inline-block; margin-left: 6px; font-weight: 500; text-transform: none;
  letter-spacing: normal; font-size: 12px; color: var(--text); opacity: 1;
  background: color-mix(in srgb, var(--accent) 20%, transparent); border-radius: 999px; padding: 3px 9px;
}

.sidebar-quick-add { display: flex; gap: 6px; margin-bottom: 8px; box-sizing: border-box; }
.sidebar-quick-add input {
  flex: 1; min-width: 0; border: 1px solid var(--surface-line); background: var(--surface-raised);
  border-radius: 7px; padding: 6px 8px; font-size: 12.5px; font-family: var(--body); color: var(--text);
}
.sidebar-quick-add input::placeholder, .sidebar-guest-search::placeholder { color: var(--text-soft); opacity: 1; }
.sidebar-quick-add button {
  border: none; background: var(--accent); color: var(--accent-ink);
  border-radius: 7px; padding: 6px 10px; font-size: 12.5px; font-weight: 600; cursor: pointer; flex-shrink: 0;
}
.sidebar-quick-add button:hover { filter: brightness(1.08); }
.sidebar-guest-search {
  width: 100%; box-sizing: border-box; border: 1px solid var(--surface-line); background: var(--surface-raised);
  border-radius: 7px; padding: 6px 9px; font-size: 12.5px; font-family: var(--body); color: var(--text);
  margin-bottom: 8px;
}
.sidebar-guest-search:focus { border-color: var(--accent); outline: none; }
.sidebar-drag-hint {
  font-size: 11px; font-weight: 700; color: var(--text); margin: 0 0 8px; line-height: 1.4;
  display: flex; align-items: center; gap: 5px;
}
/* Points toward the canvas, which sits to the right of this sidebar. */
.sidebar-drag-hint::after { content: '\2192'; color: var(--accent); font-size: 13px; flex-shrink: 0; }

.sidebar-guest-list {
  display: flex; flex-direction: column; gap: 4px; max-height: 260px; overflow-y: auto; margin-bottom: 8px;
  /* Thin, theme-dark scrollbar instead of the browser's default chunky one —
     Firefox via scrollbar-color, Chrome/Safari via the ::-webkit- rules
     below. Built from --text (already the theme's own ink tone) rather than
     a hardcoded gray, so it stays legible across light AND dark themes. */
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--text) 45%, transparent) transparent;
}
.sidebar-guest-list::-webkit-scrollbar { width: 4px; }
.sidebar-guest-list::-webkit-scrollbar-track { background: transparent; }
.sidebar-guest-list::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--text) 45%, transparent); border-radius: 999px;
}
.sidebar-guest-list::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--text) 65%, transparent); }
.sidebar-guest-empty { font-size: 12.5px; color: var(--text-soft); padding: 4px 0; }
.sidebar-guest-chip {
  /* Noticeably darker than --surface-dim (a 6% tint, meant for barely-there
     surfaces) so each name reads as its own block against the sidebar's
     own background, not just a faint outline. */
  font-size: 13px; padding: 7px 9px; border-radius: 7px; background: color-mix(in srgb, var(--text) 12%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--text) 40%, transparent); cursor: grab; user-select: none; touch-action: none;
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  border-left: 3px solid transparent; padding-left: 8px;
}
.sidebar-guest-chip:hover { border-color: var(--accent); }

/* A guest's own party — the primary chip plus one sub-chip per partner/kid,
   each independently draggable to its own seat (see composite "<id>:partner"
   / "<id>:kid:<n>" keys throughout seating/index.php). Grouped visually with
   a connecting rail so it reads as one family, not unrelated chips. */
.sidebar-guest-group { position: relative; margin-bottom: 4px; }
.sidebar-guest-group .sidebar-guest-chip { margin-bottom: 3px; }
.sidebar-guest-group .sidebar-guest-chip:last-child { margin-bottom: 0; }
/* Full name and surname on one line — the sidebar is wide enough now (see
   .builder-side) that even a long hyphenated surname fits without
   truncating or wrapping. */
.sidebar-guest-name { white-space: nowrap; font-size: 12.5px; }
/* 11px was below the point where a muted colour still reads comfortably on a
   dark plate, even once the contrast itself is compliant. */
.sidebar-guest-count { flex-shrink: 0; font-size: 12px; color: var(--text-soft); }
.sidebar-guest-sub {
  /* Partner/kid sub-chips keep their tag and name on one line (unlike the
     primary chip, which stacks name above count so a long name can wrap
     without crowding the count). */
  flex-direction: row; align-items: center; gap: 6px; flex-wrap: wrap;
  margin-left: 16px; width: calc(100% - 16px); position: relative;
  background: var(--surface); font-size: 12px; padding-top: 6px; padding-bottom: 6px;
}
/* Connecting rail showing the sub-chip belongs to the family above it —
   noticeably lighter than --surface-line's usual subtle-border job, since
   that reads as near-invisible against the sidebar's own dark background. */
.sidebar-guest-sub::before {
  content: ""; position: absolute; left: -10px; top: -8px; bottom: 50%;
  width: 8px; border-left: 1.5px solid color-mix(in srgb, var(--text) 40%, transparent);
  border-bottom: 1.5px solid color-mix(in srgb, var(--text) 40%, transparent);
  border-radius: 0 0 0 5px;
}
/* Deliberately the OPPOSITE of the dark card it sits on (unlike everything
   else in the sidebar, which now matches the dark theme) — a light pill
   with dark text reads as a distinct label/tag rather than blending into
   the family chip behind it. */
.sidebar-family-tag {
  font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
  color: color-mix(in srgb, black 78%, var(--accent) 22%);
  background: color-mix(in srgb, white 88%, var(--accent) 12%);
  border-radius: 4px; padding: 1px 5px; flex-shrink: 0;
}

/* RSVP status — brighter, more saturated "neon" tones than the muted
   --sage/--clay/--brass tokens the main Guests page pills use, since those
   read as washed-out/muddy against the sidebar's dark background. Scoped
   to the sidebar chip specifically; the Guests page pills keep their own
   quieter palette. */
/* Exact same hex values as the .pill dots on the Guests page (app.css's
   .pill.yes/.pending/.maybe/.no) — was previously drifted (a shared yellow
   for both pending and maybe here, vs. that page's amber-vs-purple split). */
.sidebar-guest-chip.rsvp-yes { border-left-color: #00e676; }
.sidebar-guest-chip.rsvp-no { border-left-color: #ff3b5c; }
.sidebar-guest-chip.rsvp-pending { border-left-color: #ffb300; }
.sidebar-guest-chip.rsvp-maybe { border-left-color: #9c6bff; }

/* The floating "you're dragging this" label that follows the pointer —
   position:fixed so it can travel anywhere on the page, not just within
   the scrollable canvas. */
.guest-drag-ghost {
  position: fixed; z-index: 200; transform: translate(-50%, -50%); pointer-events: none;
  background: var(--accent); color: var(--accent-ink); font-size: 12.5px; font-weight: 600;
  padding: 6px 12px; border-radius: 999px; box-shadow: 0 8px 20px -8px rgba(0,0,0,0.4);
  white-space: nowrap;
}
/* Chairs are normally pointer-events:none (see .table-chair) so they never
   steal clicks meant for the table beneath them — armed only while an
   actual guest-chip drag is in progress, so elementFromPoint() can find a
   specific seat as a drop target. */
body.assigning-guest .table-chair { pointer-events: auto; }
.drop-target-hover { outline: 2px solid var(--accent); outline-offset: 2px; }

.builder-main { flex: 1; padding: 32px 40px 80px; max-width: 1170px; min-width: 0; }
/* Above 1200px the builder pages use the whole window. Every module here is a
   table or a grid — guests, budget, timeline, vendors, registry, gallery,
   planners — where extra width means more columns visible at once and less
   horizontal scrolling, not longer lines of prose to read.

   Prose is protected separately: .builder-main-sub is already capped at 60ch,
   which is what stops the intro paragraph under each heading from stretching
   into an unreadable ribbon. Forms keep their own widths.

   1200px, not 1170px: releasing the cap exactly at the cap would let a window
   a pixel wider jump straight to full bleed with no gutter to speak of. */
@media (min-width: 1200px) {
  .builder-main { max-width: none; }
}
.builder-main h1 { font-size: 26px; font-weight: 500; margin-bottom: 4px; }
.builder-main-sub { color: var(--text-soft); font-size: 14px; margin-bottom: 26px; max-width: 60ch; }
/* Splits one module page into labeled sections (e.g. Registry's Gift List /
   Song List / Registry Links) without it reading as three separate pages. */
.builder-section-title {
  font-family: var(--display); font-size: 19px; font-weight: 500; margin: 0 0 4px;
  padding-top: 8px; border-top: 1px solid var(--surface-line);
}
h1 + .builder-section-title, .builder-main-sub + .builder-section-title { border-top: none; padding-top: 0; }

.stat-row { display: flex; gap: 16px; margin-bottom: 22px; flex-wrap: wrap; }
.stat-row .stat {
  flex: 1; min-width: 140px; background: var(--surface-raised); border: 1px solid var(--surface-line);
  border-radius: 12px; padding: 16px 20px;
}
.stat b { display: block; font-family: var(--mono); font-size: 22px; font-variant-numeric: tabular-nums; }
.stat span { font-size: 11.5px; color: var(--text-soft); text-transform: uppercase; letter-spacing: 0.06em; }

/* Shared "how far along is this module" bar — one bar reads at a glance in a
   way the stat tiles above it don't. Same treatment the guest list uses for
   RSVP replies (which still carries its own .guests-progress copy); anything
   adopting it since builds on these. */
.module-progress { display: flex; align-items: center; gap: 14px; margin: -8px 0 18px; }
.module-progress-track {
  flex: 1; height: 6px; border-radius: 999px; background: var(--surface-dim);
  border: 1px solid var(--surface-line); overflow: hidden; min-width: 0;
}
.module-progress-track span {
  display: block; height: 100%; background: var(--accent);
  border-radius: 999px; transition: width 0.3s ease;
}
.module-progress-caption {
  margin: 0; flex-shrink: 0; font-size: 12px; color: var(--text-soft);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.module-progress-caption b { font-family: var(--mono); font-weight: 600; color: var(--text); }

.toolbar { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 18px; }

.table-tooltip {
  /* To the right of the table, not below it — directly beneath covered
     whichever table happened to be sitting there next on a crowded floor
     plan (see the seating chart screenshot with 7 tables), which is
     exactly the thing a "preview this table's guests" popover shouldn't
     do. Chairs stick out past the table's own edge by (gap + chair
     radius) — see slotPositions()'s `gap` — so "just past 100%" isn't
     enough clearance; 50px matches the same fix on .table-edit-panel. */
  position: absolute; top: 0; left: calc(100% + 50px); z-index: 30;
  background: var(--surface-raised); border: 1px solid var(--surface-line); border-radius: 10px;
  padding: 10px 12px; width: max-content; min-width: 150px; max-width: 280px;
  box-shadow: 0 14px 30px -14px rgba(0,0,0,0.35);
  text-align: left; cursor: default;
}
.table-tooltip-title { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-soft); margin-bottom: 6px; }
.table-tooltip-guest { font-size: 13px; color: var(--text); padding: 4px 0; }
.table-tooltip-guest-name { font-weight: 600; }
.table-tooltip-guest .table-tooltip-seat-tag { margin-top: 2px; margin-right: 0; }
.table-tooltip-empty { font-size: 12.5px; color: var(--text-soft); }
.table-tooltip-seat-tag {
  display: inline-block; font-family: var(--mono); font-size: 10.5px; font-weight: 600;
  color: var(--text-soft); background: var(--surface-dim); border-radius: 5px; padding: 1px 5px; margin-right: 4px;
}

/* The "who's at this table" panel shown only while editing chairs — same
   right-of-table position as .table-tooltip (which stays hidden during
   edit mode, so the two never overlap), but interactive: a seat <select>
   and a remove button per assigned guest, instead of a static list. */
.table-edit-panel {
  /* Chairs stick out past the table's own edge by (gap + chair radius) —
     see slotPositions()'s `gap` in seating/index.php — so "just past 100%"
     (the old 10px) still overlapped the right-hand column of chairs.
     50px clears that plus a little breathing room. */
  position: absolute; top: 0; left: calc(100% + 50px); z-index: 31;
  background: var(--surface-raised); border: 1px solid var(--accent); border-radius: 10px;
  padding: 10px 12px; width: max-content; min-width: 220px; max-width: 300px;
  box-shadow: 0 14px 30px -14px rgba(0,0,0,0.35);
  text-align: left; cursor: default; touch-action: auto;
}
.table-edit-panel-title { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-soft); margin-bottom: 8px; }
.table-edit-panel-row { display: flex; align-items: center; gap: 6px; padding: 4px 0; }
.table-edit-panel-name { flex: 1; font-size: 13px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.table-edit-panel-seat {
  font-size: 12px; padding: 3px 5px; border-radius: 6px; border: 1px solid var(--surface-line);
  background: var(--surface); color: var(--text); max-width: 100px;
}
.table-edit-panel-remove {
  flex-shrink: 0; width: 20px; height: 20px; border-radius: 50%; padding: 0; line-height: 1;
  background: none; border: 1px solid color-mix(in srgb, var(--clay) 45%, var(--surface-line));
  color: var(--clay); cursor: pointer; font-size: 14px;
}
.table-edit-panel-remove:hover { background: color-mix(in srgb, var(--clay) 12%, transparent); }
.table-edit-panel-empty { font-size: 12.5px; color: var(--text-soft); padding: 2px 0 6px; }

/* Partner/kid rows nested under their party's primary guest — same
   grouping as the sidebar list, indented and slightly smaller. */
.table-edit-panel-subrow { margin-left: 14px; padding-left: 8px; border-left: 1.5px solid var(--surface-line); }
.table-edit-panel-subrow .table-edit-panel-name { font-size: 12px; }
.table-edit-panel-tag {
  font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--text-soft); background: var(--surface-dim); border-radius: 4px; padding: 1px 4px; margin-right: 3px;
}
/* A party member shown for context (they're part of this table's group)
   but not independently assigned to a seat here, so there's nothing to
   edit — no seat select or remove button, just the name and status. */
.table-edit-panel-row-noassign { color: var(--text-soft); font-style: italic; }

/* Big RSVP-status dot before a name — same color tokens as the sidebar's
   left-edge bar and the Guests page pills, just as a dot here since the
   panel already has a seat select + remove button crowding the row. */
/* Same hex values as .pill's dots on the Guests page and the sidebar's
   rsvp-* border colors — one palette for RSVP status everywhere. */
.rsvp-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.rsvp-dot.rsvp-yes { background: #00e676; }
.rsvp-dot.rsvp-no { background: #ff3b5c; }
.rsvp-dot.rsvp-pending { background: #ffb300; }
.rsvp-dot.rsvp-maybe { background: #9c6bff; }

/* Instant hover bubble with the occupant's name — see the mouseenter
   listener added only for occupied chairs in seating/index.php. */
.chair-name-tip {
  position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  background: var(--surface-raised); border: 1px solid var(--accent); border-radius: 6px;
  padding: 3px 8px; font-size: 11.5px; font-weight: 600; color: var(--text); white-space: nowrap;
  box-shadow: 0 6px 16px -8px rgba(0,0,0,0.35); pointer-events: none; z-index: 10;
}
.chair-name-tip[hidden] { display: none; }
.table-edit-panel-hint { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--surface-line); font-size: 11px; color: var(--text-soft); }

/* Chairs ringing a table — round tables get an even circle, rectangles
   split seats between the top and bottom edges. Positioned by JS per the
   table's own size/seat count (see chairPositions() in seating/index.php);
   this just styles the marker itself. */
.table-chair {
  position: absolute; transform: translate(-50%, -50%);
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--surface); border: 1.5px solid var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 9.5px; font-weight: 700; color: var(--text-soft);
  pointer-events: none; z-index: 1;
}
/* Occupied chairs opt back into pointer events just enough to receive
   hover for the name tooltip (see .chair-name-tip) — there's no
   pointerdown listener attached in non-edit mode, so a click still
   bubbles up to the table's own drag handler exactly as when this was
   pointer-events:none. */
.table-chair.occupied { background: var(--accent); color: var(--accent-ink); pointer-events: auto; }
/* The name bubble is drawn inside the chair, and the chair's own z-index makes
   it a stacking context — so no z-index on the bubble itself can lift it above
   something that outranks the chair. The table's edit/remove controls sit at
   z-index 5 and were covering the name. Raising the whole chair while hovered
   carries the bubble with it, over those controls and over any neighbouring
   table (.table-el sets no z-index, so everything shares the canvas's own
   stacking context). Kept below the sticker ghost (400) and modals (500). */
.table-chair.occupied:hover,
.table-chair.showing-name { z-index: 60; }

/* The couple's combined badge — one icon above the table instead of two
   separate chair markers for seats 1/2 (see the tableType==='sweetheart'
   block in render(), seating/index.php). Centered on the table's top
   edge, same relative position chairs would otherwise ring it from. */
.sweetheart-badge {
  position: absolute; top: 0; left: 50%; transform: translate(-50%, calc(-100% - 10px));
  display: flex; align-items: center; gap: 4px;
  background: var(--surface-raised); border: 1.5px solid var(--accent); border-radius: 999px;
  padding: 5px 10px; color: var(--accent-ink); box-shadow: 0 2px 6px rgba(0,0,0,0.15);
  pointer-events: auto; z-index: 5;
}
.sweetheart-badge-icon { display: flex; width: 34px; height: 21px; color: var(--text); }
.sweetheart-badge-icon svg { width: 100%; height: 100%; }
.sweetheart-badge .chair-name-tip { text-align: center; }

/* In chair-edit mode (toggled by the "e" button) chairs become the
   draggable thing instead of the table itself — see the "e" button's
   click handler suppressing the table's own drag while a chair is grabbed. */
.table-chair.editable { pointer-events: auto; cursor: grab; z-index: 3; box-shadow: 0 2px 6px rgba(0,0,0,0.18); }
/* The other chair a dragged one will swap with if released now — see the
   nearestOtherSlot() snap-radius check in the chair drag handler. */
.table-chair.swap-target { border-color: var(--clay); box-shadow: 0 0 0 3px color-mix(in srgb, var(--clay) 35%, transparent); }

/* The mobile "(i)" tap target — hover-capable devices never render this
   (see isFineHover in seating/index.php), they keep the plain hover
   tooltip they always had. */
.table-info-btn {
  position: absolute; top: -8px; right: -8px; width: 24px; height: 24px; border-radius: 50%;
  background: var(--surface-raised); border: 1.5px solid var(--accent); color: var(--accent-ink);
  font-family: var(--display); font-style: italic; font-weight: 700; font-size: 13px;
  display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 5;
  box-shadow: 0 2px 6px rgba(0,0,0,0.15); touch-action: manipulation;
}

/* The pencil (move chairs) and bin (remove table) buttons — mirrors
   .table-info-btn but sits in-flow, centered below the seat count (the
   table is a flex column with align-items:center, so this just becomes the
   3rd stacked child) rather than pinned to a corner — the corner was
   ambiguous next to the chairs ringing it, centered under the label reads
   unambiguously as "about this table" rather than "about whichever chair
   is nearby". Shown on every input type (unlike the touch-only info
   button) since editing a table is a distinct action from previewing its
   guest list. */
.table-controls { position: relative; z-index: 5; margin-top: 4px; display: flex; gap: 6px; flex-shrink: 0; }
.table-edit-btn, .table-remove-btn, .table-manage-remove {
  width: 22px; height: 22px; border-radius: 50%; padding: 0;
  background: var(--surface-raised); border: 1.5px solid var(--surface-line);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  box-shadow: 0 2px 6px rgba(0,0,0,0.15); touch-action: manipulation; flex-shrink: 0;
}
.table-edit-btn svg, .table-remove-btn svg, .table-manage-remove svg { width: 13px; height: 13px; }
.table-edit-btn { color: var(--text); border-color: var(--accent); }
.table-remove-btn, .table-manage-remove { color: var(--clay); border-color: color-mix(in srgb, var(--clay) 45%, var(--surface-line)); }
/* Same feedback as every other remove control on the site — the row-actions
   icons on the list pages and the remove button drawn on a table both do
   scale + brightness (see the .row-actions .icon-action.danger block). This
   one only tinted its background, so it read as a different kind of control.
   Hover-capable pointers only: :hover latches on touch and would leave the
   last-touched row's icon permanently enlarged. */
.table-manage-remove { transition: transform 0.15s ease, color 0.15s ease, filter 0.15s ease, background-color 0.15s ease; }
@media (hover: hover) and (min-width: 641px) {
  .table-manage-remove:hover,
  .table-manage-remove:focus-visible {
    transform: scale(1.2);
    color: var(--clay); filter: brightness(1.2);
    background: color-mix(in srgb, var(--clay) 12%, var(--surface-raised));
    border-color: var(--clay);
  }
}
.table-manage-remove:active { transform: scale(0.94); }

/* The two controls drawn *on* a table drop the pill chrome the sidebar's
   remove button keeps: sitting inside the table's own circle, a bordered
   white disc each read as a second circle inside a circle. Smaller too —
   they share the space with the table's name and seat count. The .active
   state keeps its filled disc, since "currently editing this table's
   chairs" is a state that has to stay readable without a border. */
.table-edit-btn, .table-remove-btn {
  width: 18px; height: 18px;
  background: none; border: 0; box-shadow: none;
}
.table-edit-btn svg, .table-remove-btn svg { width: 12px; height: 12px; }
.table-edit-btn.active { background: var(--accent); color: var(--accent-ink); }

/* Same hover the list pages' row actions use (see .row-actions .icon-action):
   a small lift, remove brightening its clay. On a real pointer the pair also
   stays hidden until the table is hovered, so a full chart isn't peppered
   with controls — kept visible while editing chairs (otherwise the button
   that ends editing disappears as soon as the cursor leaves), while focused
   for keyboard users, and while armed as a chair-drop target. Touch keeps
   them permanently visible: there is no hover to reveal them with. */
@media (hover: hover) and (min-width: 641px) {
  .table-el .table-edit-btn,
  .table-el .table-remove-btn {
    opacity: 0;
    transition: opacity 0.15s ease, transform 0.15s ease, color 0.15s ease, filter 0.15s ease;
  }
  .table-el:hover .table-edit-btn,
  .table-el:hover .table-remove-btn,
  .table-el:focus-within .table-edit-btn,
  .table-el:focus-within .table-remove-btn,
  .table-el.editing-chairs .table-edit-btn,
  .table-el.editing-chairs .table-remove-btn { opacity: 1; }

  .table-edit-btn:hover, .table-edit-btn:focus-visible { transform: scale(1.2); color: var(--text); }
  .table-remove-btn:hover, .table-remove-btn:focus-visible {
    transform: scale(1.2); color: var(--clay); filter: brightness(1.2);
  }
  /* Last, so it outranks the opacity:0 above at equal specificity. */
  .table-remove-btn.drop-armed { opacity: 1; }
}
/* Armed while a dragged chair hovers over the bin — see isOverRemoveBtn()
   in the chair drag handler — so it's clear dropping here deletes just
   that one chair, distinct from clicking the bin (which removes the whole
   table). */
.table-remove-btn.drop-armed {
  background: var(--clay); color: var(--accent-ink); border-color: var(--clay);
  transform: scale(1.25); box-shadow: 0 0 0 4px color-mix(in srgb, var(--clay) 30%, transparent);
}
.table-el.editing-chairs { outline: 2px dashed var(--accent); outline-offset: 5px; }

/* Table name as a rename control. Looks like plain text until hovered, so the
   sidebar list doesn't become a wall of buttons — the dashed underline is the
   same affordance the guest and budget lists use for click-to-edit. */
.table-rename {
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
  font: inherit; color: inherit; text-align: left;
  border-bottom: 1px dashed transparent;
}
.table-rename:hover { border-bottom-color: var(--accent); }
.table-rename:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.table-rename[disabled] { cursor: default; }
@media (hover: none) {
  /* :hover latches on touch and would leave one row permanently underlined. */
  .table-rename:hover { border-bottom-color: transparent; }
}
.table-rename-input {
  font: inherit; color: var(--text); width: 100%; min-width: 0;
  background: var(--surface-raised); border: 1px solid var(--accent);
  border-radius: 6px; padding: 2px 6px;
}

.table-manage-row {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 12px 0; border-bottom: 1px solid var(--surface-line);
}
.table-manage-row:last-child { border-bottom: none; }
.table-manage-name-row { flex: 1; min-width: 0; }
.table-manage-name { font-weight: 600; font-size: 14px; }
/* At-a-glance fill level for this table — same idea as the TABLES submenu
   summary badge, just per-table instead of totaled across all of them. */
.table-seat-progress { height: 4px; border-radius: 999px; background: var(--surface-line); margin-top: 5px; overflow: hidden; }
.table-seat-progress-fill { height: 100%; background: var(--accent); border-radius: 999px; }
/* The sidebar's copy of this list (see $sidebarExtra in seating/index.php)
   is much narrower than the card it originally lived in — name, stepper
   and Remove all on one line just wrapped mid-word there. Stack instead. */
.sidebar-extra .table-manage-row { flex-wrap: wrap; gap: 6px; padding: 10px 0; }
.sidebar-extra .table-manage-name-row { flex-basis: 100%; }
.sidebar-extra .table-manage-name { font-size: 13px; }
.sidebar-extra .party-stepper button { width: 18px; height: 18px; font-size: 12px; }
.sidebar-extra .party-stepper { gap: 6px; }

/* Only shown for a rectangle table with an odd seat count — picks which
   side gets the one chair that can't be mirrored on the opposite side
   (see rectSideCounts() in seating/index.php). */
.odd-seat-picker { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-soft); flex-shrink: 0; }
.odd-seat-picker select {
  font-size: 12px; padding: 3px 5px; border-radius: 6px; border: 1px solid var(--surface-line);
  background: var(--surface); color: var(--text);
}
.sidebar-extra .odd-seat-picker { flex-basis: 100%; }
.sidebar-extra .btn-secondary { padding: 4px 8px; font-size: 12px; }

/* overflow-y: visible (not the default "auto" that setting overflow-x alone
   implies) so the RSVP/relationship popup menus — position:absolute inside a
   table cell here — can extend past the table's own bottom edge instead of
   getting clipped by it, while the table itself still scrolls horizontally
   on narrow screens. */
.table-wrap { overflow-x: auto; overflow-y: visible; border: 1px solid var(--surface-line); border-radius: 12px; background: var(--surface-raised); }
table { border-collapse: collapse; width: 100%; min-width: 560px; }
th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-soft); padding: 12px 16px; border-bottom: 1px solid var(--surface-line); font-weight: 600; }
td { padding: 13px 16px; font-size: 14px; border-bottom: 1px solid var(--surface-line); vertical-align: top; }
tr:last-child td { border-bottom: none; }
.row-sub { font-size: 12px; color: var(--text-soft); margin-top: 2px; }
.row-actions a, .row-actions button { font-size: 12.5px; color: var(--text-soft); text-decoration: none; margin-right: 12px; background: none; border: none; font-family: inherit; cursor: pointer; padding: 0; }
.row-actions a.danger, .row-actions button.danger { color: var(--clay); }
/* Desktop hover on every list's edit/remove controls: a small lift plus a
   stronger colour, so the target reacts before it's clicked. Edit goes to the
   page's near-black ink rather than the theme accent — against the pink
   palettes the accent read as another decorative tint instead of "this is the
   thing you're about to click". Remove keeps its clay and just brightens.

   Gated on a hover-capable pointer: on touch, :hover sticks after a tap and
   would leave a row's icons permanently enlarged. Scoped to .row-actions (plus
   the RSVP-cell copy button, which is the same kind of control in a different
   cell) so it doesn't catch the other .icon-action uses (a vendor's social
   links, for one). transform works because .icon-action is inline-flex; the
   wrapping <form> is display:inline, which is why it sits on the control. */
@media (hover: hover) and (min-width: 641px) {
  .row-actions .icon-action,
  .rsvp-cell .icon-action {
    transition: transform 0.15s ease, color 0.15s ease, filter 0.15s ease;
  }
  .row-actions .icon-action:hover,
  .row-actions .icon-action:focus-visible,
  .rsvp-cell .icon-action:hover,
  .rsvp-cell .icon-action:focus-visible {
    transform: scale(1.2);
    color: var(--text);
  }
  .row-actions .icon-action.danger:hover,
  .row-actions .icon-action.danger:focus-visible {
    color: var(--clay); filter: brightness(1.2);
  }
}

/* The RSVP pill and the "copy this guest's link" button, side by side — the
   pill says where the guest stands, the button is how they say it themselves. */
.rsvp-cell { white-space: nowrap; }
.rsvp-cell .rsvp-inline { display: inline-flex; align-items: center; gap: 10px; }
.rsvp-cell .rsvp-picker { display: inline-block; }
.rsvp-cell .icon-action { color: var(--text-soft); }

/* Copy feedback. .action-text is screen-reader-only at every width, so the
   label swap alone was invisible — the icon becomes a tick instead. */
.rsvp-link-btn .rsvp-link-tick { display: none; }
.rsvp-link-btn.is-copied .rsvp-link-chain { display: none; }
.rsvp-link-btn.is-copied .rsvp-link-tick { display: block; }
/* Scoped through .rsvp-cell so these beat the cell's own resting colour above
   — matching that rule's specificity exactly would leave the winner decided by
   source order, which is too fragile to rely on for state feedback. */
.rsvp-cell .rsvp-link-btn.is-copied { color: #00a94f; }
.rsvp-cell .rsvp-link-btn.is-failed { color: var(--danger); }
/* Icon-only everywhere now (used to be text-label-only on desktop, icon
   swapping in only on narrow phones) — the text stays in the DOM for
   screen readers/aria-label but is visually hidden at every width. */
.icon-action { display: inline-flex; align-items: center; gap: 6px; }
/* .row-actions above already resets buttons in that specific container, but
   .icon-action buttons also appear outside it (e.g. the vendor document
   remove button) and were falling back to the browser's default button
   border/background/padding — reset it here so it's consistent everywhere. */
button.icon-action { background: none; border: none; padding: 0; margin: 0; font-family: inherit; cursor: pointer; color: inherit; }
.icon-action svg { width: 16px; height: 16px; flex-shrink: 0; display: block; }
.icon-action .action-text {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

.pill { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; padding: 4px 10px; border-radius: 999px; }
.pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; }
/* Brighter, more saturated "neon" tones per status — and Awaiting reply /
   Maybe are now two distinct colors instead of sharing the same brass. */
.pill.yes { background: #dcfce8; color: #00a94f; }
.pill.yes::before { background: #00e676; }
.pill.pending { background: #fff3d1; color: #c98a00; }
.pill.pending::before { background: #ffb300; }
.pill.maybe { background: #ede4ff; color: #7c3aed; }
.pill.maybe::before { background: #9c6bff; }
.pill.no { background: #ffe1e6; color: #e5124f; }
.pill.no::before { background: #ff3b5c; }
:root[data-theme="dark"] .pill.yes, :root:not([data-theme="light"]) .pill.yes { background: #113322; color: #33ff9c; }
:root[data-theme="dark"] .pill.no, :root:not([data-theme="light"]) .pill.no { background: #3a1420; color: #ff7a95; }
:root[data-theme="dark"] .pill.pending, :root:not([data-theme="light"]) .pill.pending { background: #3a2c0a; color: #ffcc4d; }
:root[data-theme="dark"] .pill.maybe, :root:not([data-theme="light"]) .pill.maybe { background: #2a1c48; color: #c4a3ff; }

/* Guest relationship tag — theme-neutral (no per-value color coding like
   RSVP has), so it just uses the surface/text vars instead of a hardcoded
   hex like the RSVP pills above. .unset is the "nothing picked yet" state,
   dashed to read as a prompt rather than a set value. */
.pill.relationship { background: var(--surface-dim); color: var(--text-soft); }
.pill.relationship::before { background: var(--text-soft); }
.pill.relationship.unset { background: none; border: 1px dashed var(--surface-line); }
.pill.relationship.unset::before { display: none; }

.rsvp-picker, .relationship-picker { position: relative; display: inline-block; }
.rsvp-trigger, .relationship-trigger {
  border: none; cursor: pointer; transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.rsvp-trigger:hover, .relationship-trigger:hover { box-shadow: 0 0 0 2px var(--surface-line); }
.rsvp-trigger:active, .relationship-trigger:active { transform: scale(0.97); }
.rsvp-menu {
  /* Above the mobile dock, which is position:fixed at z-index 500 — at the old
     150 the dock painted straight over any menu that reached the bottom of the
     screen, so on a phone the lower half of the Party menu (the kids stepper)
     was simply not there. */
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 600;
  background: var(--surface-raised); border: 1px solid var(--surface-line); border-radius: 10px;
  padding: 6px; display: flex; flex-direction: column; gap: 2px; min-width: 150px;
  box-shadow: 0 12px 28px -14px rgba(0,0,0,0.35);
}
/* Opens upward instead when there isn't room below it — see positionPartyMenu(). */
.rsvp-menu.is-flipped { top: auto; bottom: calc(100% + 6px); }
.rsvp-menu[hidden] { display: none; }
.rsvp-option { display: block; width: 100%; background: none; border: none; padding: 4px; border-radius: 7px; cursor: pointer; text-align: left; }
.rsvp-option:hover { background: var(--surface-dim); }
.rsvp-option[data-current] { opacity: 0.5; cursor: default; }

/* Beside the table, threaded through a gap in the chair ring rather than
   parked at a fixed 3 o'clock — on a round table that spot IS a chair (seat 3
   of 8), so the two sat on top of each other. placeResizeAffordance() in
   seating/index.php writes left/top per table; both are the handle's centre,
   hence the translate(-50%,-50%) here.

   Chairs sit 17px out from the table edge and are 22px across
   (slotPositions()'s `gap`), so they occupy the first 28px past it. The
   handle's 42px clears that ring with room to spare, and its 26px box is a
   usable touch target. applyZoom() reserves 120px of stage past the furthest
   table and the handle needs 68, which is why resizeAngleFor() prefers gaps
   near due east. */
.table-resize-handle {
  position: absolute; transform: translate(-50%, -50%);
  width: 26px; height: 26px;
  border-radius: 50%; background: var(--accent); color: var(--accent-ink);
  border: 2px solid var(--surface);
  display: flex; align-items: center; justify-content: center;
  cursor: nwse-resize; opacity: 0; pointer-events: none;
  /* Appears at once, but waits before going away. The handle sits outside the
     table's own box, so reaching for it crosses a gap where neither the table
     nor the handle is under the pointer — with no delay it started fading the
     moment you set off towards it and was gone before you arrived. The delay
     applies only on the way out; hovering back re-triggers the 0s rule below. */
  transition: opacity 0.15s ease 0.45s;
  touch-action: none; z-index: 4;
}
/* A two-headed diagonal arrow along the nwse-resize axis the cursor already
   promises — the old plus/cross read as "add something here". */
.table-resize-handle svg { width: 14px; height: 14px; display: block; }

/* Tethers the handle back to the table it resizes. Without it, a control
   floating 42px off in a gap between two chairs reads as belonging to the
   chairs, or to nothing. Drawn from the table's edge outwards, so it never
   crosses the table itself; rotated about its own left edge, which is why
   transform-origin is pinned there. */
.table-resize-link {
  position: absolute; height: 2px; transform-origin: 0 50%;
  background: var(--accent); opacity: 0;
  border-radius: 1px; transition: opacity 0.15s ease 0.45s; z-index: 3;
  /* Hit-testable, unlike the handle's own pointer-events:none default: the
     tether is a child of .table-el, so travelling along it keeps the table
     hovered the whole way across the gap. */
  pointer-events: auto;
}
/* 2px is nothing to aim at, so the line carries an invisible 24px-tall band
   around itself. This is the bridge that makes the trip from table to handle
   continuous rather than a leap across dead space. */
.table-resize-link::before {
  content: ""; position: absolute; left: 0; right: -14px;
  top: 50%; height: 24px; transform: translateY(-50%);
}
/* Hover still reveals it on a mouse; a tap-to-select does the same everywhere,
   which is the only route that exists on a touch screen. */
.table-el:hover .table-resize-handle,
.table-el.is-selected .table-resize-handle { opacity: 1; pointer-events: auto; transition-delay: 0s; }

/* Live "how big is this now" readout while a table is being resized. 100% is
   the size a freshly added table gets, so the number answers "bigger or
   smaller than a normal table", which is the actual question — raw pixels
   would mean nothing on a plan with no scale.

   Centred on the table, not above it: chairs ring the outside and seat 1 sits
   at top-centre on a round table. pointer-events off so it can never swallow
   the drag it is reporting on. */
.table-size-badge {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: 6; pointer-events: none; white-space: nowrap;
  font-family: var(--mono); font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em;
  color: #fff; background: rgba(24, 22, 19, 0.86);
  padding: 4px 9px; border-radius: 999px;
  box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.5);
}
.table-size-badge[hidden] { display: none; }
.table-el:hover .table-resize-link,
.table-el.is-selected .table-resize-link { opacity: 0.55; transition-delay: 0s; }
.table-resize-handle:active { transform: translate(-50%, -50%) scale(1.12); }
@media (hover: none) {
  /* :hover latches on touch, leaving the handle stuck visible on whatever
     table was last touched — selection is the only trigger there. */
  .table-el:hover .table-resize-handle { opacity: 0; pointer-events: none; }
  .table-el:hover .table-resize-link { opacity: 0; }
  .table-el.is-selected .table-resize-handle { opacity: 1; pointer-events: auto; }
  .table-el.is-selected .table-resize-link { opacity: 0.55; }
}
/* A ring on the table itself, so it's obvious which one the handle belongs to.
   outline, not box-shadow: render() writes box-shadow into .table-el's inline
   style, and an inline declaration beats any stylesheet rule. */
.table-el.is-selected { outline: 2px solid var(--accent); outline-offset: 3px; }

/* Zoom controls, pinned over the canvas card's top-right corner. See the
   zoom block in seating/index.php — the stage is scaled, the scroll container
   is not. */
.canvas-zoom {
  position: absolute; top: 10px; right: 10px; z-index: 20;
  display: flex; align-items: stretch; gap: 1px;
  background: var(--surface-line); border: 1px solid var(--surface-line);
  border-radius: 9px; overflow: hidden;
  box-shadow: 0 6px 16px -10px rgba(0,0,0,0.4);
}
.canvas-zoom button {
  background: var(--surface-raised); color: var(--text); border: 0;
  font-family: var(--body); font-size: 14px; line-height: 1;
  min-width: 30px; padding: 7px 6px; cursor: pointer;
}
.canvas-zoom button:hover { background: var(--surface-dim); }
.canvas-zoom button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
#zoom-level { font-size: 11.5px; font-variant-numeric: tabular-nums; color: var(--text-soft); }
#zoom-reset { min-width: 46px; }
@media (max-width: 640px) {
  /* Compact on a phone. These float over the plan itself, so the enlarged
     touch targets I first used ate a visible slice of the canvas — on a
     430px screen that mattered more than the extra few pixels of target.
     Still ~28px tall, which stays comfortably tappable. */
  .canvas-zoom { top: 8px; right: 8px; border-radius: 7px; }
  .canvas-zoom button { min-width: 26px; padding: 6px 5px; font-size: 13px; }
  #zoom-level { font-size: 10.5px; }
  #zoom-reset { min-width: 38px; }
}

.timeline-check {
  appearance: none; -webkit-appearance: none; margin: 0;
  width: 20px; height: 20px; flex-shrink: 0; border-radius: 6px;
  border: 2px solid var(--surface-line); background: var(--surface-raised);
  cursor: pointer; position: relative;
  transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.timeline-check:hover { border-color: var(--accent); }
.timeline-check:active { transform: scale(0.9); }
.timeline-check:checked { background: var(--accent); border-color: var(--accent); }
.timeline-check:checked::after {
  content: ''; position: absolute; left: 50%; top: 45%; width: 5px; height: 9px;
  border: solid var(--accent-ink); border-width: 0 2px 2px 0;
  transform: translate(-50%, -55%) rotate(45deg);
}
.timeline-check:disabled { cursor: default; opacity: 0.55; }

.guest-select, .seat-select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  border: 1px solid var(--surface-line); background: var(--surface-dim); color: var(--text);
  border-radius: 9px; padding: 8px 32px 8px 12px; font-size: 13.5px; font-family: var(--body);
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' viewBox='0 0 11 7' fill='none'%3E%3Cpath d='M1 1L5.5 5.5L10 1' stroke='%23888888' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
}
.guest-select::-ms-expand, .seat-select::-ms-expand { display: none; }
.guest-select:focus, .seat-select:focus { outline: none; border-color: var(--accent); background: var(--surface-raised); }
.guest-select:disabled, .seat-select:disabled { cursor: default; opacity: 0.55; }
.rsvp-option .pill { pointer-events: none; }

.party-picker { position: relative; display: block; }
.party-trigger {
  display: flex; align-items: center; justify-content: space-between; gap: 6px; width: 100%;
  border: 1px solid var(--surface-line); background: var(--surface-dim); border-radius: 9px;
  padding: 12px 14px; font-size: 14.5px; font-family: var(--body); color: var(--text);
  cursor: pointer; text-align: left; transition: border-color 0.15s ease, background-color 0.15s ease;
}
.party-trigger:hover { border-color: var(--accent); }
.party-trigger::after { content: ''; width: 8px; height: 8px; border: solid var(--text-soft); border-width: 0 1.6px 1.6px 0; transform: rotate(45deg) translateY(-2px); flex-shrink: 0; }
.party-menu { min-width: 220px; padding: 14px; }
.party-menu-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.party-menu-row + .party-menu-row { margin-top: 12px; }
.party-menu .party-menu-label { display: flex; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 400; margin-bottom: 0; cursor: pointer; }
/* Search + group filter over the Assign guests table. Mirrors the seating
   sidebar's own two controls, and the budget/timeline toolbars, so the page
   has one idea of what a list filter looks like. */
.assign-toolbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 12px;
}
.assign-search {
  flex: 1; min-width: 160px; max-width: 280px; height: 36px;
  padding: 0 12px; border-radius: 9px;
  border: 1px solid var(--surface-line); background: var(--surface-dim);
  color: var(--text); font-family: var(--body); font-size: 13.5px;
}
.assign-search:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Label and its control travel together, so wrapping can never strand a
   label at the end of one line with its dropdown on the next. */
.assign-filter { display: inline-flex; align-items: center; gap: 8px; }
.assign-group-label {
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--text-soft); white-space: nowrap;
}
.assign-group {
  height: 36px; padding: 0 10px; border-radius: 9px;
  border: 1px solid var(--surface-line); background: var(--surface-dim);
  color: var(--text); font-family: var(--body); font-size: 13.5px;
}
@media (max-width: 640px) {
  /* Search on its own line, then the two filters share the next one — each
     keeping its own label attached. */
  .assign-search { flex-basis: 100%; max-width: none; }
  .assign-filter { flex: 1; min-width: 0; }
  .assign-filter .assign-group { flex: 1; min-width: 0; }
  /* 16px, or iOS zooms the page when the field takes focus. */
  .assign-search, .assign-group { font-size: 16px; }
}

/* "Seats" beside the − 8 + stepper: on its own the stepper says nothing about
   what it counts, and in the narrow sidebar there is no column header to lean
   on. Sits above the buttons so the pair reads as one labelled control. */
.table-manage-seats { display: flex; flex-direction: column; gap: 3px; }
.table-manage-seats-label {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--text-soft);
}
/* Explains the whole submenu once, rather than repeating it on every row. */
.sidebar-submenu-hint {
  font-size: 11.5px; color: var(--text-soft);
  margin: 2px 0 8px; padding-left: 6px; line-height: 1.35;
}

.party-stepper { display: flex; align-items: center; gap: 10px; }
.party-stepper button {
  width: 24px; height: 24px; border-radius: 7px; border: 1px solid var(--surface-line); background: var(--surface-raised);
  color: var(--text); font-size: 15px; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.party-stepper button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.party-stepper button:disabled { opacity: 0.4; cursor: default; }
.party-stepper span { min-width: 14px; text-align: center; font-size: 13.5px; font-weight: 600; }
.guest-composition { color: var(--text-soft); font-size: 12px; margin-top: 2px; }

.editable-text {
  cursor: pointer; border-radius: 5px; padding: 2px 5px; margin: -2px -5px;
  border-bottom: 1px dashed transparent; transition: border-color 0.15s ease, background 0.15s ease;
}
.editable-text:hover, .editable-text:focus-visible { background: var(--surface-dim); border-bottom-color: var(--text-soft); outline: none; }
/* Touch devices have no real hover, but they latch :hover on the last-tapped
   element until something else is tapped — so tapping a field left a dashed
   underline and tinted box stuck under it indefinitely. Drop the hover
   treatment there; :focus-visible below still covers keyboard users. */
@media (hover: none) {
  .editable-text:hover { background: none; border-bottom-color: transparent; }
}
.editable-text-input {
  font: inherit; color: inherit; background: var(--surface-raised); border: 1px solid var(--accent);
  border-radius: 5px; padding: 2px 6px; width: 100%; min-width: 60px;
}
.editable-text-input[type="number"] { width: 64px; }

.empty-state { color: var(--text-soft); font-size: 13.5px; padding: 26px 0; text-align: center; }

/* Shown above the Assign-guests card only while at least one table is in
   chair-edit mode (see updateEditModeNote() in seating/index.php) — a
   reminder of what dragging/dropping actually does, since none of it is
   discoverable from the canvas alone. */
.edit-mode-note {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  background: var(--accent-soft); border: 1px solid var(--accent); border-radius: 9px;
  padding: 10px 14px; margin-bottom: 14px; font-size: 13px; color: var(--text);
}
/* The [hidden] attribute alone loses to the "display: flex" rule above —
   same specificity, author stylesheet wins — so it has to be restated
   explicitly here or the note would never actually hide. */
.edit-mode-note[hidden] { display: none; }
.edit-mode-note strong { color: var(--accent-ink); background: var(--accent); padding: 2px 8px; border-radius: 999px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
.edit-mode-note span { color: var(--text-soft); }

/* Phone-only "save to device" buttons, ABOVE the gallery card. On a handheld
   the per-photo download icons are hidden, so these are how photos come off the
   page — they belong before the grid, not buried under it. */
.photo-toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.photo-toolbar .btn-secondary {
  flex: 1 1 auto; min-width: 0;
  padding-left: 12px; padding-right: 12px;
}
.photo-toolbar-summary {
  font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--text-soft); margin-bottom: 12px;
}

/* The favourites tab, riding the gallery card's top border like a folder tab.
   The wrapper carries the padding the tab needs to sit half-outside the card;
   the card itself keeps its own box untouched. */
.gallery-card-wrap { position: relative; padding-top: 17px; }
.gallery-fav-tab {
  position: absolute; top: 0; right: 20px; z-index: 2;
  display: inline-flex; align-items: center; gap: 7px;
  height: 34px; padding: 0 14px; border-radius: 999px; cursor: pointer;
  font-family: var(--body); font-size: 12.5px; font-weight: 600;
  background: var(--surface-raised); color: var(--text);
  border: 1px solid var(--surface-line);
  box-shadow: 0 4px 12px -6px rgba(0,0,0,0.3);
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.gallery-fav-tab svg { width: 14px; height: 14px; flex-shrink: 0; }
.gallery-fav-tab:hover { border-color: var(--accent); }
.gallery-fav-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* On-state: filled, and the label changes to "Show all photos" — the fill is
   never the only signal (aria-pressed and the wording both carry it too). */
.gallery-fav-tab.is-active {
  background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
}
@media (max-width: 640px) {
  /* Closer to the corner, and the label shortens naturally by wrapping nothing
     — at 430px the card's own padding is smaller too. */
  .gallery-fav-tab { right: 12px; height: 32px; padding: 0 12px; font-size: 12px; }
}
/* Per-photo save button on a grid tile. Was inline on the element, which made
   it impossible to hide from a stylesheet — an inline `display` outranks any
   rule, media query or not. */
#photo-grid .photo-save-btn {
  position: absolute; bottom: 6px; right: 6px;
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 6px;
  background: rgba(20, 35, 28, 0.6); color: #fff; text-decoration: none;
}
/* Gone on a handheld: the toolbar above the gallery replaces them, and opening
   a photo still offers a single-photo save from the lightbox. */
@media (pointer: coarse) {
  #photo-grid .photo-save-btn { display: none; }
}

/* Gallery status banner — says plainly whether guests can upload right now.
   Colour carries the same meaning as the dot, never on its own. */
.gallery-status {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 11px 14px; border-radius: 10px; margin-bottom: 20px;
  font-size: 13.5px; line-height: 1.45;
  border: 1px solid var(--surface-line); background: var(--surface-dim);
}
.gallery-status-dot {
  width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; margin-top: 6px;
  background: var(--text-soft);
}
.gallery-status.is-open { border-color: color-mix(in srgb, var(--moss, #4a7a5c) 45%, var(--surface-line)); }
.gallery-status.is-open .gallery-status-dot { background: var(--moss, #4a7a5c); }
.gallery-status.is-test { border-color: color-mix(in srgb, var(--accent) 55%, var(--surface-line)); }
.gallery-status.is-test .gallery-status-dot { background: var(--accent); }
.gallery-status.is-closed .gallery-status-dot,
.gallery-status.is-expired .gallery-status-dot { background: var(--clay); }
.gallery-status.is-closed { border-color: color-mix(in srgb, var(--clay) 40%, var(--surface-line)); }

/* ---------- QR / gallery ---------- */
.qr-stat-row { display: flex; gap: 32px; margin-top: 8px; }
.qr-stat b { display: block; font-family: var(--mono); font-size: 24px; color: var(--brass-soft); font-variant-numeric: tabular-nums; }
.qr-stat span { font-size: 11.5px; color: var(--on-dark-soft); text-transform: uppercase; letter-spacing: 0.06em; }
.qr-card { background: var(--surface-raised); border: 1px solid var(--surface-line); border-radius: 18px; padding: 26px; text-align: center; box-shadow: 0 30px 60px -25px rgba(0,0,0,0.35); }
.qr-card img { display: block; margin: 0 auto; border-radius: 6px; }
.qr-card-label { margin-top: 16px; font-family: var(--mono); font-size: 12px; color: var(--text-soft); letter-spacing: 0.04em; }
.qr-card-url { margin-top: 4px; font-family: var(--mono); font-size: 13px; color: var(--text); font-weight: 600; word-break: break-all; }

@media (max-width: 900px) {
  .builder-shell { flex-direction: column; }
  .builder-side {
    width: 100%; padding: 16px 16px 0; position: sticky; top: 0; z-index: 20;
    background: var(--surface); border-bottom: 1px solid var(--surface-line);
  }
  .builder-side-top { margin-bottom: 12px; }
  .builder-mobile-account { display: block; }
  .builder-theme-tag { display: none; }
  .module-list {
    flex-direction: row; overflow-x: auto; gap: 6px; padding-bottom: 12px;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  .module-list::-webkit-scrollbar { display: none; }
  .module-item { white-space: nowrap; flex-shrink: 0; padding: 8px 14px; }
  .builder-account { display: none; }
  .builder-main { padding: 24px 18px 60px; }
  .theme-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  .theme-grid { grid-template-columns: 1fr; }
  /* The button drops under the name instead of squeezing it to a few
     characters on a narrow screen. */
  .theme-toggle { margin-left: 0; width: 100%; }
  .card { padding: 22px 20px; }
  .qr-wrap { gap: 32px; }
}

/* ==========================================================================
   Liquid bottom-nav dock (mobile only)

   Adapted from Meniscus (github.com/hasib41/meniscus-liquid-nav, MIT). The
   bar's whole outline is one SVG path; the notch that seats the bead is
   three tangent arcs solved in JS (assets/js/liquid-dock.js), not a welded
   stack of pseudo-element curves, so it can lean into the direction of
   travel and shallow out with speed.

   The plate's palette (--ld-bar / --ld-bar-hi / --ld-bar-lo / --ld-icon-off /
   --ld-icon-on below) is themed per event and set inline on #liquidDock by
   partials/mobile_dock.php — the values here are just the fallback if that
   inline style is ever missing. The plate itself is intentionally left ~50%
   translucent (see the alpha in those inline values) so the page shows
   through faintly instead of reading as an opaque bar; the bead's own accent
   ramp is unrelated to the plate color and keeps working regardless.
   ========================================================================== */
.liquid-dock-wrap {
  /* Solid, page-colored "shelf" the dock pill sits on — not just transparent
     padding around it. Without this, scrolled content was visible through
     the gap between the floating pill and the wrapper's own edges (and
     through a glow effect that used to bleed upward past the wrapper
     entirely), so the page never really felt like it "ended" above the
     nav. --ld-page-bg is set inline per-theme (see mobile_dock.php) so this
     matches whatever page color sits above it — the cut is meant to be
     invisible, not a visible band of some other color. */
  display: none;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 500;
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
  background: var(--ld-page-bg, transparent);
}
.liquid-dock-wrap .liquid-dock { pointer-events: auto; }

/* Dock replaces the desktop sidebar on phones AND tablets/iPads — up to
   roughly 1024px, not just narrow phones — so the sidebar toggle and the
   dock's own visibility share one breakpoint. */
@media (max-width: 1024px) {
  .liquid-dock-wrap { display: block; }
  /* The pill measures 95% of this wrapper, so the wrapper's own 12px side
     padding was compounding with that and landing the dock at ~85% of the
     screen. Zeroed here so the 95% is 95% of the viewport. The shelf behind
     it is unaffected — it spans left:0/right:0 regardless of padding, and
     the vertical padding that sets its height is untouched. */
  .liquid-dock-wrap { padding-left: 0; padding-right: 0; }
  .builder-main { padding-bottom: calc(60px + 80px + env(safe-area-inset-bottom)); }
  .builder-side { display: none; }

  /* The canvas is narrow here and a table can sit anywhere in it (including
     flush against the right edge, with nowhere for a right-of-table popover
     to go) — "just to the right" reliably ended up back on top of that same
     table's own chairs. Anchoring to the viewport instead of the table
     sidesteps the problem entirely: it never overlaps ANY table regardless
     of where that table happens to be scrolled to. */
  .table-tooltip, .table-edit-panel {
    /* Centred rather than pinned to the bottom edge: a long roster used to run
       off the bottom of the screen, and on a short viewport the panel could sit
       under the mobile dock. Centred with its own scroll, it is always fully
       visible whichever table you tapped and wherever that table is scrolled to. */
    position: fixed; top: 50%; left: 50%; right: auto; bottom: auto;
    transform: translate(-50%, -50%);
    width: calc(100vw - 32px); max-width: 340px;
    max-height: 70vh; overflow-y: auto;
    z-index: 60;
    box-shadow: 0 24px 50px -12px rgba(0,0,0,0.45);
  }
  /* Dims the plan behind the centred panel so it reads as a layer, not as
     something floating loose over the tables. */
  .table-el .table-tooltip:not([hidden])::before {
    content: ""; position: fixed; inset: 0; z-index: -1;
    background: rgba(0, 0, 0, 0.32);
  }
}

@media (max-width: 640px) {
  /* iOS Safari auto-zooms the whole page when a focused field computes under
     16px. A bare `input, select, textarea` selector (0,0,1) loses to every
     class rule that sets a size — `.field input` (0,1,1) at 14.5px,
     `.guest-select` (0,1,0) at 13.5px, and `.editable-text-input`'s
     `font: inherit` (the shorthand sets size too) at 14px — so the fix never
     actually applied and the page still zoomed on focus. The :not() pair
     costs nothing visually but lifts this to (0,2,1) so it outranks them.
     Checkboxes and radios are excluded: font-size doesn't affect their glyph
     and iOS never zooms for them. */
  input:not([type="checkbox"]):not([type="radio"]),
  select,
  textarea,
  .field input, .field select, .field textarea,
  .guest-select,
  .editable-text-input,
  .sidebar-quick-add input,
  .sidebar-guest-search { font-size: 16px; }
  .editable-text-input[type="number"] { width: 72px; }

  /* Stat tiles: min-width:140px meant only two fit across a phone, so the
     third wrapped onto a row of its own at full width — three tiles ate most
     of the first screen before any actual content. A 3-up grid with the
     padding and type scaled down puts them on one line and hands roughly 150px
     of vertical space back to the list below. */
  .stat-row {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px; margin-bottom: 16px;
  }
  .stat-row .stat {
    min-width: 0; padding: 10px 6px; border-radius: 10px; text-align: center;
  }
  .stat b { font-size: 19px; line-height: 1.15; }
  .stat span {
    display: block; font-size: 9.5px; letter-spacing: 0.03em;
    line-height: 1.25; margin-top: 2px;
  }

  /* Guests/Budget/Timeline/Vendors/Registry/Seating tables: below phone
     width, a wide <table> just becomes something to side-scroll one guest
     at a time, which defeats the point of .table-wrap's overflow-x fix.
     Instead every row becomes its own bordered "card", and every <td>
     becomes a labeled block inside it — the label comes from a
     `data-label` attribute set by each template (mirroring that column's
     <th> text) and is drawn with ::before, so no markup duplication and no
     JS. Tablet/desktop keep the original <table> layout untouched; this
     only fires under this same 640px breakpoint the dock uses. */
  .table-wrap table { min-width: 0; }
  .table-wrap thead { display: none; }
  .table-wrap tbody, .table-wrap tr, .table-wrap td { display: block; }
  .table-wrap tr {
    padding: 14px 16px; border-bottom: 1px solid var(--surface-line);
  }
  .table-wrap tr:last-child { border-bottom: none; }
  .table-wrap td {
    /* Overrides any per-column inline width="" set for the desktop table
       layout (e.g. timeline's checkbox column) — in the stacked card every
       cell should just run the full width of the row. */
    width: 100% !important;
    padding: 6px 0; border-bottom: none; vertical-align: top;
  }
  .table-wrap td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 10.5px; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.05em; color: var(--text-soft); margin-bottom: 4px;
  }
  .table-wrap td.row-actions {
    display: flex; gap: 16px; padding-top: 10px;
  }

  /* Guests specifically: name+RSVP badge share a line, party sits on its
     own line below, and meal+edit/remove share a line at the bottom — a
     denser 3-row card instead of 5 separately labeled blocks. A 2-column
     grid on the <tr> with each <td> explicitly placed does this without
     touching the HTML's actual column order (Guest, Party, RSVP, Meal,
     Actions), so the desktop <table> and other tables' generic stacking
     are unaffected. */
  /* Two tracks, and the first is minmax(0, 1fr) rather than max-content. The
     old max-content tracks sized themselves to the longest unbroken line in
     the cell — a name plus "+ Partner (partner), Kid One (5)" — which made
     the grid wider than the phone and forced the whole page to scroll
     sideways to reach the RSVP pill. minmax(0, …) lets the track shrink
     below its content and wrap instead, which is what keeps the card inside
     the viewport at any name length. */
  /* The guest cell's wrappers are flattened with display:contents so the name,
     the "+ partner, kids" line, the relationship pill and the note each become
     grid items in their own right. Otherwise they all sit inside one cell that
     stops where the RSVP pill's column begins, so the composition line wrapped
     early against an invisible wall with dead space beside the pill. As their
     own rows they span to the card's right edge and use the full width.
     Doing it structurally (rather than padding the name by the pill's width)
     keeps it correct in every language — a translated status like Polish
     "Oczekuje na odpowiedź" is far wider than "Awaiting reply", and any fixed
     reservation tuned for English would be wrong for it. */
  .guests-table td:nth-child(1),
  .guests-table .guest-cell,
  .guests-table .guest-cell-body { display: contents; }

  .guests-table tr {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    column-gap: 10px; row-gap: 4px;
    padding: 12px 14px;
    align-items: start;
  }
  .guests-table .drag-grip { grid-area: 1 / 1; align-self: center; }
  .guests-table .guest-name { grid-area: 1 / 2 / auto / span 2; align-self: center; }
  .guests-table .guest-composition { grid-area: 2 / 2 / auto / -1; }
  /* Matches the budget card: a dotted rule under the relationship pill splits
     each guest into "who they are" above and the note plus figures below.
     Drawn from column 2 so it lines up with the text block rather than running
     under the drag grip and action icons in the gutter. */
  .guests-table .guest-meta {
    grid-area: 3 / 2 / auto / -1;
    border-bottom: 1px dotted var(--surface-line);
    padding-bottom: 9px; margin-bottom: 3px;
  }
  .guests-table .guest-note { grid-area: 4 / 2 / auto / -1; }
  /* min-width:0 is the other half of the fix: grid items default to
     min-width:auto, which refuses to shrink past their content and would
     reintroduce the overflow even with a minmax(0,1fr) track. */
  .guests-table td { width: auto !important; min-width: 0; padding: 0; }
  .guests-table .guest-composition { overflow-wrap: anywhere; }
  .guests-table td:nth-child(3) { grid-area: 1 / 4; justify-self: end; align-self: center; }
  .guests-table td:nth-child(2) { grid-area: 5 / 2; align-self: center; }
  /* Meal spans to the card's right edge — the row actions used to sit in that
     last column and squeezed it, so a value like "Vegetarian x2" had barely
     any room. */
  /* Meals used to be a single value, which fitted beside Party on row 5.
     It's now a block of one line per person per course, so it takes its own
     full-width row underneath instead of being squeezed into a third of one
     while the space to its left sits empty. */
  /* Starts in column 2, not column 1: column 1 is the actions gutter, so
     spanning from there put the "MEAL" label a gutter's width left of the
     "PARTY" label directly above it. Same start column means the two labels —
     and their values — line up. */
  .guests-table td:nth-child(4) { grid-area: 6 / 2 / auto / -1; align-self: start; }
  /* Actions stack vertically in the left gutter beneath the drag grip, where
     they cost no horizontal space at all. Stacked rather than side by side so
     the gutter column stays grip-width and never pushes the content right. */
  .guests-table td.row-actions {
    grid-area: 2 / 1 / span 4 / auto;
    flex-direction: column; align-items: center;
    justify-self: center; align-self: start;
    padding-top: 8px; gap: 12px;
  }
  /* .row-actions gives its children margin-right:12px to space them out in the
     horizontal desktop layout. Stacked vertically that margin becomes trailing
     width on the link but not on the delete <form>, so the two icons centred
     on different axes and looked misaligned by 6px. The gap above handles the
     spacing here, so the margin just has to go. */
  .guests-table td.row-actions a,
  .guests-table td.row-actions button { margin-right: 0; }

  /* Party and Meal read as compact "LABEL value" pairs on one line each,
     instead of a stacked label above its value — two lines saved per guest. */
  .guests-table td[data-label]::before {
    display: inline; margin-right: 5px; margin-bottom: 0;
  }

  /* ---- Budget: same compaction as the guest list above ----
     The generic stacking gave every item five separately labelled blocks, one
     per column, so a single budget line ran most of a phone screen. Same
     approach as guests: flatten the item cell so its name and note become
     their own rows, then lay the rest out as a grid with inline labels. */
  .budget-table td:nth-child(1) { display: contents; }
  /* The money columns are content-sized and the trailing column is the flexible
     one. With the flexible track first, Estimated (the widest label + value)
     was the cell that got squeezed, and its text spilled over Actual. */
  .budget-table tr {
    display: grid;
    grid-template-columns: auto auto minmax(0, 1fr);
    column-gap: 10px; row-gap: 4px;
    padding: 12px 14px;
    align-items: start;
  }
  .budget-table td:nth-child(3),
  .budget-table td:nth-child(4) { white-space: nowrap; }
  .budget-table td { width: auto !important; min-width: 0; padding: 0; }
  .budget-table .budget-item-name { grid-area: 1 / 1 / auto / span 2; overflow-wrap: anywhere; }
  .budget-table .budget-note { grid-area: 2 / 1 / auto / -1; overflow-wrap: anywhere; }
  .budget-table td:nth-child(5) { grid-area: 1 / 3; justify-self: end; align-self: start; } /* paid pill */
  .budget-table td:nth-child(2) { grid-area: 3 / 1 / auto / span 2; }                        /* category */
  .budget-table td:nth-child(3) { grid-area: 4 / 1; align-self: center; }                    /* estimated */
  .budget-table td:nth-child(4) { grid-area: 4 / 2; align-self: center; }                    /* actual */

  /* A dotted rule across the full card width separates the note from the
     figures below it, instead of tinting the category itself — the divider
     groups the card into "what it is" above and "what it costs" below without
     adding another block of colour. Drawn on the note because that cell
     already spans every column; the category cell spans only two. */
  .budget-table .budget-note {
    border-bottom: 1px dotted var(--surface-line);
    padding-bottom: 9px; margin-bottom: 3px;
  }
  /* Divider in the gap between the two money figures, so "£350.00 ACTUAL"
     doesn't read as one run of text. */
  .budget-table td:nth-child(4) {
    border-left: 1px solid var(--surface-line);
    padding-left: 14px;
  }
  .budget-table td.row-actions {
    grid-area: 3 / 3 / span 2 / auto;
    flex-direction: column; align-items: center;
    justify-self: center; align-self: center;
    padding-top: 0; gap: 12px;
  }
  .budget-table td.row-actions a,
  .budget-table td.row-actions button { margin-right: 0; }
  /* Sentence case with a colon — "Estimated: £350.00" reads as a sentence at
     this size, where the shouty uppercase tracking of the desktop table header
     just added noise inside a small card. */
  .budget-table td[data-label]::before {
    content: attr(data-label) ":";
    display: inline; margin-right: 5px; margin-bottom: 0;
    text-transform: none; letter-spacing: normal;
    font-size: 12.5px; font-weight: 600;
  }
  /* The item cell is display:contents, but a pseudo-element on it is still
     generated and becomes a grid item in its own right — that left a stray
     "ITEM" label floating at the bottom of every card. The name speaks for
     itself anyway, exactly as on the guest list. */
  .budget-table td:nth-child(1)::before { content: none; }

  /* ---- Timeline: same compaction as the guest and budget lists ----
     Columns are Done | Task(+note) | Due(date+time) | stickers | actions. */
  .timeline-table td:nth-child(2) { display: contents; }
  /* Less padding at the bottom than the top: the Due line is the last thing in
     the card and its text sits ~4px above its own box (descender space), so an
     even 12px both ends read as a gap hanging under it. */
  .timeline-table tbody tr {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    column-gap: 10px; row-gap: 4px;
    padding: 12px 14px 7px;
    align-items: start;
  }
  .timeline-table td { width: auto !important; min-width: 0; padding: 0; }
  .timeline-table td:nth-child(1) { grid-area: 1 / 1; align-self: center; }  /* checkbox */
  .timeline-table .task-title { grid-area: 1 / 2; align-self: center; overflow-wrap: anywhere; }
  /* Stickers ride on the title line, pushed to the right edge — they're a
     decoration on the task, so they belong beside its name rather than
     occupying a row of their own at the bottom of the card. */
  .timeline-table .timeline-sticker-cell {
    grid-area: 1 / 3; width: auto !important;
    justify-self: end; align-self: center;
  }
  /* Dotted rule after the note, matching the guest and budget cards: separates
     what the task is from when it's due.
     Scoped to the task cell: the due cell reuses .timeline-note for its time,
     so an unscoped rule drew a second rule under the time as well. */
  .timeline-table td:nth-child(2) .timeline-note {
    grid-area: 2 / 2 / auto / -1;
    border-bottom: 1px dotted var(--surface-line);
    padding-bottom: 9px; margin-bottom: 3px;
  }
  .timeline-table td:nth-child(3) { grid-area: 3 / 2 / auto / -1; }          /* due */
  /* Actions stack in the gutter under the checkbox, costing no width. */
  .timeline-table td.row-actions {
    grid-area: 2 / 1 / span 2 / auto;
    flex-direction: column; align-items: center;
    justify-self: center; align-self: start;
    padding-top: 6px; gap: 12px;
  }
  .timeline-table td.row-actions a,
  .timeline-table td.row-actions button { margin-right: 0; }
  /* Date and time read as one line rather than stacking into two. */
  .timeline-table td:nth-child(3) > div { display: inline-block; vertical-align: baseline; }
  .timeline-table td:nth-child(3) > div + div { margin-left: 8px; }
  .timeline-table td:nth-child(3) .timeline-note { margin-top: 0; }
  /* Labels: sentence case with a colon, matching the budget card. The task
     title and the checkbox speak for themselves, so they lose theirs. */
  .timeline-table td[data-label]::before {
    content: attr(data-label) ":";
    display: inline; margin-right: 5px; margin-bottom: 0;
    text-transform: none; letter-spacing: normal;
    font-size: 12.5px; font-weight: 600;
  }
  .timeline-table td:nth-child(1)::before,
  .timeline-table td:nth-child(2)::before { content: none; }

  /* ---- Vendors / Accommodation / Transport ----
     Same compaction again. All three are five-column tables whose last column
     is the row actions, so they share the grid shape: content on the left,
     actions stacked in a narrow right-hand gutter. */
  .vendors-table tbody tr:not(.vendor-docs-row),
  .accommodation-table tbody tr,
  .transport-table tbody tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    column-gap: 10px; row-gap: 4px;
    padding: 12px 14px 8px;
    align-items: start;
  }
  .vendors-table td, .accommodation-table td, .transport-table td {
    width: auto !important; min-width: 0; padding: 0;
  }
  .vendors-table td[data-label]::before,
  .accommodation-table td[data-label]::before,
  .transport-table td[data-label]::before {
    content: attr(data-label) ":";
    display: inline; margin-right: 5px; margin-bottom: 0;
    text-transform: none; letter-spacing: normal;
    font-size: 12.5px; font-weight: 600;
  }
  /* The first column is the record's name in each table — it needs no label. */
  .vendors-table td:nth-child(1)::before,
  .accommodation-table td:nth-child(1)::before,
  .transport-table td:nth-child(1)::before { content: none; }
  .vendors-table td:nth-child(1),
  .accommodation-table td:nth-child(1),
  .transport-table td:nth-child(1) {
    font-weight: 600; overflow-wrap: anywhere;
  }
  .vendors-table td.row-actions,
  .accommodation-table td.row-actions,
  .transport-table td.row-actions {
    flex-direction: column; align-items: center;
    justify-self: center; align-self: start;
    padding-top: 2px; gap: 12px;
  }
  .vendors-table td.row-actions a, .vendors-table td.row-actions button,
  .accommodation-table td.row-actions a, .accommodation-table td.row-actions button,
  .transport-table td.row-actions a, .transport-table td.row-actions button { margin-right: 0; }

  /* These cells hold their primary value in a <div> (with an optional second
     <div> beneath for the phone number), and a block element breaks straight
     after the inline label — "Driver name:" then the name on the next line.
     Only the first one goes inline; the phone below keeps its own line. */
  .vendors-table td:nth-child(3) > div:first-of-type,
  .transport-table td:nth-child(4) > div:first-of-type { display: inline; }

  /* Vendors: name + cost share the top line, then category and contact. */
  .vendors-table td:nth-child(1) { grid-area: 1 / 1 / auto / span 2; }
  .vendors-table td:nth-child(4) { grid-area: 1 / 3; justify-self: end; white-space: nowrap; }
  .vendors-table td:nth-child(2) { grid-area: 2 / 1 / auto / span 2; }
  .vendors-table td:nth-child(3) { grid-area: 3 / 1 / auto / span 2; }
  /* justify-self:end rather than the shared centre: this column also holds the
     cost on the row above, so it's wider than the icons and centring left them
     short of the card edge. Lines them up under the cost instead. */
  .vendors-table td.row-actions { grid-area: 2 / 3 / span 2 / auto; justify-self: end; }
  /* Each record is its own bordered box rather than a band inside one shared
     card. With shared dividers, the documents strip hanging under a record sat
     between two rules and read as if it belonged to the record below it. */
  .vendors-table tbody tr:not(.vendor-docs-row),
  .accommodation-table tbody tr:not(.vendor-docs-row),
  .transport-table tbody tr:not(.vendor-docs-row) {
    border: 1px solid var(--surface-line);
    /* Open at the bottom: the documents strip that always follows closes the
       card off (see .doc-strip in the vendors template). */
    border-bottom: 0;
    border-radius: 14px 14px 0 0;
    background: var(--surface-raised);
  }
  /* ...which means the card wrapping the list would be a box drawn around a
     column of boxes, so it's opted out of the card chrome here. Phone only:
     on desktop the list keeps the framed-table-inside-a-card look shared with
     /budget. The card's drop shadow and its inner .table-wrap each redraw that
     frame one level down, so all three have to go. overflow stays visible so
     a record's rounded corners aren't clipped. */
  .card.card-list-flush {
    background: transparent; border: 0; padding: 0; box-shadow: none;
  }
  .card-list-flush .table-wrap {
    background: none; border: 0; border-radius: 0; overflow: visible;
  }

  /* The documents strip is a second <tr> per record spanning every column. It
     sits outside that record's box, so the attach control reads as an action
     on the box above rather than another field inside it. */
  .vendors-table tr.vendor-docs-row,
  .accommodation-table tr.vendor-docs-row,
  .transport-table tr.vendor-docs-row {
      /* No padding at all: the strip is the bottom of the record's card, so any
       inset would break the join or narrow it against the box above. */
    display: block; border: 0; padding: 0; margin-bottom: 16px;
  }
  .vendors-table tr.vendor-docs-row:last-child,
  .accommodation-table tr.vendor-docs-row:last-child,
  .transport-table tr.vendor-docs-row:last-child { margin-bottom: 0; }


  .vendors-table tr.vendor-docs-row td,
  .accommodation-table tr.vendor-docs-row td,
  .transport-table tr.vendor-docs-row td { display: block; width: 100% !important; }

  /* Accommodation: hotel, room, then check-in and check-out each on their own
     line — they now carry a time as well as a date, so pairing them side by
     side left neither with room. */
  .accommodation-table td:nth-child(1) { grid-area: 1 / 1 / auto / span 2; }
  .accommodation-table td:nth-child(2) { grid-area: 2 / 1 / auto / span 2; }
  .accommodation-table td:nth-child(3) { grid-area: 3 / 1 / auto / span 2; }
  .accommodation-table td:nth-child(4) { grid-area: 4 / 1 / auto / span 2; }
  .accommodation-table td.row-actions { grid-area: 2 / 3 / span 3 / auto; }

  /* Transport: type, then departure + guest count paired, then the driver. */
  .transport-table td:nth-child(1) { grid-area: 1 / 1 / auto / span 2; }
  .transport-table td:nth-child(2) { grid-area: 2 / 1; white-space: nowrap; }
  /* The departure time drops to its own line beneath the date. Alongside it the
     cell is wider than its column, and with nowrap it can't wrap — so it ran
     straight over the guest count in the next column. */
  .transport-table td:nth-child(2) .stay-time {
    display: flex; margin-left: 0; margin-top: 2px;
  }
  .transport-table td:nth-child(3) { grid-area: 2 / 2; white-space: nowrap; }
  .transport-table td:nth-child(4) { grid-area: 3 / 1 / auto / span 2; }
  .transport-table td.row-actions { grid-area: 2 / 3 / span 2 / auto; }
  /* Same reasoning as the guest list's RSVP pill: a coloured Paid/Unpaid pill
     states what it is, so the label above it was only wrapping to a second
     line to repeat the pill's own word. */
  .budget-table td:nth-child(5)::before { content: none; }

  /* Guest name and the RSVP badge don't need their own uppercase label
     (a name is obviously a name, a colored pill is obviously a status) —
     only Party and Meal keep theirs, since plain numbers/text alone would
     be ambiguous out of context. */
  .guests-table td:nth-child(1)::before, .guests-table td:nth-child(3)::before { content: none; }

}

.liquid-dock {
  --ld-page-hi: #2b2138;
  --ld-page-lo: #110c18;
  --ld-bar: rgba(19, 15, 30, 0.5);
  --ld-bar-hi: rgba(27, 21, 41, 0.55);
  --ld-bar-lo: rgba(12, 8, 20, 0.45);
  --ld-icon-off: #a79eb8;
  --ld-icon-on: #1a1420;
  --ld-glow-rgb: 201 242 74;
  --ld-glow: rgb(var(--ld-glow-rgb));
  --ld-dock-r: 18px;
  --ld-bead-d: 56px;
  --ld-bead-cy: 0px;
  --ld-rise: 42px;

  position: relative; margin: 0 auto; width: 95%;
  height: clamp(64px, 10vw, 76px);
  touch-action: pan-y;
  -webkit-tap-highlight-color: transparent;
  background: var(--ld-bar);
  border-radius: var(--ld-dock-r);
  color-scheme: dark;
}
html:not(.ld-js) .liquid-dock { background: var(--ld-bar); }
.ld-js .liquid-dock { background: none; }

.liquid-dock__cast {
  /* Was a heavy near-black shadow — tuned for the original dark-room design,
     where a dark plate needed a darker shadow to read as "resting on"
     something even darker. Now that the plate is the page's own (light-ish)
     background color, that same shadow just showed through as an unwanted
     dark smudge/line behind the bar. A soft, low-opacity shadow still gives
     it a little lift off the page without the harsh dark line. */
  position: absolute; inset: 46% 7% 6% 7%; border-radius: 999px;
  background: var(--ld-bar);
  box-shadow: 0 8px 16px -8px rgba(0,0,0,.16), 0 20px 34px -20px rgba(0,0,0,.14);
}

.liquid-dock__skin { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; filter: drop-shadow(0 4px 10px rgba(0,0,0,.14)); }
.liquid-dock__fill { fill: url(#ldPlate); stroke: url(#ldRim); stroke-width: 1; }
.liquid-dock__plate-hi { stop-color: var(--ld-bar-hi); }
.liquid-dock__plate-lo { stop-color: var(--ld-bar-lo); }
.liquid-dock__rim-hi { stop-color: #fff; stop-opacity: .60; }
.liquid-dock__rim-lo { stop-color: #fff; stop-opacity: .06; }

.liquid-dock__bead {
  position: absolute; top: var(--ld-bead-cy); left: 0;
  width: var(--ld-bead-d); height: var(--ld-bead-d);
  margin: calc(var(--ld-bead-d) / -2) 0 0 calc(var(--ld-bead-d) / -2);
  border-radius: 50%;
  background: linear-gradient(168deg, color-mix(in srgb, var(--ld-glow) 76%, #fff), var(--ld-glow) 62%);
  box-shadow:
    0 0 0 1px rgb(var(--ld-glow-rgb) / .5),
    0 0 26px 2px rgb(var(--ld-glow-rgb) / .32),
    0 10px 18px -7px rgba(0,0,0,.68),
    inset 0 0 0 1px rgba(255,255,255,.42),
    inset 0 -3px 6px -3px rgba(26,16,34,.18);
  will-change: transform; pointer-events: none;
}
.liquid-dock:not(.is-ready) .liquid-dock__bead { opacity: 0; }

.liquid-dock__tabs { position: absolute; inset: 0; display: flex; padding-inline: clamp(14px, 6%, 30px); }

.liquid-dock__tab {
  position: relative; flex: 1; display: block; height: 100%;
  padding: 0; border: 0; background: none; font: inherit; color: inherit;
  cursor: pointer; border-radius: 14px; text-decoration: none; --t: 0;
}
/* A stroked outline read as a stray black box sitting on the plate. A soft
   fill in the tab's own accent keeps keyboard focus visible without that. */
.liquid-dock__tab:focus-visible { outline: none; background: color-mix(in srgb, var(--acc) 22%, transparent); }

.liquid-dock__tab-icon {
  position: absolute; left: 50%; top: 50%; width: 22px; height: 22px;
  color: color-mix(in oklab, var(--ld-icon-off) calc((1 - var(--t)) * 100%), var(--ld-icon-on));
  transform: translate(-50%, -50%) translateY(calc(var(--t) * var(--ld-rise) * -1));
  will-change: transform;
}
.liquid-dock__tab-label {
  /* Centered on the tab but sized to its own content, not clamped to the
     tab's narrow flex slot — a long label (e.g. "Seating") floats above
     its neighbors rather than getting clipped mid-word. Only one label is
     ever visible at a time (opacity follows --t), so the overlap is moot. */
  position: absolute; left: 50%; bottom: 12%; width: max-content; max-width: 120px;
  font-size: 10.5px; font-weight: 600; letter-spacing: .03em; text-align: center;
  /* Not --acc: the per-tab ramp accent is tuned to read against the BEAD's
     own opaque fill, not against the translucent plate the label sits on —
     measured across every theme, several ramp stops landed under 1.2:1
     there (functionally invisible). --ld-icon-off is the same
     plate-luminance-aware black/white choice already proven to contrast
     the inactive icons, reused here for the same reason. */
  color: var(--ld-icon-off); opacity: 0; z-index: 2;
  transform: translateX(-50%) translateY(calc((1 - var(--t)) * 7px));
  pointer-events: none; white-space: nowrap;
}
.liquid-dock__tab[aria-selected="true"] .liquid-dock__tab-label,
.liquid-dock.is-dragging .liquid-dock__tab-label { opacity: var(--t); }

@media (prefers-reduced-motion: reduce) {
  .liquid-dock * { transition: none !important; }
}

/* ---- Stickers -------------------------------------------------------------
   Shared by the dashboard's schedule widget and the timeline page. */

/* Theme slots the artwork is drawn against (see config/stickers.php).
   Declared at the root, not on the tray: stickers also render on schedule and
   timeline rows, which are outside it, and scoping these to the tray left
   those falling back to black. */
:root {
  --st-1: var(--accent);
  --st-2: var(--theme-accent2);
  --st-3: var(--theme-secondary);
}
/* ---- Sticker tray ------------------------------------------------------
   Drawn from three theme slots so one set of artwork recolours itself
   across all 20 themes; see config/stickers.php. */
.sticker-tray {
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--surface-line);
}
.sticker-tray-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.sticker-tray-title { margin: 0; font-size: 13.5px; color: var(--text-soft); }
.sticker-tray-body { margin-top: 12px; }
.sticker-tray.is-collapsed .sticker-tray-body { display: none; }

.sticker-toggle {
  display: inline-flex; align-items: center; gap: 8px; padding: 0;
  border: none; background: none; font-family: inherit; cursor: pointer; color: var(--text-soft);
}
.sticker-toggle:hover { color: var(--text); }
.sticker-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }
.sticker-toggle-icon { width: 16px; height: 16px; flex-shrink: 0; transition: transform 0.2s ease; }
.sticker-tray.is-collapsed .sticker-toggle-icon { transform: rotate(-90deg); }
.sticker-toggle-label {
  font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--accent); padding: 3px 10px; border-radius: 999px; background: var(--accent-soft);
}
.sticker-cats { display: flex; flex-wrap: wrap; gap: 6px; }
.sticker-cat {
  border: 1px solid var(--surface-line); background: var(--surface-dim); color: var(--text-soft);
  font-family: inherit; font-size: 11.5px; padding: 4px 11px; border-radius: 999px; cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.sticker-cat:hover { border-color: var(--accent); color: var(--text); }
.sticker-cat.is-active { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
.sticker-cat:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.sticker-grid { display: flex; flex-wrap: wrap; gap: 6px; }
.sticker-chip {
  width: 42px; height: 42px; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--surface-line); border-radius: 10px; background: var(--surface-dim);
  cursor: grab; padding: 0; transition: transform 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.sticker-chip:hover { border-color: var(--accent); background: var(--surface-raised); transform: translateY(-2px); }
.sticker-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sticker-chip[disabled] { cursor: default; }
.sticker-chip[disabled]:hover { transform: none; border-color: var(--surface-line); background: var(--surface-dim); }
.sticker-chip[hidden] { display: none; }
.sticker-chip.is-armed { border-color: var(--accent); background: var(--accent-soft); transform: translateY(-2px); }
.sticker-svg { display: block; }
/* Sits between the category pills and the sticker grid with no top margin at
   all, which left it crowded up against the filter row. Given its own space so
   it reads as an instruction for the grid below rather than a caption hanging
   off the pills above. */
.sticker-hint { margin: 12px 0 10px; font-size: 11.5px; color: var(--text-soft); }

.sticker-ghost {
  position: fixed; z-index: 400; pointer-events: none; opacity: 0.9;
  transform: translate(-50%, -50%) scale(1.15); filter: drop-shadow(0 6px 10px rgba(0,0,0,0.25));
}
body.is-sticker-dragging { user-select: none; -webkit-user-select: none; }
.is-drop-target { border-color: var(--accent) !important; background: var(--accent-soft) !important; }
.sticker-shelf { display: flex; align-items: center; gap: 3px; flex-wrap: wrap; }
.entry-sticker {
  border: none; background: none; padding: 2px; line-height: 0; cursor: pointer; border-radius: 6px;
  transition: background 0.12s ease, transform 0.12s ease;
}
.entry-sticker:hover { background: var(--surface-raised); transform: scale(1.12); }
.entry-sticker:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- Info tip -------------------------------------------------------------
   A small (i) beside a heading that explains what a panel is for. Opens on
   click rather than hover: a hover-only tooltip is unreachable on a phone,
   which is where most of this app is used. */
.card-heading { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }

.info-tip { position: relative; display: inline-flex; }
.info-tip-btn {
  width: 17px; height: 17px; padding: 0; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--surface-line); background: var(--surface-dim); color: var(--text-soft);
  font-family: var(--body); font-size: 11px; font-weight: 700; font-style: italic; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.info-tip-btn:hover { border-color: var(--accent); color: var(--accent); }
.info-tip-btn[aria-expanded="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.info-tip-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.info-tip-panel {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 200; width: 280px; max-width: 74vw;
  padding: 11px 13px; border-radius: 10px;
  background: var(--surface-raised); border: 1px solid var(--surface-line);
  box-shadow: 0 12px 28px -14px rgba(0,0,0,0.35);
  font-size: 12.5px; line-height: 1.55; color: var(--text-soft); font-weight: 400;
}
.info-tip-panel[hidden] { display: none; }
/* Anchoring to the button runs the panel off-screen on a phone whichever edge
   it hangs from: the button sits mid-heading, so right:0 pushed most of a
   280px panel off the left of the page. Pinned to the centre of the viewport
   instead, so it's fully readable wherever its button happens to be. The
   existing outside-tap and Escape handlers still close it, and the z-index
   clears the fixed dock (500). */
@media (max-width: 640px) {
  .info-tip-panel {
    position: fixed;
    top: 50%; left: 50%; right: auto;
    transform: translate(-50%, -50%);
    width: min(340px, calc(100vw - 32px));
    max-width: none;
    z-index: 600;
  }
}

/* ---- Modal ----------------------------------------------------------------
   Small centred dialog used by the seating sidebar's add-guest flow. */
.ep-modal { position: fixed; inset: 0; z-index: 500; display: flex; align-items: center; justify-content: center; padding: 20px; }
.ep-modal[hidden] { display: none; }
.ep-modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.45); backdrop-filter: blur(2px); }
.ep-modal-panel {
  position: relative; width: 420px; max-width: 100%; max-height: 88vh; overflow-y: auto;
  background: var(--surface-raised); border: 1px solid var(--surface-line);
  border-radius: 14px; padding: 22px 24px;
  box-shadow: 0 24px 60px -24px rgba(0,0,0,0.5);
}
.ep-modal-title { margin: 0 0 6px; font-size: 19px; }
.ep-modal-intro { margin: 0 0 18px; font-size: 12.5px; color: var(--text-soft); line-height: 1.5; }
.ep-modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
.ep-modal-actions .btn-primary, .ep-modal-actions .btn-secondary { width: auto; padding: 10px 18px; }

.qa-check { display: flex !important; align-items: center; gap: 8px; margin-bottom: 8px; cursor: pointer; }
.qa-check input { width: auto !important; }
.qa-stepper { display: inline-flex; }
.qa-child-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
.qa-child-row .qa-child-label { font-size: 12px; color: var(--text-soft); min-width: 58px; }

/* Sidebar group filter, above the seating guest search. */
.sidebar-guest-filter {
  width: 100%; margin-bottom: 8px; padding: 7px 10px; border-radius: 8px; font-size: 12.5px;
  border: 1px solid var(--surface-line); background: var(--surface-dim); color: var(--text);
  font-family: inherit; cursor: pointer;
}
.sidebar-guest-filter:focus { outline: none; border-color: var(--accent); }

/* ---- Combobox -------------------------------------------------------------
   Text input with a suggestion list under it. Free text is always allowed —
   the list only saves typing (and stops "Food" / "food" drifting apart). */
.combo { position: relative; }
.combo-panel {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 220;
  max-height: 208px; overflow-y: auto; padding: 4px;
  background: var(--surface-raised); border: 1px solid var(--surface-line);
  border-radius: 10px; box-shadow: 0 12px 28px -14px rgba(0,0,0,0.35);
}
.combo-panel[hidden] { display: none; }
.combo-option {
  display: block; width: 100%; text-align: left; padding: 7px 10px; border: none;
  background: none; border-radius: 7px; cursor: pointer;
  font-family: inherit; font-size: 13.5px; color: var(--text);
}
.combo-option:hover, .combo-option.is-active { background: var(--surface-dim); }
.combo-option mark { background: none; color: var(--accent); font-weight: 700; }

/* ---- Collapsible sidebar --------------------------------------------------
   Frees the full window width for the seating canvas. Desktop only: below
   1080px the sidebar is already hidden in favour of the mobile dock, so the
   toggle would control something that isn't there. */
.side-toggle {
  position: fixed; top: 14px; left: 14px; z-index: 320;
  width: 34px; height: 34px; padding: 0; border-radius: 9px; cursor: pointer;
  display: none; align-items: center; justify-content: center;
  border: 1px solid var(--surface-line); background: var(--surface-raised); color: var(--text-soft);
  box-shadow: 0 6px 16px -10px rgba(0,0,0,0.4);
  transition: border-color 0.15s ease, color 0.15s ease, opacity 0.2s ease;
}
.side-toggle svg { width: 18px; height: 18px; }
.side-toggle:hover { border-color: var(--accent); color: var(--text); }
.side-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Above the 900px block below, where the sidebar stops being a top bar: it
   used to scroll away with the page, which left the fixed hide/show toggle
   parked at the top of the viewport over whatever had scrolled under it. The
   sidebar now stays put, so the toggle keeps sitting on its top row.
   align-self is required — .builder-shell is a flex row, and the default
   stretch makes the sidebar full-height, which sticky has nothing to do. */
@media (min-width: 901px) {
  .builder-side {
    position: sticky; top: 0; align-self: flex-start;
    /* A fixed viewport height, not max-height: the dark plate is painted by
       this element, and `align-self: flex-start` (which sticky needs) stops it
       stretching to the shell's height the way it used to — so anything short
       of 100vh leaves the page background showing under the sidebar. Content
       taller than the viewport scrolls inside it. */
    height: 100vh; height: 100dvh; overflow-y: auto;
    /* Scrolls, but without a visible bar — the sidebar is a dark plate and a
       track down its edge cut into that. Scrolling itself is untouched
       (wheel, trackpad, keyboard and drag all still work); only the chrome is
       hidden, the same way .panel-tabs and .module-list already do it. */
    scrollbar-width: none; -ms-overflow-style: none;
  }
  .builder-side::-webkit-scrollbar { width: 0; height: 0; display: none; }

  /* Same treatment for the seating canvas: with a floor plan wider than the
     card, the bars sat across the plan itself. */
  #canvas {
    scrollbar-width: none; -ms-overflow-style: none;
  }
  #canvas::-webkit-scrollbar { width: 0; height: 0; display: none; }
}

@media (min-width: 1081px) {
  .side-toggle { display: inline-flex; }
  /* Parked over the sidebar's own top row while expanded, so it reads as part
     of the panel rather than floating over the page content.

     The button is a sibling of .builder-side, not a child, so it does NOT
     inherit the sidebar's light-on-dark palette — it was picking up the
     page's dark --text-soft and nearly vanishing against the dark panel.
     White is hard-coded here because the sidebar renders dark on every theme
     (see $sidebarBgHex in app_header.php). Collapsed, the button sits on the
     light page instead and goes back to theme colours. */
  .builder-shell:not(.side-hidden) .side-toggle {
    left: auto; margin-left: 236px; background: none; border-color: transparent;
    box-shadow: none; color: #fff;
  }
  .builder-shell:not(.side-hidden) .side-toggle:hover {
    color: #fff; background: rgba(255,255,255,0.14); border-color: transparent;
  }
  .builder-shell.side-hidden .side-toggle { color: var(--text); }

  .builder-shell.side-hidden .builder-side { display: none; }
  /* The page keeps its own max-width; only the reclaimed gutter changes, and
     the left padding clears the now-floating toggle. */
  /* #builderMain, not just .builder-main: pages may pin their own max-width
     by id (the seating chart uses 80vw), and an ID beats a class no matter
     which stylesheet comes last. */
  .builder-shell.side-hidden #builderMain { padding-left: 64px; max-width: none; }
}

/* Set on <body> when a page opts out of the navigation shell ($hideNav in
   app_header.php). The shell's own rules reserve room for a sidebar and a
   mobile dock that aren't rendered, so the reserved space is given back —
   otherwise the page sits in a 64px-indented column under an empty gap. */
body.nav-hidden .builder-shell.side-hidden #builderMain {
  /* The shell's collapsed rule sets max-width:none so a canvas page can use
     the reclaimed gutter. With no nav at all there is nothing to reclaim it
     for, so the column is capped and centred instead of hugging the left edge
     of a wide screen. */
  max-width: 780px;
  margin: 0 auto;
  padding-left: 40px;
  padding-right: 40px;
}
@media (max-width: 1024px) {
  body.nav-hidden .builder-main { padding-bottom: 40px; }
}

/* ---------------------------------------------------------------- Quick access

   The sidebar block between the planner switcher and Log out, plus the two
   pickers it opens. Both dialogs share one set of rules — they differ only in
   what fills their body. */
.quick-access {
  margin: 14px 0 4px;
  padding-top: 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.10);
}
.quick-access-title {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.07em; opacity: 0.55; margin: 0 0 6px 2px;
}
.quick-access-item {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 7px 8px; border: 0; border-radius: 8px;
  background: none; color: inherit; font: inherit; font-size: 13px;
  text-align: left; cursor: pointer;
  transition: background-color 0.15s ease;
}
.quick-access-item:hover { background: rgba(255, 255, 255, 0.07); }
.quick-access-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.quick-access-item svg { width: 16px; height: 16px; flex-shrink: 0; opacity: 0.75; }

/* Above the mobile dock (.liquid-dock-wrap is z-index 500): at 300 the dock
   painted over the bottom of the dialog, hiding the last rows and the action
   button with no way to scroll them clear. */
.quick-modal { position: fixed; inset: 0; z-index: 600; display: flex; align-items: center; justify-content: center; padding: 20px; }
.quick-modal[hidden] { display: none; }
.quick-modal-backdrop { position: absolute; inset: 0; background: rgba(20, 14, 16, 0.55); }
/* Locks the page behind the dialog so a scroll gesture over the backdrop
   doesn't run the page underneath away from the reader. */
body.quick-modal-open { overflow: hidden; }

.quick-modal-panel {
  position: relative; width: min(520px, 100%);
  /* The list is the only part that scrolls, so the heading and the Download
     button stay put however many files a planner has. */
  max-height: min(80vh, 640px); display: flex; flex-direction: column;
  background: var(--surface-raised); color: var(--text);
  border: 1px solid var(--surface-line); border-radius: 16px;
  box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.45);
  padding: 20px 22px 16px;
}
.quick-modal-head { display: flex; align-items: flex-start; gap: 12px; }
.quick-modal-head h2 { margin: 0; font-size: 17px; }
.quick-modal-x {
  margin-left: auto; background: none; border: 0; cursor: pointer;
  font-size: 22px; line-height: 1; color: var(--text-soft); padding: 0 2px;
}
.quick-modal-x:hover { color: var(--text); }
.quick-modal-sub { margin: 6px 0 12px; font-size: 12.5px; color: var(--text-soft); }
.quick-modal-form { display: flex; flex-direction: column; min-height: 0; }
.quick-modal-body { overflow-y: auto; margin: 0 -6px; padding: 0 6px; }
.quick-modal-state { font-size: 13px; color: var(--text-soft); padding: 12px 2px; margin: 0; }

.quick-group + .quick-group { margin-top: 12px; }
.quick-group-name {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--text-soft);
  padding: 6px 10px; background: var(--surface-dim); border-radius: 8px;
}
.quick-row {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 9px 10px; border-radius: 9px; cursor: pointer;
  transition: background-color 0.15s ease;
}
.quick-row:hover { background: var(--surface-dim); }
.quick-row input { margin-top: 2px; accent-color: var(--accent); flex-shrink: 0; }
.quick-row-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.quick-row-name { font-size: 13.5px; word-break: break-word; }
.quick-row-meta { font-size: 11.5px; color: var(--text-soft); }

.quick-modal-actions {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--surface-line);
}
.quick-modal-spacer { flex: 1 1 auto; }
.quick-all { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--text-soft); cursor: pointer; }
.quick-all input { accent-color: var(--accent); }

@media (max-width: 640px) {
  /* Full-bleed sheet rather than a floating card: at phone width a centred
     dialog with margins wastes the room the file list needs. */
  .quick-modal { padding: 0; align-items: flex-end; }
  .quick-modal-panel {
    width: 100%; max-height: 88vh;
    border-radius: 16px 16px 0 0; border-bottom: 0;
    /* Clears the home indicator on a phone with no hardware button. */
    padding-bottom: calc(16px + env(safe-area-inset-bottom));
  }
  .quick-modal-actions button { flex: 1 1 auto; }
  .quick-all { flex-basis: 100%; }
}

/* ------------------------------------------------- Menu & per-person meals

   Meals are recorded per person, per course (see Support/Menu.php), so the
   Meal column is a small stack rather than a single value. */
.menu-panel {
  background: var(--surface-raised); border: 1px solid var(--surface-line);
  border-radius: 14px; margin-bottom: 18px; overflow: hidden;
}
.menu-panel > summary {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 13px 16px; cursor: pointer; list-style: none;
}
.menu-panel > summary::-webkit-details-marker { display: none; }
.menu-panel > summary::before {
  content: '▸'; font-size: 10px; color: var(--text-soft); flex-shrink: 0;
}
.menu-panel[open] > summary::before { content: '▾'; }
.menu-panel-title { font-size: 13.5px; font-weight: 600; }
.menu-panel-sub { font-size: 12.5px; color: var(--text-soft); }
.menu-panel-body { padding: 0 16px 16px; }

.meal-totals { display: flex; flex-direction: column; gap: 10px; }
.meal-total-course {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 9px 12px; background: var(--surface-dim); border-radius: 10px;
}
.meal-total-name {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--text-soft); min-width: 76px;
}
.meal-total-counts { display: flex; flex-wrap: wrap; gap: 6px; }
.meal-total-pill {
  display: inline-flex; align-items: baseline; gap: 5px;
  font-size: 12.5px; padding: 3px 10px; border-radius: 999px;
  background: var(--surface-raised); border: 1px solid var(--surface-line);
}
.meal-total-pill b { font-family: var(--mono); font-weight: 600; }
/* The number worth chasing before the caterer's deadline, so it's marked
   rather than left to blend in with the choices that are settled. */
.meal-total-pill.is-missing { border-style: dashed; color: var(--text-soft); }
.meal-total-none { font-size: 12.5px; color: var(--text-soft); }

.menu-form { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--surface-line); }
.menu-form-hint { margin: 0 0 12px; font-size: 12.5px; color: var(--text-soft); }
.menu-courses { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
.menu-course { display: flex; flex-direction: column; gap: 6px; }
.menu-course-name, .menu-course-options {
  width: 100%; font-family: inherit; font-size: 13px; color: var(--text);
  background: var(--surface-dim); border: 1px solid var(--surface-line);
  border-radius: 9px; padding: 8px 10px;
}
.menu-course-name { font-weight: 600; }
.menu-course-options { resize: vertical; min-height: 70px; line-height: 1.5; }
.menu-course-name:focus, .menu-course-options:focus {
  outline: none; border-color: var(--accent); background: var(--surface-raised);
}
.menu-form-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }

/* The Meal column is a summary button plus a peek toggle. A party of four
   across three courses is twelve lines, which buried every other column when
   they were all shown inline — the detail moved into the dialog, and the eye
   brings it back in place when it's wanted. */
.meal-cell { line-height: 1.35; }
.meal-summary { display: inline-flex; align-items: center; gap: 4px; }
.meal-open {
  display: inline-flex; align-items: baseline; gap: 6px;
  padding: 4px 9px; border-radius: 999px; cursor: pointer;
  background: var(--surface-dim); border: 1px solid var(--surface-line);
  font-family: inherit; font-size: 12.5px; color: var(--text);
  transition: border-color 0.15s ease, color 0.15s ease;
}
.meal-open:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.meal-open:disabled { cursor: default; opacity: 0.7; }
.meal-open-count { font-family: var(--mono); font-weight: 600; }
.meal-open-label { color: var(--text-soft); }
/* Nobody has chosen anything yet — the count is 0/n, so the pill is muted
   rather than reading as a filled-in value. */
.meal-summary.is-empty .meal-open { border-style: dashed; }
.meal-summary.is-empty .meal-open-count { color: var(--text-soft); }

.meal-peek {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0; border: 0; border-radius: 50%;
  background: none; color: var(--text-soft); cursor: pointer;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.meal-peek svg { width: 15px; height: 15px; }
.meal-peek:hover { color: var(--accent); background: var(--surface-dim); }
.meal-peek.is-open { color: var(--accent); background: var(--surface-dim); }

.meal-peek-list { margin-top: 8px; font-size: 12.5px; }
.meal-peek-person + .meal-peek-person {
  margin-top: 6px; padding-top: 6px; border-top: 1px dotted var(--surface-line);
}
.meal-peek-name { font-size: 11px; color: var(--text-soft); }
.meal-peek-line { display: flex; align-items: baseline; gap: 6px; }
.meal-peek-course {
  font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-soft); min-width: 46px; flex-shrink: 0;
}
.meal-peek-line.is-empty { color: var(--text-soft); font-style: italic; opacity: 0.7; }

.meal-modal { position: fixed; inset: 0; z-index: 600; display: flex; align-items: center; justify-content: center; padding: 20px; }
.meal-modal[hidden] { display: none; }
.meal-modal-backdrop { position: absolute; inset: 0; background: rgba(20, 14, 16, 0.55); }
body.meal-modal-open { overflow: hidden; }
.meal-modal-panel {
  position: relative; width: min(560px, 100%);
  max-height: min(82vh, 680px); display: flex; flex-direction: column;
  background: var(--surface-raised); color: var(--text);
  border: 1px solid var(--surface-line); border-radius: 16px;
  box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.45);
  padding: 20px 22px 16px;
}
.meal-modal-head { display: flex; align-items: flex-start; gap: 12px; }
.meal-modal-head h2 { margin: 0; font-size: 17px; }
.meal-modal-x {
  margin-left: auto; background: none; border: 0; cursor: pointer;
  font-size: 22px; line-height: 1; color: var(--text-soft); padding: 0 2px;
}
.meal-modal-x:hover { color: var(--text); }
.meal-modal-sub { margin: 6px 0 12px; font-size: 12.5px; color: var(--text-soft); }
.meal-modal-body { overflow-y: auto; margin: 0 -6px; padding: 0 6px; }

.meal-party-person + .meal-party-person {
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--surface-line);
}
.meal-party-name { font-size: 13.5px; font-weight: 600; margin-bottom: 7px; }
.meal-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; }
.meal-field { display: flex; flex-direction: column; gap: 3px; }
.meal-field-label {
  font-size: 9.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--text-soft);
}
.meal-input {
  width: 100%; font-family: inherit; font-size: 13px; color: var(--text);
  background: var(--surface-dim); border: 1px solid var(--surface-line);
  border-radius: 8px; padding: 7px 9px;
}
.meal-input:focus { outline: none; border-color: var(--accent); background: var(--surface-raised); }

.meal-modal-actions {
  display: flex; align-items: center; gap: 10px;
  margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--surface-line);
}
.meal-modal-status { flex: 1 1 auto; font-size: 12.5px; color: var(--text-soft); }
.meal-modal-status.is-error { color: var(--clay); }

@media (max-width: 640px) {
  .menu-courses { grid-template-columns: 1fr; }
  .menu-form-actions button { flex: 1 1 auto; }
  .meal-total-course { flex-direction: column; gap: 6px; }
  /* Full width in the stacked card (see the grid-area above). */
  .meal-cell { width: 100% !important; margin-top: 4px; }
  .meal-modal { padding: 0; align-items: flex-end; }
  .meal-modal-panel {
    width: 100%; max-height: 88vh;
    border-radius: 16px 16px 0 0; border-bottom: 0;
    /* Clears the home indicator on a phone with no hardware button. */
    padding-bottom: calc(16px + env(safe-area-inset-bottom));
  }
  /* One field per line on a phone: two 150px columns side by side left no room
     for the value once the course label was above it. */
  .meal-fields { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------- Cookie banner

   Fixed to the bottom on every public page until the visitor answers. Above
   the mobile dock (z-index 500) and the dialogs (600) — it has to be
   dismissable before anything else is usable, and it is the one thing on the
   page that must never be covered. */
.cookie-banner {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 900;
  background: var(--ink, #16241c); color: var(--on-dark, #ece6d6);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: 0 -14px 40px -20px rgba(0, 0, 0, 0.5);
  padding: 16px 20px calc(16px + env(safe-area-inset-bottom));
  animation: cookie-rise 0.28s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes cookie-rise { from { transform: translateY(100%); } to { transform: translateY(0); } }
.cookie-banner.is-going { transform: translateY(100%); transition: transform 0.2s ease; animation: none; }
.cookie-banner-inner {
  max-width: 1080px; margin: 0 auto;
  display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
}
.cookie-banner-text { flex: 1 1 340px; min-width: 0; }
.cookie-banner-text strong { display: block; font-size: 14px; margin-bottom: 3px; }
.cookie-banner-text p { margin: 0; font-size: 13px; line-height: 1.55; opacity: 0.85; }
.cookie-banner-text a { color: inherit; text-underline-offset: 3px; }
.cookie-banner-actions { display: flex; gap: 10px; flex-shrink: 0; }
/* Both buttons are the same size and weight. A reject that is quieter than
   the accept is the exact pattern regulators object to, and with this little
   to consent to there would be nothing to gain by nudging. */
.cookie-btn {
  font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  padding: 10px 20px; border-radius: 999px; white-space: nowrap;
  background: none; color: inherit; border: 1px solid rgba(255, 255, 255, 0.35);
  transition: background-color 0.15s ease, border-color 0.15s ease;
}
.cookie-btn:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.6); }
.cookie-btn-primary {
  background: var(--brass-soft, #d8b26a); color: #1d1508; border-color: transparent;
}
.cookie-btn-primary:hover { filter: brightness(1.06); background: var(--brass-soft, #d8b26a); }
.cookie-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* Inside the app, the banner wears the planner's own theme instead of the
   marketing site's dark green and brass. Every token below is one the shell
   already sets per theme (see app_header.php), so a Blush Rose planner gets a
   rose banner and a Charcoal one gets charcoal — without this it was the only
   thing on the page still coloured like the homepage. */
.cookie-banner.is-app {
  background: var(--surface-raised);
  color: var(--text);
  border-top: 1px solid var(--surface-line);
  box-shadow: 0 -14px 40px -22px rgba(0, 0, 0, 0.28);
}
.cookie-banner.is-app .cookie-btn {
  color: var(--text);
  border-color: var(--surface-line);
}
.cookie-banner.is-app .cookie-btn:hover {
  background: var(--surface-dim);
  border-color: var(--accent);
}
.cookie-banner.is-app .cookie-btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
  border-color: transparent;
}
.cookie-banner.is-app .cookie-btn-primary:hover {
  background: var(--accent);
  filter: brightness(1.06);
}
.cookie-banner.is-app .cookie-btn:focus-visible { outline-color: var(--accent); }

@media (max-width: 640px) {
  .cookie-banner-inner { gap: 14px; }
  .cookie-banner-actions { width: 100%; }
  .cookie-btn { flex: 1 1 auto; }
}
@media (prefers-reduced-motion: reduce) {
  .cookie-banner { animation: none; }
  .cookie-banner.is-going { transition: none; }
}


/* ------------------------------------------------------- Language switcher

   Shared by the marketing nav and the direct-checkout page — both dark, both
   showing the same thirty flags. It used to live inside home.php's <style>,
   which meant the checkout page rendered the markup unstyled: a full-size flag
   stacked above the language name. Anything using .lang-switcher gets the
   whole control from here now. */
.lang-switcher { position: relative; }
.lang-switcher-trigger {
  display: flex; align-items: center; gap: 6px; background: none; border: none; cursor: pointer;
  color: var(--on-dark-soft); font-family: inherit; font-size: 13.5px; padding: 4px 2px;
}
.lang-switcher-trigger:hover { color: var(--on-dark); }
.flag-icon { display: inline-block; width: 20px; height: 14px; border-radius: 3px; overflow: hidden; box-shadow: 0 0 0 1px rgba(255,255,255,0.18); flex-shrink: 0; vertical-align: middle; }
.flag-icon svg { display: block; width: 100%; height: 100%; }
.lang-switcher-caret { font-size: 9px; opacity: 0.7; transition: transform 0.15s ease; }
.lang-switcher.is-open .lang-switcher-caret { transform: rotate(180deg); }
.lang-switcher-menu {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 150;
  background: var(--ink); border: 1px solid var(--pine-line); border-radius: 12px;
  box-shadow: 0 24px 48px -20px rgba(0,0,0,0.55); padding: 6px;
  max-height: min(60vh, 420px); overflow-y: auto;
  /* Sized to its longest row rather than a fixed 210px, which left a ribbon of
     empty panel beside every short name. max-content measures the widest item
     — currently "Bahasa Indonesia" — and follows automatically if a language
     is ever added or a translation changes length.

     The cap is what stops max-content becoming a problem: without it a long
     name on a narrow phone would push the panel off the edge of the screen. */
  width: max-content;
  min-width: 150px;
  max-width: min(88vw, 240px);
  /* Still scrolls — thirty languages will not fit — but without the bar. It
     took a visible slice out of the panel and sat oddly inside the rounded
     corners. Same treatment the seating canvas already uses. */
  scrollbar-width: none; -ms-overflow-style: none;
}
.lang-switcher-menu::-webkit-scrollbar { width: 0; height: 0; display: none; }
html[dir="rtl"] .lang-switcher-menu { right: auto; left: 0; }
.lang-switcher-menu a {
  display: flex; align-items: center; gap: 9px; padding: 8px 10px; border-radius: 8px;
  color: var(--on-dark-soft); text-decoration: none; font-size: 13.5px;
}
.lang-switcher-menu a:hover { background: color-mix(in srgb, var(--brass-soft) 14%, transparent); color: var(--on-dark); }
.lang-switcher-menu a.is-current { color: var(--brass-soft); font-weight: 600; }

/* --------------------------------------------------- Dashboard "at a glance"

   One tile per module under the two widgets. It exists because the dashboard
   ended abruptly: two cards and then empty page on anything wider than a
   laptop. Each tile links to the page its number came from, so the strip is a
   shortcut row as well as a status row. */
.glance-row {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; margin-top: 24px;
}
.glance-tile {
  display: flex; align-items: center; gap: 14px;
  background: var(--surface-raised); border: 1px solid var(--surface-line);
  border-radius: 14px; padding: 18px 20px;
  text-decoration: none; color: inherit;
  transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.glance-tile:hover {
  transform: translateY(-2px); border-color: var(--accent);
  box-shadow: 0 16px 30px -22px rgba(0, 0, 0, 0.4);
}
.glance-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; flex-shrink: 0; border-radius: 11px;
  background: var(--surface-dim); color: var(--accent);
}
.glance-icon svg { width: 19px; height: 19px; }
.glance-body { display: flex; flex-direction: column; min-width: 0; }
.glance-value { font-family: var(--mono); font-size: 22px; font-weight: 600; line-height: 1.15; }
.glance-label { font-size: 13px; font-weight: 600; }
.glance-sub { font-size: 11.5px; color: var(--text-soft); }

@media (max-width: 1080px) { .glance-row { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) {
  .glance-row { grid-template-columns: 1fr; gap: 12px; }
  .glance-tile { padding: 14px 16px; }
}
@media (hover: none) {
  .glance-tile:hover { transform: none; box-shadow: none; }
}

/* ------------------------------------------------- Account data (GDPR card)

   Sits below the settings form, visually separate from it: these two actions
   are not "settings you save", they are things that happen immediately, and
   one of them cannot be undone. */
/* Capped while the rest of the builder goes full width: this card is a label
   on the left and a form on the right, and at 1600px those two halves end up
   over a metre apart on screen with nothing between them. Tables gain from
   width; a two-column form does not. */
.account-data { margin-top: 34px; padding: 26px 28px; max-width: 900px; }
.account-data h2 { font-size: 18px; font-weight: 500; margin: 0 0 4px; }
.account-data-intro { color: var(--text-soft); font-size: 13.5px; margin: 0 0 20px; }
.account-data-error {
  font-size: 13px; color: #9d1c28; background: rgba(157, 28, 40, 0.08);
  border-radius: 9px; padding: 9px 12px; margin: 0 0 18px;
}
.account-data-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 24px; padding: 18px 0; border-top: 1px solid var(--surface-line);
}
.account-data-text { display: flex; flex-direction: column; gap: 4px; max-width: 46ch; }
.account-data-text strong { font-size: 14.5px; font-weight: 600; }
.account-data-text span { font-size: 13px; color: var(--text-soft); line-height: 1.5; }
.account-data-note { font-size: 11.5px !important; opacity: 0.8; }
.account-data-form { display: flex; flex-direction: column; gap: 8px; width: 240px; flex-shrink: 0; }
.account-data-form input {
  width: 100%; font-family: inherit; font-size: 13px; color: var(--text);
  background: var(--surface-dim); border: 1px solid var(--surface-line);
  border-radius: 9px; padding: 9px 11px;
}
.account-data-form input:focus { outline: none; border-color: var(--accent); }
/* Both buttons wear the planner's own palette and share the lift-then-settle
   motion of .btn-primary, so they read as part of the same family rather than
   as two stray controls at the bottom of Settings. They stay OUTLINE buttons,
   not filled ones: neither is the thing you came to this page to do, and a
   solid red block would pull the eye straight to the irreversible one. */
.account-data-form .btn {
  justify-content: center; font-size: 13px; padding: 9px 12px; font-weight: 600;
  background: none; border: 1px solid var(--surface-line); color: var(--text);
  transition: transform 0.18s cubic-bezier(0.34, 1.4, 0.64, 1),
              background 0.18s ease, border-color 0.18s ease,
              color 0.18s ease, box-shadow 0.18s ease;
}
.account-data-row.is-danger .btn {
  /* --danger, not a hardcoded red: the old #9d1c28 ignored the theme and sat
     awkwardly on the darker palettes. */
  color: var(--danger); border-color: rgba(var(--danger-rgb), 0.4);
}

@media (hover: hover) {
  .account-data-form .btn:hover {
    transform: translateY(-2px);
    border-color: var(--accent);
    background: rgba(var(--accent-rgb), 0.1);
    box-shadow: 0 10px 22px -14px rgba(0, 0, 0, 0.5);
  }
  /* The destructive one fills instead of tinting — at the moment of pressing
     it, it should look unmistakably different from "download a copy". */
  .account-data-row.is-danger .btn:hover {
    background: var(--danger); border-color: var(--danger); color: #fff;
    box-shadow: 0 10px 22px -14px rgba(var(--danger-rgb), 0.75);
  }
}

.account-data-form .btn:active {
  transform: translateY(0) scale(0.985);
  box-shadow: none;
  transition-duration: 0.08s;
}
.account-data-form .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.account-data-row.is-danger .btn:focus-visible { outline-color: var(--danger); }

@media (max-width: 720px) {
  .account-data-row { flex-direction: column; gap: 12px; }
  .account-data-form { width: 100%; }
}

/* ---------------------------------------- Seating sidebar: who has a seat yet

   A seated name stays in place and dims, rather than the list re-sorting
   itself. Reordering under a pointer that is mid-drag is disorienting, and the
   indent structure (partner and children under their primary) carries more
   information than assigned/unassigned order would. */
.sidebar-guest-chip.is-seated { opacity: 0.55; }
.sidebar-guest-chip.is-seated .sidebar-guest-name { text-decoration: none; }
.sidebar-guest-seat {
  display: inline-block; margin-left: auto; padding: 1px 7px; border-radius: 999px;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.02em; white-space: nowrap;
  background: rgba(255, 255, 255, 0.14); color: inherit; flex-shrink: 0;
}
.sidebar-guest-chip[hidden] { display: none; }

/* ------------------------------- Seating page: a taller guest list

   On the seating chart the sidebar IS the workspace — you drag names out of it
   onto the plan — so a 260px window showing five or six rows means constant
   scrolling inside a panel while dragging out of it.

   Grown with clamp() against the viewport rather than a flat larger number:
     - floor 260px  = exactly today's height, so a short laptop never loses out
     - the middle term hands over whatever the viewport has spare
     - ceiling 560px keeps the Tables section and the account switcher below it
       reachable without an absurd scroll on a tall monitor

   Scoped to the seating page: everywhere else this list is a quick-add aid,
   not the thing being worked in. */
body[data-module="seating"] .sidebar-guest-list {
  max-height: clamp(260px, calc(100vh - 560px), 560px);
}

/* ------------------------------------------- Seating: assign a chair by tap

   The plan is the one part of the seating page that fits a phone screen, and
   the sidebar guest list it is normally dragged from is not there. Tapping a
   chair opens this instead: a sheet on a phone (thumb reach), a centred dialog
   on a larger screen. */
.seat-assign { position: fixed; inset: 0; z-index: 500; display: flex; }
.seat-assign[hidden] { display: none; }
.seat-assign-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.5); }
.seat-assign-panel {
  position: relative; z-index: 1; margin: auto;
  width: min(420px, 100%); max-height: min(80vh, 560px);
  display: flex; flex-direction: column; gap: 12px;
  background: var(--surface-raised); color: var(--text);
  border: 1px solid var(--surface-line); border-radius: 16px; padding: 18px;
  box-shadow: 0 30px 60px -24px rgba(0, 0, 0, 0.6);
}
.seat-assign-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.seat-assign-title { font-family: var(--display); font-size: 17px; font-weight: 500; }
.seat-assign-sub { font-family: var(--mono); font-size: 11.5px; color: var(--text-soft); }
.seat-assign-close {
  background: none; border: none; color: var(--text-soft); font-size: 22px; line-height: 1;
  cursor: pointer; padding: 0 4px;
}
.seat-assign-search {
  width: 100%; font-family: inherit; font-size: 14px; color: var(--text);
  background: var(--surface-dim); border: 1px solid var(--surface-line);
  border-radius: 10px; padding: 10px 12px;
}
.seat-assign-search:focus { outline: none; border-color: var(--accent); }
.seat-assign-list { overflow-y: auto; display: flex; flex-direction: column; gap: 4px; min-height: 0; }
.seat-assign-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; text-align: left; cursor: pointer;
  background: var(--surface-dim); border: 1px solid transparent; border-radius: 10px;
  padding: 11px 12px; font-family: inherit; font-size: 14px; color: var(--text);
}
.seat-assign-row:hover { border-color: var(--accent); }
/* Already sitting somewhere: still tappable (that is how you move someone),
   just visibly not one of the free choices. */
.seat-assign-row.is-seated { opacity: 0.6; }
.seat-assign-where { font-family: var(--mono); font-size: 11px; color: var(--text-soft); flex-shrink: 0; }
.seat-assign-empty { font-size: 13px; color: var(--text-soft); padding: 14px 2px; text-align: center; }
.seat-assign-clear {
  font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  background: none; border: 1px solid rgba(var(--danger-rgb), 0.4); color: var(--danger);
  border-radius: 10px; padding: 10px;
}
.seat-assign-clear[hidden] { display: none; }

@media (max-width: 560px) {
  /* Bottom sheet: the list sits under the thumb instead of the middle of the
     screen, and the keyboard pushes it up rather than covering it. */
  .seat-assign-panel {
    margin: auto 0 0; width: 100%; max-height: 82vh;
    border-radius: 18px 18px 0 0; border-bottom: none;
    padding-bottom: calc(18px + env(safe-area-inset-bottom));
  }
}
