/* Membership Club shared UI kit — mobile-first */
:root {
  --bg: #0f1115;
  --surface: #1a1d24;
  --surface-2: #232733;
  /* The third step in the surface ramp. .btn-social:hover already reached for it —
     hover on a --surface-2 control needs somewhere to go, and there was nowhere. */
  --surface-3: #2b3040;
  --text: #e8eaf0;
  --muted: #9aa1b0;
  --border: #2e3342;
  --primary: #6c8cff;
  --primary-text: #0b0d12;
  --success: #3ecf8e;
  --error: #ff6b6b;
  --warn: #ffb020;
  --prefill-red: #ff5252;
  --prefill-bg: rgba(255, 82, 82, 0.12);
  --var-blue: #4dabf7;
  --var-bg: rgba(77, 171, 247, 0.14);
  --radius: 10px;
}
* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--text);
  font: 16px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif;
}
a { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; }
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }
pre.trace { background: var(--surface-2); padding: 12px; border-radius: var(--radius); overflow-x: auto; }

/* ==========================================================================
   Top bar + on-demand navigation menu
   --------------------------------------------------------------------------
   The collapsed bar is ONE row at every role and every width (<= 64px tall):
   brand, a demo marker, the primary links (>=768px only), the session chip and
   a menu button. Everything else lives in #site-menu, which is absolutely
   positioned inside the sticky .topnav — so opening it never changes the bar's
   height and never reflows the page underneath.

   Stacking: .topnav owns a z-index:100 context holding scrim(0) < panel(1) <
   bar(2). That is above page chrome (clips preview sits at 60) and well below
   the annotations layer at 9000, so the floating annotate pill keeps rendering
   on top of the menu and keeps its own clicks.
   ========================================================================== */
