/* FSW Exhibitor Portal — the one stylesheet.

   Binding rules: oracle/architecture/design-doctrine.md (FSW's doctrine, on
   top of Threshold's). The short version, for anyone editing this file:

   - Every colour, size, gap and radius is a TOKEN defined in the three token
     blocks below. A hex, a px size or a px gap written into a rule where a
     token exists is off-doctrine. scripts/spacing_lint.py --portal fails the
     gate on a new off-scale gap, a font size under 14px, or a font size in a
     template's style attribute.
   - NO BORDER LINES. Separation comes from background elevation (ground ->
     --panel -> --panel-2), soft shadows and spacing. Focus rings are not
     borders.
   - Colour means something (doctrine 4.2): orange = act here / current;
     red = wrong or destructive; green = done or paid; blue = links, focus and
     "in progress"; muted = unavailable or not yet. Never decorative.
   - "Small" means DIMMER, not smaller. Nothing is under 14px except a badge
     (--fs-badge, 13px).

   Dark is the default theme; [data-theme="light"] re-points COLOURS only.
   Type, spacing, radii and motion are theme-independent (doctrine 09) and
   live in their own block. Every colour pair below was measured with the
   WCAG formula on the grounds it actually sits on; the numbers are in
   portal/dashboard/tests_des3_contract.py, which recomputes them. */

/* ── Tokens 1/3: theme-independent (type, spacing, shape) ──────────────── */
:root {
  /* Type (doctrine 1b.3): Roboto for text, Orbitron for titles and headings,
     both self-hosted below (the CSP allows font-src 'self' only), each with the
     system stack as its fallback. ONE family token each; nothing else names a
     font family. */
  --font: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --font-title: "Orbitron", var(--font);
  /* The ladder (doctrine 6.1), in rem so a larger-text mode is one line later. */
  --fs-badge: .8125rem;  /* 13px - counters and badges ONLY */
  --fs-ui: .875rem;      /* 14px - the floor: labels, tables, buttons, nav, hints */
  --fs-body: .9375rem;   /* 15px - paragraphs, card notes, messages */
  --fs-input: 1rem;      /* 16px - every form control (also the iOS no-zoom floor) */
  --fs-title: 1.0625rem; /* 17px - card and section titles, dialog title */
  --fs-h2: 1.1875rem;    /* 19px - a page's secondary heading */
  --fs-page: 1.625rem;   /* 26px - the page title */
  --fs-hero: 1.875rem;   /* 30px - the dashboard's big numbers */
  --lh-body: 1.5;
  --lh-title: 1.3;
  /* Spacing: FSW's scale (doctrine O2), the same numbers as the admin. */
  --sp-4: 4px;
  --sp-8: 8px;
  --sp-12: 12px;
  --sp-16: 16px;
  --sp-20: 20px;
  --sp-24: 24px;
  --sp-28: 28px;
  --sp-32: 32px;
  --sp-40: 40px;
  --sp-stack: var(--sp-16);   /* between two stacked blocks */
  --sp-inset: var(--sp-16);   /* a card's content from its own edge */
  --sp-page-x: var(--sp-32);  /* page gutter, left/right */
  --sp-page-y: var(--sp-28);  /* page gutter, top */
  /* Shape: doctrine 05's ladder. No pills except a true circle. */
  --r-sm: 6px;   /* chips, badges, small controls */
  --r-md: 10px;  /* buttons, inputs, cards */
  --r-lg: 12px;  /* dialogs and lone floating panels */
  /* Touch (doctrine 10): the portal is a phone surface. */
  --tap: 44px;
  --tap-sm: 40px;
  --sidebar-w: 248px;
}

/* ── Tokens 2/3: colours, dark (the default) ───────────────────────────── */
:root, :root[data-theme="dark"] {
  --bg: #0a1626;
  --sidebar: #0c1b30;
  --panel: #12243e;
  --panel-2: #1a2f4d;
  --text: #e7edf5;
  --muted: #8ea1b6;          /* 5.1-6.9:1 on every dark ground */
  --faint: #788ca3;          /* 4.5-5.3:1 on page, sidebar, panel. NOT on
                                --panel-2 (3.9): text there uses --muted. */
  /* FSW orange #FF9A00 (the press kit, doctrine 1b) leads. */
  --accent: #ff9a00;
  --accent-soft: rgba(255, 154, 0, 0.14);
  --on-accent: #0b1a2e;      /* navy on orange: 8.2:1 */
  /* Links, focus and "in progress" are FSW blue #1A5AA4's hue, lightened
     for the dark ground: 4.55-6.1:1 on every dark ground. */
  --link: #5899e4;
  /* Buttons (doctrine 5, Magnus 2026-09-25): the ONE primary per view is
     FSW orange with navy text; every other action is the secondary fill,
     which is one step off --panel-2 so it still reads as a control on a
     zebra row; red only for destructive actions. */
  --btn: var(--accent);
  --btn-text: var(--on-accent);
  --btn-2: #334661;          /* label 8.3:1 */
  --btn-2-hover: #3d5270;
  /* "You are here" in the nav, the admin's language: a panel fill, the text
     colour, and a 3px orange stripe (review A4.5). */
  --active-bg: var(--panel);
  --active-text: var(--text);
  --hover: rgba(255, 255, 255, 0.06);
  --danger: #c53030;         /* white on it: 5.5:1 */
  --on-solid: #ffffff;       /* text on a saturated solid fill: danger, toasts */
  --shadow-side: 3px 0 14px rgba(0, 0, 0, 0.16);
  --on-accent-soft: rgba(11, 26, 46, 0.16);
  --on-accent-hover: rgba(11, 26, 46, 0.28);
  --shadow: 0 2px 10px rgba(0, 0, 0, 0.24);
  --shadow-sm: 0 1px 5px rgba(0, 0, 0, 0.16);
  --shadow-drawer: 3px 0 24px rgba(0, 0, 0, 0.4);
  --overlay: rgba(4, 10, 20, 0.55);
  --backdrop: rgba(0, 0, 0, 0.5);
  --selection-ring: var(--link);
  /* Status pairs (doctrine 4.3): a low-alpha wash of the hue plus an ink
     that measures >= 4.5:1 on the wash composited over BOTH the card and a
     zebra row (--panel-2). Measured values in brackets. */
  --status-ok-bg: rgba(70, 167, 88, 0.16);       /* ink 5.6 / 6.4 */
  --status-ok-ink: #6fcf82;
  --status-attn-bg: rgba(255, 154, 0, 0.14);     /* ink 5.1 / 5.8 */
  --status-attn-ink: #ff9a00;
  --status-bad-bg: rgba(229, 72, 77, 0.14);      /* ink 5.6 / 6.3 */
  --status-bad-ink: #ff8f93;
  --status-info-bg: rgba(26, 90, 164, 0.22);     /* ink 5.8 / 6.5 */
  --status-info-ink: #8db9ed;
  --status-neutral-bg: rgba(142, 161, 182, 0.14); /* ink 5.9 / 6.8 */
  --status-neutral-ink: #b4c2d2;
  --status-special-bg: rgba(139, 92, 246, 0.20); /* ink 5.6 / 6.3 */
  --status-special-ink: #c3aef4;
  /* Toasts are solid fills with white text, the same in both themes. */
  --toast-ok-bg: #177a45;    /* 5.4:1 */
  --toast-bad-bg: #a12d2d;   /* 7.2:1 */
}

