/* =============================================================================
   OperatorHub Plus — platform design system

   Implements "OperatorHub Plus - Style Guide.dc.html" and
   "OperatorHub Plus - Login Page.dc.html" (Claude Design project
   58c62761-142d-426f-9476-7d481aea4b35). Read those before changing any visual
   decision here — they carry the reasoning, not just the values.

   TWO THEME LAYERS. Everything in this file is LAYER 1, the platform: login,
   tile menu, tool navigation, client settings, ops review, platform admin. It
   is fixed — our green, our type, our components, never themed by a client, and
   no operator logo in the chrome. Layer 2 (guest-facing waiver pages) is themed
   per client from the three-token model and lives in
   pages-ophub-waivers-guest. A component built for one is not automatically
   valid in the other.

   Colour is authored in oklch so lightness stays perceptually even across hues.
   There is no build step to emit fallbacks, so each token declares hex first and
   oklch second: a browser that cannot parse oklch keeps the hex and everything
   still works.

   Three rules that are easy to undo by accident:

   1. --c-primary is NEVER a large background fill and never green text on
      light — it is 3.71:1 on white. Buttons carry white on it; green *text*
      uses --c-primary-deep (6.15:1+).
   2. The plus lifts to --c-plus on navy. The primary green goes muddy against a
      dark ground, so the lockup's plus is a different green by context, not by
      accident.
   3. Borders carry structure, not shadows. Cards, tables and tiles are flat —
      a 1px border, no shadow. Shadow is only for things genuinely above the
      page (dropdowns, modals, sheets).
   ========================================================================== */

:root {
  /* ── Green ─────────────────────────────────────────────────────────────── */
  --c-primary: #219761;            /* buttons, active nav, focus rings          */
  --c-primary: oklch(0.60 0.13 158);
  --c-primary-hover: #067D4D;
  --c-primary-hover: oklch(0.52 0.12 158);
  --c-primary-deep: #0E653F;       /* green TEXT on light — clears 4.5:1        */
  --c-primary-deep: oklch(0.45 0.10 158);
  --c-primary-soft: #DFF5E7;       /* tinted panels, badges, active tile fill   */
  --c-primary-soft: oklch(0.95 0.03 158);
  --c-plus: #37C080;               /* the plus, on navy only                    */
  --c-plus: oklch(0.72 0.15 158);
  --c-focus-ring: #C3E9D2;
  --c-focus-ring: oklch(0.90 0.05 158);

  /* ── Ink ───────────────────────────────────────────────────────────────── */
  --c-ink: #16202B;                /* headings, body, navy panel fill           */
  --c-ink: oklch(0.24 0.025 250);
  --c-ink-label: #333C45;
  --c-ink-label: oklch(0.35 0.02 250);
  --c-ink-body: #4D5660;
  --c-ink-body: oklch(0.45 0.02 250);
  --c-ink-muted: #555F69;
  --c-ink-muted: oklch(0.48 0.02 250);
  --c-on-navy-dim: #BDC5CE;
  --c-on-navy-dim: oklch(0.82 0.015 250);
  --c-ink-raised: #242F3A;         /* control fill on the navy chrome           */
  --c-ink-raised: oklch(0.30 0.025 250);
  --c-ink-line: #404952;           /* its hairline                              */
  --c-ink-line: oklch(0.40 0.02 250);

  /* ── Ground ────────────────────────────────────────────────────────────── */
  --c-canvas: #F8FBF9;             /* a trace of green, so white reads warm     */
  --c-canvas: oklch(0.985 0.004 150);
  --c-surface: #ffffff;
  --c-border: #DBDEE2;
  --c-border: oklch(0.90 0.006 250);
  --c-border-input: #CDD1D6;
  --c-border-input: oklch(0.86 0.008 250);
  --c-rule: #E5E8EC;
  --c-rule: oklch(0.93 0.006 250);

  /* ── Status. NEVER themed by a client — a completed waiver is green whatever
        the operator's brand is. ─────────────────────────────────────────── */
  --c-success: var(--c-primary);
  --c-info: #3F83CB;
  --c-info: oklch(0.60 0.13 252);
  --c-warning: #E1A035;
  --c-warning: oklch(0.75 0.14 75);
  --c-danger: #C84341;
  --c-danger: oklch(0.57 0.17 25);

  --c-link: #077045;
  --c-link: oklch(0.48 0.11 158);
  --c-link-hover: #00502F;
  --c-link-hover: oklch(0.38 0.09 158);

  /* ── Type ──────────────────────────────────────────────────────────────── */
  --font-head: Archivo, "Helvetica Neue", Arial, sans-serif;
  --font-body: "Instrument Sans", system-ui, -apple-system, Arial, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "Courier New", monospace;

  /* ── Space (4px base) and radius ───────────────────────────────────────── */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px;
  --s-6: 24px; --s-10: 40px; --s-16: 64px;
  --r-input: 8px;   /* inputs, buttons */
  --r-card: 12px;   /* cards, tiles    */
  --r-modal: 16px;  /* modals, sheets  */
}

* { box-sizing: border-box; }

/* The `hidden` attribute has to WIN.
 *
 * The UA rule is `[hidden] { display: none }` at user-agent origin, which ANY
 * author rule outranks. This sheet sets `display` on .login (grid),
 * .form-stack (flex) and .tiles (grid) — so `node.hidden = true` on
 * #view-login, #form-email/#form-code or #tiles did nothing at all, and the
 * login panel rendered both the email step and the six-digit step at once.
 *
 * !important because the alternative is remembering to pair every future
 * `display` rule with a `:not([hidden])`, which is the kind of thing that holds
 * for a year and then quietly does not. app.js drives all three views and both
 * login steps through this attribute; it is not decoration. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--c-canvas);
  color: var(--c-ink);
  font: 16px/1.6 var(--font-body);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--c-link); text-decoration: none; }
a:hover { color: var(--c-link-hover); text-decoration: underline; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Keyboard focus only, per the guide: 2px solid primary, offset 2px. */
:where(a, button, input, select, [tabindex]):focus-visible {
  outline: 2px solid var(--c-primary);
  outline-offset: 2px;
}

/* ── The lockup ───────────────────────────────────────────────────────────
   Set in type rather than shipped as an image, so it stays sharp at any size
   and the plus can take its context colour. "Operator" bold, "Hub" regular,
   plus bold — one optical gap, never a space. Never stacked; it is a single
   line at all sizes. */

.lockup {
  display: inline-flex;
  align-items: baseline;
  font-family: var(--font-head);
  letter-spacing: -0.035em;
  line-height: 1;
  white-space: nowrap;
}

