/*
 * Echofair — component styles, used alongside the Tailwind CDN.
 *
 * Sapphire and gold. Two rules keep it from looking cheap:
 *
 *   1. Gold is never body text. It fails contrast on white at about 2.4:1, so it
 *      is a fill, a rule, a hairline under a heading. `--gold-text` is the
 *      darkened one for anything that must be read.
 *   2. One gold thing per screen. If everything is highlighted, nothing is.
 *
 * The colours below are duplicated from lib/brand.js because CSS cannot require
 * it. If you change one, change both — the tailwind-config partial is generated
 * from brand.js and will tell you which is stale.
 */

:root {
    --navy-50:  #eef3fa;
    --navy-100: #dce6f4;
    --navy-200: #b6c9e6;
    --navy-500: #2f5b96;
    --navy-600: #1d4275;
    --navy-700: #12315b;
    --navy-800: #0c2445;
    --navy-900: #08192f;
    --navy-950: #050f1d;

    --gold-50:  #fbf7ea;
    --gold-100: #f6efd3;
    --gold-200: #ebd9a0;
    --gold-400: #d4b342;
    --gold-500: #c9a227;
    --gold-600: #ab8a1c;
    /* The ink that sits *on* gold. Referenced by the selected state of every
       choice button at the stand; undefined, it inherited and the label
       disappeared into the fill. */
    --gold-950: #2a2007;
    --gold-text: #8a6d1f;

    --canvas:  #f7f8fa;
    --surface: #ffffff;
    /** The page behind everything. What a section curve has to be painted in. */
    --page:    #ffffff;
    --hairline: #dfe3ea;
    --text: #0e1726;
    --muted: #5a6478;

    --success: #1f7a55;
    --warning: #996416;
    --danger:  #a32d2d;
    --tone-info: var(--navy-700);

    /* Tints for badges and flashes, so both themes have a legible pairing. */
    --tint-neutral: #eef0f4;
    --tint-success: #e6f4ee;
    --tint-warning: #fdf3e2;
    --tint-danger:  #fbeaea;
    --tint-info:    var(--navy-50);
    --tint-gold:    var(--gold-100);
    --edge-success: #bfe3d3;
    --edge-danger:  #f0cccc;
    --edge-info:    var(--navy-100);

    --radius: 0.75rem;
}

.dark {
    --canvas: #070c16;
    --surface: #101a2b;
    --page:    #070c16;
    --hairline: #1e293d;
    --text: #eef3fa;
    --muted: #98a3b5;
    /* On a dark surface gold finally *can* be read, so it earns its place. */
    --gold-text: #dfc46d;

    /* Lifted foregrounds and deep, desaturated tints: the light-theme pastels
       glare on a dark surface, and the light-theme text colours fail on them. */
    --success: #6ee7b7;
    --warning: #e8b866;
    --danger:  #f2a3a3;
    --tone-info: #b6c9e6;

    --tint-neutral: #1b2740;
    --tint-success: #0f3d2c;
    --tint-warning: #3b2c10;
    --tint-danger:  #401a1a;
    --tint-info:    #142744;
    --tint-gold:    #3a2f0e;
    --edge-success: #17553d;
    --edge-danger:  #5a2727;
    --edge-info:    #1e3a63;
}

/* === Base === */
html { scroll-behavior: smooth; -webkit-font-smoothing: antialiased; }
body { font-family: Inter, system-ui, -apple-system, sans-serif; }
html, body { overflow-x: clip; max-width: 100vw; }
/* Deliberately no min-height on the body. The marketing and app layouts set
   their own with `min-h-screen`, and the capture station sizes itself to the
   height that is really visible — a floor of 100vh underneath either one just
   makes the document taller than the thing inside it. */
::selection { background-color: var(--gold-200); color: var(--navy-900); }
.dark ::selection { background-color: var(--navy-700); color: var(--gold-200); }

/*
 * Headings are the same family as body text, one weight heavier and a touch
 * tighter. A display serif was tried and rejected: the good ones on Google
 * Fonts have no Greek, and a heading that falls back to Times next to Greek
 * body copy looks like a bug rather than a choice.
 */
/* Inter tightens as it grows, which is what stops big headings looking loose.
   The optical-size axis does most of it; the negative tracking is the rest. */
.font-display { font-family: Inter, system-ui, sans-serif; font-weight: 600; letter-spacing: -0.021em; }
h1.font-display, .font-display.text-4xl, .font-display.text-5xl { letter-spacing: -0.032em; }