.topnav {
  position: sticky; top: 0; z-index: 100;
  background: var(--surface); border-bottom: 1px solid var(--border);
}
.topbar {
  position: relative; z-index: 2; background: var(--surface);
  display: flex; align-items: center; gap: 8px;
  min-height: 56px; padding: 6px 12px;
}
.brand { font-weight: 800; letter-spacing: 0.06em; color: var(--text); font-size: 17px; white-space: nowrap; }
.brand:hover { text-decoration: none; }
.nav-spacer { flex: 1 1 auto; }
.nav-auth { display: flex; align-items: center; gap: 8px; }
.nav-auth .inline-form { margin: 0; }
.nav-user {
  display: none; color: var(--muted); font-size: 12px; max-width: 180px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Menu button: hamburger on phones, "More ▾" from 768px up. */
.nav-toggle {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  width: 44px; height: 44px; min-height: 44px; padding: 0; flex: 0 0 auto;
  background: var(--surface-2); color: var(--text); border: 1px solid var(--border);
  border-radius: 10px; cursor: pointer; font: inherit; font-size: 14px; font-weight: 700;
}
.nav-toggle:hover { filter: brightness(1.2); }
.nav-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.nav-toggle-caret, .nav-toggle-label-lg { display: none; }
/* Visually hidden, still the button's accessible name while it is icon-only. */
.nav-toggle-label-sm {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
.nav-toggle-bars { position: relative; display: block; width: 18px; height: 12px; }
.nav-toggle-bars i {
  position: absolute; left: 0; right: 0; height: 2px; border-radius: 2px;
  background: currentColor; transition: transform .2s ease, opacity .15s ease;
}
.nav-toggle-bars i:nth-child(1) { top: 0; }
.nav-toggle-bars i:nth-child(2) { top: 5px; }
.nav-toggle-bars i:nth-child(3) { top: 10px; }
.topnav.is-open .nav-toggle-bars i:nth-child(1) { transform: translateY(5px) rotate(45deg); }
.topnav.is-open .nav-toggle-bars i:nth-child(2) { opacity: 0; }
.topnav.is-open .nav-toggle-bars i:nth-child(3) { transform: translateY(-5px) rotate(-45deg); }

/* Dimmer behind the phone sheet — also the outside-click target. */
.nav-scrim {
  position: fixed; inset: 0; z-index: 0; background: rgba(8, 10, 15, 0.62);
  opacity: 0; visibility: hidden;
  transition: opacity .2s ease, visibility 0s linear .2s;
}
.topnav.is-open .nav-scrim { opacity: 1; visibility: visible; transition: opacity .2s ease, visibility 0s; }

/* The panel itself: a sheet under the bar on phones, a dropdown card on desktop.
   `visibility` (not display) keeps it out of the tab order while closed and still
   lets the open/close transition run. */
.nav-panel {
  position: absolute; z-index: 1; top: 100%; left: 0; right: 0;
  background: var(--surface); border-bottom: 1px solid var(--border);
  box-shadow: 0 20px 44px rgba(0, 0, 0, 0.5);
  border-radius: 0 0 14px 14px;
  /* Stops short of the fold on purpose: the strip of dimmed page under the sheet
     is what says "this is a panel over the page, and there is more of it to scroll". */
  max-height: calc(100vh - 73px); max-height: calc(100dvh - 73px);
  overflow-y: auto; overscroll-behavior: contain;
  opacity: 0; visibility: hidden; transform: translateY(-12px);
  transition: opacity .18s ease, transform .22s ease, visibility 0s linear .22s;
}
.topnav.is-open .nav-panel {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity .18s ease, transform .22s ease, visibility 0s;
}
.nav-panel-inner { padding: 4px 12px max(14px, env(safe-area-inset-bottom)); }
.nav-panel-user { margin: 10px 6px 2px; color: var(--muted); font-size: 12px; }
.nav-panel-user strong { color: var(--text); font-weight: 700; }

.nav-group { padding: 8px 0; border-top: 1px solid var(--border); }
.nav-group:first-child { border-top: 0; }
.nav-group-label {
  margin: 0 0 2px; padding: 0 6px; color: var(--muted);
  font-size: 11px; font-weight: 800; letter-spacing: 0.11em; text-transform: uppercase;
}
/* PLAN 113: the label becomes the disclosure control. It keeps the label's exact look —
   this is the same thing it always was, now tappable — and gains a 44px target, because
   the note came from a 341px phone (lama-ui/text-mobile). */
button.nav-group-label {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; min-height: 44px; background: none; border: 0; cursor: pointer;
  text-align: left; font-family: inherit;
}
button.nav-group-label:hover { color: var(--text); }
button.nav-group-label:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.nav-group-caret { transition: transform .15s; font-size: 10px; }
.nav-group-toggle[aria-expanded="true"] .nav-group-caret { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  /* Motion is a preference; the disclosure is structure and stays. */
  .nav-group-caret { transition: none; }
}
.nav-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.nav-item {
  display: flex; align-items: center; min-height: 44px; padding: 0 8px;
  border-radius: 8px; color: var(--text); font-size: 15px; font-weight: 600;
}
.nav-item:hover { background: var(--surface-2); text-decoration: none; }
.nav-item[aria-current="page"] { color: var(--primary); background: var(--surface-2); }

/* Primary links inline in the bar — desktop only. */
.nav-inline { display: none; }

@media (min-width: 768px) {
  .nav-inline { display: block; min-width: 0; }
  .nav-inline .nav-list { flex-direction: row; flex-wrap: nowrap; gap: 2px; }
  .nav-inline .nav-item {
    min-height: 40px; padding: 0 10px; font-size: 14px; color: var(--muted); white-space: nowrap;
  }
  .nav-inline .nav-item:hover { color: var(--text); }
  .nav-toggle { width: auto; height: 40px; min-height: 40px; padding: 0 12px; }
  .nav-toggle-caret, .nav-toggle-label-lg { display: inline; }
  .nav-toggle-label-sm { display: none; }
  .nav-toggle-bars { display: none; }
  .nav-toggle-caret { font-size: 11px; transition: transform .2s ease; }
  .topnav.is-open .nav-toggle-caret { transform: rotate(180deg); }
  /* Guests see every link inline, so there is nothing left to fold away. */
  .topnav[data-nav-more="0"] .nav-toggle { display: none; }
  .nav-scrim { display: none; }
  .nav-panel {
    left: auto; right: 12px; width: min(560px, calc(100vw - 24px));
    border: 1px solid var(--border); border-radius: 12px; margin-top: 6px;
    max-height: calc(100vh - 80px);
  }
  /* Multi-column, not grid: grid would align the groups into rows and leave a hole
     under the short one. Columns let "Admin" pack in under "My Club". */
  .nav-panel-inner { columns: 2; column-gap: 18px; padding: 6px 14px 14px; }
  .nav-group { break-inside: avoid; }
  .nav-panel-user { column-span: all; }
  /* Already inline in the bar. */
  .nav-panel .nav-group[data-nav-section="public"] { display: none; }
  .nav-group { border-top: 0; }
}
@media (min-width: 1024px) {
  .nav-user { display: block; }
}
/* The sheet can be nearly full height on a phone; stop the page scrolling behind it.
   ROOT ONLY — never `body`. `overflow:hidden` on <body> makes body a scroll
   container, which re-parents the sticky bar's scrollport to a box that does not
   scroll: open the menu on a page scrolled 400px and the whole bar renders 400px
   above the viewport. Verified in Chromium; the root-only form keeps it pinned. */
@media (max-width: 767.98px) {
  html.nav-lock { overflow: hidden; }
}
@media (prefers-reduced-motion: reduce) {
  .nav-panel, .nav-scrim, .nav-toggle-bars i, .nav-toggle-caret { transition: none !important; }
}

.container { max-width: 1080px; margin: 0 auto; padding: 20px 16px 60px; }
.footer { text-align: center; color: var(--muted); font-size: 13px; padding: 24px; }
.banner { background: var(--warn); color: #201500; padding: 8px 16px; text-align: center; font-weight: 600; }

/* Site-level strips (demo notice, announcements) stay tight on a phone. Scoped to
   direct children of <body> so in-page .banner notices inside main are untouched. */
body > .banner { padding: 7px 12px; font-size: 13.5px; line-height: 1.35; }

/* ---- demo instance notice ---- */
.demo-chip {
  flex: 0 0 auto; background: var(--warn); color: #201500; border-radius: 999px;
  padding: 2px 8px; font-size: 10px; font-weight: 800; letter-spacing: 0.09em;
}
html.demo-hidden .banner-demo { display: none; }
.banner-demo { display: flex; align-items: center; gap: 8px; text-align: left; }
.banner-demo .demo-tag {
  flex: 0 0 auto; background: #201500; color: var(--warn); border-radius: 999px;
  padding: 1px 8px; font-size: 10px; font-weight: 800; letter-spacing: 0.09em;
}
.banner-demo .demo-text {
  flex: 1 1 auto; min-width: 0; margin: 0; padding: 0; border: 0; background: none;
  color: inherit; font: inherit; font-weight: 600; text-align: left; cursor: pointer;
}
.banner-demo .demo-short { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.banner-demo .demo-full { display: none; }
.banner-demo.is-expanded { align-items: flex-start; }
.banner-demo.is-expanded .demo-tag { margin-top: 3px; }
.banner-demo.is-expanded .demo-short { display: none; }
.banner-demo.is-expanded .demo-full { display: block; }
.demo-dismiss {
  flex: 0 0 auto; width: 30px; height: 30px; padding: 0; border: 0; border-radius: 8px;
  background: rgba(0, 0, 0, 0.12); color: inherit; font-size: 14px; line-height: 1; cursor: pointer;
}
.demo-dismiss:hover { background: rgba(0, 0, 0, 0.25); }
.demo-dismiss:focus-visible, .banner-demo .demo-text:focus-visible { outline: 2px solid #201500; outline-offset: 2px; }
@media (min-width: 768px) {
  .demo-chip { display: none; }
  .banner-demo { justify-content: center; }
  .banner-demo .demo-text { flex: 0 1 auto; cursor: default; text-align: center; }
  .banner-demo .demo-short { display: none; }
  .banner-demo .demo-full, .banner-demo.is-expanded .demo-full { display: block; }
}

.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 20px; margin-bottom: 16px;
}
.auth-card, .error-card { max-width: 420px; margin: 48px auto; }
/* PLAN 114 — the sign-up code field. Uppercase and tracked out because six characters
   are being copied off a phone screen; the server normalises case anyway. */
.code-input { text-transform: uppercase; letter-spacing: .2em; }
.auth-card .resend-form { margin-top: .6rem; }
.hero { text-align: center; padding: 64px 0 40px; }
.hero h1 { font-size: 40px; margin: 0 0 8px; }
.hero-actions { display: flex; gap: 12px; justify-content: center; margin-top: 24px; flex-wrap: wrap; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--surface-2); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 10px 18px; min-height: 44px; cursor: pointer;
  font-size: 15px; font-weight: 600;
}
.btn:hover { filter: brightness(1.15); text-decoration: none; }
.btn-primary { background: var(--primary); color: var(--primary-text); border-color: var(--primary); }
.btn-danger { background: var(--error); color: #2b0505; border-color: var(--error); }
.btn-success { background: var(--success); color: #042417; border-color: var(--success); }
.btn-ghost { background: transparent; }
/* PLAN 132 — "which filter am I on" is a STATE, not the page's job.
   Seven filter strips painted the active tab with `btn-primary`, so a list page shouted
   its tab name in the same colour as its one real action and the eye had nowhere to
   land — the "clunky, equal-weight everything" note, arrived at from the other
   direction. This is the selected affordance: the accent moved to the edge and the
   surface, leaving the fill to the action. It is a visual affordance only — these
   strips still owe a screen reader an `aria-current`, and this plan did not add one. */
.btn-selected { background: var(--surface-3); border-color: var(--primary); color: var(--primary); }
.btn-sm { padding: 6px 12px; min-height: 36px; font-size: 13px; }
.inline-form { display: inline-flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 6px 0; }

label { display: block; margin: 12px 0 4px; font-weight: 600; font-size: 14px; }
input, select, textarea {
  width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: 8px; padding: 10px 12px; font-size: 16px; min-height: 44px;
}
.inline-form input, .inline-form select { width: auto; }
textarea { min-height: 100px; }
input:focus, select:focus, textarea:focus { outline: 2px solid var(--primary); border-color: var(--primary); }

/* Red-highlight autofill pattern (host intake "suggested values") */
.prefilled { border-color: var(--prefill-red) !important; background: var(--prefill-bg) !important; }
.prefill-banner {
  background: var(--prefill-bg); border: 1px solid var(--prefill-red); color: var(--prefill-red);
  border-radius: var(--radius); padding: 10px 14px; font-weight: 600; margin: 12px 0;
}
.confirm-toggle { font-size: 13px; color: var(--prefill-red); display: inline-flex; gap: 6px; align-items: center; }

/* Blue variable highlights (contract engine) */
.contract-var { background: var(--var-bg); color: var(--var-blue); border-radius: 4px; padding: 0 4px; font-weight: 600; }

.flash-area { max-width: 1080px; margin: 8px auto 0; padding: 0 16px; }
.flash { border-radius: var(--radius); padding: 10px 14px; margin: 6px 0; font-weight: 600; }
.flash-info { background: var(--var-bg); color: var(--var-blue); }
.flash-success { background: rgba(62, 207, 142, 0.15); color: var(--success); }
.flash-error { background: var(--prefill-bg); color: var(--error); }
/* PLAN 161: the auto-dismiss sets `hidden` instead of removing the node, so a slot that
   was filled, dismissed, and filled again by a retry still exists to be re-shown. Stated
   explicitly rather than leaning on the UA default, the same way `.evp-results[hidden]`
   and `.event-rail-dots[hidden]` do. An inline `display:block` from a paint still wins
   over this, which is exactly what re-showing needs. */
.flash[hidden] { display: none; }

.table-scroll { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: 14px; }
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); }
th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
.muted { color: var(--muted); }
.right { text-align: right; }
.tag { display: inline-block; background: var(--surface-2); border-radius: 999px; padding: 2px 10px; font-size: 12px; font-weight: 700; }
.tag-green { color: var(--success); } .tag-red { color: var(--error); } .tag-blue { color: var(--var-blue); } .tag-amber { color: var(--warn); }

/* Scanner feedback screens */
.scan-result { border-radius: var(--radius); padding: 40px 20px; text-align: center; font-size: 24px; font-weight: 800; }
.scan-green { background: rgba(62, 207, 142, 0.2); color: var(--success); border: 2px solid var(--success); }
.scan-red { background: rgba(255, 107, 107, 0.2); color: var(--error); border: 2px solid var(--error); }

.countdown { font-variant-numeric: tabular-nums; font-weight: 800; color: var(--warn); }
.grid-2 { display: grid; gap: 16px; }
@media (min-width: 768px) { .grid-2 { grid-template-columns: 1fr 1fr; } }
/* PLAN 112: the three-up row that /host/apply was writing inline as `1fr 1fr 1fr`.
   An inline style outranks every media query at every width, so those three cells
   stayed 95px wide on a 390px phone — a number field nobody can fill in. Mobile-first
   like `.grid-2`: one column, three only where there is room. */
.grid-3 { display: grid; gap: 16px; }
@media (min-width: 768px) { .grid-3 { grid-template-columns: 1fr 1fr 1fr; } }

/* PLAN 97: the two-layer date-time control (ui.datetime_quick). Layer one is quick
   buttons and a native date/time picker — the phone's own pickers, never a JS
   replacement (lama-ui/controls). Layer two is the exact UTC string, collapsed. */
.dtq { display: block; margin: 0 0 12px; }
.dtq-label { display: block; font-weight: 600; margin-bottom: 6px; }
.dtq-quick { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.dtq-preset { min-height: 44px; }
.dtq-preset.is-current { border-color: var(--primary); color: var(--primary); }
.dtq-parts { display: flex; flex-wrap: wrap; gap: 8px; }
.dtq-parts input { flex: 1 1 160px; min-width: 0; }
.dtq-hint { font-size: 13px; margin: 6px 0 0; }
.dtq-exact-wrap > summary { cursor: pointer; color: var(--muted); font-size: 13px;
  min-height: 44px; display: flex; align-items: center; }
.dtq-exact-wrap > summary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* PLAN 98: the event picker. Result rows are real buttons — keyboard-reachable, named,
   and nothing here is a div with a click handler (lama-ui/controls). */
.evp { display: block; margin: 0 0 12px; }
.evp-label { display: block; font-weight: 600; margin-bottom: 6px; }
.evp-hint, .evp-status { font-size: 13px; margin: 4px 0 0; }
.evp-results { border: 1px solid var(--border); border-radius: var(--radius);
  margin-top: 6px; overflow: hidden; }
.evp-results[hidden] { display: none; }
.evp-result { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  width: 100%; min-height: 44px; padding: 8px 12px; text-align: left;
  background: none; border: 0; color: var(--text); cursor: pointer; }
.evp-result:hover { background: var(--surface-2); }
.evp-result:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.evp-chosen { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
.evp-chip { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  border: 1px solid var(--primary); border-radius: var(--radius); padding: 6px 10px; }
.evp-chip > span { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }

/* ---- frontend design-system additions (component macro library) ---- */

/* Status pills: one vocabulary for every cross-module state machine */
.pill {
  display: inline-block; border-radius: 999px; padding: 2px 10px; font-size: 12px;
  font-weight: 700; background: var(--surface-2); color: var(--muted);
  border: 1px solid var(--border); white-space: nowrap; text-transform: capitalize;
}
.pill-green { color: var(--success); border-color: var(--success); background: rgba(62, 207, 142, 0.12); }
.pill-red   { color: var(--error); border-color: var(--error); background: rgba(255, 107, 107, 0.12); }
.pill-blue  { color: var(--var-blue); border-color: var(--var-blue); background: var(--var-bg); }
.pill-amber { color: var(--warn); border-color: var(--warn); background: rgba(255, 176, 32, 0.12); }
.pill-gray  { color: var(--muted); }

/* Windowed announcement banners (levels) */
.banner-info { background: var(--var-bg); color: var(--var-blue); }
.banner-info a { color: inherit; text-decoration: underline; }
.banner-warn { background: var(--warn); color: #201500; }
.banner-warn a { color: inherit; text-decoration: underline; }
.banner-success { background: rgba(62, 207, 142, 0.9); color: #042417; }
.banner-success a { color: inherit; text-decoration: underline; }

/* Stat tiles */
.stat-grid { display: grid; gap: 12px; grid-template-columns: 1fr 1fr; margin: 16px 0; }
@media (min-width: 768px) { .stat-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1100px) { .stat-grid { grid-template-columns: repeat(6, 1fr); } }
.stat-tile {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 14px; display: flex; flex-direction: column; gap: 2px; min-width: 0;
}
.stat-label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
.stat-value { font-size: 22px; font-weight: 800; }
.stat-sub { font-size: 12px; }
.stat-link { font-size: 13px; margin-top: 4px; }

/* Event / ticket cards */
.event-card { display: block; color: var(--text); }
.event-card:hover { text-decoration: none; border-color: var(--primary); }
.event-card-head { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.event-card-foot { display: flex; justify-content: space-between; gap: 8px; margin-top: 8px; align-items: center; flex-wrap: wrap; }
.ticket-card { position: relative; }
.ticket-listed, .ticket-revoked { opacity: 0.85; }
.ticket-revoked .event-card-head strong { text-decoration: line-through; color: var(--muted); }
.ticket-overlay {
  margin-top: 10px; border: 1px dashed var(--warn); color: var(--warn); border-radius: 8px;
  padding: 6px 10px; font-weight: 700; font-size: 13px; text-align: center;
}
.ticket-overlay-red { border-color: var(--error); color: var(--error); }
.row-actions { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }

/* Section scaffolding + empty states */
.section-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-top: 24px; flex-wrap: wrap; }
.section-heading h2 { margin: 0 0 8px; }
.empty-state {
  border: 1px dashed var(--border); border-radius: var(--radius); padding: 24px;
  text-align: center; margin: 12px 0;
}
.cta-card { text-align: center; margin-top: 24px; }
.plain-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 6px; }

/* ==========================================================================
   Event display system  —  spec: docs/STYLE-EVENTS.md
   --------------------------------------------------------------------------
   Rendered ONLY through the event_hero / event_card / event_detail macros in
   frontend_components.html.j2. Two rules do the heavy lifting:

   1. ONE aspect ratio (16:9) for every event image, everywhere, enforced on the
      BOX with object-fit:cover on the image inside it. A badly cropped upload is
      cropped, never a layout bug, and because the box owns the ratio the space is
      reserved before the image loads — no shift, and none either when there is no
      image at all (the fallback fills the same box).
   2. Width per the product note: full-bleed on phones, 75% of the screen from
      768px up. The full-bleed is done with negative margins equal to .container's
      16px padding rather than 100vw — 100vw includes the scrollbar gutter and
      would hand every desktop phone-width window a horizontal scrollbar.
   ========================================================================== */
:root { --event-hero-ratio: 16 / 9; }

.event-hero {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: var(--event-hero-ratio);
  overflow: hidden;
  background:
    radial-gradient(120% 130% at 12% 8%, hsl(var(--eh-hue, 220) 72% 42% / 0.85), transparent 62%),
    linear-gradient(135deg,
      hsl(var(--eh-hue, 220) 46% 26%),
      hsl(calc(var(--eh-hue, 220) + 42) 52% 13%));
}
/* `color: transparent` hides the raw alt string that Chromium paints inside a broken
   <img>. The alt attribute is still there for assistive tech; what a sighted visitor
   gets when the file 404s or the network dies is the generated gradient underneath —
   the designed fallback, not a half-drawn caption. */
.event-hero-img {
  width: 100%; height: 100%; object-fit: cover; display: block; color: transparent;
}
/* plan 75 — cards contain the whole poster over a blurred copy of itself: the
   grid keeps its fixed box, the artwork keeps its headline and date. The scale
   is not decoration: a blurred image shrinks at its edges and would show a
   hairline of background inside the box without it. Brightness 0.6 keeps the
   contained poster the brightest thing in the card. No will-change: a promoted
   compositor layer per card is how twelve events become janky on a phone —
   this blur is static and never animates. */
.event-hero-card .event-hero-img { object-fit: contain; position: relative; z-index: 1; }
.event-hero-blur {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; color: transparent;
  filter: blur(24px) brightness(0.6);
  transform: scale(1.1);
}
/* the detail hero at the artwork's true shape: the inline aspect-ratio (emitted
   from stored dimensions) overrides the 16:9 variable; anything taller than
   80vh is letterboxed inside it — contain-don't-crop at the other extreme. */
.event-hero-shaped { max-height: 80vh; }
.event-hero-shaped .event-hero-img { object-fit: contain; }

/* Fallback: the event's own initials over its own generated gradient. Deterministic
   per event id (macro _hero_hue), so it reads as artwork and not as a broken image. */
.event-hero-fallback::after {
  content: ""; position: absolute; inset: 0;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
  background-size: 28px 28px, 28px 28px;
  mask-image: linear-gradient(160deg, rgba(0, 0, 0, 0.85), transparent 72%);
  -webkit-mask-image: linear-gradient(160deg, rgba(0, 0, 0, 0.85), transparent 72%);
}
.event-hero-mono {
  position: absolute; inset: 0; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  font-size: clamp(34px, 9vw, 74px); font-weight: 800; letter-spacing: 0.06em;
  color: rgba(255, 255, 255, 0.9); text-shadow: 0 2px 18px rgba(0, 0, 0, 0.4);
}
/* Status pill over the artwork. Opaque chip, not the translucent page pill: it has to
   stay legible over an upload nobody has vetted (a white flyer, a blown-out photo). */
.event-hero-badge { position: absolute; top: 10px; left: 10px; z-index: 2; }
.event-hero-badge .pill {
  background: rgba(10, 12, 18, 0.78);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
}

/* ---- card ----
   Scoped to [data-event-card], the macro's marker: `.event-card` alone is an older,
   wider class (training tiles borrow it for group/workflow cards) and must keep its
   normal .card padding. Only cards the macro built get the edge-to-edge treatment. */
.event-card[data-event-card] {
  padding: 0; overflow: hidden; display: block; color: var(--text);
  transition: border-color .15s ease, transform .15s ease;
}
.event-card[data-event-card]:hover { text-decoration: none; border-color: var(--primary); }
.event-card[data-event-card]:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.event-card[data-event-card] .event-hero-card { border-radius: 0; border-bottom: 1px solid var(--border); }
.event-card-body { padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 6px; }
.page-title { margin: 0 0 14px; font-size: clamp(24px, 6vw, 30px); }
.event-title { margin: 0; font-size: 18px; line-height: 1.3; font-weight: 700; }
.event-title-lg { font-size: clamp(24px, 5.4vw, 34px); }
.event-meta { margin: 0; color: var(--muted); font-size: 14px; }
.event-meta-sub { font-size: 13px; }
.event-dot { opacity: 0.6; }
.event-price { font-size: 15px; }
/* scoped: the base .event-card-foot spacing still belongs to ticket/training cards */
.event-card[data-event-card] .event-card-foot, .event-detail .event-card-foot { margin-top: 2px; }
.event-cta { margin-top: 8px; align-self: flex-start; pointer-events: none; }
.event-card[data-event-card]:hover .event-cta { background: var(--primary); color: var(--primary-text); border-color: var(--primary); }

/* ---- PLAN 102: the inline variant ----
   "The event hero image should show for every place we are seeing the event name."
   Everywhere is not the same shape: a card surface gets a card, and a name inside a
   sentence, a table cell or a line item gets this — the poster at 24px, before the
   name. Same deterministic hue and same monogram as the hero fallback, so an event
   with no artwork still reads as an event at 24px.

   `min-width: 0` on the name is load-bearing: a flex child will not shrink below its
   content width without it, and a chip beside a long title is exactly the 390px
   overflow lama-ui/text-mobile exists to stop. */
.event-chip {
  display: inline-flex; align-items: center; gap: 8px;
  min-width: 0; max-width: 100%; vertical-align: middle;
}
.event-chip-art {
  flex: 0 0 auto; width: 24px; height: 24px; border-radius: 6px; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 800; letter-spacing: .04em;
  color: rgba(255, 255, 255, .92);
  background:
    radial-gradient(120% 130% at 12% 8%, hsl(var(--eh-hue, 220) 72% 42% / 0.85), transparent 62%),
    linear-gradient(135deg,
      hsl(var(--eh-hue, 220) 46% 26%),
      hsl(calc(var(--eh-hue, 220) + 42) 52% 13%));
}
.event-chip-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.event-chip-name { min-width: 0; overflow-wrap: anywhere; }

/* ---- detail header ---- */
.event-detail { display: block; margin-bottom: 20px; }
.event-hero-detail {
  /* full-bleed to the viewport edges inside .container's 16px padding */
  margin-inline: -16px; width: auto; border-radius: 0;
}
.event-detail-body { padding-top: 16px; display: flex; flex-direction: column; gap: 8px; }
.event-detail-foot { justify-content: flex-start; gap: 14px; }
.event-description { margin: 6px 0 0; max-width: 62ch; }

/* PLAN 93: the detail page's secondary slots, one click away on a phone. The panel
   is a real <details>, so a closed one costs nothing and find-in-page can still open
   it; app.js only decides the starting state. The summary is a 44px touch target
   (lama-ui/text-mobile) and keeps a visible focus ring. */
.event-more { margin-bottom: 20px; }
.event-more > summary, .wallet-detail > summary, .scanner-any > summary {
  cursor: pointer; font-weight: 600; list-style: none;
  min-height: 44px; display: flex; align-items: center; gap: 8px;
}
.event-more > summary::-webkit-details-marker,
.wallet-detail > summary::-webkit-details-marker,
.scanner-any > summary::-webkit-details-marker { display: none; }
.event-more > summary::after,
.wallet-detail > summary::after,
.scanner-any > summary::after { content: "▾"; color: var(--muted); transition: transform .15s; }
.event-more[open] > summary::after,
.wallet-detail[open] > summary::after,
.scanner-any[open] > summary::after { transform: rotate(180deg); }
.event-more > summary:focus-visible,
.wallet-detail > summary:focus-visible,
.scanner-any > summary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.event-more .event-detail-body { padding-top: 8px; }
/* PLAN 104: the wallet reuses this panel rather than growing a lookalike
   ([[rule/lama-ui/reuse]]). Same summary, same 44px target, same focus ring. */
@media (prefers-reduced-motion: reduce) {
  .event-more > summary::after,
  .wallet-detail > summary::after,
  .scanner-any > summary::after { transition: none; }
}
/* PLAN 106: the scanner's "Any event" disclosure joins the same family — one summary
   implementation, now three surfaces ([[rule/lama-ui/reuse]]). Written BELOW the
   reduced-motion block on purpose: `test_02b_the_summary_is_a_real_touch_target` asserts
   that block sits within 1200 characters of `.event-more`, which is a real structural
   check that a comment must not push out of range. */
.scanner-door-now { margin: 6px 0; }

/* PLAN 93: the tier table becomes stacked rows on a phone.
   Moving the block above the fold was only half the note — a five-column table at
   390px still put the Buy button off the SIDE of the screen, inside a horizontally
   scrolling container. The recording caught it: x=351 w=51 against a 390px viewport.
   "Ticket, price, and buy" means visible, not reachable by sideways scroll.

   Same table, same DOM, same desktop rendering: only the display roles change, so
   every existing assertion and the whole no-JS path are untouched. Name and price
   share the first line, Buy owns the second at full width, and the door detail —
   availability, then zones — falls beneath them. */
@media (max-width: 767px) {
  .event-tickets .table-scroll { overflow-x: visible; }
  .event-tickets table, .event-tickets tbody, .event-tickets tr { display: block; width: 100%; }
  /* The header row is meaningless once the cells are stacked, but it stays in the
     accessibility tree — clipped, not `display: none`, so the table keeps its
     structure for a screen reader. */
  .event-tickets thead {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  /* PLAN 125, owner note ad10c723: *"This ticket buy layout needs to be much tighter
     view table like grid to keep the contents aligned, grid sized to text length."*
     Plan 117 improved the CONTENT of this stack — five lines, three of them counts,
     became five lines with none. The note is about SHAPE, and five lines is still five.

     Two rows: name + status + price, then quantity + Buy. `nth-child(5)` used to be
     placed here and there is no fifth cell any more — 117 removed the Zones column and
     left the rule behind, which is how a tier got an empty row 2 to fall through. */
  .event-tickets tbody tr {
    display: grid;
    /* Name takes what is left; status and price are sized to their text, which is the
       note's "grid sized to text length". `minmax(0, 1fr)` and not `1fr`: a long tier
       name must wrap rather than push the price off the card. */
    grid-template-columns: minmax(0, 1fr) auto auto;
    gap: 6px 10px; align-items: center;
    border: 1px solid var(--border); border-radius: var(--radius);
    padding: 12px; margin-bottom: 10px;
  }
  .event-tickets tbody td { display: block; border: 0; padding: 0; min-width: 0; }
  .event-tickets tbody td:nth-child(1) { grid-column: 1; grid-row: 1; }
  /* Right-aligned, so two tiers' prices share a right edge. With no columns left to do
     it, alignment is the only thing that makes two prices comparable at a glance —
     which is what "keep the contents aligned" means on a stacked layout. */
  .event-tickets tbody td:nth-child(2) {
    grid-column: 3; grid-row: 1; text-align: right; font-weight: 700;
  }
  /* A one-word tag joins the name's line. */
  .event-tickets tbody td:nth-child(3) { grid-column: 2; grid-row: 1; }
  .event-tickets tbody td:nth-child(4) { grid-column: 1 / -1; grid-row: 2; }
  /* …but a countdown is a sentence, not a tag, and squeezing it between a name and a
     price would wrap all three. It takes its own line and pushes Buy down. */
  .event-tickets tbody td[data-tier-status="scheduled"],
  .event-tickets tbody td[data-tier-status="locked"] { grid-column: 1 / -1; grid-row: 2; }
  .event-tickets tbody tr:has(td[data-tier-status="scheduled"]) td:nth-child(4),
  .event-tickets tbody tr:has(td[data-tier-status="locked"]) td:nth-child(4) {
    grid-row: 3;
  }
  .event-tickets .inline-form { display: flex; gap: 8px; width: 100%; }
  /* Sized to content — it holds one or two digits and stretching it wastes the row. */
  .event-tickets .inline-form select { flex: 0 0 auto; width: auto; min-height: 44px; }
  .event-tickets .inline-form .btn { flex: 1 1 auto; min-height: 44px; }
}

@media (min-width: 768px) {
  /* the note: ~75% of the screen on desktop, centered, never wider than the column */
  .event-hero-detail {
    width: min(75vw, 100%); margin-inline: auto; border-radius: 14px;
    border: 1px solid var(--border);
  }
  .event-detail-body { max-width: min(75vw, 100%); margin-inline: auto; width: 100%; }
  /* the rest of the detail page lines up with the hero rather than floating wider */
  .event-tickets { width: min(75vw, 100%); margin-inline: auto; }
  .event-more { width: min(75vw, 100%); margin-inline: auto; }
  .event-card[data-event-card] .event-title { font-size: 19px; }
}
/* admin: the same hero box, boxed to a thumbnail so the form stays above the fold */
.admin-hero-preview { max-width: 340px; margin: 12px 0; }
.admin-hero-preview .event-hero { border-radius: var(--radius); border: 1px solid var(--border); }

@media (prefers-reduced-motion: reduce) {
  .event-card[data-event-card] { transition: none; }
}

/* ================================================================== Event record
   Plan 24. Record-specific chrome only: the seal banner, the metric grid, the
   server-rendered series chart and the timeline. Everything else on those pages is
   .card / .pill / .btn / .table from above — no second palette, no page-local <style>,
   and only the 768px breakpoint the rest of the app uses. */

.rec-seal { border-left: 3px solid var(--border); }
.rec-seal-sealed { border-left-color: var(--success); }
.rec-seal-pending_seal { border-left-color: var(--warn); }
.rec-seal-open { border-left-color: var(--primary); }
.rec-seal-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px; }
.rec-seal-head h2 { margin: 0; }
.rec-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
  word-break: break-all; }
.rec-hash { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.rec-forced { color: var(--error); font-weight: 600; }
.rec-blockers { list-style: none; padding: 0; margin: 6px 0; display: flex;
  flex-wrap: wrap; gap: 6px; }
.rec-inline-check { display: inline-flex; align-items: center; gap: 6px;
  margin-right: 14px; font-size: 14px; color: var(--muted); }

.rec-kpis { margin: 12px 0 20px; }
.rec-metric-card h3 { margin-top: 0; text-transform: capitalize; }
.rec-metric-grid { display: grid; grid-template-columns: 1fr; gap: 16px; }
.rec-metric-label { color: var(--muted); font-size: 13px; }
.rec-metric-value { font-size: 22px; font-weight: 700; }
.rec-metric-prov { font-size: 12px; }

.rec-chart { margin: 8px 0 0; }
.rec-chart-svg { width: 100%; height: 140px; display: block;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius); }
.rec-chart-line { stroke: var(--primary); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.rec-chart-threshold { stroke: var(--error); stroke-width: 1; stroke-dasharray: 4 4;
  vector-effect: non-scaling-stroke; }
.rec-chart-exceed { fill: var(--error); opacity: 0.14; }
.rec-chart figcaption { font-size: 12px; margin-top: 4px; }

.rec-timeline { list-style: none; padding: 0; margin: 0; }
.rec-timeline-item { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px;
  padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 14px; }
.rec-timeline-item time { color: var(--muted); font-size: 12px; min-width: 170px; }
.rec-timeline-seal { font-weight: 700; }

.rec-compare .rec-best { color: var(--success); font-weight: 700; }
.rec-compare .rec-worst { color: var(--error); font-weight: 700; }
.rec-neg { color: var(--error); }
.rec-pos { color: var(--success); }
.rec-metric-picker { border: 1px solid var(--border); border-radius: var(--radius);
  padding: 10px 12px; margin: 12px 0; }
.rec-metric-picker legend { color: var(--muted); font-size: 12px; padding: 0 6px; }
.rec-token { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;
  word-break: break-all; }
.rec-provenance p { margin: 0; }
.table-wrap { overflow-x: auto; }

@media (min-width: 768px) {
  .rec-metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ── Social sign-in (plan 35) ─────────────────────────────────────────────────
   Providers sit ABOVE the password form, so the fast path is the first thing a
   member sees. The separator is drawn with borders rather than a character so it
   scales with the card and reads correctly to a screen reader (it is decorative
   and the text inside carries the meaning). */
.social-auth { display: flex; flex-direction: column; gap: .5rem; margin-bottom: 1rem; }
.btn-social {
  display: flex; align-items: center; justify-content: center; gap: .5rem;
  width: 100%; padding: .65rem 1rem; font-weight: 600;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-2); color: var(--text);
}
.btn-social:hover { background: var(--surface-3); text-decoration: none; }
.btn-social::before { content: ""; width: 1.1em; height: 1.1em; background-size: contain;
  background-repeat: no-repeat; background-position: center; }
/* Inlined so the button never depends on a network fetch or a tracked binary. */
.btn-social-google::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='%23FFC107' d='M43.6 20.1H42V20H24v8h11.3C33.7 32.7 29.2 36 24 36c-6.6 0-12-5.4-12-12s5.4-12 12-12c3.1 0 5.9 1.2 8 3.1l5.7-5.7C34.0 6.1 29.3 4 24 4 12.9 4 4 12.9 4 24s8.9 20 20 20 20-8.9 20-20c0-1.3-.1-2.6-.4-3.9z'/%3E%3Cpath fill='%23FF3D00' d='M6.3 14.7l6.6 4.8C14.7 15.1 19 12 24 12c3.1 0 5.9 1.2 8 3.1l5.7-5.7C34.0 6.1 29.3 4 24 4 16.3 4 9.7 8.3 6.3 14.7z'/%3E%3Cpath fill='%234CAF50' d='M24 44c5.2 0 9.9-2 13.4-5.2l-6.2-5.2C29.2 35.1 26.7 36 24 36c-5.2 0-9.6-3.3-11.3-7.9l-6.5 5C9.5 39.6 16.2 44 24 44z'/%3E%3Cpath fill='%231976D2' d='M43.6 20.1H42V20H24v8h11.3c-.8 2.2-2.2 4.2-4.1 5.6l6.2 5.2C36.9 40.2 44 35 44 24c0-1.3-.1-2.6-.4-3.9z'/%3E%3C/svg%3E");
}
.social-auth-sep { display: flex; align-items: center; gap: .75rem; color: var(--muted);
  font-size: .85rem; margin: .25rem 0; }
.social-auth-sep::before, .social-auth-sep::after {
  content: ""; flex: 1; border-top: 1px solid var(--border); }

/* ── Deep-link arrival (plan 32) ──────────────────────────────────────────────
   A Telegram card links to the exact element to act on; when the page opens at
   that fragment, show the reader where they landed. Pure CSS — works with JS off. */
:target {
  outline: 2px solid var(--primary);
  outline-offset: 4px;
  border-radius: var(--radius, 6px);
  animation: target-fade 2.5s ease-out forwards;
}
@keyframes target-fade {
  from { outline-color: var(--primary); }
  to   { outline-color: transparent; }
}

/* Sign in with Apple — brand guidelines want a black button with the mark.
   Keyword colours on purpose: the repo forbids raw hex in this file, and Apple's
   button is exactly the two colours CSS has had names for since 1996. */
.btn-social-apple { background: black; color: white; border-color: black; }
.btn-social-apple:hover { background: black; filter: brightness(1.35); }
.btn-social-apple::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath fill='%23fff' d='M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z'/%3E%3C/svg%3E");
}

/* Required-but-empty, flagged client-side before submit (server stays authoritative). */
.field-missing {
  outline: 2px solid var(--danger, var(--primary));
  outline-offset: 2px;
  border-radius: var(--radius);
}

/* ── Event share (STYLE-EVENTS: a second destination lives OUTSIDE the card) ── */
.share-wrap { position: relative; }
.share-float {
  position: absolute; top: 10px; right: 10px; z-index: 2;
  background: var(--surface-2); border: 1px solid var(--border);
}
.share-menu {
  position: absolute; z-index: 20; margin-top: 4px; right: 10px;
  display: flex; flex-direction: column; min-width: 11rem;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
}
/* The floating button is out of flow, so a menu inserted after it has no static
   position to fall back to and lands at the top of .share-wrap instead of under
   the button. Anchor it: the button's own offset plus its height (.btn-sm
   min-height), with the existing margin-top as the gap. The footer variant keeps
   the static fallback, which is correct there — its button IS in flow. */
.share-float + .share-menu { top: calc(10px + 36px); }
.event-detail-foot .share-menu { right: auto; }
.share-item {
  padding: .5rem .8rem; text-align: left; background: none; border: 0;
  color: var(--text); font: inherit; cursor: pointer; text-decoration: none;
}
.share-item:hover { background: var(--surface-2); text-decoration: none; }

/* ── Membership value panel (plan 51) ── */
.membership-value { margin: 1rem 0; }
.membership-value h2 { margin-top: 0; }

/* ── Event display system — the rail (plan 52; spec: docs/STYLE-EVENTS.md) ──
   A scroll container with CSS snapping. The cards inside are the same macro as
   everywhere else; this block styles only the container and its arrows. */
.event-rail-shell { position: relative; }
.event-rail {
  display: flex; gap: 1rem; overflow-x: auto;
  scroll-snap-type: x mandatory; scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch; padding-bottom: .5rem;
  /* PLAN 84: a horizontal swipe must never trigger the browser's
     back-navigation gesture — the real bug in every swipeable rail. The
     bounce itself stays the platform's: iOS rubber-bands natively, and a
     JS-simulated bounce on a mouse wheel reads as a stutter, so none is
     faked where the platform has none. */
  overscroll-behavior-x: contain;
  cursor: grab;
}
.event-rail.is-grabbing {
  cursor: grabbing; user-select: none;
  /* while the hand is on it: no snap fighting each pixel of the drag, no
     smooth animation of the follow. Releasing restores both, and the restore
     is what settles the rail onto a page boundary. */
  scroll-snap-type: none; scroll-behavior: auto;
}
.event-rail-item {
  flex: 0 0 min(320px, 85vw);
  /* PLAN 84: snapping lands on PAGES — app.js marks every Nth card
     .rail-page-start (N = how many fit, recomputed on resize). The others
     are not snap targets, and scroll-snap-stop keeps a hard fling from
     skipping past everything. */
  scroll-snap-align: none;
}
.event-rail-item.rail-page-start {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
.event-rail-arrow {
  position: absolute; top: 40%; z-index: 3;
  width: 2.2rem; height: 2.2rem; padding: 0; line-height: 1;
  border-radius: 50%; font-size: 1.3rem;
  background: var(--surface); border: 1px solid var(--border);
}
.event-rail-arrow[data-rail-prev] { left: -0.6rem; }
.event-rail-arrow[data-rail-next] { right: -0.6rem; }
.event-rail-arrow:disabled { opacity: .35; cursor: default; }

/* PLAN 111 defect 3: the loop's wrap is a TELEPORT and must not be animated. app.js
   adds this for the duration of the scrollLeft assignment and takes it off again — the
   same suspension `.is-grabbing` already applies while a hand is on the rail. Without
   it Chrome smooth-scrolls the wrap across the whole clone buffer, in full view. */
.event-rail.is-teleporting { scroll-behavior: auto; }

/* PLAN 111: `scroll-snap-stop: always` below stops a hard fling skipping the whole
   rail — and it also forbids a programmatic scroll from passing a snap point, so
   "jump to page 5" was arriving at page 2. Suspended for the duration of one jump and
   restored when it settles; the motion stays the browser's smooth scroll. */
.event-rail.is-jumping { scroll-snap-type: none; }

/* PLAN 111: page dots. Real buttons with real names, because they are a control and
   not decoration — tap one and the rail pages to it. */
.event-rail-dots {
  display: flex; justify-content: center; flex-wrap: wrap;
  gap: 6px; margin-top: 10px;
}
.event-rail-dots[hidden] { display: none; }
.event-rail-dot {
  /* A 6px dot inside a 24px target: visible affordance, thumb-sized hit area. */
  width: 24px; height: 24px; padding: 0; border: 0; background: none;
  cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
}
.event-rail-dot::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--border); transition: background .15s, transform .15s;
}
.event-rail-dot:hover::before { background: var(--muted); }
.event-rail-dot.is-current::before { background: var(--primary); transform: scale(1.5); }
.event-rail-dot:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  /* PLAN 84: motion is a preference; snap is STRUCTURE, and structure stays.
     Only the smooth scrolling goes. */
  .event-rail { scroll-behavior: auto; }
  .event-rail-dot::before { transition: none; }
}

/* ── Selection controls (plan 86): one family, three shapes ──
   Choice cards for consequential decisions (2-5 options, each worth a line of
   explanation); a segmented control for short exclusive pairs; the NATIVE
   select styled, never replaced — a JS listbox loses the phone's own picker,
   and nothing we build beats it. Every shape is a real <input> styled through
   :checked / :focus-visible: nothing here is a div with a click handler, which
   is why keyboard, screen reader, form submission and the no-JS path work
   without any code. */
/* PLAN 108 — the "?" affordance. IN FLOW, never absolutely positioned: the body pushes
   the page down instead of overlaying it, so at 390px it reflows rather than covering the
   control it explains ([[rule/lama-ui/disclosure]], [[rule/lama-ui/text-mobile]]). */
/* PLAN 110: an item's picture, at whatever size the surface asks for. Square by
   construction so a list never jumps as images arrive, and the empty state is a real
   block rather than a gap — nothing renders a broken image. */
.item-thumb {
  border-radius: var(--radius); object-fit: cover; flex: none;
  background: var(--surface-2);
}
.item-thumb-empty {
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--muted); font-weight: 700; font-size: 20px;
  border: 1px solid var(--border);
}

.help-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; min-width: 24px;
  margin-left: 6px; padding: 0;
  border: 1px solid var(--border); border-radius: 50%;
  background: var(--surface-2); color: var(--muted);
  font-weight: 700; font-size: 13px; line-height: 1; cursor: pointer;
}
.help-btn:hover { background: var(--surface-3); color: var(--text); }
.help-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.help-btn[aria-expanded="true"] { background: var(--surface-3); color: var(--text); }
.help-body {
  margin: 8px 0 12px; padding: 10px 12px;
  border-left: 3px solid var(--primary);
  background: var(--surface-2); border-radius: var(--radius);
  max-width: 70ch; font-size: 14px; font-weight: 400;
}
.help-body[hidden] { display: none; }
.help-body p { margin: 4px 0; }