.lockup b { font-weight: 700; }
.lockup i { font-weight: 400; font-style: normal; }

/* One optical gap, never a space. The plus is part of the word. */
.lockup__plus { font-weight: 700; padding-left: 0.09em; }

.lockup--on-navy b,
.lockup--on-navy i { color: #ffffff; }
.lockup--on-navy .lockup__plus { color: var(--c-plus); }

/* On light, the plus takes --c-primary. It is a mark, not text, so its 3.71:1
   is fine — and the wordmark beside it carries the meaning for a screen reader. */
.lockup--on-light b,
.lockup--on-light i { color: var(--c-ink); }
.lockup--on-light .lockup__plus { color: var(--c-primary); }

/* =============================================================================
   Login — split panel. Navy fixed at 620px, form on white.
   Below 900px the navy becomes a 260px header band; the full hero never stacks
   above the fold on a phone.
   ========================================================================== */

.login {
  min-height: 100dvh;
  display: grid;
  grid-template-columns: 620px 1fr;
  background: var(--c-surface);
}

.login__hero {
  position: relative;
  background: var(--c-ink);
  overflow: hidden;
  padding: 56px 60px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  --login-photo: url("assets/operatorhub-plus-login-hero.webp");
}

/* Scrims. They do nothing over flat navy, and everything the day a photograph
   is dropped behind them — the lockup and headline stay legible on a bright
   image, so the panel still reads as ours rather than as a stock hero.
   Set --login-photo to a url() to use one. */
.login__hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to top, oklch(0.18 0.02 250 / 0.72) 0%, oklch(0.18 0.02 250 / 0.28) 55%, transparent 100%),
    linear-gradient(160deg, oklch(0.24 0.025 250 / 0.28) 0%, oklch(0.24 0.025 250 / 0.40) 55%, oklch(0.20 0.03 158 / 0.52) 100%),
    var(--login-photo, none) center / cover no-repeat;
  pointer-events: none;
}

.login__hero > * { position: relative; }

.login__lockup { font-size: 30px; }

.login__pitch {
  display: flex;
  flex-direction: column;
  gap: 22px;
}

.login__headline {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 46px;
  line-height: 1.08;
  letter-spacing: -0.032em;
  color: #ffffff;
  max-width: 21ch;
}

.login__sub {
  margin: 0;
  font-size: 17px;
  line-height: 1.6;
  color: var(--c-on-navy-dim);
  max-width: 38ch;
}

.login__panel {
  padding: 0 96px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 34px;
}

.login__title {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 38px;
  letter-spacing: -0.03em;
  line-height: 1.1;
}

.login__blurb {
  margin: 12px 0 0;
  font-size: 16.5px;
  line-height: 1.6;
  color: var(--c-ink-body);
  max-width: 44ch;
}

.login__rule {
  height: 1px;
  background: var(--c-rule);
}

.login__foot {
  display: flex;
  flex-direction: column;
  gap: 14px;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--c-ink-body);
  max-width: 44ch;
}

.login__foot p { margin: 0; }

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

  .login__hero {
    height: 260px;
    padding: 28px 24px;
  }

  .login__lockup { font-size: 23px; }
  .login__sub { display: none; }

  .login__headline {
    font-size: 27px;
    line-height: 1.15;
    letter-spacing: -0.03em;
    max-width: 18ch;
  }

  .login__panel {
    padding: 32px 24px;
    gap: 26px;
    justify-content: flex-start;
  }

  .login__title { font-size: 27px; letter-spacing: -0.028em; }
  .login__blurb { font-size: 15px; }
}

/* ── Forms ────────────────────────────────────────────────────────────────
   Labels above fields, always visible — never placeholder-as-label. Help text
   and validation share one slot so nothing reflows when an error appears. */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

label {
  font-size: 13.5px;
  font-weight: 500;
  color: var(--c-ink-label);
}

/* :not([type]) matters as much as the two explicit types - a plain <input>
   with no type attribute at all (every dynamically-built field in
   form-builder.js and admin.js, plus a few static ones) defaults to "text"
   behaviour but does NOT match [type="text"], so it was falling through to
   the browser's unstyled control: small, square, no border-radius, sitting
   next to a fully-styled <select> in the same row. Found comparing the
   waiver-question builder's Key/Label inputs against its Type dropdown.

   :not(.code__box) matters too - the six-digit login code boxes are ALSO
   typeless <input>s, with their own fixed-size mono styling below. Without
   this exclusion, :not([type])'s specificity (0,1,1) beats .code__box's
   (0,1,0) and silently overrides its width, padding and font back to a
   normal text field - caught by diffing computed style before/after. */
input[type="email"],
input[type="text"],
input[type="password"],
input[type="number"],
input[type="date"],
input:not([type]):not(.code__box),
textarea {
  width: 100%;
  font-family: var(--font-body);
  font-size: 16px;          /* 16px minimum, or iOS zooms the page on focus */
  padding: 15px 16px;
  border-radius: var(--r-input);
  border: 1px solid var(--c-border-input);
  background: var(--c-surface);
  color: var(--c-ink);
}

textarea { resize: vertical; }

input::placeholder, textarea::placeholder { color: var(--c-ink-muted); opacity: 0.75; }

input:focus, textarea:focus {
  border-color: var(--c-primary);
  outline: 2px solid var(--c-focus-ring);
  outline-offset: 0;
}