/* ── Tokens 3/3: colours, light ────────────────────────────────────────── */
:root[data-theme="light"] {
  --bg: #f4f7fb;
  --sidebar: #ffffff;
  --panel: #ffffff;
  --panel-2: #eef2f7;
  --text: #0f2036;
  --muted: #5a6b7f;          /* 4.9-5.5:1 */
  --faint: #5f7085;          /* 4.5-5.1:1 (was #8a9bb0, 2.5-2.8:1) */
  /* FSW orange darkened ONLY as far as the 3:1 control rule needs, same hue
     (36 deg): 3.20 on the page, 3.43 on a card, 3.06 on --panel-2. */
  --accent: #c77800;
  --accent-soft: rgba(199, 120, 0, 0.12);
  --on-accent: #0b1a2e;      /* 5.1:1 */
  --link: #1a5aa4;           /* FSW blue itself: 6.1-6.9:1 */
  --btn: var(--accent);
  --btn-text: var(--on-accent);
  --btn-2: #d8dfe8;          /* label 12:1 */
  --btn-2-hover: #ccd5e0;
  --active-bg: var(--panel-2); /* the sidebar is white, so the fill is one step down */
  --active-text: var(--text);
  --hover: rgba(15, 32, 54, 0.05);
  --shadow: 0 2px 10px rgba(15, 32, 54, 0.08);
  --shadow-sm: 0 1px 5px rgba(15, 32, 54, 0.06);
  --shadow-drawer: 3px 0 24px rgba(15, 32, 54, 0.18);
  --overlay: rgba(15, 32, 54, 0.45);
  --status-ok-bg: rgba(70, 167, 88, 0.14);       /* ink 4.7 / 5.2 */
  --status-ok-ink: #1a722d;                       /* was #1e7d32: 4.1 on a zebra row */
  --status-attn-bg: rgba(199, 120, 0, 0.14);     /* ink 4.6 / 5.1 */
  --status-attn-ink: #8f5600;
  --status-bad-bg: rgba(229, 72, 77, 0.12);      /* ink 5.0 / 5.6 */
  --status-bad-ink: #b3261e;
  --status-info-bg: rgba(26, 90, 164, 0.12);     /* ink 5.2 / 5.8 */
  --status-info-ink: #1a5aa4;
  --status-neutral-bg: rgba(90, 107, 127, 0.12); /* ink 5.4 / 6.0 */
  --status-neutral-ink: #4a5a6d;
  --status-special-bg: rgba(139, 92, 246, 0.14); /* ink 5.4 / 6.0 */
  --status-special-ink: #6d28d9;
}

/* ── Fonts: self-hosted, no third-party calls ──────────────────────────── */
/* Orbitron: the press kit's title face. One variable file covers every weight
   (400-900), 16 KB. SIL Open Font License 1.1, text in ../fonts/orbitron-OFL.txt;
   from github.com/google/fonts (ofl/orbitron), converted to woff2 unchanged. */
@font-face { font-family: "Orbitron"; font-style: normal; font-weight: 400 900; font-display: swap;
  src: url("../fonts/orbitron-variable.woff2") format("woff2"); }
/* Roboto: the text face. Apache 2.0; the same files the admin's vendored
   Threshold CRM bundle already ships. */
@font-face { font-family: "Roboto"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("../fonts/roboto-regular.woff2") format("woff2"); }
@font-face { font-family: "Roboto"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("../fonts/roboto-bold.woff2") format("woff2"); }
@font-face { font-family: "Roboto"; font-style: italic; font-weight: 400; font-display: swap;
  src: url("../fonts/roboto-italic.woff2") format("woff2"); }

/* ── Base ──────────────────────────────────────────────────────────────── */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
}
a { color: inherit; text-decoration: none; }
h1, h2, h3 { font-family: var(--font-title); line-height: var(--lh-title); }
/* One focus ring for everything a keyboard can reach (doctrine 5.2). */
:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }

/* ── Layout ────────────────────────────────────────────────────────────── */
.app { display: flex; min-height: 100vh; }
.sidebar {
  width: var(--sidebar-w); flex: 0 0 var(--sidebar-w); background: var(--sidebar); position: fixed;
  inset: 0 auto 0 0; height: 100vh; display: flex; flex-direction: column;
  padding: var(--sp-20) var(--sp-16); box-shadow: var(--shadow-side);
  /* Fifteen 44px nav items, the labels, logo and user chip need ~1060px:
     taller than most laptop windows. Without this, Help and Sign out were
     below the fold with no way to reach them (DES-3 audit A1). */
  overflow-y: auto; overscroll-behavior: contain;
}
.main { margin-left: var(--sidebar-w); flex: 1; min-width: 0; }

/* brand / logo */
.brand { padding: var(--sp-4) var(--sp-8) var(--sp-20); }
.brand-logo-img { height: 26px; width: auto; display: block; }
.logo-light { display: none; }
:root[data-theme="light"] .logo-dark { display: none; }
:root[data-theme="light"] .logo-light { display: block; }
.brand-sub { color: var(--faint); font-size: var(--fs-ui); margin-top: var(--sp-8); }

/* nav */
.nav-group { margin-top: var(--sp-16); }
.nav-label { color: var(--faint); font-size: var(--fs-ui); font-weight: 700; letter-spacing: 0.6px;
  text-transform: uppercase; padding: 0 var(--sp-12) var(--sp-8); }
.nav-item {
  display: flex; align-items: center; gap: var(--sp-12); padding: var(--sp-8) var(--sp-12);
  border-radius: var(--r-md); color: var(--muted); font-size: var(--fs-ui); font-weight: 500;
  margin-bottom: var(--sp-4); min-height: var(--tap); transition: background 0.12s, color 0.12s;
}
.nav-item svg { width: 18px; height: 18px; flex: 0 0 18px; stroke-width: 1.9; }
.nav-item:hover { background: var(--hover); color: var(--text); }
.nav-item.is-active { background: var(--active-bg); color: var(--active-text); font-weight: 600;
  box-shadow: inset 3px 0 0 var(--accent); }

/* user chip */
.sidebar-foot { margin-top: auto; padding-top: var(--sp-16); }
.user-chip { display: flex; align-items: center; gap: var(--sp-12); padding: var(--sp-8);
  border-radius: var(--r-md); background: var(--panel); }
.avatar {
  width: 34px; height: 34px; border-radius: 50%; flex: 0 0 34px; display: grid; place-items: center;
  background: var(--panel-2); color: var(--text); font-weight: 700; font-size: var(--fs-ui);
}
.user-name { font-weight: 600; font-size: var(--fs-ui); }
.user-role { color: var(--faint); font-size: var(--fs-ui); }
.chip-chev { margin-left: auto; color: var(--faint); }
.chip-logout { margin-left: auto; }

/* top bar */
.topbar {
  height: 64px; display: flex; align-items: center; gap: var(--sp-20); padding: 0 var(--sp-page-x);
  position: sticky; top: 0; z-index: 5; box-shadow: var(--shadow-sm);
  background: color-mix(in srgb, var(--bg) 90%, transparent); backdrop-filter: blur(10px);
}
.topbar-title { font-weight: 700; font-size: var(--fs-body); line-height: 1.1; }
.topbar-date { color: var(--faint); font-size: var(--fs-ui); }
.topbar-right { display: flex; align-items: center; gap: var(--sp-12); color: var(--muted); margin-left: auto; }
.icon-btn { display: grid; place-items: center; width: var(--tap); height: var(--tap); border-radius: var(--r-md);
  color: var(--muted); border: 0; cursor: pointer; background: transparent; }
.icon-btn:hover { background: var(--hover); color: var(--text); }
.help-link { display: flex; align-items: center; gap: var(--sp-8); color: var(--muted); font-size: var(--fs-ui); }
.nav-toggle { display: none; margin-right: var(--sp-4); }   /* phone only, see the 820px query */
.sidebar-backdrop { display: none; }

/* content */
.content { padding: var(--sp-page-y) var(--sp-page-x) calc(var(--sp-40) + var(--sp-20)); max-width: 1400px; }
.page-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--sp-20);
  margin-bottom: var(--sp-16); }
h1.welcome { font-size: var(--fs-page); font-weight: 700; margin: 0 0 var(--sp-8); }
.subline { color: var(--muted); font-size: var(--fs-body); }
.subline strong { color: var(--text); }

/* ── Buttons ───────────────────────────────────────────────────────────── */
/* min-height, never height: a label that wraps grows the button instead of
   clipping (doctrine 5.4). 44px on the portal's touch surface, 40 for small. */
.btn { display: inline-flex; align-items: center; gap: var(--sp-8); min-height: var(--tap);
  padding: var(--sp-8) var(--sp-16); border-radius: var(--r-md); border: 0; cursor: pointer;
  font-family: inherit; font-size: var(--fs-ui); font-weight: 600; line-height: 1.2;
  justify-content: center; }
.btn-sm { min-height: var(--tap-sm); padding: var(--sp-8) var(--sp-12); }
.btn-block { width: 100%; justify-content: center; }
.btn-primary { background: var(--btn); color: var(--btn-text); }
.btn-primary:hover { filter: brightness(0.94); }
.btn-primary:active { filter: brightness(0.88); }
/* .btn-ghost is kept as the SAME secondary fill because portal-shell.js (which
   this pass must not touch) builds the dialog's Cancel button with it. Every
   template uses .btn-secondary. There is no outline-only button (doctrine 5.3). */