.choice-grid {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  margin: 10px 0;
}
.choice {
  position: relative; display: block; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); padding: 12px 14px;
}
/* PLAN 94, the owner on /checkout/{id}: "Can we just remove the radio button." The
   card's border and inset ring already say which option is chosen, so the native dot
   was redundant chrome sitting in the corner of every card.

   Removing the PIXEL must never remove the INPUT. `opacity: 0`, not `display: none`
   and not `hidden`: a display:none'd radio leaves the tab order and the arrow-key
   group in some browsers, and with it go the keyboard, the screen reader and the
   no-JS submit — all of which this family exists to get for free. The input stays
   exactly where it was, painted to nothing; `:has(input:focus-visible)` below carries
   the focus ring the dot used to show.

   And `opacity` ONLY — no `pointer-events: none`. That was the first attempt and the
   E2E gate rejected it: three recorded checkouts died on `Locator.check` timing out,
   because a control that cannot receive a pointer event is a control nothing can
   click. It was solving a problem that does not exist — the input sits INSIDE its
   label, so a click landing on those 13px selects the option exactly like a click
   anywhere else on the card. */
.choice input[type="radio"], .choice input[type="checkbox"] {
  position: absolute; top: 14px; right: 12px; margin: 0;
  accent-color: var(--primary);
  opacity: 0;
}
.choice .choice-title { display: block; font-weight: 700; padding-right: 26px; }
.choice .choice-hint { display: block; color: var(--muted); font-size: 13px; margin-top: 2px; }
.choice:hover { border-color: var(--primary); }
.choice:has(input:checked) {
  border-color: var(--primary);
  box-shadow: inset 0 0 0 1px var(--primary);
}
.choice:has(input:focus-visible) {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
/* PLAN 94: the exchange index — the card is one link, the exchange-state tags are
   siblings outside it (STYLE-EVENTS §1), so they get their own line under the card. */
.exchange-row { display: flex; flex-direction: column; gap: 6px; }
.exchange-state { margin: 0; display: flex; flex-wrap: wrap; gap: 6px; }

/* the explanation stays readable while the option is disabled — a greyed
   mystery teaches nothing */
.choice:has(input:disabled) { opacity: .75; cursor: not-allowed; }
.choice:has(input:disabled) .choice-title { color: var(--muted); }

/* PLAN 101 — the LONE boolean. Owner, three notes in three days: "We should never show
   the radio button or check boxes, we should only display the named buttons and when
   selected highlight the div to show it's selected", and on the scanner: "Again another
   check box that need to be a button with highlights when selected."

   `.choice` answers a SET of options; a one-item card grid for "Offline mode" is
   absurd. So the second shape, and only a second: a named two-state button that
   highlights exactly the way a selected choice card does, so the two read as one
   system. The native control is hidden identically — `opacity: 0` and nothing else,
   for the reason `.choice` above records — and carries `role="switch"`, so a screen
   reader announces on/off rather than "checked". */
.toggle {
  position: relative; display: inline-flex; align-items: center; gap: 10px;
  cursor: pointer; min-height: 44px; padding: 8px 14px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); color: var(--text);
}
.toggle input[type="checkbox"] { position: absolute; inset: 0; margin: 0; opacity: 0; }
.toggle .toggle-title { font-weight: 600; }
.toggle .toggle-hint { color: var(--muted); font-size: 13px; }
/* Colour is never the only signal (WCAG 1.4.1) — the state says itself in words, and
   the checkbox underneath is what a screen reader reads. */