/* Stacks a form's controls at the 'between controls' step of the space scale. */
.form-stack {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.field--code { gap: 10px; }

/* Body copy outside the login panel. */
.prose {
  margin: 0 0 var(--s-6);
  font-size: 16.5px;
  line-height: 1.6;
  color: var(--c-ink-body);
  max-width: 60ch;
}

/* One slot for help and validation, so the button never jumps. */
.slot {
  min-height: 1.35em;
  margin: 0;
  font-size: 14px;
  line-height: 1.35;
  color: var(--c-ink-muted);
}

.slot--error { color: var(--c-danger); }

/* #brevo-status is a standing status line above a form, not inline
   save-result feedback like every other .slot - .slot's own margin:0
   (deliberate, for the inline case) left it flush against the label
   below it. Scoped to this one id rather than a general .slot variant,
   since every other .slot usage on this screen is the inline case. */
#brevo-status { margin: 0 0 var(--s-4); }

/* ── Buttons ──────────────────────────────────────────────────────────────
   One primary per view. Medium (42px) is the default; the login's committing
   action is large. No touch target below 44px. */

.btn {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 16px;
  padding: 16px 26px;
  min-height: 50px;
  border-radius: var(--r-input);
  border: none;
  background: var(--c-primary);
  color: #ffffff;
  cursor: pointer;
  transition: background 120ms ease;
}

.btn:hover { background: var(--c-primary-hover); }
.btn[disabled] { opacity: 0.5; cursor: default; }

.btn--ghost {
  padding: 0;
  min-height: 0;
  font-size: 14.5px;
  font-weight: 400;
  background: none;
  color: var(--c-link);
  text-decoration: none;
}

.btn--ghost:hover { background: none; color: var(--c-link-hover); text-decoration: underline; }

/* ── Six-digit code ───────────────────────────────────────────────────────
   Six single-character inputs, not one field. 58x66 — past the 44px touch
   minimum and wide enough for the browser's one-time-code autofill. Mono,
   because a code is operational data: something you read aloud or copy. */

.code {
  display: flex;
  gap: 10px;
}

.code__box {
  width: 58px;
  height: 66px;
  padding: 0;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 26px;
  border: 1px solid var(--c-border-input);
  border-radius: var(--r-input);
  background: var(--c-surface);
  color: var(--c-ink);
}

.code__box:focus {
  border: 2px solid var(--c-primary);
  outline: 2px solid var(--c-focus-ring);
  outline-offset: 0;
}

@media (max-width: 420px) {
  .code { gap: 6px; }
  .code__box { width: 100%; height: 58px; font-size: 22px; }
}

/* The address the code went to — operational data, so mono. */
.mono {
  font-family: var(--font-mono);
  font-size: 0.94em;
  color: var(--c-ink);
}

/* =============================================================================
   App shell. Directional: the rail and tool pages will change as the build
   goes on. What holds is the frame — navy chrome, the in-app lockup, the client
   selector in the header, content on the light canvas.
   ========================================================================== */

.appbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
  padding: var(--s-3) var(--s-6);
  background: var(--c-ink);
}

.appbar__lockup { font-size: 20px; }

.appbar__right {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  flex-wrap: wrap;
  font-size: 14px;
  color: var(--c-on-navy-dim);
}

.appbar__who { display: flex; flex-direction: column; line-height: 1.25; }
.appbar__who b { color: #ffffff; font-weight: 500; }

.appbar__role {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--c-on-navy-dim);
}

.appbar .btn--ghost { color: var(--c-on-navy-dim); }
.appbar .btn--ghost:hover { color: #ffffff; }

/* A bare `select` was inheriting the appbar's on-navy control fill
   (--c-ink-raised, white text) everywhere, including every dropdown sitting
   on a light .panel - the timezone/role/waiver-question-type selects were
   all rendering as a dark pill against a white card. Only the appbar's own
   client switcher is actually on navy, so that one gets its own rule below
   and this one goes back to matching a text input. */
select {
  font-family: var(--font-body);
  font-size: 14px;
  padding: 9px 12px;
  border-radius: var(--r-input);
  border: 1px solid var(--c-border-input);
  background: var(--c-surface);
  color: var(--c-ink);
}

.appbar select {
  border: 1px solid var(--c-ink-line);
  background: var(--c-ink-raised);
  color: #ffffff;
}

.content {
  max-width: 1060px;
  margin: 0 auto;
  padding: var(--s-10) var(--s-6) var(--s-16);
}

.page-title {
  margin: 0 0 var(--s-6);
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 44px;
  line-height: 1.05;
  letter-spacing: -0.03em;
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-ink-muted);
}

.notice {
  margin: 0 0 var(--s-6);
  padding: var(--s-4) var(--s-4) var(--s-4) var(--s-3);
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--c-ink-body);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-left: 3px solid var(--c-warning);
  border-radius: var(--r-card);
}

/* ── Tool tile ────────────────────────────────────────────────────────────
   The landing menu IS the portal, and for a while it holds one live tile and a
   few promises — so each tile carries a status line rather than being a bare
   icon and label. Monogram, not illustration: a letter in a rounded square,
   tinted green when active and grey when not. */

.tiles {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
}

.tile {
  display: grid;
  gap: var(--s-3);
  align-content: start;
  padding: var(--s-6);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-card);
  color: inherit;
  text-decoration: none;
}

/* Raised only on hover of something actually interactive. */
a.tile:hover {
  border-color: var(--c-primary);
  box-shadow: 0 6px 20px rgba(22, 32, 43, 0.08);
  box-shadow: 0 6px 20px oklch(0.24 0.025 250 / 0.08);
  text-decoration: none;
}

.tile__mono {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 20px;
  background: var(--c-primary-soft);
  color: var(--c-primary-deep);
}

.tile__name {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 19px;
  line-height: 1.26;
  letter-spacing: -0.015em;
}

.tile__desc {
  margin: 0;
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--c-ink-body);
}

/* The status line is the reason to click. A tile with nothing to report says
   so — it never says nothing. */
.tile__status {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--c-primary-deep);
}

/* Coming tiles are dashed and unclickable. They set expectations without
   pretending to be features — two maximum on screen, or the portal reads as
   vapour. */
.tile--coming {
  border-style: dashed;
  background: transparent;
}

.tile--coming .tile__mono {
  background: var(--c-rule);
  color: var(--c-ink-muted);
}

.tile--coming .tile__name { color: var(--c-ink-body); }
.tile--coming .tile__status { color: var(--c-ink-muted); }

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

/* =============================================================================
   Admin — tables, tabs, badges, forms

   Platform admin only. Everything here follows the style guide's structural
   rules rather than inventing a second look:

     - BORDERS, NOT SHADOWS. Tables and panels are flat, 1px --c-border. Shadow
       is reserved for things genuinely above the page.
     - --c-primary is never green text and never a large fill. Green text is
       --c-primary-deep; the active tab underline is a 2px rule, which is the
       one place --c-primary is correct at this weight.
     - IBM Plex Mono carries OPERATIONAL DATA — client ids, binding names,
       timestamps. Prose stays in Instrument Sans. A ClientID is something
       someone reads aloud to OperatorHub support, which is precisely the test.
     - STATUS COLOUR IS NEVER THEMED. Green means configured whatever the
       operator's brand is.
   ========================================================================== */

.admin-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-6);
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: var(--s-6);
}

/* BT-80 - settings/index.html's own way back to the hub, and which client
   this screen is currently scoped to. Plain text, not a second row of
   buttons - the appbar's own "Hub" link still exists for the same action,
   this just names the path getting there. */
.breadcrumb {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--c-ink-muted);
  margin-bottom: var(--s-3);
}

.breadcrumb a {
  color: var(--c-ink-muted);
  text-decoration: none;
}

.breadcrumb a:hover {
  color: var(--c-ink);
  text-decoration: underline;
}

.breadcrumb [aria-current="page"] {
  color: var(--c-ink);
  font-weight: 600;
}