.btn-secondary, .btn-ghost { background: var(--btn-2); color: var(--text); }
.btn-secondary:hover, .btn-ghost:hover { background: var(--btn-2-hover); }
.btn-danger { background: var(--danger); color: var(--on-solid); }
.btn-danger:hover { filter: brightness(0.90); }
/* Disabled: the secondary fill with a muted label, not a faded version of
   the colour it would have. A disabled primary is not orange (orange means
   "act here"), and its label stays readable: >=3.6:1 (audit A16). */
.btn:disabled { background: var(--btn-2); color: var(--muted); filter: none; cursor: not-allowed; }

/* ── Stat tiles and cards ──────────────────────────────────────────────── */
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-16); margin: var(--sp-24) 0; }
.tile { background: var(--panel); border-radius: var(--r-md); padding: var(--sp-20); min-height: 132px;
  box-shadow: var(--shadow); }
.tile-kicker { color: var(--faint); font-size: var(--fs-ui); font-weight: 700; letter-spacing: 0.6px;
  text-transform: uppercase; }
/* A figure, not a heading: it stays in the text face. Orbitron has no narrow
   no-break space (U+202F), which the money formatter groups thousands with,
   so "1 210,00" would read "1210,00" in it. */
.tile-big { font-size: var(--fs-hero); font-weight: 700;
  margin: var(--sp-12) 0 var(--sp-4); }
.tile-big.dash { color: var(--faint); }
.tile-sub { color: var(--muted); font-size: var(--fs-ui); }
.tile-link { color: var(--link); font-size: var(--fs-ui); font-weight: 600; display: inline-flex;
  align-items: center; gap: var(--sp-4); margin-top: var(--sp-12); }

.cols { display: grid; grid-template-columns: 1.7fr 1fr; gap: var(--sp-16); align-items: start; }
.card { background: var(--panel); border-radius: var(--r-md); padding: var(--sp-20) var(--sp-24);
  box-shadow: var(--shadow); }
.card-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: var(--sp-4); }
.card-title { font-family: var(--font-title); font-size: var(--fs-title); font-weight: 700; }
.card-note { color: var(--muted); font-size: var(--fs-body); margin-bottom: var(--sp-12); }
.card-count { color: var(--faint); font-size: var(--fs-ui); font-weight: 600; }
.card-body { font-size: var(--fs-body); line-height: 1.55; margin: 0; }  /* sets no colour on purpose,
                                                  so "card-body muted" still reads quiet */

/* setup checklist (spacing only, no dividers) */
.check-row { display: flex; align-items: flex-start; gap: var(--sp-12); padding: var(--sp-12) 0; color: inherit; }
a.check-row:hover .check-title { color: var(--link); }
.check-circle { width: 18px; height: 18px; border-radius: 50%; background: var(--panel-2); flex: 0 0 18px;
  margin-top: 2px; /* spacing-lint: allow - optical: centres the 18px dot on a 21px line */
  display: grid; place-items: center; }
.check-circle.is-done { background: var(--status-ok-bg); color: var(--status-ok-ink); }
.check-circle.is-done svg { width: 11px; height: 11px; }
.check-body { flex: 1; min-width: 0; }
.check-title { font-weight: 600; font-size: var(--fs-ui); }
.check-desc { color: var(--muted); font-size: var(--fs-ui); margin-top: var(--sp-4); }

/* ── Staff preview bar (EX-2): never mistaken for the real thing ───────── */
.preview-bar {
  display: flex; align-items: center; gap: var(--sp-12); padding: var(--sp-8) var(--sp-page-x);
  font-size: var(--fs-ui); font-weight: 600; background: var(--accent); color: var(--on-accent);
  position: sticky; top: 0; z-index: 20;
}
.preview-bar svg { flex: 0 0 16px; }
.preview-bar span { flex: 1; }
.preview-bar strong { font-weight: 800; }
.preview-exit { background: var(--on-accent-soft); color: var(--on-accent); font-weight: 700;
  padding: var(--sp-4) var(--sp-12); border-radius: var(--r-sm); }
.preview-exit:hover { background: var(--on-accent-hover); }

/* ── Flash messages and notices ────────────────────────────────────────── */
.flash-stack { margin-bottom: var(--sp-16); }
.flash { border-radius: var(--r-md); padding: var(--sp-12) var(--sp-16); font-size: var(--fs-body);
  font-weight: 500; box-shadow: var(--shadow-sm); }
.flash + .flash { margin-top: var(--sp-8); }
.flash-ok { background: var(--status-ok-bg); color: var(--status-ok-ink); }
.flash-error { background: var(--status-bad-bg); color: var(--status-bad-ink); }
.flash-info { background: var(--panel); color: var(--text); }

/* empty states */
.empty { text-align: center; color: var(--muted); padding: var(--sp-28) var(--sp-12); }
.empty-icon { width: 34px; height: 34px; color: var(--faint); margin: 0 auto var(--sp-12); display: block; }
.empty strong { display: block; color: var(--text); margin-bottom: var(--sp-4); }
.link { color: var(--link); font-weight: 600; }

.stack > * + * { margin-top: var(--sp-stack); }

/* stub page */
.stub-wrap { display: grid; place-items: center; min-height: 50vh; text-align: center; }
.stub-wrap h1 { font-size: var(--fs-page); margin: var(--sp-16) 0 var(--sp-8); }
.stub-wrap p { color: var(--muted); max-width: 440px; }

@media (max-width: 1000px) {
  .stat-grid { grid-template-columns: repeat(2, 1fr); }
  .cols { grid-template-columns: 1fr; }
}