/* The wordmark: sapphire with a gold second half, the whole identity in one line. */
.wordmark { font-weight: 600; letter-spacing: -0.02em; color: var(--navy-700); }
.wordmark span { color: var(--gold-600); }
.dark .wordmark { color: #fff; }
.dark .wordmark span { color: var(--gold-400); }

/* A hairline of gold under a section heading. The restrained use of the accent. */
.rule-gold::after {
    content: ''; display: block; width: 2.5rem; height: 2px;
    margin-top: 0.75rem; background: var(--gold-500); border-radius: 2px;
}

/* === Card === */
.card {
    background-color: var(--surface);
    border-radius: var(--radius);
    padding: 1.5rem;
    border: 1px solid var(--hairline);
    box-shadow: 0 1px 2px rgba(14,23,38,0.04), 0 1px 3px rgba(14,23,38,0.06);
    max-width: 100%;
}
.dark .card { box-shadow: none; }
.card--flush { padding: 0; overflow: hidden; }
.card table { min-width: 100%; }

@media (max-width: 640px) {
    .card { padding: 1rem; }
    h1 { font-size: 1.5rem !important; }
    h2 { font-size: 1.25rem !important; }
    h1, h2, h3, h4, p { overflow-wrap: break-word; word-break: break-word; }
}

/* === Buttons === */
.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
    padding: 0.625rem 1.25rem; font-size: 0.875rem; font-weight: 500;
    border-radius: 0.5rem; transition: all 0.15s ease; cursor: pointer;
    border: 1px solid transparent; outline: none; white-space: nowrap; text-decoration: none;
}
.btn:focus-visible { outline: 2px solid var(--gold-500); outline-offset: 2px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.btn-primary { background-color: var(--navy-700); color: #fff; }
.btn-primary:hover:not(:disabled) { background-color: var(--navy-800); }

/* The gold button. One per screen, on the thing you actually want pressed. */
.btn-gold { background-color: var(--gold-500); color: #2a2007; font-weight: 600; }
.btn-gold:hover:not(:disabled) { background-color: var(--gold-600); color: #fff; }

.btn-secondary {
    background-color: var(--surface); color: var(--text); border-color: var(--hairline);
}
.btn-secondary:hover:not(:disabled) { background-color: var(--canvas); border-color: var(--navy-200); }

.btn-ghost { background: transparent; color: var(--muted); }
.btn-ghost:hover:not(:disabled) { background-color: var(--canvas); color: var(--text); }

.btn-danger { background-color: var(--danger); color: #fff; }
.btn-danger:hover:not(:disabled) { filter: brightness(0.9); }

.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8125rem; }
.btn-lg { padding: 0.875rem 1.75rem; font-size: 1rem; }

/* === Forms === */
.form-input, .form-select, .form-textarea {
    width: 100%; padding: 0.625rem 0.875rem;
    /* 16px: anything smaller makes iOS Safari zoom on focus, which on a tablet
       mid-conversation throws the rep out of the form. */
    font-size: 1rem;
    border: 1px solid var(--hairline); border-radius: 0.5rem;
    background-color: var(--surface); color: var(--text);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
    min-width: 0;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
    outline: none; border-color: var(--navy-500);
    box-shadow: 0 0 0 3px rgba(47,91,150,0.12);
}
.form-input::placeholder, .form-textarea::placeholder { color: var(--muted); opacity: 0.7; }
.form-input:disabled { background-color: var(--canvas); opacity: 0.6; }
.form-label { display: block; font-size: 0.8125rem; font-weight: 500; color: var(--muted); margin-bottom: 0.375rem; }
.form-help { font-size: 0.75rem; color: var(--muted); margin-top: 0.25rem; }
.form-error { font-size: 0.75rem; color: var(--danger); margin-top: 0.25rem; }

/* iOS ignores width:100% on native date inputs and sizes them by content,
   which pushes them out of their grid cell. */
input[type="date"], input[type="datetime-local"], input[type="time"] {
    -webkit-appearance: none; appearance: none;
    min-width: 0; max-width: 100%; width: 100%;
    box-sizing: border-box; min-height: 42px; line-height: normal;
}

/* === The capture shell ===
   A fixed-height app, not a tall page.

   `min-h-screen` is `min-height: 100vh`, and on a phone `100vh` is the viewport
   with the browser's address bar *collapsed* — taller than what is actually on
   screen. A column that ends with a button therefore ends below the fold, and a
   rep has to scroll past a screen of empty space to reach "Next". `100dvh` is
   the height that is really visible, and it changes as the bar hides.

   `100vh` first as the fallback: a browser that does not know `dvh` ignores the
   second declaration and keeps the old behaviour rather than collapsing to
   nothing. */
.capture-shell {
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  /* Clear of the notch and the home bar. */
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}

/* The middle scrolls; the bar and the buttons never move. `min-height: 0` is
   load-bearing — without it a flex child refuses to shrink below its content
   and the whole column grows instead of the middle scrolling. */
.capture-shell__body { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.capture-shell__bar, .capture-shell__actions { flex-shrink: 0; }

/* The exhibitor's own mark, at the top of their own form. */
.capture-logo { max-height: 1.6rem; width: auto; display: block; }

/* === Form columns ===
   The row model from FormSchema.rowsOf, rendered. Real CSS rather than Tailwind
   utilities because these classes are attached by JavaScript after the page has
   loaded, and the layout of a client's form should not depend on a CDN's
   mutation observer noticing in time.

   One gap variable, so a block's width is always derived from the gap actually
   in use rather than hardcoded against one of them. */
.col-row {
  --col-gap: 0.5rem;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--col-gap);
}
.col-row--wide { --col-gap: 1rem; }

.col-full  { width: 100%; }
.col-half  { width: calc(50% - var(--col-gap) / 2); }
.col-third { width: calc(33.333% - var(--col-gap) * 2 / 3); }

/* On a phone every block takes the line. A visitor self-registering is holding
   one, and a third-width phone number box is not something anyone can type in.
   Only where it is marked: inside the builder the point is to *see* the layout
   you are designing, however narrow the window is. */
@media (max-width: 640px) {
  .col-row--stack .col-half, .col-row--stack .col-third { width: 100%; }
}

/* === Image upload ===
   Used by settings, the form builder's image block and the picture on a choice
   option. A drop zone rather than a text field: pasting a URL asked a client to
   go and host the picture somewhere else first, which nobody did. */
.upload-zone {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  min-height: 8rem; padding: 1rem; text-align: center; cursor: pointer;
  border: 1.5px dashed var(--hairline); border-radius: 0.75rem;
  background: var(--canvas); transition: border-color 0.15s, background-color 0.15s;
}
.upload-zone:hover, .upload-zone:focus-visible { border-color: var(--gold-500); outline: none; }
.upload-zone--over { border-color: var(--gold-500); background: var(--tint-gold); }
.upload-preview {
  max-height: 7rem; max-width: 100%; object-fit: contain;
  border-radius: 0.5rem; background: #fff; padding: 0.25rem;
}
/* Compact variant for the option rows in the builder, where a full drop zone
   would be taller than the row it belongs to. */
.upload--tiny .upload-zone { min-height: 3.25rem; padding: 0.5rem; }
.upload--tiny .upload-preview { max-height: 2.5rem; }
.upload--tiny .upload-zone p { font-size: 0.7rem; }

/* === Password field ===
   The eye sits inside the box rather than beside it, so the input keeps its
   full width and the control is where a thumb already is on a phone. */
.pw { position: relative; }
.pw__input { padding-right: 2.75rem; }
.pw__eye {
  position: absolute; right: 0.25rem; top: 50%; transform: translateY(-50%);
  width: 2.25rem; height: 2.25rem; display: flex; align-items: center; justify-content: center;
  color: var(--muted); border-radius: 0.5rem;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.pw__eye:hover { color: var(--text); background: var(--canvas); }

/* Four bars, filling left to right. Colour carries the meaning and the count
   carries it again, because a red/green-only signal is no signal to about one
   man in twelve. */
.pw__meter { display: flex; gap: 0.25rem; margin-top: 0.4rem; }
.pw__meter span {
  height: 3px; flex: 1; border-radius: 999px; background: var(--hairline);
  transition: background-color 0.25s ease;
}
.pw__meter[data-level="1"] span.is-on { background: var(--danger); }
.pw__meter[data-level="2"] span.is-on { background: var(--warning); }
.pw__meter[data-level="3"] span.is-on { background: var(--gold-500); }
.pw__meter[data-level="4"] span.is-on { background: var(--success); }

/* === Badges === */
.badge {
    display: inline-flex; align-items: center; gap: 0.25rem;
    padding: 0.125rem 0.5rem; border-radius: 999px;
    font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.01em;
}
/*
 * Every badge and flash tint is a variable, because the light versions were
 * hard-coded hex and stayed pale mint and pale pink on the dark theme — the one
 * place a dark mode always breaks first.
 */
.badge-draft   { background: var(--tint-neutral); color: var(--muted); }
.badge-active  { background: var(--tint-success); color: var(--success); }
.badge-closed  { background: var(--tint-info);    color: var(--tone-info); }
.badge-gold    { background: var(--tint-gold);    color: var(--gold-text); }
.badge-warning { background: var(--tint-warning); color: var(--warning); }
.badge-danger  { background: var(--tint-danger);  color: var(--danger); }

/* === Tables === */
.table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.table th {
    text-align: left; font-size: 0.6875rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted);
    padding: 0.75rem 1rem; border-bottom: 1px solid var(--hairline);
}
.table td { padding: 0.875rem 1rem; border-bottom: 1px solid var(--hairline); color: var(--text); }

/*
 * `.table th` sets text-align:left, and app.css loads after the Tailwind CDN —
 * same specificity, later wins — so `class="text-center"` on a <th> silently did
 * nothing while the cells under it centred properly. Every right-aligned header
 * in the app was misaligned against its own column for the same reason.
 */
.table th.text-center, .table td.text-center { text-align: center; }
.table th.text-right,  .table td.text-right  { text-align: right; }
.table tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: var(--canvas); }
.num { font-variant-numeric: tabular-nums; }

/* === Empty state === */
.empty {
    text-align: center; padding: 3rem 1.5rem; color: var(--muted);
    border: 1px dashed var(--hairline); border-radius: var(--radius);
}
.empty i { font-size: 2rem; color: var(--navy-200); display: block; margin-bottom: 0.75rem; }
.dark .empty i { color: var(--navy-600); }

/* === Page containers ===
 *
 * A single-column form page has to be centred, not just capped. `max-w-lg` on
 * its own pins the card to the left edge and leaves half a widescreen empty,
 * which is what these two classes exist to stop happening again — and there is
 * a test that fails the build if a view caps a width without centring it.
 */
.page-narrow { max-width: 44rem; margin-inline: auto; }
.page-mid    { max-width: 60rem; margin-inline: auto; }

/* === Page header === */
.page-head {
    display: flex; flex-wrap: wrap; align-items: flex-start;
    justify-content: space-between; gap: 1rem; margin-bottom: 1.5rem;
}
.page-head h1 { font-size: 1.5rem; line-height: 1.2; }
.page-head p { font-size: 0.875rem; color: var(--muted); margin-top: 0.375rem; max-width: 60ch; }

/* === Stat === */
.stat { padding: 1.125rem 1.25rem; }
.stat__value { font-size: 1.875rem; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1; }
.stat__label {
    font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--muted); margin-top: 0.5rem;
}

/* === Flash === */
.flash {
    display: flex; align-items: flex-start; gap: 0.625rem;
    padding: 0.75rem 1rem; border-radius: 0.5rem; font-size: 0.875rem;
    margin-bottom: 0.75rem; border: 1px solid transparent;
}
.flash-success { background: var(--tint-success); color: var(--success); border-color: var(--edge-success); }
.flash-error   { background: var(--tint-danger);  color: var(--danger);  border-color: var(--edge-danger); }
.flash-info    { background: var(--tint-info);    color: var(--tone-info); border-color: var(--edge-info); }

/* === Motion ===
   Scroll reveal, the stacking cards and the hover lift. Paired with
   public/js/motion.js, which adds `will-reveal` itself — so if that script never
   runs, nothing is hidden and the page is simply a page. A CSS-first version
   hides the content up front and strands it there on any script error.

   Order matters here, and it bit once. `[data-reveal="left"].will-reveal` and
   `.will-reveal.is-revealed` are both one attribute-or-class pair, so they have
   identical specificity and the later one wins outright. With the directional
   rules written last, an element that had revealed kept its -24px offset
   forever: a shrug on a wide screen, and text clipped clean off the left edge of
   a phone. The revealed state is now last *and* more specific, so it cannot lose
   again — and test/motion.test.js checks that it stays that way. */
.will-reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
              transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Directional variants, for the alternating rows. */
[data-reveal="left"].will-reveal  { transform: translateX(-24px); }
[data-reveal="right"].will-reveal { transform: translateX(24px); }
[data-reveal="scale"].will-reveal { transform: scale(0.97); }

/* On a narrow screen nothing travels sideways. There is no room for it to come
   from, and a 24px offset is a quarter of the margin on a 390px phone. */
@media (max-width: 640px) {
  [data-reveal="left"].will-reveal,
  [data-reveal="right"].will-reveal { transform: translateY(18px); }
}

/* An image that wipes up rather than slides — no horizontal travel at all, so
   it cannot overflow a narrow viewport however wide the picture is. */
[data-reveal="clip"].will-reveal {
  transform: none;
  clip-path: inset(0 0 12% 0);
  transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
              clip-path 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}
/* `none`, not `inset(0)`. An inset of zero is still a clip, and it cropped
   `.framed::before` — the offset gold rule — out of existence the moment the
   picture finished revealing. */
[data-reveal="clip"].will-reveal.is-revealed { clip-path: none; }

/* Last, and deliberately the most specific thing in this block. */
.will-reveal.is-revealed,
[data-reveal].will-reveal.is-revealed { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .will-reveal,
  [data-reveal].will-reveal { opacity: 1; transform: none; clip-path: none; transition: none; }
  * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; }
}

/* A sideways reveal can widen the page by its own offset before it lands. The
   base rules already clip that (`overflow-x: clip` on html and body, near the
   top of this file) — deliberately `clip` and not `hidden`, because `hidden`
   makes the body a scroll container and quietly kills every `position: sticky`
   on the site, including the header and the card stack below.

   That guard is why the stuck reveal above showed up as *clipped* text on a
   phone rather than a horizontal scrollbar: the offset was there either way, the
   page just refused to widen for it. */

/* A card that answers the pointer. Kept to 2px — a card that leaps is a card
   you notice instead of reading. */
.lift { transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease; }
.lift:hover { transform: translateY(-2px); box-shadow: 0 10px 30px -12px rgba(14, 23, 38, 0.25); }

/* === Scroll progress ===
   Two pixels of gold across the top. Cheap, and it tells a long page that it is
   a long page. Width is set from rAF in motion.js. */
.scroll-progress {
  position: fixed; top: 0; left: 0; height: 2px; width: 0;
  background: linear-gradient(90deg, var(--gold-600), var(--gold-400));
  z-index: 60; pointer-events: none;
  transition: width 0.08s linear;
}

/* === Stacking cards ===
   The text column holds still while the cards scroll up and pile on top of each
   other. Pure `position: sticky` — no scroll listener, no library, no jank,
   because the browser is doing it rather than JavaScript guessing at it.

   Each card sticks a little lower than the one before, so the stack fans out
   instead of hiding the ones underneath. `--i` is set per card in the markup. */
.stack-sticky { position: sticky; top: 7rem; }

.stack-card {
  position: sticky;
  top: calc(7rem + var(--i) * 1.25rem);
  /* An opaque background is what makes it read as a stack rather than as
     transparent cards sliding over each other. `--surface` already flips with
     the theme, and in dark it is a step lighter than the section behind it,
     which is exactly the separation the pile needs. */
  background: var(--surface);
}

/* The cards further down the pile shrink slightly, which reads as depth. */
.stack-card__inner { transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); }