/* Both admin/index.html and settings/index.html carried this as an inline
   style="margin:0" on the <h1> - which a real CSP (script-src 'self', no
   unsafe-inline) silently refuses to apply, confirmed against an actual
   CSP-enforcing server. Neither page noticed because nothing had tested
   with the real header before; the visible effect was just a bit of extra
   space above/below the heading, easy to miss. */
.admin-head .page-title {
  margin: 0;
}

/* Tabs. The underline sits on the tab, not on the strip, so the active tab
   reads as continuous with the panel below it. */
.tabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--c-border);
  margin-bottom: var(--s-6);
  /* WRAP, never scroll. `overflow-x: auto` computes overflow-y to auto as well,
     and the tabs' -1px pull-down overflowed it by a pixel — which is where the
     stray scroll control at the right of the strip came from. Wrapping also
     matches the rule against horizontal scrolling on a phone. */
  flex-wrap: wrap;
}

.tab {
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: 10px 14px;
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 500;
  color: var(--c-ink-muted);
  cursor: pointer;
  white-space: nowrap;
}

.tab:hover { color: var(--c-ink); }

.tab[aria-selected="true"] {
  color: var(--c-primary-deep);
  border-bottom-color: var(--c-primary);
}

.tab:focus-visible {
  outline: 2px solid var(--c-focus-ring);
  outline-offset: -2px;
}

/* Panel — the flat container everything else sits in. */
.panel {
  border: 1px solid var(--c-border);
  border-radius: var(--r-card);
  background: var(--c-surface, #ffffff);
  padding: var(--s-6);
  margin-bottom: var(--s-6);
}

.panel__title {
  margin: 0 0 4px;
  font-family: var(--font-head);
  font-size: 18px;
  font-weight: 600;
  color: var(--c-ink);
}

.panel__hint {
  margin: 0 0 var(--s-6);
  font-size: 14px;
  line-height: 1.55;
  color: var(--c-ink-muted);
}

/* Tables. Metrics come from the style guide's "Cards and tables" panel, and the
   reasoning there is about scanning a long list rather than reading it:

     rows 48px, not 64        staff scan 40 rows at a time; density beats comfort
     cell text 13.5px         same reason
     header row tinted        10.5px mono, 0.09em, so it reads as a rule not a row

   Rows are separated with border-TOP so the header carries no doubled line and
   the last row needs no exception. */
/* Full-bleed inside the panel, so the hairlines run edge to edge of the card
   rather than stopping short at the panel's padding. The negative margin is
   exactly the panel inset, and the cells carry it back as 24px of their own —
   so the text still lines up with everything else in the panel. */
.table-scroll {
  overflow-x: visible;
  margin-left: calc(-1 * var(--s-6));
  margin-right: calc(-1 * var(--s-6));
}

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}

table.data th,
table.data td {
  text-align: left;
  /* 14 + 20 + 14 = 48px rows, which is the spec. The line-height has to be
     explicit or the row height follows the font's default leading and drifts. */
  padding: 14px var(--s-6);
  line-height: 20px;
  vertical-align: middle;
}

table.data thead th {
  padding: 12px var(--s-6);
  background: var(--c-canvas);
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--c-ink-muted);
  white-space: nowrap;
}

table.data tbody td { border-top: 1px solid var(--c-border); }
table.data tbody tr:hover { background: var(--c-canvas); }

/* Rows become CARDS below 720px. The guide is explicit: do not horizontally
   scroll a table on a phone. The header row is dropped and each cell carries its
   own label from data-label, which is why every td in admin.js sets one — a cell
   without it renders as a value with nothing saying what it is. */
@media (max-width: 720px) {
  /* Cards sit inside the panel again, so the full-bleed comes off. */
  .table-scroll { margin-left: 0; margin-right: 0; }

  table.data thead { display: none; }

  table.data,
  table.data tbody,
  table.data tr,
  table.data td {
    display: block;
    width: 100%;
  }

  table.data tbody tr {
    border: 1px solid var(--c-border);
    border-radius: var(--r-card);
    padding: 4px 0;
    margin-bottom: 12px;
  }

  table.data tbody tr:hover { background: none; }

  table.data tbody td {
    display: flex;
    gap: 16px;
    align-items: baseline;
    justify-content: space-between;
    border-top: 0;
    padding: 8px 16px;
  }

  table.data tbody td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--c-ink-muted);
  }

  /* A cell with no label is an actions cell — it gets the full width. */
  table.data tbody td:not([data-label]) { justify-content: flex-end; }
  table.data tbody td:not([data-label])::before { content: none; }
}

/* Operational data: ids, binding names, timestamps. */
td.mono, .mono { font-family: var(--font-mono); font-size: 13px; }

.row-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* Status pills. The guide's spec exactly: 4px 10px, 12px, weight 600, a tinted
   fill and NO border. Sentence case, not uppercase mono — mono is for
   operational data you might read aloud, and a status is a word, not a value.
   "Status is a pill, never a bare colour. Colour plus a word, so it survives
   greyscale printing and colour blindness." */
.badge {
  display: inline-block;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  color: var(--c-ink-body);
  background: #eef0f2;
  background: oklch(0.95 0.005 250);
  white-space: nowrap;
}

.badge--ok {
  color: #1f6647;
  color: oklch(0.40 0.10 158);
  background: #e6f5ec;
  background: oklch(0.95 0.03 158);
}

.badge--warn {
  color: #7a5410;
  color: oklch(0.48 0.12 75);
  background: #fdf3e1;
  background: oklch(0.96 0.04 75);
}

.badge--danger {
  color: #96302d;
  color: oklch(0.50 0.16 25);
  background: #f9eae9;
  background: oklch(0.94 0.02 25);
}

/* Unknown is NOT a failure. It means we could not ask — a different fact from
   "no", and rendering it as "no" would send someone to fix a working client.
   The dashed outline is the one place a pill carries a border, because the
   neutral grey fill alone reads as a settled answer rather than an absent one. */
.badge--unknown {
  color: var(--c-ink-muted);
  background: none;
  border: 1px dashed var(--c-border-input);
  padding: 3px 9px;
}

/* Forms inside panels.
 *
 * SINGLE COLUMN. The guide is explicit, and auto-fit was quietly breaking it —
 * a four-field form landed four-up on a wide screen, which is the layout the
 * rule exists to prevent. Add .grid-form--pairs only for genuinely paired
 * values (a date and its time), and it still collapses below 720px. */
.grid-form {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-6);
  align-items: start;
  max-width: 32rem;
}

.grid-form--pairs { max-width: 46rem; }

@media (min-width: 721px) {
  .grid-form--pairs { grid-template-columns: 1fr 1fr; }
}