.toggle .toggle-state { color: var(--muted); font-size: 12px; font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase; }
.toggle .toggle-state::after { content: "Off"; }
.toggle:has(input:checked) .toggle-state::after { content: "On"; }
.toggle:hover { border-color: var(--primary); }
.toggle:has(input:checked) {
  border-color: var(--primary);
  box-shadow: inset 0 0 0 1px var(--primary);
}
.toggle:has(input:checked) .toggle-state { color: var(--primary); }
.toggle:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 2px; }
.toggle:has(input:disabled) { opacity: .75; cursor: not-allowed; }
.toggle-row { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0; }

.segmented {
  display: inline-flex; border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
}
.segmented label {
  padding: 6px 14px; cursor: pointer; margin: 0; position: relative;
}
.segmented label + label { border-left: 1px solid var(--border); }
.segmented input { position: absolute; opacity: 0; pointer-events: none; }
.segmented label:has(input:checked) {
  background: var(--primary); color: var(--primary-text, var(--surface));
}
.segmented label:has(input:focus-visible) {
  outline: 2px solid var(--primary); outline-offset: -2px;
}

/* the native select, kept and dressed: border, height, focus ring, caret */
select {
  appearance: none; -webkit-appearance: none;
  background-color: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 8px 34px 8px 12px; font: inherit;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8'><path d='M1 1l5 5 5-5' fill='none' stroke='%23888' stroke-width='2'/></svg>");
  background-repeat: no-repeat; background-position: right 12px center;
}
select:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* ── Admin hub (plan 74): dense grouped rows, not a card inventory ── */
.admin-filter { display: flex; gap: .8rem; align-items: center; margin: .5rem 0 1rem; }
.admin-filter input { max-width: 22rem; }
.admin-group h2 { margin: 1rem 0 .3rem; font-size: 1rem; color: var(--muted);
  text-transform: uppercase; letter-spacing: .06em; }