/* ── Phone: the sidebar becomes an off-canvas drawer ───────────────────── */
@media (max-width: 820px) {
  .nav-toggle { display: grid; }
  .sidebar {
    transform: translateX(-100%);
    transition: transform 0.2s ease;
    z-index: 40; box-shadow: none;
    /* Closed, the drawer is gone: no shadow spilling ~25px into the page,
       and no off-screen links in the Tab order (audit A4). Visibility flips
       after the slide-out, and at once on the way in. */
    visibility: hidden; transition: transform 0.2s ease, visibility 0s linear 0.2s;
  }
  .app.nav-open .sidebar { transform: translateX(0); box-shadow: var(--shadow-drawer);
    visibility: visible; transition: transform 0.2s ease; }
  .sidebar-backdrop { display: block; position: fixed; inset: 0; z-index: 30; background: var(--backdrop); }
  /* The drawer script opens and closes the backdrop with the hidden
     attribute. A class rule beats the browser's own [hidden] rule, so
     without this line the backdrop was ALWAYS on under 821px: an invisible
     layer over the whole page that dimmed it and caught every tap. */
  .sidebar-backdrop[hidden] { display: none; }
  .main { margin-left: 0; }
  .topbar { padding: 0 var(--sp-16); gap: var(--sp-12); }
  .help-link { display: none; }
  .content { padding: var(--sp-20) var(--sp-16) var(--sp-40); }
  .page-head { flex-direction: column; align-items: stretch; }
  .preview-bar { padding: var(--sp-8) var(--sp-16); }
  /* wide tables scroll inside their card instead of pushing the page */
  .card { overflow-x: auto; }
}

@media (max-width: 560px) {
  .stat-grid { grid-template-columns: 1fr; }
  .topbar-date { display: none; }
}

/* ── Auth pages (a standalone card, no shell) ──────────────────────────── */
.auth-body { min-height: 100vh; display: grid; place-items: center; padding: var(--sp-28) var(--sp-16); }
.auth-wrap { width: 100%; max-width: 420px; }
.auth-brand { text-align: center; margin-bottom: var(--sp-24); }
.auth-brand .brand-logo-img { height: 30px; margin: 0 auto; }
.auth-brand-sub { color: var(--faint); font-size: var(--fs-ui); margin-top: var(--sp-8); }
.auth-card { background: var(--panel); border-radius: var(--r-lg); padding: var(--sp-28); box-shadow: var(--shadow); }
.auth-title { font-family: var(--font-title); font-size: var(--fs-h2); font-weight: 700;
  line-height: var(--lh-title); margin: 0 0 var(--sp-8); }
.auth-sub { color: var(--muted); font-size: var(--fs-body); margin: 0 0 var(--sp-16); }
.auth-links { margin-top: var(--sp-16); font-size: var(--fs-ui); text-align: center; }
.auth-foot { text-align: center; color: var(--muted); font-size: var(--fs-ui); margin-top: var(--sp-16); }

/* honeypot (bot trap): out of view and off the accessibility tree */
.hp-wrap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ── Forms ─────────────────────────────────────────────────────────────── */
.field { display: block; margin-bottom: var(--sp-16); }
.field-label { display: block; color: var(--muted); font-size: var(--fs-ui); font-weight: 600;
  margin-bottom: var(--sp-8); }
/* Every text-like control: 16px (the iOS no-zoom floor), 44px touch height.
   Checkboxes, radios and file pickers are reset just below. The base rule is
   kept at low specificity on purpose, so .is-readonly can still quiet it. */
.field input, .field select, .field textarea, .input {
  width: 100%; border: 0; border-radius: var(--r-md); background: var(--panel-2); color: var(--text);
  min-height: var(--tap); padding: var(--sp-8) var(--sp-12); font-family: inherit; font-size: var(--fs-input);
}
.field textarea, textarea.input { min-height: 88px; padding: var(--sp-12); resize: vertical; line-height: var(--lh-body); }
/* A placeholder is text on --panel-2, where --faint is under 4.5:1. */
.field input::placeholder, .field textarea::placeholder, .input::placeholder { color: var(--muted); }
.field input[type="checkbox"], .field input[type="radio"] {
  width: 16px; height: 16px; min-height: 0; flex: 0 0 16px; margin: 0; padding: 0;
  background: none; border-radius: 0; accent-color: var(--accent);
}
/* File pickers: the control itself is not a text box, but its button must
   look like one of ours, not the operating system's grey (A18.1 / A18.2). */
.field input[type="file"] { min-height: 0; padding: 0; background: none; border-radius: 0;
  font-size: var(--fs-ui); color: var(--muted); }
input[type="file"]::file-selector-button {
  margin-right: var(--sp-12); min-height: var(--tap-sm); padding: var(--sp-8) var(--sp-12); border: 0;
  border-radius: var(--r-md); background: var(--panel-2); color: var(--text); font-family: inherit;
  font-size: var(--fs-ui); font-weight: 600; cursor: pointer;
}
input[type="file"]::file-selector-button:hover { filter: brightness(1.08); }
.field-error { display: block; color: var(--status-bad-ink); font-size: var(--fs-ui); margin-top: var(--sp-4); }
.form-error { background: var(--status-bad-bg); color: var(--status-bad-ink); border-radius: var(--r-md);
  padding: var(--sp-12) var(--sp-12); font-size: var(--fs-body); margin-bottom: var(--sp-16); }
.form-ok { background: var(--status-ok-bg); color: var(--status-ok-ink); border-radius: var(--r-md);
  padding: var(--sp-12) var(--sp-12); font-size: var(--fs-body); margin-bottom: var(--sp-16); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-12); }