.grid-form .field { margin: 0; }

/* BT-82: the waiver roster's filter toolbar - five short fields scanned and
   adjusted together, not a data-entry form. A deliberate, scoped exception
   to .grid-form's single-column rule above (see this class's own comment),
   not a change to it - nothing else on the platform opts into a row layout
   this way. Each field gets a fixed basis rather than flex:1, so five
   short fields compact to their own width instead of stretching to fill
   the panel; wraps to two lines on a narrow viewport rather than
   overflowing. */
.filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-6);
  align-items: start;
}

.filter-row .field {
  margin: 0;
  flex: 0 1 180px;
}

/* A form has no layout rule of its own giving its direct children vertical
   rhythm - fine while a form was always exactly one .grid-form then
   .actions, but the question builder's forms stack a .grid-form, a plain
   .field (terms key) and another .field (the builder itself), which
   without this ran flush together. */
form > .field:not(:first-child),
form > .grid-form:not(:first-child) {
  margin-top: var(--s-4);
}

/* ── Terms rich-text editor (Quill, vendored) ────────────────────────────
   Quill supplies its own toolbar/border chrome (the ql-snow theme) - this
   only sets the height of the editable area and lets the flat-border house
   style apply to its outer edge, matching inputs elsewhere on this page. */
.field__head {
  display: flex;
  gap: var(--s-4);
  align-items: center;
  justify-content: space-between;
}

.ql-container.ql-snow {
  border-color: var(--c-border-input);
  border-radius: 0 0 var(--r-input) var(--r-input);
  font-family: var(--font-body);
  font-size: 14px;
}

.ql-toolbar.ql-snow {
  border-color: var(--c-border-input);
  border-radius: var(--r-input) var(--r-input) 0 0;
}

.ql-editor {
  min-height: 160px;
}

.field__hint {
  margin: 6px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--c-ink-muted);
}

.check {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  font-size: 15px;
  color: var(--c-ink-body);
}

.check input { margin-top: 3px; }

/* Branding & theme's colour scheme fields - a native colour picker has its
   own browser chrome (a swatch button, no consistent default size), so it
   gets sized and bordered to sit at home next to a regular input rather
   than left at whatever the browser defaults to. */
.color-fields {
  display: flex;
  gap: var(--s-6);
  flex-wrap: wrap;
}

.color-field {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

input[type="color"] {
  width: 56px;
  height: 40px;
  padding: 2px;
  border: 1px solid var(--c-border-input);
  border-radius: var(--r-input);
  background: var(--c-surface);
  cursor: pointer;
}

.btn--small { padding: 7px 12px; font-size: 14px; }

.btn--danger {
  background: none;
  border: 1px solid var(--c-danger);
  color: #8f2d2b;
}

.btn--danger:hover { background: #fbeceb; }

/* BT-81: a real, clickable secondary action - distinct from both the
   solid .btn (one primary per view, per this file's own header comment)
   and .btn--ghost's plain-link styling, which reads as too subtle for a
   genuinely consequential action (waiving a requirement, recording a
   manual completion) that just shouldn't be the LOUDEST thing on screen.
   Neutral, not .btn--danger's red - these aren't destructive actions, they're
   routine ops decisions that happen to sit next to a louder primary one. */
.btn--secondary {
  background: none;
  border: 1px solid var(--c-border-input);
  color: var(--c-ink-body);
}

.btn--secondary:hover { background: var(--c-canvas); }

.actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
  margin-top: var(--s-6);
}

/* BT-105 review finding: these read as plain links at rest (.btn--ghost's
   own "plain-link styling", per BT-81's comment on that class above) -
   too subtle for the screen's own primary navigation, sitting right under
   the date strip. .btn--secondary gives them a real, always-visible
   button box; .is-active marks whichever one (if any) matches the
   filters currently in effect, in the same soft-green .badge--ok carries
   for "this is the live state" elsewhere on this platform. */
.quick-filters {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: var(--s-4);
}

.quick-filters .btn.is-active {
  background: var(--c-primary-soft);
  border-color: var(--c-primary-soft);
  color: var(--c-primary-deep);
}

.quick-filters .btn.is-active:hover { background: var(--c-primary-soft); }

/* BT-105 - a row of selectable days above the filter form, the screen's
   own primary navigation now that "look at today/a nearby day" is the
   90% case (staff feedback). Sits ABOVE .quick-filters/the "More
   filters" disclosure, not inside either - it's the first thing scanned,
   not a refinement of something else.
   Nav buttons page the whole visible window at once (dateStripSize(),
   waivers.js - 7 wide on desktop, 3 on a narrow phone, both fixed here
   and there so they can't silently disagree), not day-by-day - "show me
   the days after these" reads more naturally than nudging by a single
   day at this width. */
.date-strip-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
}

.date-strip {
  display: flex;
  gap: 6px;
  /* Room for the box-shadow-free focus outline without clipping it. */
  padding: 2px;
}

/* Small, fixed squares - reversed from an earlier pass that flex-stretched
   these to fill the row (staff feedback: "too big"). Fixed size also means
   a phone showing only 3 of them (dateStripSize()) gets three properly
   square boxes rather than three stretched-wide ones filling the same
   space seven would have. */
.date-strip__day {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  border: 1px solid var(--c-border-input);
  border-radius: var(--r-input);
  background: var(--c-surface);
  color: var(--c-ink-body);
  font-family: var(--font-body);
  cursor: pointer;
}

.date-strip__day:hover { border-color: var(--c-primary); }

.date-strip__day-name {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--c-ink-muted);
}

.date-strip__day-number {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 15px;
}

/* Today gets its own marker even when not selected, so the strip stays
   orientated after paging away from it - a plain dot under the number,
   the same way a physical calendar circles "today" independently of
   whatever date happens to be circled for an appointment. */
.date-strip__day.is-today .date-strip__day-name { color: var(--c-primary-deep); }

.date-strip__day.is-today::after {
  content: "";
  width: 4px;
  height: 4px;
  border-radius: 999px;
  background: var(--c-primary);
}

.date-strip__day.is-selected {
  border-color: var(--c-primary);
  background: var(--c-primary);
  color: #ffffff;
}

.date-strip__day.is-selected .date-strip__day-name { color: rgba(255, 255, 255, 0.85); }
.date-strip__day.is-selected.is-today::after { background: #ffffff; }

.date-strip__nav {
  flex: 0 0 auto;
  width: 36px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--c-border-input);
  border-radius: var(--r-input);
  background: var(--c-surface);
  color: var(--c-ink-body);
  font-size: 18px;
  cursor: pointer;
}