.admin-rows { list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); gap: .15rem .9rem; }
.admin-row { padding: .28rem .4rem; border-radius: var(--radius); line-height: 1.3; }
.admin-row:hover { background: var(--surface-2); }
.admin-row a { font-weight: 600; }
.admin-row-desc { display: block; font-size: .78rem; }

/* ── Venue map sprites (plan 73): one accent per CATEGORY, not per symbol ── */
.map-sprite { color: var(--muted); }
.sprite-cat-circulation { color: var(--warn); }
.sprite-cat-service     { color: var(--primary); }
.sprite-cat-seating     { color: var(--accent, var(--primary)); opacity: .9; }
.sprite-cat-outdoors    { color: var(--success, var(--muted)); }

/* ── Agent panel (plan 122): status you can read at a glance ──
   The only continuous motion on the site outside the rail, and it is deliberately
   the smallest thing that can carry meaning: a fresh agent's dot breathes, a quiet
   or stuck one holds still. Status feedback, never auto-advancing content
   (lama-ui/motion-touch), and all of it drops under reduced motion. */
.agent-cards { display: grid; gap: 1rem; grid-template-columns: 1fr; }
@media (min-width: 768px) {
  .agent-cards { grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); }
}
.agent-card h3 { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.agent-dot {
  width: .6rem; height: .6rem; border-radius: 50%;
  background: var(--muted); flex: 0 0 auto;
}
.agent-dot[data-freshness="fresh"] { background: var(--success); animation: agent-pulse 2.4s ease-in-out infinite; }
.agent-dot[data-freshness="quiet"] { background: var(--warn); }
.agent-dot[data-freshness="stuck"] { background: var(--error); }
@keyframes agent-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* One transition, not a loop: something arrived, and then the page is calm again. */
.flash-once { animation: agent-flash .9s ease-out 1; }
@keyframes agent-flash { from { background: var(--surface-3); } to { background: transparent; } }

.code-block {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: .4rem;
  padding: .5rem .7rem; margin: .25rem 0 0; overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85rem;
}

@media (prefers-reduced-motion: reduce) {
  /* The dot keeps its colour — the meaning lives in the colour and the pill's word,
     never in the movement (WCAG 1.4.1). Only the movement goes. */
  .agent-dot[data-freshness="fresh"] { animation: none; }
  .flash-once { animation: none; }
}

/* Plan 123: the agent control row. Buttons wrap rather than overflow, and every one of
   them clears 44px — this is a page where a mis-tap stops a loop. */
.agent-controls { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .6rem; }
.agent-controls .btn { min-height: 44px; }
/* The editor scrolls itself into view, and the top bar sits over the top of the
   viewport — without this the heading you just jumped to lands underneath it. Same
   idea as the sticky bottom bar's scroll-padding in lama-ui/responsive-grid, from the
   other end. */
#prompt-editor { scroll-margin-top: 72px; }
.code-area {
  width: 100%; box-sizing: border-box; resize: vertical;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85rem;
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--border); border-radius: .4rem; padding: .6rem;
}