.form-row-1-2 { grid-template-columns: 1fr 2fr; }   /* declared before the 560px collapse: */
.form-row-3 { grid-template-columns: 1fr 1fr 1fr; } /* same specificity, source order decides */
.form-actions { display: flex; gap: var(--sp-12); margin-top: var(--sp-8); }
/* A non-owner's Profile form: the server already refuses the POST and the
   Save button is hidden; this stops the fields reading as editable. */
.is-readonly .field input, .is-readonly .field select, .is-readonly .field textarea {
  color: var(--muted); cursor: not-allowed;
}
@media (max-width: 560px) {
  .form-row, .form-row-1-2, .form-row-3 { grid-template-columns: 1fr; }
}

/* ── Tables (no lines: rows separate by a zebra tint and spacing) ──────── */
.tbl { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--fs-ui); }
.tbl th { text-align: left; color: var(--faint); font-size: var(--fs-ui); font-weight: 700;
  letter-spacing: 0.4px; text-transform: uppercase; padding: var(--sp-8) var(--sp-12); }
.tbl td { padding: var(--sp-12); vertical-align: middle; }
.tbl tbody tr { background: transparent; }
.tbl tbody tr:nth-child(odd) td { background: var(--panel-2); }
.tbl tbody tr td:first-child { border-radius: var(--r-md) 0 0 var(--r-md); }
.tbl tbody tr td:last-child { border-radius: 0 var(--r-md) var(--r-md) 0; }
.tbl th.num, .tbl td.num { text-align: right; }
/* Secondary text inside a zebra row sits on --panel-2: --muted, not --faint. */
.tbl .muted { color: var(--muted); }
.tbl .thumb, .tbl .thumb-placeholder { width: 64px; height: 64px; border-radius: var(--r-sm); display: block; }
.tbl .thumb { object-fit: cover; }
.tbl .thumb-placeholder { background: var(--panel-2); }
/* A table whose last column is the action restacks as a block per row under
   560px, so the button is on screen. Opt in with .tbl--stack; each cell
   carries data-label for the header it loses. */
@media (max-width: 560px) {
  .tbl--stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .tbl--stack tbody tr { display: block; background: var(--panel-2); border-radius: var(--r-md);
    padding: var(--sp-12) var(--sp-16); }
  .tbl--stack tbody tr + tr { margin-top: var(--sp-12); }
  .tbl--stack tbody tr td { display: block; padding: var(--sp-4) 0; background: none; border-radius: 0; }
  .tbl--stack tbody tr td.num { text-align: left; }
  .tbl--stack tbody tr td[data-label]::before {
    content: attr(data-label); display: block; color: var(--muted); font-size: var(--fs-ui);
    font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase;
  }
}
.input-qty { width: 84px; text-align: center; }

/* ── Badges: ONE family, six meanings (doctrine 4.3) ───────────────────── */
/* A tint of the hue, never a pill, never a stripe (doctrine 05); the one size
   allowed under 14px. A badge's class is chosen from what its TEXT means:
     --ok       done, paid, confirmed          --attn     act, due, owed
     --bad      wrong, failed, refused, full   --info     in progress, informational
     --neutral  not yet, inactive, a fact      --special  locked/final (shared CRM purple)
   A bare .badge falls back to neutral, so it can never render invisible. */
.badge { display: inline-flex; align-items: center; font-size: var(--fs-badge); font-weight: 600;
  padding: var(--sp-4) var(--sp-8); border-radius: var(--r-sm); white-space: nowrap;
  background: var(--status-neutral-bg); color: var(--status-neutral-ink); }
.badge--ok { background: var(--status-ok-bg); color: var(--status-ok-ink); }
.badge--attn { background: var(--status-attn-bg); color: var(--status-attn-ink); }
.badge--bad { background: var(--status-bad-bg); color: var(--status-bad-ink); }
.badge--info { background: var(--status-info-bg); color: var(--status-info-ink); }
.badge--neutral { background: var(--status-neutral-bg); color: var(--status-neutral-ink); }
.badge--special { background: var(--status-special-bg); color: var(--status-special-ink); }

/* ── Themed dialog + toast (mirrors the admin's app-shell) ─────────────── */
.fsw-modal-overlay { position: fixed; inset: 0; background: var(--overlay); display: grid; place-items: center;
  z-index: 12000; padding: var(--sp-20); animation: fsw-fade-in 0.16s ease-out; }
.fsw-modal { background: var(--panel); border-radius: var(--r-lg); box-shadow: var(--shadow); padding: var(--sp-24);
  max-width: 380px; width: 100%; animation: fsw-modal-in 0.2s cubic-bezier(0.16, 1, 0.3, 1); }
.fsw-modal__title { margin: 0 0 var(--sp-8); font-family: var(--font-title); font-size: var(--fs-title);
  font-weight: 700; color: var(--text); }
.fsw-modal__msg { margin: 0 0 var(--sp-20); color: var(--muted); font-size: var(--fs-body); line-height: var(--lh-body); }
.fsw-modal__label { display: block; margin-bottom: var(--sp-8); font-size: var(--fs-ui); font-weight: 600; color: var(--muted); }
.fsw-modal__btns { display: flex; justify-content: flex-end; gap: var(--sp-12); }
@keyframes fsw-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes fsw-modal-in { from { opacity: 0; transform: translateY(8px) scale(0.98); } to { opacity: 1; transform: none; } }

.fsw-toast-wrap { position: fixed; left: 50%; bottom: var(--sp-24); transform: translateX(-50%); z-index: 13000;
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-8); pointer-events: none; }
.fsw-toast { pointer-events: auto; max-width: 90vw; padding: var(--sp-12) var(--sp-20); border-radius: var(--r-md);
  font-size: var(--fs-ui); font-weight: 600; color: var(--on-solid); box-shadow: var(--shadow);
  opacity: 0; transform: translateY(10px); transition: opacity 0.2s ease-out, transform 0.2s ease-out; }