.date-strip__nav:hover { border-color: var(--c-primary); color: var(--c-primary-deep); }

/* BT-105 - the detailed filter fields (search, status, tour code, an
   explicit date range) collapse behind this disclosure by default, same
   visual treatment as detail-terms's own (.terms-disclosure) so "there's
   more here" reads as a real, clickable control rather than a plain
   link. Below the date strip and quick filters, not above - those two
   are the screen's primary navigation now. */
.filter-accordion {
  margin-top: var(--s-4);
}

.filter-accordion summary {
  display: inline-block;
  padding: 9px 16px;
  border: 1px solid var(--c-border-input);
  border-radius: var(--r-input);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--c-ink-body);
  cursor: pointer;
}

.filter-accordion summary:hover { background: var(--c-canvas); }
.filter-accordion[open] summary { margin-bottom: var(--s-4); }

/* BT-82: one line per offered action button, in .btn-* declaration order.
   Plain <li>s, not a definition list - this is a caption under a toolbar,
   not question/answer data. */
.action-hints {
  list-style: none;
  margin: 6px 0 0;
  padding: 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--c-ink-muted);
}

.empty {
  padding: var(--s-6);
  text-align: center;
  color: var(--c-ink-muted);
  font-size: 14.5px;
}

/* The environment marker. Dev shares production's database, so which one you
   are looking at has to be visible at all times, not inferred from the URL. */
.env-flag {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 4px;
  border: 1px solid var(--c-warning);
  background: #fdf4e3;
  color: #7a5410;
}

.appbar__link {
  color: #ffffff;
  text-decoration: none;
  font-size: 14.5px;
  font-weight: 500;
  padding: 6px 10px;
  border-radius: var(--r-input);
  border: 1px solid var(--c-ink-line);
}

.appbar__link:hover { background: var(--c-ink-raised); }

/* Panel header with an action on the right. The trigger is a ghost button, not
   a second green one — the primary green belongs to the submit inside the form
   it opens, and two of them competing makes neither read as the main action. */
.panel__head {
  display: flex;
  gap: var(--s-6);
  align-items: center;
  justify-content: space-between;
  margin-bottom: 4px;
}

.panel__head .panel__title { margin: 0; }

/* The client name, pinned to every panel header inside the client detail view.
   "Client context is always visible" — a platform admin can act inside any
   client account, so the name must not scroll away above the panel where they
   disable somebody. Mono because it identifies a tenant. */
.panel__context {
  font-size: 12px;
  color: var(--c-ink-muted);
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--c-canvas);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 22ch;
}

/* Numbered setup steps. Ordered because the order matters — adding the client
   before the webhook is not a preference, it is the difference between a hook
   that works and one that silently ignores everything. */
.steps { padding-left: 1.3em; }
.steps li { margin-bottom: 10px; }

/* A URL that exists to be copied. It wraps rather than truncating: a webhook URL
   shown with an ellipsis is one somebody pastes half of. */
.url-row {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 12px 0;
  border-top: 1px solid var(--c-border);
}

.url-row:first-child { border-top: 0; }

.url-row__label {
  flex: 0 0 5.5rem;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--c-ink-muted);
  padding-top: 6px;
}

.url-row__value {
  flex: 1 1 auto;
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.5;
  word-break: break-all;
  color: var(--c-ink-body);
  background: var(--c-canvas);
  border: 1px solid var(--c-border);
  border-radius: var(--r-input);
  padding: 8px 10px;
}

/* ── Waiver form question builder ──────────────────────────────────────
   One bordered container with rows divided by a top border - the same
   shape as table.data, not a stack of separately-bordered cards (that
   read as a pile of boxes, which is the "gross" the redesign was for).

   The 4-up grid this replaced put Key/Type/Label/Required side by side on
   a form - the guide's forms rule is explicit that a row like that is
   single column, two-up only for genuinely paired values (a date and its
   time). These aren't a pair. But a repeating row of short fields is what
   the guide's own tables already are, so above 720px this renders as a
   dense table row instead: one shared header row supplies the labels,
   and the per-field <label> becomes screen-reader-only, matching how
   table.data drops its header and re-labels per cell below that width -
   just the reverse direction, because here the labels start attached to
   the field, not the header. */
/* Live feedback: every question used to share ONE outer border with only
   a faint 1px hairline between rows - with 5+ fields per question packed
   edge-to-edge into the next question's fields, there was nothing telling
   the eye where one question ended and the next began. Each `.question-row`
   is now its own genuinely separate card (background/border/radius, real
   gap between them) rather than a shared box with internal dividers -
   the `<div class="question-row">` was already locally named `card` in
   form-builder.js before the CSS ever matched that. */
.question-builder {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

.question-builder__head {
  display: none;
}

@media (min-width: 720px) {
  .question-builder__head {
    display: grid;
    grid-template-columns: 1fr 1fr 1.8fr 0.7fr 148px;
    gap: var(--s-3);
    padding: 12px var(--s-4);
    background: var(--c-canvas);
    border: 1px solid var(--c-border);
    border-radius: var(--r-input);
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    font-weight: 500;
    color: var(--c-ink-muted);
  }
}

.question-row {
  padding: var(--s-4);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-input);
}

/* The "Question N" anchor (form-builder.js) - a plain label, not a field,
   so it stays visually distinct from the Key/Type/Label row it sits above
   rather than reading as one more input to fill in. */
.question-row__number {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--c-ink-muted);
}

/* A table-density row can't carry full-size (16px/15px-padding) fields
   without blowing its own height back out - match every input to the
   select next to it at the smaller size instead. */
.question-row input,
.question-row select,
.question-row textarea {
  padding: 9px 12px;
  font-size: 14px;
}

.question-row__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-3);
}

@media (min-width: 720px) {
  .question-row__grid {
    grid-template-columns: 1fr 1fr 1.8fr 0.7fr 148px;
    align-items: center;
  }
  .question-row__grid .field {
    position: relative;
  }
  .question-row__grid .field label {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
  }
  /* "Must be answered" restates what the Required column header already
     says once that header exists - keep it for the stacked mobile layout
     only, where the checkbox has no other label attached to it. */
  .question-row__grid .check span {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
  }
}

.question-row__grid .field {
  margin: 0;
}

/* BT-84 - section/highlight, always shown (unlike Options, select-only).
   Two fields, not five - a plain two-column grid like grid-form--pairs
   rather than reusing question-row__grid's five-column layout, which is
   sized for Key/Type/Label/Required and would leave Section starved for
   width. */
.question-row__extras {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-3);
}

@media (min-width: 480px) {
  .question-row__extras { grid-template-columns: 1fr 1fr; }
}