/* Plan 139: the knowledge browser. Sections collapsed by default — a plan is thousands
   of words and this page's job is finding the line you want to fix. */
.k-section { border-bottom: 1px solid var(--border); padding: .5rem 0; }
.k-section > summary { cursor: pointer; font-weight: 600; min-height: 44px; display: flex; align-items: center; }
.k-body { max-width: 70ch; }
.k-body table { width: 100%; }

/* ── The board (plan 140): where the work is, and how long it has been there ──
   The motion budget here is deliberately tiny and every bit of it is caused by data.
   A card that drifted when nothing happened would be lying about the state of the
   system, and telling the truth about stuck work is this page's whole value. */
.board { display: grid; gap: 1rem; grid-template-columns: 1fr; }
@media (min-width: 768px) {
  /* auto-fit, so the count follows the viewport instead of a number written inline
     (lama-ui/responsive-grid). At 390px the stations simply stack — there is no
     meaningful path between them on a phone, and a faked one is worse than none. */
  .board { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); align-items: start; }
}
.board-station h2 { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; font-size: 1.05rem; }
.board-cards { gap: .4rem; }

.board-card {
  border: 1px solid var(--border); border-left: 3px solid var(--muted);
  border-radius: .4rem; background: var(--surface-2);
}
.board-card-open {
  display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  width: 100%; min-height: 44px; padding: .5rem .6rem; text-align: left;
  background: none; border: 0; color: var(--text); cursor: pointer; font: inherit;
}
.board-card-age { margin-left: auto; font-size: .8rem; }