.fsw-toast.show { opacity: 1; transform: none; }
.fsw-toast--success { background: var(--toast-ok-bg); }
.fsw-toast--error { background: var(--toast-bad-bg); }
.fsw-toast--info { background: var(--panel-2); color: var(--text); }
@media (prefers-reduced-motion: reduce) {
  .fsw-modal-overlay, .fsw-modal { animation: none; }
  .fsw-toast { transition: none; }
  .sidebar { transition: none; }
}

/* ── Small shared classes the templates use ────────────────────────────── */
.muted { color: var(--muted); }
.strong { color: var(--text); font-weight: 600; }
.eyebrow { color: var(--link); font-size: var(--fs-ui); font-weight: 600; margin: 0 0 var(--sp-4); }
.btnrow { display: flex; align-items: center; gap: var(--sp-12); flex-wrap: wrap; }
.fu-row { display: flex; align-items: center; gap: var(--sp-12); padding: var(--sp-8) 0; }
.fu-meta { color: var(--faint); font-size: var(--fs-ui); }
/* One message in a conversation: a stacked row, separated by SPACING. */
.fu-row--msg { flex-direction: column; align-items: flex-start; gap: var(--sp-4); padding: 0; }
.fu-row--msg + .fu-row--msg { margin-top: var(--sp-20); }

/* ── Help page ─────────────────────────────────────────────────────────── */
.hp-keypoints { margin-bottom: var(--sp-28); }
.hp-keypoints-title { font-family: var(--font-title); font-size: var(--fs-title); margin: 0 0 var(--sp-12); }
/* A21.2: was a 3px border-left stripe, against the no-lines rule. Now a
   tinted card: the info wash layered over the card's own panel colour.
   Blue, not orange: "worth knowing" is informational (doctrine 4.2). */
.hp-keypoint { background: linear-gradient(var(--status-info-bg), var(--status-info-bg)), var(--panel);
  margin-bottom: var(--sp-12); }
.hp-sections { display: flex; flex-direction: column; gap: var(--sp-stack); }
/* A section whose page is not open yet reads quieter through colour, not
   opacity: opacity took its muted text to 3.7:1 (audit A8). */
.hp-section-pending { color: var(--muted); }
.hp-line { margin: var(--sp-8) 0 0; line-height: 1.55; }
.hp-q { font-weight: 600; }
.hp-heads-up { margin: var(--sp-12) 0 0; padding-top: var(--sp-12); line-height: 1.55; }

/* ── Page rhythm (doctrine 7.3: the same gap between the same things) ──── */
/* Every block directly in the page body is --sp-stack apart: one rule for
   every pair that will ever exist (the .stack idea, for the whole page),
   instead of a hand-typed margin-bottom on each card. */
.content > * + * { margin-top: var(--sp-stack); }
/* Inside a card: a title sits one step above its content; the last child
   never adds space at the bottom; a second titled section inside the same
   card starts a clear step further down. */
.card-title { margin-bottom: var(--sp-8); }
.card-head .card-title { margin-bottom: 0; }
.card-head { margin-bottom: var(--sp-8); }
.card > :last-child { margin-bottom: 0; }
.card > .tbl + .card-title { margin-top: var(--sp-24); }
.card > .tbl + * { margin-top: var(--sp-16); }
.card > form + .card-note { margin-top: var(--sp-12); }
/* A secondary note under the page heading (the catalogue's "prices exclude
   VAT"): the subline's colour, no margin of its own. */
.page-note { color: var(--muted); margin: 0; }
/* A group label inside a card or form (a programme day, "Guests"). */
.group-label { display: block; color: var(--muted); font-size: var(--fs-ui); font-weight: 600;
  margin: var(--sp-16) 0 var(--sp-8); }
.title-aside { color: var(--muted); font-family: var(--font); font-size: var(--fs-ui); font-weight: 400; }

/* ── Notices (review A6.2): the callout that was hand-rolled three times ── */
/* Blue, not orange: "this is a request, not a confirmed booking" informs, it
   does not ask the exhibitor to act (doctrine 4.2). */
.notice { background: var(--status-info-bg); color: var(--text); border-radius: var(--r-md);
  padding: var(--sp-12) var(--sp-16); font-size: var(--fs-body); line-height: var(--lh-body);
  margin: 0 0 var(--sp-16); }
/* Secondary text beside or under a value: dimmer, never smaller (doctrine 6.1). */
.hint { color: var(--muted); font-size: var(--fs-ui); }
.empty-action { margin-top: var(--sp-12); }

/* ── Small layout pieces the templates used to hand-roll ───────────────── */
.actionrow { display: flex; align-items: center; gap: var(--sp-8); }
.inline-form { display: inline; }
.measure { max-width: 640px; }
/* A checkbox with its label beside it (review A19.2). */
.field-check { display: flex; align-items: center; gap: var(--sp-12); }
.field-check .field-label { margin: 0; }
/* Fields inside a row carry no margin of their own: the row does. So a row
   of three sits exactly like a single field does, and rows repeat evenly. */
.form-row { margin-bottom: var(--sp-16); }
.form-row .field { margin-bottom: 0; }
.auth-actions { display: flex; flex-direction: column; gap: var(--sp-12); margin-top: var(--sp-8); }
.auth-actions + * { margin-top: var(--sp-16); }
/* The brand palette preview. The exhibitor's colour arrives as a CSS custom
   property (review A18.3), so a user value never becomes a style rule. */
.swatches { display: flex; gap: var(--sp-12); margin-top: var(--sp-12); }
.swatch { flex: 1; height: 56px; border-radius: var(--r-md); background: var(--swatch, var(--panel-2)); }
.thumb-sm { width: 40px; height: 40px; object-fit: cover; border-radius: var(--r-sm);
  margin-right: var(--sp-8); vertical-align: middle; }