.question-row__extras .field { margin: 0; }

.question-row__showif {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  font-size: 13.5px;
  color: var(--c-ink-body);
}

.question-row__showif select,
.question-row__showif input {
  width: auto;
}

.question-row__actions {
  display: flex;
  gap: var(--s-2);
  justify-content: flex-end;
}

.empty-questions {
  color: var(--c-ink-muted);
  font-size: 13.5px;
  padding: var(--s-3) 0;
}

.json-preview {
  font-size: 12px;
  line-height: 1.5;
  background: var(--c-canvas);
  border: 1px solid var(--c-border);
  border-radius: var(--r-input);
  padding: var(--s-3);
  overflow-x: auto;
  white-space: pre;
}

/* BT-85 - the readable question-list preview (form-builder.js's
   renderReadablePreview), a bordered card standing in for "what a guest
   will see" - genuinely readable, unlike the raw JSON view next to it. */
.form-preview {
  border: 1px solid var(--c-border);
  border-radius: var(--r-input);
  padding: var(--s-3) var(--s-4);
  background: var(--c-canvas);
}

.form-preview__section {
  margin: var(--s-4) 0 var(--s-2);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--c-ink-muted);
}

.form-preview__section:first-child { margin-top: 0; }

.form-preview__item {
  padding: var(--s-3) 0;
  border-top: 1px solid var(--c-border);
}

/* Review fix, BT-85: `:first-of-type` only ever matches the very FIRST
   .form-preview__item div across the WHOLE list (every item shares one
   flat parent with the h4.form-preview__section headings interleaved as
   siblings, not nested per section) - every section after the first still
   had a redundant border-top directly under its own heading. Covers both
   "the very first item has no heading before it at all" (:first-child)
   and "the first item of every later section" (immediately after a
   .form-preview__section heading). */
.form-preview__item:first-child,
.form-preview__section + .form-preview__item {
  border-top: none;
  padding-top: 0;
}

.form-preview__item-head {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
}

.form-preview__label { font-weight: 600; }

.form-preview__meta {
  margin: 2px 0 0;
  font-size: 12.5px;
  color: var(--c-ink-muted);
}

.form-preview__help {
  margin: 4px 0 0;
  font-size: 13px;
  color: var(--c-ink-body);
}

/* ── Ops review (Phase 8, waivers/) ──────────────────────────────────────
   The one screen on this platform built as two panes rather than a stack of
   form panels - CLAUDE.md's own lift note for this build ("two-pane review
   layout" from worker-aag-ops-waivers). Wider than the standard .content:
   a roster list and a full waiver detail genuinely need the room, and this
   is the only screen that asks for it. */

.content--wide { max-width: 1400px; }

.review-layout {
  display: grid;
  grid-template-columns: minmax(320px, 420px) 1fr;
  gap: var(--s-6);
  align-items: start;
}

@media (max-width: 900px) {
  .review-layout { grid-template-columns: 1fr; }
}

/* Staff feedback: a long roster (13+ guests) meant the detail pane
   scrolled away with the page - selecting a guest far down the list left
   their waiver off-screen above, with no way to see it without scrolling
   back to the top. Sticky, not a modal: this screen's own keyboard
   Up/Down navigation (BT-88) relies on the detail pane updating IN PLACE
   while staying on the list, which a modal (open/close per guest) would
   fight rather than fit - `.modal`'s own established pattern here
   (BT-81) is for a single blocking confirmation, not browsing a list.
   Scoped to the two-column desktop layout only (min-width matches
   .review-layout's own stacking breakpoint exactly) - below that the
   panes stack into one column, where a permanently-sticky detail pane
   would cover the roster list as you scroll past it instead of helping;
   waivers.js's own selectGuest scrolls the detail into view there
   instead, since Up/Down navigation doesn't apply on a touch device
   anyway. */
@media (min-width: 901px) {
  .review-detail {
    position: sticky;
    top: var(--s-6);
  }
}

.review-list table.data tbody tr[role="button"] { cursor: pointer; }

/* BT-88 "slightly larger table text" - scoped to this one screen's roster,
   not the shared table.data spec every other data table on the platform
   still follows (that 13.5px is the style guide's own documented figure -
   see the comment above table.data - changing it globally would be a much
   bigger, riskier change than this ticket asks for). */
.review-list table.data { font-size: 14.5px; }

/* BT-83: sortable column headers. Same clickable-cell pattern as a roster
   row (role="button", tabindex, waivers.js's own keydown handler) rather
   than a real <button> - a <button> inside a <th> would need its own
   padding/typography reset to look like a plain header cell at all. */
.review-list table.data thead th[role="button"] {
  cursor: pointer;
  user-select: none;
}

.review-list table.data thead th[role="button"]::after {
  content: "";
  display: inline-block;
  width: 0.6em;
}

.review-list table.data thead th.is-sorted-asc::after { content: "▲"; }
.review-list table.data thead th.is-sorted-desc::after { content: "▼"; }

/* Selected-row treatment, fixed on review (BT-83): this used to share
   --c-primary-soft with .badge--ok's own background (styles.css's own
   comment on that token: "tinted panels, badges, active tile fill" -
   correct for a badge sitting on plain white, wrong once the ROW behind
   it turns the same colour). A selected row containing a "Completed"
   badge read as a near-invisible green-on-green. Neutral background (the
   same hairline tint --c-rule already uses elsewhere) plus a left accent
   in --c-primary keeps "this row is selected" legible regardless of which
   status badge happens to be in it. */
.review-list table.data tbody tr.is-selected {
  background: var(--c-rule);
  box-shadow: inset 3px 0 0 var(--c-primary);
}

/* One sub-header row per date, only when a search actually spans more than
   one (see waivers.js's renderRosterRows) - same visual register as the
   table's own header (mono, tinted, uppercase-adjacent weight) so it reads
   as structure, not as a guest row. */
.roster-group-header td {
  background: var(--c-canvas);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  color: var(--c-ink-muted);
}

/* Below 720px every row becomes its own bordered "card" (see the generic
   table mobile rules above) - right for a guest row, wrong for a plain
   date divider. Un-cards it back down to a flat section label. */
@media (max-width: 720px) {
  .roster-group-header {
    border: none;
    margin-bottom: 0;
  }

  .roster-group-header td {
    display: block;
    padding: 8px 16px;
  }

  .roster-group-header td::before { content: none; }
}

.review-list table.data tbody tr.is-selected:hover { background: var(--c-rule); }

/* BT-88 - a still-pending guest departing soon (isAtRisk, classify.js).
   --c-warning, not --c-danger - this is "worth a look soon", not a
   failure state, and not the same red/green vocabulary the status badges
   already own. */