@media (max-width: 1023px) {
  /* On a phone the text scrolling away is fine — there is no second column for
     it to hold still beside, and a pinned heading would eat a third of the
     screen. The cards still stack. */
  .stack-sticky { position: static; }
  .stack-card { top: calc(4.5rem + var(--i) * 0.75rem); }
}
@media (prefers-reduced-motion: reduce) {
  .stack-sticky, .stack-card { position: static; }
}

/* === Full-screen mobile menu ===
   `hidden` until opened, so a page whose menu is never touched pays nothing for
   it and nothing animates behind the scenes. `is-open` runs the entrance; the
   rows come in on their own beat from `--d`, which is what separates a menu that
   was built from a menu that was toggled. */
.nav-panel {
  /* Above the header (40) and the progress bar (60), below the cookie banner. */
  position: fixed; inset: 0; z-index: 80;
  background: rgba(7, 12, 22, 0.35);
  backdrop-filter: blur(2px);
  opacity: 0;
  transition: opacity 0.28s ease;
}
.nav-panel.is-open { opacity: 1; }

.nav-panel__sheet {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  background: var(--surface);
  /* Below the notch, above the home bar. */
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  transform: translateY(-14px);
  opacity: 0;
  transition: transform 0.34s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.24s ease;
}
.dark .nav-panel__sheet { background: var(--navy-950); }
.nav-panel.is-open .nav-panel__sheet { transform: none; opacity: 1; }

.nav-panel__link {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.95rem 0;
  font-size: 1.5rem; font-weight: 500; letter-spacing: -0.01em;
  color: var(--text);
  border-bottom: 1px solid var(--hairline);
}
.nav-panel__link:active { color: var(--gold-text); }

/* Everything inside arrives in sequence. `--d` is per row, set in the markup. */
.nav-panel__link,
.nav-panel__row {
  opacity: 0;
  transform: translateY(12px);
}
.nav-panel.is-open .nav-panel__link,
.nav-panel.is-open .nav-panel__row {
  opacity: 1;
  transform: none;
  transition: opacity 0.4s ease var(--d, 0ms),
              transform 0.45s cubic-bezier(0.16, 1, 0.3, 1) var(--d, 0ms);
}

@media (prefers-reduced-motion: reduce) {
  .nav-panel, .nav-panel__sheet,
  .nav-panel__link, .nav-panel__row { transition: none; transform: none; opacity: 1; }
}

/* Locking the page behind the menu.

   `overflow: hidden` on the body is the usual answer and it is wrong here twice
   over. The shorthand overwrites the `overflow-x: clip` set in the base rules,
   which is what keeps a sideways reveal from widening the page. And on iOS the
   body is not the scrolling element, so hiding its overflow locks nothing while
   still collapsing its scroll position — you open the menu, close it, and find
   yourself back at the top.

   Pinning the body with `position: fixed` at a negative `top` holds the page
   exactly where it was; motion.js sets that offset and scrolls back to it on
   close. `overflow-x` is left alone. */