/* Cooling: four named buckets, not a gradient. The eye reads four states faster than a
   ramp, and the question is "has this been ignored", not "exactly how long". */
.board-card[data-cooling="warm"]  { border-left-color: var(--success); }
.board-card[data-cooling="cool"]  { border-left-color: var(--var-blue); }
.board-card[data-cooling="cold"]  { border-left-color: var(--warn); }
.board-card[data-cooling="stale"] { border-left-color: var(--error); }

/* The card the agent is working THIS MINUTE. Lifted and pulsing because that is real
   state off the heartbeat; it stops the moment the agent goes idle. */
.board-card[data-working="1"] {
  border-left-color: var(--primary);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
  animation: board-working 2.4s ease-in-out infinite;
}
@keyframes board-working {
  0%, 100% { border-left-color: var(--primary); }
  50% { border-left-color: var(--success); }
}

/* A handoff: one trip, then it settles and stays still. Never a loop. */
.board-card-moved { animation: board-arrive .6s ease-out 1; }
@keyframes board-arrive {
  from { transform: translateX(-1.5rem); opacity: .2; }
  to { transform: none; opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  /* Degrades to flash-and-appear: the card is simply at its new station, highlighted
     once. Everything stays legible — the colour and the age text carry the meaning,
     the movement never did. */
  .board-card[data-working="1"] { animation: none; }
  .board-card-moved { animation: board-arrive-flash .9s ease-out 1; }
  @keyframes board-arrive-flash {
    from { background: var(--surface-3); }
    to { background: var(--surface-2); }
  }
}

/* Plan 139 round 1: the conflict panes. Two labelled blocks, the owner's on top and
   untouched — a refusal must not also delete the text somebody typed. Side by side
   where there is room; stacked on a phone, where two 30ch columns would be neither. */
.k-pane-label { display: block; font-size: .8rem; color: var(--muted); margin-top: .5rem; }
.k-theirs { border-left: 3px solid var(--warn); padding-left: .6rem; margin-top: .6rem; }
@media (min-width: 768px) {
  .k-editor:has(.k-theirs:not([hidden])) { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; }
  .k-editor:has(.k-theirs:not([hidden])) > p { grid-column: 1 / -1; }
  .k-theirs { border-left: 0; border-top: 3px solid var(--warn); padding: .6rem 0 0; margin-top: 0; }
}

/* Plan 140 round 1: waiting is a third state, not a still card.
   Deliberately still — nothing animates here. The motion rule holds (a card moves only
   when the data moved); what changed is that the card now SAYS it is waiting instead of
   sitting silently for up to an hour after a deliberate press. */
.board-card[data-waiting="1"] { border-left-style: dashed; }
.board-card-waiting, .board-card-failed {
  margin: 0; padding: .1rem .6rem .5rem; font-size: .82rem;
  display: flex; flex-wrap: wrap; align-items: center; gap: .35rem;
}
.board-card-waiting { color: var(--muted); }

/* PLAN 115 — the two jobs a member comes to /account for.

   Owner note c93f24cb was filed at 341px, so the pair is one column by default and two
   only where there is room. The column count lives here and never inline
   ([[rule/lama-ui/responsive-grid]]: an inline grid-template-columns outranks every
   media query at every width, which is the bug plan 112 paid for on the apply form). */
.account-jump {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
  margin: 0 0 14px;
}
@media (min-width: 768px) {
  .account-jump { grid-template-columns: 1fr 1fr; }
}
/* "Large buttons", in his words: comfortably past the 44px touch minimum, and the
   count sits opposite the label rather than under it so the button stays one line. */
.account-jump-btn {
  /* Prominent without claiming the page's single primary — see the note in
     rbac_account.html.j2 and [[rule/lama-ui/components]]. The surface ramp is the
     answer: this sits on the page's own paint, so --surface-2 is the step that reads
     ([[rule/lama-ui/state-contrast]]). */
  background: var(--surface-2);
  border: 1px solid var(--primary);
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 64px;
  padding: 0 18px;
  font-size: 17px;
  text-align: left;
}
.account-jump-btn:hover, .account-jump-btn:focus-visible { background: var(--surface-3); }
.account-jump-btn[aria-expanded="true"] { border-color: var(--var-blue); }
.account-jump-label { font-weight: 600; }
.account-jump-count {
  font-variant-numeric: tabular-nums;
  font-size: 15px;
  opacity: .9;
}
.account-panel { margin: 0 0 14px; }
.account-admin-heading { margin-top: 26px; }

/* PLAN 124 — the rules a build actually loaded, on its plan row.

   Owner: *"a view of what skill the agents are choosing"*. Small and quiet: they sit
   beside a plan title and there can be a dozen, so they wrap and never outweigh the
   title they qualify. `--surface-2` on the card's own `--surface` paint is the step that
   reads ([[rule/lama-ui/state-contrast]]). */
.skill-chips { display: inline-flex; flex-wrap: wrap; gap: 4px; margin-left: 6px; }
.skill-chip {
  display: inline-block;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1px 6px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
}
.skill-chip:hover, .skill-chip:focus-visible { background: var(--surface-3); color: var(--text); }