.risk-flag {
  margin-left: 6px;
  color: var(--c-warning);
  cursor: help;
}

/* Staff feedback: with the Primary Order column (BT-120) narrowing this
   one further, the Status cell's badge + risk-flag triangle could wrap
   onto a second line - and since the row's real height then follows
   whichever cell wraps tallest (the Tour column's own long product name
   usually), the wrapped triangle ends up sitting right up against the
   next column instead of staying on one clear line beside its badge. */
.review-list table.data td[data-label="Status"] { white-space: nowrap; }

.review-detail { min-height: 320px; }

/* Question/answer pairs from a pinned form_definition_json snapshot - a
   description list, not a table: this is one record's own fields read
   top to bottom, not rows to scan/compare the way the roster's table is. */
.qa-list {
  display: grid;
  grid-template-columns: minmax(140px, 40%) 1fr;
  gap: var(--s-2) var(--s-6);
  margin: 0 0 var(--s-6);
}

/* BT-84 - one per question, wrapping its own dt+dd so highlight styling
   below has something to select without touching dt/dd directly - but
   `display: contents` drops the wrapper from the box tree entirely, so
   .qa-list's grid still sees dt/dd as if this div weren't there at all. */
.qa-item { display: contents; }

.qa-list dt {
  font-size: 13.5px;
  font-weight: 500;
  color: var(--c-ink-label);
}

.qa-list dd {
  margin: 0;
  font-size: 14.5px;
  color: var(--c-ink-body);
}

/* A question's own helpText (form-builder.js), rendered under its answer -
   review finding: this was captured end-to-end (schema, builder, D1) but
   never actually shown anywhere until this rule existed. Smaller and
   muted, so it reads as supporting context, not a second answer. */
.qa-item__help {
  margin: 4px 0 0;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--c-ink-muted);
}

/* An operationally-important answer (a dietary/medical response, say) -
   deliberately NOT --c-primary-soft/--c-warning-as-badge-tone, which
   already carry a specific meaning elsewhere on this screen (a completed
   waiver, a pending one) - this is a plain "pay attention" cue, unrelated
   to waiver status. The left border on dt only (not dd) is what reads as
   one accented row rather than two separately-tinted cells either side of
   the grid gap. */
.qa-item--highlight dt,
.qa-item--highlight dd {
  background: oklch(0.97 0.03 75);
  padding: 6px 10px;
  border-radius: var(--r-input);
}

.qa-item--highlight dt {
  border-left: 3px solid var(--c-warning);
}

@media (max-width: 560px) {
  .qa-list { grid-template-columns: 1fr; }
  .qa-list dd { margin-bottom: var(--s-3); }
}

/* BT-84 - a form with no sections at all (every form today, until a
   client starts using them) renders ONE group with no heading - see
   waivers.js's renderQaList. A heading only appears once there's actually
   more than one group to distinguish. */
.qa-group + .qa-group { margin-top: var(--s-6); }

.qa-group__title {
  margin: 0 0 var(--s-3);
  padding-bottom: 6px;
  border-bottom: 1px solid var(--c-rule);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--c-ink-muted);
}

/* BT-84 staff feedback: "View terms as signed" existed but read as easy-
   to-miss plain link text. A bordered, button-like summary makes it a
   visible affordance rather than a disclosure someone has to notice on
   their own - .terms-disclosure only touches the <summary>, not the whole
   .field this <details> already inherits. */
.terms-disclosure {
  margin-bottom: var(--s-4);
}

.terms-disclosure summary {
  display: inline-block;
  padding: 9px 16px;
  border: 1px solid var(--c-border-input);
  border-radius: var(--r-input);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--c-ink-body);
  cursor: pointer;
}

.terms-disclosure summary:hover { background: var(--c-canvas); }
.terms-disclosure[open] summary { margin-bottom: var(--s-3); }

/* BT-84 - "Print signed record" prints exactly the guest detail panel
   (answers, signature, terms, timestamp) as it's currently rendered - the
   roster, filter card, chrome and every action button/status slot are
   hidden rather than re-laid-out for print, since none of them are part
   of the evidentiary record itself. */
@media print {
  body * { visibility: hidden; }
  #detail-content, #detail-content * { visibility: visible; }
  #detail-content {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
  }
  #detail-actions, #detail-action-hints, #detail-action-slot, #detail-submission .actions {
    display: none !important;
  }
}

.signature-preview {
  display: block;
  max-width: 320px;
  max-height: 140px;
  margin: 0 0 var(--s-6);
  padding: var(--s-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-card);
  background: var(--c-surface, #ffffff);
}

/* BT-81 - the first <dialog>-based modal on this platform, replacing the
   native prompt()/confirm() this screen used for override actions. A real
   <dialog> gives focus-trapping, Escape-to-cancel and a native ::backdrop
   for free - no third-party modal library needed for one form. */
.modal {
  width: min(440px, calc(100vw - 48px));
  /* Review fix, BT-85: this dialog was originally sized only for
     BT-81's own short title+textarea content, with nothing capping its
     total height. BT-85's Publish preview can carry a whole terms
     document (its own .terms-preview scrolls internally at 240px, but
     the dialog AROUND it didn't) - on a short viewport that pushed the
     Confirm/Cancel actions off-screen with no way to reach them. Scrolls
     the whole dialog instead once its content genuinely exceeds the
     viewport, for both this and BT-81's own use. */
  max-height: calc(100vh - 48px);
  overflow-y: auto;
  padding: var(--s-6);
  border: none;
  border-radius: var(--r-card);
  background: var(--c-surface, #ffffff);
  box-shadow: 0 6px 20px rgba(22, 32, 43, 0.08);
  box-shadow: 0 6px 20px oklch(0.24 0.025 250 / 0.08);
}

.modal::backdrop {
  background: rgba(22, 32, 43, 0.45);
  background: oklch(0.24 0.025 250 / 0.45);
}

.modal__title {
  margin: 0 0 var(--s-4);
  font-size: 18px;
}

.modal__form {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

.modal__form .actions { justify-content: flex-end; }

/* BT-85 - the rendered terms body inside publishTerms's preview-and-
   confirm modal (settings.js). Scrolls rather than growing the dialog
   past a sane height for a long terms document - same reasoning
   pages-ophub-waivers-guest's own .terms-body scroll box uses. */
.terms-preview {
  max-height: 240px;
  overflow-y: auto;
  padding: var(--s-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-input);
  background: var(--c-canvas);
  font-size: 14px;
  line-height: 1.5;
}

.terms-preview :first-child { margin-top: 0; }
.terms-preview :last-child { margin-bottom: 0; }