body.nav-open {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
}

/* === Form preview ===
   The builder's preview modal. The frame inside is deliberately the capture
   station's palette rather than the dashboard's — the point is to see what the
   visitor sees, and a light-theme approximation of a dark screen is exactly the
   kind of preview that gets believed and then surprises someone in a hall. */
.preview-overlay {
  position: fixed; inset: 0; z-index: 90;
  display: flex; align-items: center; justify-content: center;
  padding: 1.5rem;
  background: rgba(7, 12, 22, 0.6);
  backdrop-filter: blur(3px);
  animation: preview-in 0.2s ease both;
}
@keyframes preview-in { from { opacity: 0; } to { opacity: 1; } }

.preview-panel {
  display: flex; flex-direction: column;
  width: 100%; max-width: 60rem; max-height: 100%;
  border-radius: 1rem; overflow: hidden;
  background: var(--surface);
  box-shadow: 0 24px 70px -20px rgba(0, 0, 0, 0.55);
  animation: preview-rise 0.28s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes preview-rise { from { transform: translateY(12px) scale(0.99); } to { transform: none; } }

.preview-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--hairline);
  flex-shrink: 0;
}

.preview-devices { display: inline-flex; gap: 0.25rem; padding: 0.2rem; border-radius: 0.6rem; background: var(--canvas); }
.preview-device {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.35rem 0.7rem; border-radius: 0.45rem;
  font-size: 0.75rem; color: var(--muted);
  transition: background-color 0.15s ease, color 0.15s ease;
}
.preview-device.is-active { background: var(--surface); color: var(--text); box-shadow: 0 1px 2px rgba(14,23,38,0.08); }

/* The stage scrolls; the frame inside it does not change height with content,
   so switching phone/tablet does not make the modal jump about. */
.preview-stage {
  flex: 1; min-height: 0; overflow: auto;
  display: flex; justify-content: center;
  padding: 1.5rem;
  background: var(--canvas);
}

.preview-frame {
  display: flex; flex-direction: column;
  max-width: 100%; height: 34rem; max-height: 100%;
  border-radius: 1.25rem; overflow: hidden;
  background: var(--navy-900);
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 18px 50px -22px rgba(0, 0, 0, 0.6);
}

.preview-body {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: 1.5rem 1.25rem;
  display: flex; flex-direction: column; gap: 1.25rem;
}

.preview-progress {
  text-align: center; font-size: 0.7rem;
  color: rgba(255, 255, 255, 0.45);
  padding: 0 1.25rem 0.4rem;
}