/* ── Controls inside a zebra table (seen in the DES-3 screenshots) ──────── */
/* A field or a picture slot filled with --panel-2 vanished on every other
   row, because the row IS --panel-2. The control fill differs from both
   row tints; the empty picture slot takes the neutral wash. */
.tbl .input { background: var(--btn-2); }
.tbl .thumb-placeholder { background: var(--status-neutral-bg); }

/* ── Staff preview (review A4.3) ───────────────────────────────────────── */
/* The preview bar and the top bar were both sticky at top:0 and slid over
   each other on scroll. While previewing, only the preview bar stays pinned;
   the top bar scrolls with the page. No height guess, so a bar that wraps on
   a phone cannot overlap either. */
.preview-bar + .topbar { position: relative; }

/* ── Phone: the stat tiles stop costing a screen of scrolling (A5.2) ───── */
@media (max-width: 560px) {
  .tile { min-height: 0; padding: var(--sp-16); }
}

/* ── An icon inside a button (review A8.4) ─────────────────────────────── */
/* The cart's remove control was the text glyph; it is now the same inline
   SVG family as every other icon on the surface (doctrine 07). */
.btn-glyph { width: 16px; height: 16px; stroke-width: 2; display: block; }

/* ── DES-3 audit fixes ─────────────────────────────────────────────────── */
/* Secondary text beside a bold title is never bold itself (doctrine 6.1). */
.hint { font-weight: 400; }

/* An error never relies on colour alone (doctrine 9): a filled "!" mark
   leads every form and field error. It is decoration for sighted readers;
   the "/ ''" keeps it out of what a screen reader says. */
.form-error::before, .field-error::before {
  content: "!"; content: "!" / "";
  display: inline-grid; place-items: center; width: 1.15em; height: 1.15em;
  margin-right: var(--sp-8); border-radius: 50%; vertical-align: -0.15em;
  background: var(--status-bad-ink); color: var(--bg); font-weight: 800; line-height: 1;
}

/* Stand-alone text links are touch targets too (doctrine 5.4): an invisible
   hit area reaches >=44px tall without moving anything on the page. */
.tile-link, .eyebrow a, .auth-links a, .auth-foot a, .empty-action a, .preview-exit { position: relative; }
.tile-link::after, .eyebrow a::after, .auth-links a::after, .auth-foot a::after,
.empty-action a::after, .preview-exit::after { content: ""; position: absolute; inset: -12px -6px; }

/* An icon-only button is a full 44px square (doctrine 5.4). */
.btn-icon { min-width: var(--tap); min-height: var(--tap); padding: 0; }

/* Auth cards: two forms on one card (the resend link above sign-in) keep
   the form gap, and the card's last child adds nothing at the bottom, so
   the padding is the same top and bottom (audit A14). */
.auth-card > form + form { margin-top: var(--sp-16); }
.auth-card > :last-child { margin-bottom: 0; }

@media (max-width: 560px) {
  /* A stacked row has no picture column to hold open (audit A20). */
  .tbl--stack .thumb-placeholder { display: none; }
}

/* ── Draft order: the summary under the items (Magnus 2026-09-25) ───────── */
/* Text on the left, the two actions stacked on the right at one width, so
   the item rows above get the whole page. On a phone the actions drop
   under the text, full width. */
.cart-summary-body { display: grid; grid-template-columns: minmax(0, 1fr) 240px; gap: var(--sp-24); align-items: start; }
.cart-summary-info > :last-child { margin-bottom: 0; }
.cart-summary-actions { display: flex; flex-direction: column; gap: var(--sp-12); }
@media (max-width: 560px) {
  .cart-summary-body { grid-template-columns: 1fr; }
}

/* ── Exhibitor Terms & Conditions (TERMS-1) ──────────────────────────────── */
/* The accept step is the auth card, widened to a reading column; the text sits
   in its own scroll region so the checkbox and the button stay close by. On a
   phone the region lets go and the page itself scrolls: one scroll, not two. */
.auth-wrap--reading { max-width: 760px; }
.terms-scroll { max-height: 60vh; overflow-y: auto; background: var(--panel-2); border-radius: var(--r-md);
  padding: var(--sp-16) var(--sp-20); }
.terms-text { font-size: var(--fs-body); line-height: var(--lh-body); max-width: 72ch; }
.terms-text > :first-child { margin-top: 0; }
.terms-text > :last-child { margin-bottom: 0; }
.terms-title { font-family: var(--font-title); font-size: var(--fs-h2); line-height: var(--lh-title);
  margin: 0 0 var(--sp-12); }
.terms-heading { font-family: var(--font-title); font-size: var(--fs-title); line-height: var(--lh-title);
  margin: var(--sp-24) 0 var(--sp-8); }
.terms-para { margin: 0 0 var(--sp-12); }
.terms-list { margin: 0 0 var(--sp-12); padding-left: var(--sp-20); }
.terms-item + .terms-item { margin-top: var(--sp-8); }
.terms-form { margin-top: var(--sp-20); }
.terms-check { align-items: flex-start; min-height: var(--tap); }
.terms-check input[type="checkbox"] { margin-top: var(--sp-4); }
.terms-check .field-label { color: var(--text); font-size: var(--fs-body); }
.terms-card { max-width: 760px; }
.terms-status { margin: 0 0 var(--sp-20); font-size: var(--fs-body); font-weight: 600; }
@media (max-width: 560px) {
  .terms-scroll { max-height: none; overflow-y: visible; padding: var(--sp-12); }
}