.preview-actions {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.9rem 1.25rem;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

/* Only the vertical axis. `overflow: hidden` would take `overflow-x: clip` with
   it, which is what stops a sideways reveal widening the page. */
body.modal-open { overflow-y: hidden; }

@media (max-width: 640px) {
  .preview-overlay { padding: 0; }
  .preview-panel { max-width: none; height: 100%; border-radius: 0; }
  .preview-stage { padding: 0; }
  .preview-frame { width: 100% !important; height: 100%; border-radius: 0; border: 0; }
}

/* === Depth ===
   A grain overlay on the dark sections. Flat navy at this size reads as a
   colour-picker swatch; two percent of noise reads as a surface. It is one
   inline SVG turned into a data URI — no request, no image to forget to deploy.

   `pointer-events: none` on every decorative layer here, always: a beautiful
   section that eats clicks on the button inside it is a broken section. */
.grain::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  opacity: 0.035; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Soft light that drifts between sections as you scroll. Blurred to the point
   of being a glow rather than a shape — an orb you can identify is decoration
   you have to justify. */
.orb {
  position: absolute; border-radius: 50%; pointer-events: none;
  filter: blur(70px); opacity: 0.5; will-change: transform;
}
.orb--gold { background: radial-gradient(circle, rgba(201,162,39,0.55), transparent 70%); }
.orb--navy { background: radial-gradient(circle, rgba(47,91,150,0.5), transparent 70%); }
@media (prefers-reduced-motion: reduce) { .orb { display: none; } }

/* === Curves ===
   Sections meet on an arc rather than a straight edge. Drawn with a pseudo
   element and a very large border-radius, so it scales with the viewport and
   needs no SVG that has to be kept in step with the background colour either
   side. The parent supplies the colour through `--curve`. */
.curve-b, .curve-t { position: relative; }
.curve-b::after, .curve-t::after {
  content: ""; position: absolute; left: -5%; right: -5%; height: 4.5rem;
  background: var(--curve, var(--surface)); pointer-events: none;
}
.curve-b::after { bottom: -1px; border-radius: 50% 50% 0 0 / 100% 100% 0 0; }
.curve-t::after { top: -1px; border-radius: 0 0 50% 50% / 0 0 100% 100%; }
@media (min-width: 768px) { .curve-b::after, .curve-t::after { height: 6.5rem; } }

/* === Headline accent ===
   One phrase in gold, as a gradient rather than a flat fill so it has some
   light in it. `background-clip: text` needs a transparent colour, and the
   fallback matters: without it an unsupported browser shows nothing at all. */
.accent {
  color: var(--gold-600);
  background: linear-gradient(100deg, var(--gold-600), var(--gold-400) 55%, var(--gold-600));
  -webkit-background-clip: text; background-clip: text;
}
@supports (-webkit-background-clip: text) or (background-clip: text) {
  .accent { -webkit-text-fill-color: transparent; }
}
.hero-navy .accent, .dark .accent {
  background: linear-gradient(100deg, var(--gold-400), #f0dfa4 55%, var(--gold-400));
  -webkit-background-clip: text; background-clip: text;
}

/* === Framed image ===
   A gold rule offset behind the picture. Cheap, and it stops a screenshot on a
   white page from floating with nothing holding it. */
.framed { position: relative; }
.framed::before {
  content: ""; position: absolute; inset: 0;
  transform: translate(0.85rem, 0.85rem);
  border: 1.5px solid var(--gold-500); border-radius: 1rem;
  opacity: 0.4; pointer-events: none;
  transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.framed:hover::before { transform: translate(0.4rem, 0.4rem); }
.framed > img, .framed > .framed__inner { position: relative; border-radius: 1rem; }

/* === Notched card ===
   A corner cut away rather than rounded. Used sparingly — it is a strong shape
   and three of them on one screen is a pattern, not an accent. */
.notch {
  border-radius: 1rem;
  clip-path: polygon(0 0, calc(100% - 1.75rem) 0, 100% 1.75rem, 100% 100%, 0 100%);
}

/* === Cards with more life ===
   The border warms and a thin gold line grows along the top. Two cheap signals
   beat one expensive one: the eye reads the colour change before the movement. */
.lift-gold {
  position: relative; overflow: hidden;
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s ease, border-color 0.3s ease;
}
.lift-gold::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: linear-gradient(90deg, var(--gold-500), transparent);
  transform: scaleX(0); transform-origin: left;
  transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}
.lift-gold:hover {
  transform: translateY(-4px);
  border-color: var(--gold-500);
  box-shadow: 0 18px 40px -20px rgba(14, 23, 38, 0.35);
}
.lift-gold:hover::before { transform: scaleX(1); }

/* === The thread ===
   A line that runs down through the numbered steps and fills as they arrive, so
   the three of them read as one sequence rather than three cards that happen to
   be adjacent. Set from JS via `--fill`. */
.thread { position: relative; }
.thread::before {
  content: ""; position: absolute; left: 1.35rem; top: 2.75rem; bottom: 2.75rem;
  width: 2px; background: var(--hairline); z-index: 0;
}
.thread::after {
  content: ""; position: absolute; left: 1.35rem; top: 2.75rem;
  width: 2px; height: calc(var(--fill, 0) * 1%);
  background: linear-gradient(180deg, var(--gold-500), var(--gold-400));
  max-height: calc(100% - 5.5rem);
  /* Behind the cards, like its own track. Painted over them, the connector
     became a line drawn across the content it was meant to connect. */
  z-index: 0;
  transition: height 0.25s linear;
}
.thread > .stack-card { z-index: 1; }
@media (min-width: 1024px) { .thread::before, .thread::after { display: none; } }

/* === Section eyebrow rule === */
.eyebrow-rule { display: inline-flex; align-items: center; gap: 0.6rem; }
.eyebrow-rule::before {
  content: ""; width: 1.75rem; height: 1px; background: var(--gold-500); opacity: 0.7;
}

/* === Bigger, softer buttons on the marketing site === */
.btn-lg { border-radius: 0.7rem; }
.hero-navy .btn-gold { box-shadow: 0 10px 30px -12px rgba(201, 162, 39, 0.6); }
.hero-navy .btn-gold:hover { transform: translateY(-1px); }
.btn { transition: transform 0.15s ease, background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; }

/* === GSAP scaffolding ===
   Only the bits that have to exist in CSS: the word wrapper needs its own
   overflow so a lifted word is clipped by its line rather than by the section,
   and the rail needs a track that can be wider than the viewport. */
.gsap-word { display: inline-block; will-change: transform; }
/* Room above as well as below: the clip is there to hide a word travelling up
   from underneath, and with no top padding it takes the tonos off Greek capitals
   and the ascenders off `f` and `l`. The negative margin keeps the heading's
   spacing where the designer put it. */
[data-gsap-words] { overflow: hidden; padding-top: 0.18em; margin-top: -0.18em; padding-bottom: 0.1em; }

.rail { overflow: hidden; }
.rail__track { display: flex; gap: 1.25rem; will-change: transform; }
.rail__card { flex: 0 0 clamp(15rem, 24vw, 20rem); }

/* Below the pin breakpoint it is an ordinary swipeable row. A pinned section on
   a phone fights the address bar collapsing and reads as a frozen page. */
@media (max-width: 1023px) {
  .rail { overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; }
  .rail::-webkit-scrollbar { display: none; }
  .rail__track { transform: none !important; padding-inline: 1.25rem; }
  .rail__card { scroll-snap-align: start; }
}

/* A picture cropped by its frame, so it can drift inside without moving the
   layout by a pixel. */
.crop { overflow: hidden; border-radius: 1.25rem; }
.crop > img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* === Cut-in tab ===
   A label that reads as carved out of the card rather than stuck on top of it.

   Three pieces make the shape: the tab's own convex bottom-right corner, and
   two square pseudo elements either side of it, each masked with a radial
   gradient so a quarter-disc is bitten out. Those two bites are the concave
   fillets — they are what turns a floating rectangle into one continuous
   outline, and they are the whole difference between this and a sticker.

   The mask is the load-bearing part, so it is behind an @supports: a browser
   without it gets a plain tab with a rounded corner, which is a perfectly good
   label rather than two mysterious squares. */
.tab-card { position: relative; }

.tab {
  --tab-r: 1.15rem;
  position: absolute; top: 0; left: 0; z-index: 3;
  max-width: calc(100% - 2.5rem);
  display: inline-flex; align-items: baseline; gap: 0.45rem;
  padding: 0.62rem 1rem 0.68rem 1.05rem;
  background: var(--tab-bg, var(--page));
  color: var(--tab-fg, var(--text));
  /* Follows the card's own corner, so the two share an outline. */
  border-top-left-radius: inherit;
  border-bottom-right-radius: var(--tab-r);
  font-size: 0.82rem; font-weight: 600; line-height: 1.25;
  letter-spacing: -0.005em;
}

@supports (mask: radial-gradient(#000, #000)) or (-webkit-mask: radial-gradient(#000, #000)) {
  .tab::before, .tab::after {
    content: ""; position: absolute; pointer-events: none;
    width: var(--tab-r); height: var(--tab-r);
    background: var(--tab-bg, var(--page));
    /* Everything except a quarter-disc at the bottom-right corner. The half
       pixel closes the seam antialiasing would otherwise leave. */
    -webkit-mask: radial-gradient(circle at 100% 100%, transparent calc(var(--tab-r) - 0.5px), #000 var(--tab-r));
            mask: radial-gradient(circle at 100% 100%, transparent calc(var(--tab-r) - 0.5px), #000 var(--tab-r));
  }
  /* Along the top edge, to the right of the tab. */
  .tab::after { top: 0; right: calc(var(--tab-r) * -1); }
  /* Down the left edge, below the tab. */
  .tab::before { left: 0; bottom: calc(var(--tab-r) * -1); }
}

/* A quiet second line inside the tab, for a number or a count. */
.tab__note { font-weight: 400; font-size: 0.72rem; color: var(--muted); }

/* On a dark card the tab is the page; on a light one it is the section behind
   it. Both are set per use with --tab-bg rather than guessed at here. */
.tab--onDark { --tab-fg: var(--text); }

/* === Tiles ===
   The collections rail. Label sits on the image under a gradient, which is the
   only way to keep white text legible over a photograph nobody has picked yet. */
.tile { position: relative; display: block; border-radius: 1.25rem; overflow: hidden; }
.tile img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}
.tile:hover img { transform: scale(1.06); }
.tile::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, transparent 42%, rgba(5, 15, 29, 0.82));
}
.tile__label {
  position: absolute; left: 1.15rem; right: 1.15rem; bottom: 1.05rem; z-index: 1;
  color: #fff; font-size: 18px; font-weight: 600; opacity: 0.9;
  line-height: 1.35;
}

/* === The fanning stack === */
.fan { position: relative; display: grid; place-items: center; min-height: 22rem; }
.fan > * { grid-area: 1 / 1; will-change: transform; }

/* === Rounder, throughout the marketing site ===
   The reference this is chasing is soft everywhere; the dashboard stays tighter
   because a table with 20px corners looks like a toy. */
.hero-navy .card, section .card { border-radius: 1.15rem; }
.curve-b::after, .curve-t::after { height: calc(4.5rem + var(--curve-lift, 0rem)); }
@media (min-width: 768px) {
  .curve-b::after, .curve-t::after { height: calc(6.5rem + var(--curve-lift, 0rem)); }
}

/* === Section rhythm ===
   One scale, applied everywhere, because the page had py-8, py-14, py-20,
   py-24, py-28 and a pt-16/pb-4 all within four sections of each other — which
   is what "strange paddings" looks like from the outside.

   `.band` is the default. `.band--lead` is the hero and the closing call, which
   earn more air. `.band--tight` is the strips that are a rule with content on
   it rather than a section.

   A curved section needs its own bottom padding *plus* the arc, or the arc eats
   the last line of text — hence the extra allowance under `.curve-b`. That was
   the other half of the padding problem. */
.band { padding-top: 4.5rem; padding-bottom: 4.5rem; }
.band--lead { padding-top: 5rem; padding-bottom: 5rem; }
.band--tight { padding-top: 2.5rem; padding-bottom: 2.5rem; }

@media (min-width: 640px) {
  .band { padding-top: 6rem; padding-bottom: 6rem; }
  .band--lead { padding-top: 7rem; padding-bottom: 7rem; }
  .band--tight { padding-top: 3rem; padding-bottom: 3rem; }
}

/* The arc is drawn on top of the section's own box, so anything sitting in the
   last 4.5–6.5rem of a curved section is underneath it. */
.curve-b > .band, .curve-b > * > .band { padding-bottom: 8rem; }
.curve-t > .band, .curve-t > * > .band { padding-top: 8rem; }
@media (min-width: 768px) {
  .curve-b > .band, .curve-b > * > .band { padding-bottom: 10.5rem; }
  .curve-t > .band, .curve-t > * > .band { padding-top: 10.5rem; }
}

/* The one width the marketing page uses. */
.shell { max-width: 72rem; margin-inline: auto; padding-inline: 1.25rem; }

/* === Section dots ===
   Position on a long page. The label appears on hover rather than always, so
   the thing is a hint at rest and a menu when you look at it. */
.dots {
  position: fixed; right: 1.1rem; top: 50%; transform: translateY(-50%);
  /* Under the mobile menu (80) and the cookie banner, over the page. */
  z-index: 30; display: none; flex-direction: column; gap: 0.85rem;
}
@media (min-width: 1280px) { .dots { display: flex; } }

.dots__dot {
  position: relative; width: 8px; height: 8px; border-radius: 999px;
  background: var(--hairline);
  transition: background-color 0.3s ease, transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.dots__dot:hover { transform: scale(1.4); background: var(--gold-400); }
.dots__dot.is-active { background: var(--gold-500); transform: scale(1.6); }

.dots__label {
  position: absolute; right: 1.4rem; top: 50%; transform: translateY(-50%) translateX(6px);
  white-space: nowrap; font-size: 0.7rem; letter-spacing: 0.02em;
  padding: 0.28rem 0.6rem; border-radius: 0.4rem;
  background: var(--navy-900); color: #fff;
  opacity: 0; pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.dots__dot:hover .dots__label { opacity: 1; transform: translateY(-50%) translateX(0); }

/* === Pointer spotlight ===
   One composited gradient following two custom properties. Sits above the
   background and below everything readable. */
[data-gsap-spotlight] { position: relative; }
[data-gsap-spotlight]::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background: radial-gradient(22rem 22rem at var(--mx, 50%) var(--my, -20%),
              rgba(201, 162, 39, 0.16), transparent 70%);
  transition: opacity 0.4s ease;
}
/* `:not(.orb)` is load-bearing. `[data-gsap-spotlight] > *` and `.orb` both
   weigh (0,1,0), so the later one wins on source order — which dropped two
   26rem blurred discs out of absolute positioning and into normal flow,
   injecting about 400px of blank page above two sections. That was the
   "strange paddings". */
[data-gsap-spotlight] > *:not(.orb) { position: relative; z-index: 1; }

/* === The travelling mark ===
   Lives in the right margin and turns as the page scrolls. Hidden below the
   width where there is a margin for it to live in. */
.traveller {
  position: fixed; right: 3.2rem; top: 50%; margin-top: -1.4rem;
  width: 2.8rem; height: 2.8rem; z-index: 40; opacity: 0;
  display: none; pointer-events: none; color: var(--gold-500);
}
@media (min-width: 1280px) { .traveller { display: block; } }

/* === Scroll hint === */
/* Its own breakpoint rather than Tailwind's `hidden lg:flex`: a bare
   `display: inline-flex` here ties with `.hidden` on specificity and wins on
   source order, which put the desktop-only hint on every phone. */
.hint {
  display: none; align-items: center; gap: 0.5rem;
  font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase;
}
@media (min-width: 1024px) { .hint { display: inline-flex; } }
.hint__line {
  width: 1px; height: 2.2rem; background: linear-gradient(180deg, currentColor, transparent);
  animation: hint-drop 2.1s ease-in-out infinite;
}
@keyframes hint-drop {
  0%, 100% { transform: scaleY(0.4); transform-origin: top; opacity: 0.4; }
  50% { transform: scaleY(1); transform-origin: top; opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .dots, .traveller { display: none; }
  .hint__line { animation: none; }
  [data-gsap-spotlight]::before { display: none; }
}

/* === Marquee ===
   Duplicated content in the markup and translated by exactly -50%, so the loop
   is seamless with no JS and no measuring. */
.marquee { overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
           mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
.marquee__track { display: flex; width: max-content; animation: marquee 34s linear infinite; }
.marquee:hover .marquee__track { animation-play-state: paused; }
/* Greyed back at rest, like the integrations grid: sixteen brand colours
   sliding past the top of the page would be the loudest thing on it. Height is
   capped rather than width — the marks are different shapes, and a row of logos
   reads as a row only when their optical size matches. */
.marquee__logo {
  height: 1.75rem; width: auto; max-width: none;
  filter: grayscale(1); opacity: 0.55;
  transition: filter 0.25s ease, opacity 0.25s ease;
}
.marquee:hover .marquee__logo { filter: none; opacity: 1; }

/*
 * Dark mode: white silhouettes rather than plates.
 *
 * A strip of sixteen white chips sliding across the top of the page would be
 * louder than anything else on it, and this row is deliberately quiet — so the
 * marks are flattened to white instead. `brightness(0)` crushes every colour to
 * black and `invert(1)` lifts it to white, which works whatever ink the logo
 * was drawn in. Hovering pauses the strip and restores the real artwork on a
 * plate, so the brands are still shown honestly to anyone who looks.
 */
.dark .marquee__logo { filter: brightness(0) invert(1); opacity: 0.45; }
.dark .marquee:hover .marquee__logo {
  filter: none; opacity: 1;
  background: #fff; border-radius: 0.35rem; padding: 0.15rem 0.3rem;
  color: #5a6478;
}
@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* === Sticky header === */
.site-header { transition: box-shadow 0.25s ease, background-color 0.25s ease, border-color 0.25s ease; }
.site-header.is-scrolled { box-shadow: 0 4px 24px -14px rgba(14,23,38,0.4); }

/* === Segmented toggle ===
   Single events or packs. */
.seg { display: inline-flex; padding: 0.25rem; border-radius: 999px; background: var(--canvas); border: 1px solid var(--hairline); }
.seg button {
  padding: 0.4rem 1.1rem; border-radius: 999px; font-size: 0.8125rem; font-weight: 500;
  color: var(--muted); transition: background-color 0.2s ease, color 0.2s ease;
}
.seg button.is-active { background: var(--navy-700); color: #fff; }
/* Four tabs do not fit on a phone in one row, and a segmented control that
   overflows its own border looks broken rather than scrollable. */
.seg { max-width: 100%; overflow-x: auto; scrollbar-width: none; }
.seg::-webkit-scrollbar { display: none; }
.seg button { white-space: nowrap; }

/* === Event lifecycle ===
   Three stages in a row, the current one lit. Wraps to a column on a phone,
   where three side by side would leave each hint two words wide. */
.lifecycle { display: grid; gap: 0.75rem; grid-template-columns: 1fr; list-style: none; padding: 0; }
@media (min-width: 768px) { .lifecycle { grid-template-columns: repeat(3, 1fr); } }
.lifecycle__step {
  display: flex; gap: 0.75rem; align-items: flex-start;
  padding: 0.85rem 1rem; border-radius: 0.85rem;
  border: 1px solid var(--hairline); background: var(--surface);
  opacity: 0.6;
}
.lifecycle__step.is-done { opacity: 0.85; }
.lifecycle__step.is-current {
  opacity: 1;
  border-color: var(--accent-400);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-500) 14%, transparent);
}
.lifecycle__dot {
  flex-shrink: 0; width: 1.6rem; height: 1.6rem; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.75rem; font-weight: 600;
  background: var(--canvas); color: var(--muted); border: 1px solid var(--hairline);
}
.lifecycle__step.is-done .lifecycle__dot { background: var(--success); color: #fff; border-color: transparent; }
.lifecycle__step.is-current .lifecycle__dot { background: var(--accent-500); color: #fff; border-color: transparent; }
.lifecycle__label { display: block; font-weight: 600; font-size: 0.875rem; color: var(--text); }
.lifecycle__hint { display: block; font-size: 0.75rem; color: var(--muted); line-height: 1.4; margin-top: 0.1rem; }

/* === The three options, in the hero ===
   Two struck through and one not. The strikethrough is on the *name* rather
   than on the whole block, because a crossed-out sentence is unreadable and the
   sentence is the part doing the persuading. */
.option {
  display: flex; align-items: center; gap: 0.9rem;
  padding: 0.95rem 1.1rem;
  border-radius: 0.9rem;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.04);
}
.option__icon { font-size: 1.15rem; color: rgba(255, 255, 255, 0.45); flex-shrink: 0; }
.option__name {
  font-weight: 600; color: rgba(255, 255, 255, 0.62);
  text-decoration: line-through; text-decoration-thickness: 1px;
  text-decoration-color: rgba(255, 255, 255, 0.4);
}
.option__note { font-size: 0.8125rem; color: rgba(255, 255, 255, 0.42); line-height: 1.4; margin-top: 0.1rem; }
.option__tick { margin-left: auto; color: var(--accent-400); font-size: 1.1rem; }

/* The one that is not crossed out earns the accent, the weight and the light. */
.option--win {
  border-color: color-mix(in srgb, var(--accent-400) 55%, transparent);
  background: color-mix(in srgb, var(--accent-500) 14%, transparent);
  box-shadow: 0 10px 40px -22px var(--accent-500);
}
.option--win .option__icon { color: var(--accent-400); }
.option--win .option__name { color: #fff; text-decoration: none; }
.option--win .option__note { color: rgba(255, 255, 255, 0.7); }

/* === The comparison === */
.cmp__row-head {
  font-weight: 500; color: var(--text);
  white-space: nowrap; padding-right: 1.5rem;
}
.cmp__yes { color: var(--success); font-size: 1rem; }
.cmp__no  { color: var(--muted); opacity: 0.55; font-size: 0.9rem; }
.cmp__strong { color: var(--text); font-weight: 500; }
/* Our column, tinted the whole way down so the eye can follow it without
   reading — the row labels are what you read, the column is what you scan. */
.cmp__us { background: color-mix(in srgb, var(--accent-500) 8%, transparent); }
.cmp thead th { vertical-align: bottom; }

/* === Integration cards ===
   A grid of marks rather than a stack of forms: a customer is looking for their
   own tool, and finds a logo faster than a heading. */
.int-tile {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.75rem; padding: 1.5rem 1rem;
  border-radius: 1rem;
  background: var(--surface); border: 1px solid var(--hairline);
  color: var(--muted); text-align: center;
  transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.int-tile:hover { border-color: var(--accent-400); transform: translateY(-2px); box-shadow: 0 10px 30px -18px rgba(0,0,0,0.45); }
/*
 * The mark sits on a plate, and in dark mode that plate is white.
 *
 * Brand logos are drawn in their own ink on transparency, and half of them —
 * Notion's black, Airtable's dark blue, Google's grey wordmark — disappear
 * completely on a dark surface. Inverting them would turn the coloured ones
 * into nonsense, so the surface changes instead: every mark is guaranteed the
 * background it was designed for.
 */
.int-tile__mark {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 3.25rem; padding: 0.5rem 0.75rem;
  border-radius: 0.6rem;
  transition: background-color 0.2s ease;
}
.dark .int-tile__mark { background: #fff; color: #5a6478; }

.int-tile img { height: 1.9rem; width: auto; max-width: 100%; }
/* Grey until it is doing something. The colour *is* the status. */
.int-tile img { filter: grayscale(1); opacity: 0.6; transition: filter 0.2s ease, opacity 0.2s ease; }
.int-tile.is-on img, .int-tile:hover img { filter: none; opacity: 1; }
/*
 * In dark mode the dimming moves to the plate, not the mark.
 *
 * Greyscale at 60% opacity reads as "muted" against a dark card and as
 * "washed out and broken" against a white one. So the logo keeps its full
 * opacity — greyscale alone still says "not connected" — and the plate carries
 * the dimming.
 */
.dark .int-tile img { opacity: 1; }
.dark .int-tile__mark { opacity: 0.72; }
.dark .int-tile.is-on .int-tile__mark, .dark .int-tile:hover .int-tile__mark { opacity: 1; }
.int-tile__name { font-size: 0.8125rem; font-weight: 500; color: var(--text); }

/* The status pip, top right. A dot rather than a word, because sixteen tiles
   each carrying the word "Connected" is a page of text, not a status. */
.int-tile__pip {
  position: absolute; top: 0.7rem; right: 0.7rem;
  width: 0.5rem; height: 0.5rem; border-radius: 999px;
  /* Not `--hairline`: on a dark surface that is the border colour and the pip
     vanished into the edge of its own tile. */
  background: var(--muted); opacity: 0.35;
}
.int-tile.is-on .int-tile__pip { opacity: 1; background: var(--success); box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 22%, transparent); }
.int-tile.is-error .int-tile__pip { opacity: 1; background: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 22%, transparent); }

/* === Sheet ===
   A modal opened by `:target`, so a link opens it and a link closes it with no
   JavaScript involved. app.js adds Escape and a backdrop click on top. */
.sheet {
  position: fixed; inset: 0; z-index: 90;
  display: none; align-items: center; justify-content: center;
  padding: 1.25rem;
  background: rgba(7, 12, 22, 0.6);
  backdrop-filter: blur(3px);
}
.sheet:target { display: flex; animation: preview-in 0.2s ease both; }
.sheet__panel {
  width: 100%; max-width: 27rem; max-height: 100%; overflow-y: auto;
  padding: 1.5rem;
  border-radius: 1rem;
  background: var(--surface);
  box-shadow: 0 24px 70px -20px rgba(0, 0, 0, 0.55);
  animation: preview-rise 0.28s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.sheet__logo { height: 2rem; width: auto; }
.sheet__mark { display: inline-flex; padding: 0.4rem 0.6rem; border-radius: 0.5rem; }
.dark .sheet__mark { background: #fff; color: #5a6478; }

@media (max-width: 640px) {
  /* Bottom sheet on a phone, which is where a thumb is. */
  .sheet { padding: 0; align-items: flex-end; }
  .sheet__panel { max-width: none; border-radius: 1.25rem 1.25rem 0 0; max-height: 92vh; }
}

/* === Integration logos ===
   A wordmark sits in a card and stays legible in both themes: the placeholder
   SVGs paint with `currentColor`, and a real PNG is greyed back at rest so a
   grid of twelve brand colours does not fight the page. */
.logo-tile {
  display: flex; align-items: center; justify-content: center;
  padding: 1.15rem 1rem; border-radius: 0.9rem;
  background: var(--canvas); border: 1px solid var(--hairline);
  color: var(--muted);
  transition: color 0.25s ease, border-color 0.25s ease, transform 0.25s ease;
}
.logo-tile img {
  max-height: 2rem; width: auto; max-width: 100%;
  filter: grayscale(1); opacity: 0.65;
  transition: filter 0.25s ease, opacity 0.25s ease;
}
/* Same reason as the settings grid: a black wordmark on a dark card is not a
   subtle logo, it is an empty card. */
.dark .logo-tile {
  background: #fff;
  border-color: rgba(255, 255, 255, 0.12);
  /* Dark ink for the placeholder wordmarks, which draw in `currentColor` and
     would otherwise be light grey on white. */
  color: #5a6478;
}
.dark .logo-tile img { opacity: 0.55; }
.logo-tile:hover { color: var(--text); border-color: var(--accent-400); transform: translateY(-2px); }
.dark .logo-tile:hover { color: #0e1726; }
.logo-tile:hover img { filter: none; opacity: 1; }

/* === Cookie banner === */
.cookie-pop { animation: cookie-rise 0.4s cubic-bezier(0.16, 1, 0.3, 1) both; }
@keyframes cookie-rise { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }

/* === Numbered steps ===
   The connector between them is a pseudo-element rather than a real one, so the
   markup stays a plain list and the line never survives into a screen reader. */
.step-line { position: relative; }
@media (min-width: 768px) {
  .step-line:not(:last-child)::after {
    content: ""; position: absolute; top: 1.4rem; left: calc(50% + 2.2rem); right: calc(-50% + 2.2rem);
    height: 1px; background: linear-gradient(90deg, var(--gold-500), transparent);
    opacity: 0.45;
  }
}

/* === Code blocks ===
   Developer documentation. Scrolls on its own rather than widening the page —
   a curl line with a bearer token in it is longer than any phone. */
.code {
  background: var(--navy-950); color: #d7ecf0;
  border-radius: 0.75rem; padding: 1rem 1.15rem;
  font-size: 0.78rem; line-height: 1.65;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  font-variant-ligatures: none;
}
.code code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: pre; }
code.num { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.86em; }

/* === Legal prose ===
   The long documents. Wider measure than the marketing copy would take, because
   these are read in order rather than scanned. */
.legal h2 { font-size: 1.125rem; font-weight: 600; margin-top: 2.25rem; margin-bottom: 0.6rem; }
.legal h2:first-child { margin-top: 0; }
.legal p, .legal li { color: var(--muted); line-height: 1.7; }
.legal p { margin-bottom: 0.85rem; }
.legal ul { list-style: disc; padding-left: 1.15rem; margin-bottom: 0.85rem; }
.legal li { margin-bottom: 0.35rem; }
.legal a { color: var(--gold-text); text-decoration: underline; }
.legal table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; margin-bottom: 1rem; }
.legal th, .legal td { text-align: left; padding: 0.55rem 0.75rem; border-bottom: 1px solid var(--hairline); vertical-align: top; }
.legal th { font-weight: 600; color: var(--text); }
.legal td { color: var(--muted); }

/* === FAQ ===
   A <details> element, so it opens with no JavaScript at all and is already
   accessible. Only the marker is replaced. */
.faq { border-bottom: 1px solid var(--hairline); }
.faq summary {
  cursor: pointer; list-style: none; padding: 1.05rem 2.5rem 1.05rem 0;
  font-weight: 500; position: relative;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "+"; position: absolute; right: 0.4rem; top: 0.8rem;
  color: var(--muted); font-size: 1.35rem; font-weight: 300; line-height: 1;
  transition: transform 0.25s ease;
}
.faq[open] summary::after { transform: rotate(45deg); }
.faq p { padding-bottom: 1.1rem; color: var(--muted); line-height: 1.7; max-width: 68ch; }

/* === Marketing === */
.hero-navy {
    background:
        radial-gradient(900px 420px at 78% -10%, rgba(201,162,39,0.18), transparent 60%),
        linear-gradient(180deg, var(--navy-800) 0%, var(--navy-900) 100%);
    color: #fff;
}
.eyebrow {
    font-size: 0.75rem; font-weight: 600; letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--gold-400);
}

/*
 * Print.
 *
 * The device page doubles as the stand poster: hitting print should give a
 * sheet with a large QR and the URL under it, not a screenshot of a dashboard.
 * So the chrome goes and the QR is blown up to something scannable across a
 * couple of metres.
 */
@media print {
    .no-print, .sidebar, header { display: none !important; }
    body { background: #fff !important; }
    main { margin: 0 !important; padding: 0 !important; max-width: none !important; }
    .card { box-shadow: none; border-color: #ccc; break-inside: avoid; }
    .btn, .form-input, .form-select { display: none !important; }
    .print-qr { display: block !important; margin: 0 auto; width: 60mm; height: 60mm; }
    .print-url { display: block !important; text-align: center; font-size: 12pt; margin-top: 6mm; }
}
.print-url { display: none; }

/* === Merge-field chips (follow-up) === */
.token-chip {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.25rem 0.55rem; border-radius: 0.5rem;
  border: 1px solid var(--hairline); background: var(--surface);
  font-size: 0.75rem; color: var(--muted);
  transition: border-color 0.15s ease, color 0.15s ease;
}
.token-chip code { font-size: 0.7rem; color: var(--text); }
.token-chip:hover { border-color: var(--accent-400); color: var(--text); }

/* === Settings: branding preview ===
   A miniature of the capture screen in the customer's own colours. The station
   is dark, so the preview is too — a light mock would show colours against a
   background no visitor will ever see them on. */
.brand-preview {
  border-radius: 0.9rem; overflow: hidden;
  border: 1px solid var(--hairline);
  background: #0b1520;
  font-size: 0.7rem;
}
.brand-preview__bar {
  display: flex; align-items: center; min-height: 2.5rem;
  padding: 0.5rem 0.75rem; background: var(--bp-primary);
}
.brand-preview__logo { max-height: 1.4rem; max-width: 60%; width: auto; }
.brand-preview__name { color: #fff; font-weight: 600; font-size: 0.8rem; }
.brand-preview__body { padding: 0.85rem 0.75rem 1rem; }
.brand-preview__title { color: rgba(255,255,255,0.65); margin-bottom: 0.25rem; }
.brand-preview__field { height: 1.6rem; border-radius: 0.4rem; background: rgba(255,255,255,0.08); margin-bottom: 0.6rem; }
.brand-preview__button {
  margin-top: 0.4rem; padding: 0.5rem; border-radius: 0.5rem; text-align: center;
  font-weight: 600; color: #fff; background: var(--bp-accent);
}

/* The save bar sits under the last card while there is page below it. */
.settings-savebar {
  position: sticky; bottom: 0; z-index: 10;
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem;
  padding: 0.85rem 0; background: var(--canvas);
  border-top: 1px solid var(--hairline);
}

/* === Language switcher === */
.lang-switch { position: relative; }
.lang-switch > summary {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.3rem 0.55rem; border-radius: 0.5rem;
  font-size: 0.75rem; font-weight: 600; color: var(--muted);
  border: 1px solid transparent;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.lang-switch > summary::-webkit-details-marker { display: none; }
.lang-switch > summary:hover, .lang-switch[open] > summary { color: var(--text); border-color: var(--hairline); }
.lang-switch__chev { font-size: 0.6rem; transition: transform 0.15s ease; }
.lang-switch[open] .lang-switch__chev { transform: rotate(180deg); }
.lang-switch--lg > summary { font-size: 0.875rem; padding: 0.5rem 0.75rem; border-color: var(--hairline); }

.lang-switch__menu {
  position: absolute; right: 0; top: calc(100% + 0.35rem); z-index: 60;
  min-width: 11rem; padding: 0.3rem; margin: 0; list-style: none;
  border-radius: 0.75rem; background: var(--surface);
  border: 1px solid var(--hairline);
  box-shadow: 0 16px 40px -18px rgba(0, 0, 0, 0.45);
}
.lang-switch--left .lang-switch__menu { right: auto; left: 0; }
.lang-switch--up .lang-switch__menu { top: auto; bottom: calc(100% + 0.35rem); }
.lang-switch__menu a {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.45rem 0.6rem; border-radius: 0.5rem;
  font-size: 0.875rem; color: var(--text);
}
.lang-switch__menu a:hover { background: var(--canvas); }
.lang-switch__menu a.is-current { font-weight: 600; }
.lang-switch__menu a.is-current::after { content: "✓"; color: var(--accent-500); font-size: 0.8rem; margin-left: auto; }
.lang-switch__code { font-size: 0.7rem; color: var(--muted); letter-spacing: 0.04em; }
.lang-switch__menu a.is-current .lang-switch__code { display: none; }

/* The capture station's language picker: a native select, styled to sit in the
   dark status bar without looking like a form field dropped onto it. */
.capture-lang {
  appearance: none; -webkit-appearance: none;
  background: rgba(255,255,255,0.08) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23fff' stroke-width='1.5' fill='none'/%3E%3C/svg%3E") no-repeat right 0.55rem center;
  color: #fff; border: 1px solid rgba(255,255,255,0.15); border-radius: 0.5rem;
  padding: 0.3rem 1.6rem 0.3rem 0.6rem; font-size: 0.75rem;
}
.capture-lang option { color: #0b1520; }
