/* ============================================================
   Sheri Garba Etobicoke, register.sherigarba.ca
   Shared design foundation for THREE surfaces across TWO origins:
   register.sherigarba.ca serves the attendee page (/) and the verify
   page (/verify.html); portal.sherigarba.ca serves the operator
   console at its own root (/). One stylesheet, mirrored into both
   build roots, so every url() here stays relative to assets/css/.

   Design system lifted from the festival site (sheri-garba
   public/garba/index.html inline block): purple night, saffron and
   gold, Playfair Display over Work Sans, pennant dividers, photo hero,
   paper alternation. Console component families carried from the
   prototype chassis and recoloured through these tokens.

   Dark is the default theme (the brand is lived after dark); light is
   the cream day theme. Content is visible by default; motion is
   additive; native document scroll only; body{overflow-x:clip}.
   Authored in src/assets/css/app.css and mirrored by finalize.py.
   ============================================================ */

/* ---------- fonts: self-hosted, font-src 'self', no Google Fonts ---------- */
@font-face{font-family:"Playfair Display";font-style:normal;font-weight:400;font-display:swap;src:url("../fonts/playfair-display-latin-400-normal.woff2") format("woff2")}
@font-face{font-family:"Playfair Display";font-style:italic;font-weight:400;font-display:swap;src:url("../fonts/playfair-display-latin-400-italic.woff2") format("woff2")}
@font-face{font-family:"Playfair Display";font-style:normal;font-weight:500;font-display:swap;src:url("../fonts/playfair-display-latin-500-normal.woff2") format("woff2")}
@font-face{font-family:"Playfair Display";font-style:italic;font-weight:500;font-display:swap;src:url("../fonts/playfair-display-latin-500-italic.woff2") format("woff2")}
@font-face{font-family:"Playfair Display";font-style:normal;font-weight:600;font-display:swap;src:url("../fonts/playfair-display-latin-600-normal.woff2") format("woff2")}
@font-face{font-family:"Playfair Display";font-style:normal;font-weight:700;font-display:swap;src:url("../fonts/playfair-display-latin-700-normal.woff2") format("woff2")}
@font-face{font-family:"Work Sans";font-style:normal;font-weight:400;font-display:swap;src:url("../fonts/work-sans-latin-400-normal.woff2") format("woff2")}
@font-face{font-family:"Work Sans";font-style:normal;font-weight:500;font-display:swap;src:url("../fonts/work-sans-latin-500-normal.woff2") format("woff2")}
@font-face{font-family:"Work Sans";font-style:normal;font-weight:600;font-display:swap;src:url("../fonts/work-sans-latin-600-normal.woff2") format("woff2")}
@font-face{font-family:"Noto Sans Gujarati";font-style:normal;font-weight:400;font-display:swap;src:url("../fonts/noto-sans-gujarati-gujarati-400-normal.woff2") format("woff2");unicode-range:U+0951-0952,U+0964-0965,U+0A80-0AFF,U+200C-200D,U+20B9,U+25CC,U+A830-A839}
@font-face{font-family:"Noto Sans Gujarati";font-style:normal;font-weight:500;font-display:swap;src:url("../fonts/noto-sans-gujarati-gujarati-500-normal.woff2") format("woff2");unicode-range:U+0951-0952,U+0964-0965,U+0A80-0AFF,U+200C-200D,U+20B9,U+25CC,U+A830-A839}
@font-face{font-family:"Noto Sans Gujarati";font-style:normal;font-weight:600;font-display:swap;src:url("../fonts/noto-sans-gujarati-gujarati-600-normal.woff2") format("woff2");unicode-range:U+0951-0952,U+0964-0965,U+0A80-0AFF,U+200C-200D,U+20B9,U+25CC,U+A830-A839}
@font-face{font-family:"Noto Sans Devanagari";font-style:normal;font-weight:400;font-display:swap;src:url("../fonts/noto-sans-devanagari-devanagari-400-normal.woff2") format("woff2");unicode-range:U+0900-097F,U+1CD0-1CF9,U+200C-200D,U+20A8,U+20B9,U+20F0,U+25CC,U+A830-A839,U+A8E0-A8FF,U+11B00-11B09}
@font-face{font-family:"Noto Sans Devanagari";font-style:normal;font-weight:500;font-display:swap;src:url("../fonts/noto-sans-devanagari-devanagari-500-normal.woff2") format("woff2");unicode-range:U+0900-097F,U+1CD0-1CF9,U+200C-200D,U+20A8,U+20B9,U+20F0,U+25CC,U+A830-A839,U+A8E0-A8FF,U+11B00-11B09}

/* ---------- tokens ---------- */
:root{
  /* festival constants, theme independent */
  --purple-900:#160B26; --purple-800:#1E1036; --purple-700:#2B1550;
  --purple-500:#6C32A7; --purple-300:#9861CF; --purple-200:#C9ABEC;
  --saffron:#F6A93B; --gold:#E7B94B; --marigold:#F58024;
  --magenta:#D6266E; --maple:#C0392B; --jade:#2FB08A; --teal:#1F9E9E;
  --cream:#F6EAD6; --on-cream:#26163F;

  --wide:1200px; --gutter:clamp(16px,4vw,44px); --radius:20px; --radius-sm:13px;
  --radius-lg:20px;

  /* Gujarati and Devanagari faces carry their own unicode-range, so putting them in
     every stack costs nothing for Latin text and makes mixed-script copy render in one
     family wherever it lands. */
  --serif:"Playfair Display","Noto Sans Gujarati","Noto Sans Devanagari",Georgia,"Times New Roman",serif;
  --sans:"Work Sans","Noto Sans Gujarati","Noto Sans Devanagari",system-ui,-apple-system,"Segoe UI",sans-serif;
  --guj:"Noto Sans Gujarati","Noto Sans Devanagari","Nirmala UI",var(--sans);
  /* the letterspaced kicker face: Work Sans, as the festival site has it. The Indic faces
     carry their own unicode-range here too: this stack renders the Buddy status strap, which
     is translated, and without them a Gujarati or Hindi strap fell out onto whatever system
     Indic font the device happened to have (measured: Nirmala UI on Windows) or onto nothing
     at all on a device with none, which is the exact failure the vendored fonts prevent. */
  --mono:"Work Sans","Noto Sans Gujarati","Noto Sans Devanagari",system-ui,sans-serif;
  /* machine codes only: a real monospace keeps a pass code unambiguous at a gate, and
     costs no font request */
  --code:ui-monospace,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;

  --focus:#F6A93B;
  --photo-grade:saturate(1.08) contrast(1.05) brightness(.99);
  /* text that must stay light in BOTH themes: the hero scrim, the posters, the footer,
     the console rail */
  --on-dark:#FFF7EA; --on-dark-2:#EFE1CC; --on-dark-3:#C3B2DF;
  --aside-idle:#D9CBEF; --aside-live:#FFF6E6; --aside-sub:#C9ABEC;

  /* gate verdict fills: solid, theme independent, chosen for sunlight at a mall door.
     Each fill carries its own ink; measured 5.84 / 9.92 / 4.94 / 10.91 / 5.51.
     --v-down is its own blue because "Cannot check" means let them in and "Already checked
     in" means no second entry: at a mall door the fill is read before the words, so those
     two opposite instructions must never share the dup amber. Blue against amber also
     survives every common colour vision deficiency, and the blue sits 2.22:1 in luminance
     away from the --v-unknown purple. forged keeps the revoked red on purpose (both mean
     refuse); its headline and glyph separate it. */
  --v-ok:#2FB08A; --v-ok-ink:#07271E;
  --v-dup:#E7B94B; --v-dup-ink:#22110A;
  --v-revoked:#C0392B; --v-revoked-ink:#FFF1EE;
  --v-unknown:#3A2A55; --v-unknown-ink:#F4ECDF;
  --v-down:#2B67AD; --v-down-ink:#F7FAFF;
}
/* night, the default. The brand is lived after dark, so no OS sniffing. */
:root,:root[data-theme="dark"]{
  --paper:#140B24; --paper-2:#1A0F30; --surface:#211436; --surface-2:#291A43;
  --text:#F4ECDF; --muted:#B4A6CE; --brand:#C9ABEC; --brand-ink:#E7D7FA; --on-brand:#160B26;
  --accent:#F6A93B; --accent-2:#F58024;
  --line:rgba(255,255,255,.10); --line-strong:rgba(255,255,255,.20);
  --shadow:0 30px 70px -34px rgba(0,0,0,.8); --shadow-sm:0 14px 34px -20px rgba(0,0,0,.7);
  /* split tokens: --accent is a fill and a display colour, --accent-ink is for small text.
     One token cannot do both jobs across two themes and still clear 4.5:1. */
  --accent-ink:#F6A93B; --ok:#2FB08A; --warn-ink:#E7B94B; --danger:#E8756A; --info:#C9ABEC;
  --on-accent:#22110A;
  /* form field edges need 3:1 against the field fill (WCAG 1.4.11); --line-strong is
     tuned for decorative card outlines and is too faint for a control boundary */
  --field-line:rgba(255,255,255,.40);
  --swatch-ring:rgba(255,255,255,.22);
  --stage:radial-gradient(120% 92% at 16% 0%,color-mix(in srgb,var(--purple-500) 24%,transparent),transparent 62%),
          radial-gradient(96% 72% at 94% 100%,color-mix(in srgb,var(--saffron) 15%,transparent),transparent 58%);
  color-scheme:dark;
}
/* day, cream daylight */
:root[data-theme="light"]{
  --paper:#FBF6EC; --paper-2:#F4ECD8; --surface:#FFFFFF; --surface-2:#FBF5E7;
  --text:#26163F; --muted:#6B5E80; --brand:#5B2A8C; --brand-ink:#411E64; --on-brand:#FFFFFF;
  --accent:#C4611A; --accent-2:#D6266E;
  --line:rgba(65,30,100,.14); --line-strong:rgba(65,30,100,.26);
  --shadow:0 30px 66px -34px rgba(44,18,71,.45); --shadow-sm:0 14px 32px -20px rgba(44,18,71,.4);
  /* --ok is #1A6B50 here, not the festival site's #1A7A5E: that one measures 4.47:1 on
     paper-2 and the console paints status text on every ground. Held from the sweep. */
  --accent-ink:#9A4A0E; --ok:#1A6B50; --warn-ink:#8A5600; --danger:#B3413B; --info:#5B2A8C;
  --on-accent:#22110A;
  --field-line:rgba(65,30,100,.55);
  --swatch-ring:rgba(38,22,63,.22);
  /* saffron rings cannot be seen on cream; match verify.html so both visitor surfaces ring alike */
  --focus:#8A4A00;
  --stage:radial-gradient(120% 92% at 16% 0%,color-mix(in srgb,var(--purple-500) 14%,transparent),transparent 62%),
          radial-gradient(96% 72% at 94% 100%,color-mix(in srgb,var(--saffron) 12%,transparent),transparent 58%);
  color-scheme:light;
}
/* compatibility aliases for the prototype chassis vocabulary: every one resolves to a
   festival token, so a leftover var(--panel) in a shell still paints the right colour */
:root{
  --ground:var(--paper); --panel:var(--surface); --panel-2:var(--surface-2); --chip:var(--surface-2);
  --line-soft:var(--line); --text-2:var(--muted); --text-3:var(--muted);
  --r-xs:8px; --r-sm:12px; --r:16px; --r-lg:20px; --r-xl:26px;
  --maxw:var(--wide); --pad:var(--gutter);
  --f-display:var(--serif); --f-body:var(--sans); --f-mono:var(--mono);
}

/* ---------- reset ---------- */
*,*::before,*::after{box-sizing:border-box}
/* author display rules beat the UA [hidden] rule, so make hidden authoritative */
[hidden]{display:none !important}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;scroll-padding-top:92px}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
/* never a hidden x overflow on html or body (nested scroll trap); clip is the honest cut */
body{margin:0;background:var(--paper);color:var(--text);
  font-family:var(--sans);font-size:17px;line-height:1.65;font-weight:400;
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
  overflow-x:clip}
img{max-width:100%;display:block;height:auto}
svg,video,canvas{max-width:100%}
a{color:inherit;text-decoration:none}
button{font-family:inherit;cursor:pointer}
button,input,select,textarea{font:inherit;color:inherit}
:focus-visible{outline:3px solid var(--focus);outline-offset:2px;border-radius:6px}
::selection{background:var(--saffron);color:#160B26}
h1,h2,h3,h4{font-family:var(--serif);font-weight:600;line-height:1.08;margin:0;letter-spacing:-.01em;color:var(--text)}
h1{font-size:clamp(2.6rem,6.4vw,4.6rem);font-weight:500}
h2{font-size:clamp(1.9rem,4.2vw,3.1rem)}
h3{font-size:clamp(1.1rem,2.2vw,1.4rem);font-family:var(--sans);font-weight:600;letter-spacing:0}
h4{font-size:1.02rem;font-family:var(--sans);font-weight:600;letter-spacing:0}
p{margin:0 0 1rem}
.guj{font-family:var(--guj)}
.hi{font-family:var(--guj)}
.mono{font-family:var(--mono);font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;letter-spacing:.03em}
.code{font-family:var(--code);font-variant-numeric:tabular-nums;letter-spacing:.04em}
.num{font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}
.sr,.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ---------- layout ---------- */
.wrap{width:min(100%,var(--wide));margin-inline:auto;
  padding-left:max(var(--gutter),env(safe-area-inset-left,0px));padding-right:max(var(--gutter),env(safe-area-inset-right,0px))}
section{padding-block:clamp(60px,9vw,120px);position:relative}
section.tight{padding-block:clamp(44px,6vw,84px)}
section[id]{scroll-margin-top:92px}
.alt{background:var(--paper-2)}
.lede{font-size:clamp(1.04rem,1.6vw,1.26rem);color:var(--muted);max-width:60ch}
.note{font-size:.86rem;color:var(--muted)}
.muted{color:var(--muted)}
.dim{color:var(--muted)}
.sechead{max-width:66ch}
.kicker,.eyebrow{font-family:var(--mono);font-size:.75rem;letter-spacing:.26em;text-transform:uppercase;
  color:var(--accent-ink);font-weight:600;display:inline-flex;align-items:center;flex-wrap:wrap;gap:.55em;margin:0 0 1rem}
.kicker .guj,.eyebrow .guj{font-family:var(--guj);letter-spacing:.02em;text-transform:none;font-size:1rem;color:var(--brand)}
.eyebrow.cool{color:var(--brand)}
.hide{display:none!important}
/* A demonstration notice is true only while the site runs on the browser store. In live
   mode the pass really is in the festival register, so a sample-data badge would be a lie. */
:root[data-sg-mode="live"] [data-demo-only]{display:none}
.min0{min-width:0}
.split{display:grid;gap:clamp(28px,4.6vw,56px);align-items:start}
.split>*{min-width:0}
@media(min-width:920px){
  .split{grid-template-columns:1.04fr .96fr}
  .split.narrow{grid-template-columns:.9fr 1.1fr}
  .split.mid{align-items:center}
}
.grid{display:grid;gap:16px}
.g2{grid-template-columns:repeat(2,minmax(0,1fr))}
.g3{grid-template-columns:repeat(3,minmax(0,1fr))}
.g4{grid-template-columns:repeat(4,minmax(0,1fr))}
.grid>*{min-width:0}
.row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.between{justify-content:space-between}
.stack{display:flex;flex-direction:column;gap:12px}
.sp{flex:1}
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:22px;box-shadow:var(--shadow-sm)}
.card-2{background:var(--surface-2)}
hr.rule{border:0;height:1px;background:var(--line);margin:0}
/* type-scale helpers the console chassis uses */
.h1{font-family:var(--serif);font-weight:500;font-size:clamp(2.1rem,5vw,3.4rem);line-height:1.08;letter-spacing:-.01em}
.h2{font-family:var(--serif);font-weight:600;font-size:clamp(1.6rem,3.4vw,2.4rem);line-height:1.1}
.h3{font-family:var(--sans);font-weight:600;font-size:clamp(1.1rem,2.2vw,1.4rem);line-height:1.2}

/* ---------- buttons, full pills ---------- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.55em;
  padding:.88em 1.6em;border-radius:999px;font-weight:600;font-size:.97rem;
  border:1.5px solid transparent;line-height:1;text-align:center;cursor:pointer;
  background:linear-gradient(135deg,var(--marigold),var(--saffron));color:var(--on-accent);
  box-shadow:0 14px 30px -16px rgba(246,169,59,.75);
  transition:transform .18s ease,box-shadow .18s ease,background .18s ease}
.btn:hover{transform:translateY(-2px)}
.btn:active{transform:translateY(0)}
.btn-primary{background:linear-gradient(135deg,var(--marigold),var(--saffron));color:var(--on-accent)}
.btn.ghost,.btn-ghost{background:transparent;color:var(--text);border-color:var(--line-strong);box-shadow:none}
.btn.ghost:hover,.btn-ghost:hover{background:var(--surface-2);border-color:var(--brand)}
.btn.brand,.btn-cool{background:var(--brand);color:var(--on-brand);box-shadow:var(--shadow-sm)}
.btn.danger,.btn-danger{background:transparent;border-color:var(--danger);color:var(--danger);box-shadow:none}
.btn.danger:hover,.btn-danger:hover{background:color-mix(in srgb,var(--danger) 12%,transparent)}
.btn.sm,.btn-sm{padding:.4em 1em;font-size:.84rem;min-height:44px}
.btn.lg,.btn-lg{padding:1.02em 1.95em;font-size:1.04rem}
.btn.block,.btn-block{width:100%}
.btn:disabled,.btn[disabled]{opacity:.45;cursor:not-allowed;transform:none}
@media (prefers-reduced-motion:reduce){.btn{transition:none}.btn:hover{transform:none}}
.icon-btn{width:44px;height:44px;border-radius:999px;border:1px solid var(--line-strong);background:var(--surface);
  display:grid;place-items:center;cursor:pointer;color:var(--text);transition:border-color .15s,color .15s,transform .2s;flex:none}
.icon-btn:hover{border-color:var(--brand);color:var(--brand-ink)}
.icon-btn svg{width:19px;height:19px}
.linkish{background:none;border:0;padding:0;color:var(--accent-ink);text-decoration:underline;font-size:inherit;cursor:pointer}

/* ---------- chips and status ---------- */
.pill{display:inline-flex;align-items:center;gap:.4em;padding:.26em .72em;border-radius:999px;font-size:.68rem;
  font-weight:600;font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase;white-space:nowrap;
  border:1px solid var(--line-strong);color:var(--muted);background:transparent}
.pill.jade,.pill.ok{border-color:color-mix(in srgb,var(--ok) 60%,transparent);color:var(--ok)}
.pill.gold,.pill.hot{border-color:color-mix(in srgb,var(--gold) 60%,transparent);color:var(--accent-ink)}
.pill.cool,.pill.brand{border-color:color-mix(in srgb,var(--brand) 60%,transparent);color:var(--brand)}
.pill.bad{border-color:color-mix(in srgb,var(--danger) 60%,transparent);color:var(--danger)}
.dot{width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}
.live .dot{animation:pulse 1.7s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.72)}}
/* status tags stay outline only: a table row carries its own hover tint, and a second
   fill behind an 11px tag dropped the lighter statuses under 4.5:1 on the cream theme */
.tag{display:inline-block;border:1px solid var(--line-strong);border-radius:999px;padding:.12rem .6rem;font-family:var(--mono);
  font-size:.68rem;letter-spacing:.08em;text-transform:uppercase;font-weight:600;white-space:nowrap;color:var(--muted)}
.tag.Active,.tag.Published,.tag.Confirmed,.tag.ok,.tag.live{border-color:color-mix(in srgb,var(--ok) 62%,transparent);color:var(--ok)}
.tag.Draft,.tag.Closed,.tag.Completed,.tag.demo{border-color:var(--line-strong);color:var(--muted)}
.tag.Revoked,.tag.Suspended,.tag.Cancelled,.tag.Declined,.tag.bad,.tag.low{border-color:color-mix(in srgb,var(--danger) 62%,transparent);color:var(--danger)}
.tag.New,.tag.Open,.tag.warn,.tag.dup{border-color:color-mix(in srgb,var(--warn-ink) 62%,transparent);color:var(--warn-ink)}
.tag.Contacted,.tag.info{border-color:color-mix(in srgb,var(--info) 62%,transparent);color:var(--info)}
.cdot,.swatch{display:inline-block;width:13px;height:13px;border-radius:50%;background:var(--nc,var(--saffron));
  border:1px solid var(--swatch-ring);vertical-align:-2px;flex:none}
.colour{display:inline-flex;align-items:center;gap:.45rem;font-size:.8rem;color:var(--muted);margin-bottom:.6rem}

/* v3.0.1: the foundation band above the header is gone (client request 2026-10-08). */

/* ---------- demo ribbon ---------- */
.demo-ribbon{background:var(--purple-900);color:#EADFF6;font-family:var(--mono);font-size:.72rem;
  letter-spacing:.14em;text-transform:uppercase;text-align:center;padding:.58rem 1rem;line-height:1.6}
.demo-ribbon b{color:var(--gold);font-weight:600}
:root[data-theme="light"] .demo-ribbon{background:#2B1550;color:#EADFF6}
@media(max-width:430px){.demo-ribbon{font-size:.63rem;letter-spacing:.1em}}

/* ---------- pennant divider, dashed rule with hanging triangles ---------- */
.divider{width:100%;height:24px;position:relative;border:0;margin:0;
  background-image:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='64'%20height='15'%3E%3Cpath%20d='M1%200h13l-6.5%2014z'%20fill='%23D94A3D'/%3E%3Cpath%20d='M17%200h13l-6.5%2014z'%20fill='%23F1892E'/%3E%3Cpath%20d='M33%200h13l-6.5%2014z'%20fill='%232FB08A'/%3E%3Cpath%20d='M49%200h13l-6.5%2014z'%20fill='%23E7B94B'/%3E%3C/svg%3E");
  background-repeat:repeat-x;background-position:center 6px;background-size:64px 15px}
.divider::before{content:"";position:absolute;top:5px;left:0;right:0;border-top:2px dashed var(--line-strong)}
.divider.on-paper{background-color:var(--paper)}
.divider.on-paper-2{background-color:var(--paper-2)}
.divider.on-surface{background-color:var(--surface)}
.divider.inset{margin:22px 0 0}
/* small pennant strip for the pass card and the night dialog */
.toran{height:15px;background-image:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='64'%20height='15'%3E%3Cpath%20d='M1%200h13l-6.5%2014z'%20fill='%23D94A3D'/%3E%3Cpath%20d='M17%200h13l-6.5%2014z'%20fill='%23F1892E'/%3E%3Cpath%20d='M33%200h13l-6.5%2014z'%20fill='%232FB08A'/%3E%3Cpath%20d='M49%200h13l-6.5%2014z'%20fill='%23E7B94B'/%3E%3C/svg%3E");
  background-repeat:repeat-x;background-size:64px 15px}
.toran.thin{height:11px;background-size:48px 11px}

/* ---------- header ---------- */
.site-header,.hdr{position:sticky;top:0;z-index:60;background:color-mix(in srgb,var(--paper) 86%,transparent);
  backdrop-filter:saturate(1.3) blur(16px);-webkit-backdrop-filter:saturate(1.3) blur(16px);
  border-bottom:1px solid transparent;transition:border-color .3s,background .3s}
.site-header.scrolled,.hdr.stuck{border-bottom-color:var(--line);background:color-mix(in srgb,var(--paper) 95%,transparent)}
/* In day theme the header's dark ink composites against the dark hero photograph until
   .scrolled fires, so the translucent paper has to be thicker than it is at night. */
:root[data-theme="light"] .site-header,:root[data-theme="light"] .hdr{background:color-mix(in srgb,var(--paper) 94%,transparent)}
:root[data-theme="light"] .site-header.scrolled,:root[data-theme="light"] .hdr.stuck{background:color-mix(in srgb,var(--paper) 97%,transparent)}
.site-header .wrap{width:min(100%,1340px)}
.nav,.hdr-in{display:flex;align-items:center;justify-content:space-between;gap:1rem;min-height:74px}
.brand{display:flex;align-items:center;gap:.7rem;min-width:0;min-height:44px;color:inherit}
.brand>span{min-width:0;overflow:hidden}
/* The lockup is the foundation's own mark (img/osf/logo-112.webp, the 112 px WebP finalize.py
   makes from logo.png, transparent), painted as a background so it never competes for layout.
   It carries a WHITE wordmark under the disc, so it is the night file; the day theme paints the
   staged light variant wherever the ground is cream or white. The console rail, the hero scrim
   and the footer stay ink in both themes and keep the night file. */
.brand .osf,.brand .mark{flex:none;width:40px;height:42px;
  background:url("../img/osf/logo-112.webp") 50%/contain no-repeat}
:root[data-theme="light"] .brand .osf,:root[data-theme="light"] .brand .mark,:root[data-theme="light"] .pres .mark{
  background-image:url("../img/osf/ohm-shanti-logo-light.webp")}
:root[data-theme="light"] .side .brand .osf,:root[data-theme="light"] .side .brand .mark{background-image:url("../img/osf/logo-112.webp")}
.brand img{flex:none;width:40px;height:42px;object-fit:contain}
/* v3.0.1: the offline page and the 404 show the client logo mark (tools/make_mark.py) and the
   Gujarati name as the title; .osf stays as it was for the portal masthead. */
.brand .osf-mark{flex:none;width:44px;height:44px;border-radius:50%;background:#ebedee center/cover no-repeat url("../img/osf/osf-mark.webp")}
.brand .bn.guj{font-family:var(--guj);font-weight:600;letter-spacing:0}
.brand .bn{font-family:var(--serif);font-weight:600;font-size:1.06rem;letter-spacing:-.01em;line-height:1.15;
  display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.brand .bs,.brand small{display:block;font-family:var(--mono);font-size:.58rem;letter-spacing:.22em;text-transform:uppercase;
  color:var(--accent-ink);font-weight:600;margin-top:.22rem}
/* v3.0.1: under the Gujarati title the festival name reads as written, never upper cased.
   Scoped to the client mark lockup (offline, both 404s), so the portal masthead keeps its
   own small caps subtitle. Its specificity also beats the max-width 520px rule below. */
.brand .osf-mark~span .bs{font-family:var(--sans);font-size:.8rem;font-weight:500;letter-spacing:.01em;text-transform:none;margin-top:.1rem}
/* v3.0.1 review: the title is set the size the four light pages set it (register-live.css,
   1.32rem) at every width, so offline and the 404s read title over subtitle the same way.
   The name is short and never needs the ellipsis, so its matras are never clipped either. */
.brand .osf-mark~span .bn.guj{font-size:1.32rem;line-height:1.15;overflow:visible}
.nav-links{display:flex;align-items:center;gap:0;list-style:none;margin:0;padding:0}
.nav-links a{display:block;padding:.5rem .56rem;border-radius:999px;font-size:.875rem;font-weight:500;
  color:var(--muted);white-space:nowrap;transition:color .15s,background .15s}
.nav-links a:hover,.nav-links a[aria-current="page"]{color:var(--text);background:var(--surface-2)}
.nav-links .m-only{display:none}
.nav-actions,.hdr-cta{display:flex;align-items:center;gap:.5rem;flex-shrink:0}
.theme-toggle{width:44px;height:44px;border-radius:50%;border:1px solid var(--line-strong);background:var(--surface);
  color:var(--text);display:grid;place-items:center;transition:transform .2s,border-color .2s;flex:none}
.theme-toggle:hover{transform:rotate(-15deg);border-color:var(--brand)}
.theme-toggle svg{width:19px;height:19px}
.theme-toggle .i-sun{display:none}
:root[data-theme="dark"] .theme-toggle .i-sun{display:block}
:root[data-theme="dark"] .theme-toggle .i-moon{display:none}
.burger{display:none;width:44px;height:44px;border-radius:999px;border:1px solid var(--line-strong);
  background:var(--surface);flex-direction:column;gap:5px;align-items:center;justify-content:center;color:var(--text)}
.burger span{width:19px;height:2px;background:var(--text);border-radius:2px}
.burger svg{width:19px;height:19px}
@media(max-width:1279px){
  .nav-links{display:none}
  .site-header .burger{display:flex}
  /* Anchored to the header it hangs from, not to a guessed viewport offset: the demo ribbon
     above the header wraps on a phone, so a fixed 74px inset and a viewport-measured cap
     disagree by the ribbon's height and push the pass CTA off screen. --menu-max is set from
     the header's measured bottom when the panel opens. */
  .nav-links.open{display:flex;position:absolute;inset:100% 0 auto 0;flex-direction:column;align-items:stretch;
    background:var(--paper);padding:1rem var(--gutter) 1.6rem;gap:.1rem;border-bottom:1px solid var(--line);
    box-shadow:var(--shadow);z-index:60;max-height:var(--menu-max,70dvh);overflow:auto}
  .nav-links.open a{padding:.9rem .6rem;font-size:1.04rem;border-bottom:1px solid var(--line);color:var(--text)}
  .nav-links.open .m-only{display:block}
  .nav-links.open a.btn{margin-top:1rem;border:none;color:var(--on-accent);text-align:center}
}
/* the brand lockup has to give way before the menu button does */
@media(max-width:520px){
  .nav{gap:.6rem}
  .brand{gap:.55rem}
  .brand .osf,.brand .mark,.brand img{height:36px}
  .brand .bn{font-size:.95rem}
  .brand .bn .guj{display:none}
  .brand .bs,.brand small{font-size:.62rem;letter-spacing:.16em}
}
@media (prefers-reduced-motion:reduce){.theme-toggle,.icon-btn{transition:none}.theme-toggle:hover{transform:none}}

/* ---------- hero: full bleed photograph, left weighted scrim ---------- */
/* The copy side is darkened so the type reads while the colourful centre of the
   photograph stays visible. Hero text is light in BOTH themes, so it uses the
   theme independent constants, never the theme tokens. */
/* v2: the hero is a masthead, not a poster. The page has one job and the sign in card has
   to be reachable without a scroll, so the 90dvh/800px full bleed came down to a band that
   holds the lockup, the h1 and the facts strip and nothing else. */
.hero{position:relative;min-height:min(58dvh,460px);padding-block:0;display:flex;align-items:center;overflow:hidden;isolation:isolate;
  background:linear-gradient(115deg,#160C2A 0%,#20124A 46%,#2C1656 100%)}
.hero-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:64% center;z-index:0;
  filter:var(--photo-grade)}
/* The .30 stop used to land inside the 640px copy column, so small warm type sat on a
   bright warm photograph. The copy side is held dark the whole width of the column. */
.hero::after{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;background:
  linear-gradient(90deg,rgba(14,7,24,.88) 0%,rgba(14,7,24,.80) 18%,rgba(14,7,24,.52) 38%,rgba(14,7,24,.26) 56%,rgba(14,7,24,.22) 100%),
  linear-gradient(180deg,rgba(14,7,24,.24) 0%,rgba(14,7,24,.06) 42%,rgba(14,7,24,.48) 100%)}
.hero .wrap{position:relative;z-index:2}
.hero-inner{max-width:640px;padding-block:clamp(34px,4.6vw,56px)}
/* The two smallest strings in the hero move up the warm family so they clear the
   photograph. The hero stays one family: this is the kicker's own Gujarati tone. */
.hero .kicker{color:#F5DDAC}
.hero .kicker .guj{color:#F5DDAC}
.hero h1{margin:.3rem 0 .2rem;color:var(--on-dark);text-shadow:0 2px 30px rgba(0,0,0,.45)}
.hero h1 .amber{color:var(--saffron);font-style:italic}
.hero .season{font-family:var(--serif);font-weight:500;font-style:italic;color:#F3DCB2;
  font-size:clamp(1.25rem,2.8vw,1.9rem);margin:.1rem 0 .9rem;letter-spacing:-.01em;text-shadow:0 2px 22px rgba(0,0,0,.6)}
.hero .lede{color:#EDE3F7;max-width:48ch;text-shadow:0 1px 14px rgba(0,0,0,.55)}
/* WHERE AND WHEN, in the hero, because v2 removes the twelve nights table and the venue
   section and after that the hero is the ONLY place the page renders the full date range
   (both ends), the venue, doors and the close rule. Rendered from SG.FEST and SG.NIGHTS by
   the page, never typed here.
   It carries its OWN backing rather than trusting the photograph underneath. The hero scrim
   is tuned per breakpoint and its thinnest stop is alpha .66 on a phone and about .386
   across the desktop copy column; over a pure white photo pixel (the lightest ground any
   photograph can present) a .55 backing on top of that lands the ground at #332D3B and
   #514C58, where #EDE3F7 measures 10.73 and 6.75 and --on-dark measures 12.51 and 7.87.
   That is an analytic bound, not a sample: nothing in the image can be lighter than white. */
.herofacts{display:flex;flex-wrap:wrap;gap:.25rem 1rem;margin:1rem 0 0;padding:.7rem .95rem;list-style:none;
  background:rgba(14,7,24,.55);border:1px solid rgba(255,255,255,.14);border-radius:var(--radius-sm);
  color:#EDE3F7;font-size:.92rem;line-height:1.6;max-width:52ch;
  -webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}
.herofacts li{display:flex;align-items:baseline;gap:.5rem;min-width:0}
/* A marker on EVERY row, not a separator between rows. This strip wraps to one item per line
   at phone width, where li+li gave the first fact no dot and the other three one each, which
   reads as a broken bullet list; and align-self:center put the dot at the midpoint of a row
   that wrapped to two lines, 11.5px below the label it precedes. flex-start plus the .62em
   nudge lands it on the first line instead, and the rule holds at desktop width too. */
.herofacts li::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--saffron);flex:none;
  align-self:flex-start;margin-top:.62em;margin-right:.1rem}
.herofacts b{color:var(--on-dark);font-weight:600}
@media(max-width:520px){.herofacts{font-size:.88rem;gap:.1rem .85rem;max-width:100%}}
.hero .btn.ghost,.hero .btn-ghost{color:var(--on-dark);border-color:rgba(255,255,255,.55);background:rgba(20,11,36,.34);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
.hero .btn.ghost:hover,.hero .btn-ghost:hover{background:rgba(255,255,255,.14);border-color:#fff}
.hero-cta{display:flex;flex-wrap:wrap;gap:.8rem;margin-top:1.5rem;align-items:center}
.presents{display:flex;align-items:center;gap:.9rem;margin:0 0 .6rem;
  font-family:var(--mono);font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;line-height:1.7;color:var(--on-dark-2)}
.presents img{height:56px;width:auto;flex:none}
.presents b{display:block;font-weight:600;letter-spacing:.24em;color:var(--gold)}
/* The countdown chip (.countdown, .flame, @keyframes flick) is gone with the chip itself:
   V2-SPEC section 6 gives the hero no countdown, and nothing else on either origin used it. */
@media(max-width:860px){
  .hero{min-height:min(46dvh,360px)}
  .hero-inner{max-width:100%;padding-block:clamp(28px,7vw,44px)}
  .hero-img{object-position:center}
  /* On phones the copy spans almost the whole hero, so the photograph can only ever be a
     texture here. The earlier .50/.44/.84 scrim was measured on the lede, the countdown chip
     and the h1 only; the two 11px strings and the 20px season line sit over the brightest band of the
     photograph and 27% of "presents", 16% of the kicker (the venue fact) and 7% of the season
     fell under 4.5:1. Measured pixel by pixel against the composited image at 390x844 and
     360x640, .74/.66/.86 is the lightest scrim that leaves NO pixel of any hero string under
     its threshold: presents 5.75, kicker 5.94, season 5.43, h1 6.74, lede 6.14 (minimums). */
  .hero::after{background:linear-gradient(180deg,rgba(14,7,24,.74) 0%,rgba(14,7,24,.66) 40%,rgba(14,7,24,.86) 100%)}
  .hero .presents,.hero .kicker{text-shadow:0 1px 12px rgba(0,0,0,.6)}
  .presents img{height:52px}
}

/* ---------- night cards ----------
   The ops Overview "tonight" card is the only consumer left (src/assets/js/ops.js:506, which
   renders .night.tonight with .row, .no, .date, h3, p, .meta and .foot), so the whole family
   stays. The .night-grid wrapper and the dense .board list that both fed the attendee
   programme section are gone: zero references in src/ after the nights table leaves. */
.night{position:relative;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-sm);
  padding:1.3rem 1.3rem 1.3rem 1.5rem;overflow:hidden;display:flex;flex-direction:column;gap:.1rem;min-width:0;
  transition:transform .22s ease,box-shadow .22s ease,border-color .22s ease}
/* ringed, because night 4 is White and would vanish on the day theme's white card */
.night::before{content:"";position:absolute;top:0;left:0;width:4px;height:100%;background:var(--nc,var(--saffron));
  box-shadow:inset -1px 0 0 var(--line-strong)}
.night:hover{transform:translateY(-4px);box-shadow:var(--shadow-sm);border-color:var(--line-strong)}
@media (prefers-reduced-motion:reduce){.night{transition:none}.night:hover{transform:none}}
.night .row{display:flex;align-items:baseline;justify-content:space-between;gap:.6rem}
.night .no{font-family:var(--mono);font-size:.7rem;letter-spacing:.16em;color:var(--muted);text-transform:uppercase}
.night .date{font-family:var(--mono);font-size:.78rem;color:var(--accent-ink);font-weight:600}
.night h3{margin:.5rem 0 .35rem;font-family:var(--serif);font-weight:600;font-size:1.3rem}
.night p{font-size:.9rem;color:var(--muted);margin:0 0 .9rem}
.night .meta{font-size:.75rem;color:var(--muted);font-family:var(--mono);letter-spacing:.02em;
  border-top:1px solid var(--line);padding-top:.7rem;display:flex;flex-wrap:wrap;gap:.35rem .8rem;margin-top:auto}
.night .foot{display:flex;flex-wrap:wrap;gap:.6rem;align-items:center;justify-content:space-between;margin-top:.8rem}
.night.dussehra{border-color:color-mix(in srgb,var(--gold) 55%,var(--line))}
.night.tonight{box-shadow:0 0 0 2px color-mix(in srgb,var(--saffron) 55%,transparent)}
/* the same --nc contract on a table row. The attendee nights table is gone; the ops Nights
   view still renders tr.night-row (src/assets/js/ops.js:861), so this pair stays. */
tr.night-row td:first-child{border-left:4px solid var(--nc,var(--saffron))}
tr.night-row.dussehra td{background:color-mix(in srgb,var(--gold) 9%,transparent)}
/* Removed with the sections they dressed, each one grepped across src/ first and found
   nowhere else: .ninedots (the nine colour dots above the nights table; the verify page's
   strip is the separate .nine family and stays), .figure and .poster (the 2026 poster and
   the photography figures, both festival-site content now), .stats and .stat (already
   unreferenced in v1), .etiq and .echip ("what to bring" chips) and .arrive (the
   getting-here bullets). */

/* ---------- checklists ---------- */
.checklist{list-style:none;padding:0;margin:1.2rem 0 0;display:grid;gap:.7rem}
.checklist li{position:relative;padding-left:1.8rem;color:var(--text);font-size:.95rem}
.checklist li::before{content:"";position:absolute;left:0;top:.52em;width:10px;height:10px;border-radius:50%;
  background:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 22%,transparent)}

/* ---------- forms: every control is at least 16px so iOS never zooms ---------- */
.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
  padding:clamp(20px,2.8vw,30px);box-shadow:var(--shadow-sm)}
.panel .hd{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap}
.panel .hd h2{font-family:var(--sans);font-size:1.02rem;font-weight:600;letter-spacing:0}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px}
.row2>*,.row3>*{min-width:0}
@media(max-width:640px){.row2,.row3{grid-template-columns:1fr}}
label{display:block;font-size:.84rem;font-weight:600;margin:0 0 .4rem;color:var(--text)}
input,select,textarea{width:100%;padding:.8rem .95rem;border-radius:12px;border:1.5px solid var(--field-line);
  background:var(--surface-2);color:var(--text);font-family:inherit;font-size:max(16px,1em);
  transition:border-color .15s,box-shadow .15s}
input[type="checkbox"],input[type="radio"]{width:auto;padding:0;accent-color:var(--accent)}
select{appearance:auto}
textarea{min-height:112px;resize:vertical}
/* A mouse click tints the border; the keyboard gets the same 3px ring every button and link
   on the page already has. The old 18% brand halo measured 1.4:1 against the panel, so a
   keyboard user lost the cursor the moment they tabbed into a field. */
input:focus,select:focus,textarea:focus{border-color:var(--brand)}
input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid var(--focus);outline-offset:2px;box-shadow:none;border-radius:12px}
/* --muted is already the small-text token held to 4.5:1 on every field fill; the extra .85
   opacity took the cream theme's placeholder down to 3.98:1 on --surface-2. Opacity dropped,
   so the placeholder measures 5.46 on cream and 7.02 on night. */
input::placeholder,textarea::placeholder{color:var(--muted);opacity:1}
.hp{position:absolute;left:-9999px;width:1px;height:1px;opacity:0;overflow:hidden}
.success{border:1px solid color-mix(in srgb,var(--ok) 60%,transparent);
  background:color-mix(in srgb,var(--ok) 10%,var(--surface));border-radius:var(--radius-sm);
  padding:16px 18px;margin-top:16px}
.failure,.err{border:1px solid color-mix(in srgb,var(--danger) 60%,transparent);
  background:color-mix(in srgb,var(--danger) 8%,var(--surface));border-radius:var(--radius-sm);
  padding:16px 18px;margin-top:16px;color:var(--text)}
.err b{color:var(--danger)}
/* the console field wrapper: caption above control */
.field{display:block;margin-bottom:13px}
.field span,.field>label{display:block;font-family:var(--mono);font-size:.68rem;font-weight:600;letter-spacing:.15em;text-transform:uppercase;
  color:var(--muted);margin:0 0 8px}
.field input,.field select,.field textarea{width:100%}
.field textarea{min-height:96px}
.check{display:flex;gap:.65rem;align-items:flex-start;font-size:.9rem;color:var(--muted);font-weight:400;margin:0}
.check input{margin-top:.25em;width:18px;height:18px;flex:none}
.demo-note{margin-top:18px;padding:13px 15px;border-radius:var(--radius-sm);background:var(--surface-2);border:1px dashed var(--line-strong);
  font-size:.86rem;color:var(--muted);line-height:1.55}
.demo-note b{color:var(--text)}
/* The account's own email, shown and not editable: a disabled <input> reads as a broken
   field, so the value gets its own dashed panel instead. --text on --surface measures
   14.70 on night and 16.55 on cream; the lock glyph is --muted at 7.63 and 5.98. */
/* Doubled selector on purpose. The natural markup puts this inside the console's .field
   wrapper, whose `.field span` caption rule is ALSO (0,2,0) and sets display:block,
   10.9px mono, uppercase, letterspaced, --muted and a bottom margin. A single-class
   selector loses that tie and the account's email renders as a caption: blocked, tiny,
   shouting and under the 16px floor. Every property that rule sets is answered here. */
.rofield,.field .rofield{display:flex;align-items:center;gap:.6rem;min-width:0;margin:0;
  padding:.8rem .95rem;border-radius:12px;border:1.5px dashed var(--field-line);background:transparent;
  color:var(--text);font-family:var(--sans);font-size:max(16px,1em);font-weight:400;
  letter-spacing:normal;text-transform:none;line-height:1.35}
/* this one WRAPS rather than ellipsising: it is the authoritative "the pass goes to this
   address" line and a truncated address cannot be checked. The compact .whoami strip
   above the form is the one that truncates. */
.rofield .v,.field .rofield .v{display:block;min-width:0;flex:1;margin:0;font-family:var(--sans);
  font-size:max(16px,1em);font-weight:600;letter-spacing:normal;text-transform:none;color:var(--text);
  overflow-wrap:anywhere}
.rofield .lk,.field .rofield .lk{flex:none;margin:0;color:var(--muted);display:grid;place-items:center}
.rofield .lk svg{width:16px;height:16px}

/* ============================================================
   REGISTRATION FLOW (attendee surface)
   Sign in is required before the form exists, so this is the first thing a visitor sees
   under the masthead: a sign in card, then the signed in identity strip, then the two
   registration options as tiles and the party stepper. Every control here is at least
   44px in its short dimension and every type size at least 16px, because the busiest
   night is a dark mall forecourt and a phone held in one hand.
   ============================================================ */
/* --- the sign in card --- */
.signin{max-width:520px}
.signin-note{margin:0 0 18px;font-size:.92rem;color:var(--muted);line-height:1.6}
.signin-note b{color:var(--text);font-weight:600}
/* Google's own light button, held light in BOTH themes the way .passcard and .pchip are:
   the mark is the provider's and recolouring it per theme would misrepresent it. Ink
   #1F1F1F on #FFFFFF measures 16.48; the #747775 edge measures 4.53 against the button,
   3.51 against the night card behind it and 4.16 against the cream one. */
.btn.google{background:#FFFFFF;color:#1F1F1F;border:1px solid #747775;box-shadow:none;
  font-weight:600;letter-spacing:0;gap:.7em}
.btn.google:hover{background:#F2F2F2;border-color:#5F6368;transform:none}
.btn.google:active{background:#E8E8E8}
.btn.google .gmark{width:18px;height:18px;flex:none;display:block}
/* the ring has to be visible on a white button in the night theme too */
.btn.google:focus-visible{outline:3px solid var(--purple-700);outline-offset:2px}
:root[data-theme="light"] .btn.google:focus-visible{outline-color:var(--focus)}
.signin-or{display:flex;align-items:center;gap:.8rem;margin:16px 0;font-family:var(--mono);font-size:.68rem;
  font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
.signin-or::before,.signin-or::after{content:"";flex:1;height:1px;background:var(--line-strong)}
.emailform{display:grid;gap:11px}
@media(min-width:460px){.emailform{grid-template-columns:minmax(0,1fr) auto;align-items:end}}
.emailform .field{margin:0}
.emailform .btn{min-height:50px}
.emailform .sent{grid-column:1/-1}

/* --- the signed in identity strip --- */
.whoami{display:flex;align-items:center;gap:12px;min-width:0;margin:0 0 18px;padding:9px 11px 9px 13px;
  border:1px solid var(--line);border-radius:999px;background:var(--surface-2)}
.whoami .av{flex:none;width:38px;height:38px;border-radius:50%;overflow:hidden;display:grid;place-items:center;
  background:linear-gradient(135deg,var(--marigold),var(--saffron));color:var(--on-accent);
  font-family:var(--sans);font-weight:700;font-size:.95rem;line-height:1}
.whoami .av img{width:100%;height:100%;object-fit:cover}
.whoami .id{flex:1;min-width:0}
.whoami .em,.field .whoami .em{display:block;margin:0;font-family:var(--sans);font-size:.92rem;
  font-weight:600;letter-spacing:normal;text-transform:none;color:var(--text);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.whoami small{display:block;font-family:var(--mono);font-size:.62rem;font-weight:600;letter-spacing:.13em;
  text-transform:uppercase;color:var(--muted);margin-top:3px}
.whoami .btn{flex:none}
@media(max-width:430px){
  .whoami{border-radius:var(--radius-sm);flex-wrap:wrap;padding:11px 13px}
  .whoami .btn{margin-left:auto}
}

/* --- the two registration options, as tiles rather than a select --- */
/* This is the main choice on the page. A native select puts it behind a tap, hides the
   second line of explanation and gives a row that is not 44px on every engine, so the two
   options are tiles with the radio inside them. Checked state is expressed twice on
   purpose: .opt.on for a page that sets a class, and :has(input:checked) for one that does
   not; whichever the page does, the tile paints. */
.optset{display:grid;gap:12px;grid-template-columns:1fr;border:0;padding:0;margin:0 0 15px;min-width:0}
@media(min-width:520px){.optset{grid-template-columns:repeat(2,minmax(0,1fr))}}
.optset>legend,.optset .lg{display:block;float:none;width:auto;padding:0;margin:0 0 8px;
  font-family:var(--mono);font-size:.68rem;font-weight:600;letter-spacing:.15em;text-transform:uppercase;color:var(--muted)}
.optset>legend{grid-column:1/-1}
.opt{position:relative;display:flex;flex-direction:column;justify-content:center;gap:4px;min-width:0;
  min-height:92px;padding:15px 52px 15px 17px;border:1.5px solid var(--field-line);border-radius:var(--radius-sm);
  background:var(--surface-2);color:var(--text);cursor:pointer;margin:0;font-weight:400;
  transition:border-color .15s,background .15s}
/* the radio is the real control and keeps a 44px box, sat under the tick so a tap anywhere
   on the tile lands on the label and activates it */
.opt input{position:absolute;top:50%;right:6px;transform:translateY(-50%);width:44px;height:44px;margin:0;
  opacity:0;cursor:pointer}
.opt b{font-size:1.02rem;font-weight:600;letter-spacing:-.01em}
.opt small{color:var(--muted);font-size:.84rem;line-height:1.45;font-weight:400}
/* pointer-events:none because the tick is painted OVER the 44x44 visually hidden radio. A
   real thumb is unaffected either way (the tick sits inside the label, so the label gets the
   tap), but any click aimed at the input itself is intercepted by the tick and lands nowhere.
   Letting the tap pass through makes the control behave the same for a thumb and for a
   script, which is what a gate author will assume. */
.opt .tick{position:absolute;top:50%;right:16px;transform:translateY(-50%);width:24px;height:24px;flex:none;pointer-events:none;
  border:1.5px solid var(--field-line);border-radius:50%;display:grid;place-items:center;color:transparent}
.opt .tick::before{content:"";width:9px;height:5px;border-left:2px solid currentColor;border-bottom:2px solid currentColor;
  transform:rotate(-45deg) translate(1px,-2px)}
.opt:hover{border-color:var(--brand)}
.opt:focus-within{outline:3px solid var(--focus);outline-offset:2px}
/* SPLIT INTO SEPARATE RULES, never one selector list. :has() is not forgiving: an engine
   that does not support it throws away the WHOLE list it appears in, which would take the
   .opt.on fallback down with it on exactly the old browsers the fallback exists for. */
.opt.on{border-color:var(--accent);background:color-mix(in srgb,var(--saffron) 12%,var(--surface-2))}
.opt:has(input:checked){border-color:var(--accent);background:color-mix(in srgb,var(--saffron) 12%,var(--surface-2))}
/* the tick fill is the marigold/saffron gradient every other filled control uses, NOT
   var(--accent): --accent is #C4611A on the cream theme and --on-accent measures 4.40 on
   it. On the gradient --on-accent measures 6.94 at the marigold end and 9.25 at saffron. */
.opt.on .tick{border-color:transparent;color:var(--on-accent);
  background:linear-gradient(135deg,var(--marigold),var(--saffron))}
.opt:has(input:checked) .tick{border-color:transparent;color:var(--on-accent);
  background:linear-gradient(135deg,var(--marigold),var(--saffron))}
@media (prefers-reduced-motion:reduce){.opt{transition:none}}

/* --- the party stepper, Friends & Family only --- */
/* Five values, so a stepper and not a select: plus and minus are 44px each and the readout
   is the only number on the form. The page hides the whole wrapper with the hidden
   attribute for Individual, which [hidden]{display:none !important} at the top of this
   file makes authoritative over any display rule. */
.stepper{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:0 0 4px}
.stepper button{flex:none;width:44px;height:44px;padding:0;border-radius:999px;border:1.5px solid var(--field-line);
  background:var(--surface);color:var(--text);display:grid;place-items:center;
  font-family:var(--sans);font-size:1.35rem;font-weight:600;line-height:1}
.stepper button:hover:not(:disabled){border-color:var(--brand)}
.stepper button:disabled{opacity:.4;cursor:not-allowed}
/* doubled for the same .field span tie as .rofield above: a <span> inside a .field would
   otherwise come out 10.9px, uppercase, letterspaced, muted and carrying a bottom margin */
.stepper .n,.field .stepper .n{display:block;min-width:2.6ch;margin:0;text-align:center;
  font-family:var(--sans);font-weight:700;font-size:1.5rem;font-variant-numeric:tabular-nums;
  letter-spacing:normal;text-transform:none;color:var(--text);line-height:1}
.stepper .hint,.field .stepper .hint{display:block;flex:1;min-width:11ch;margin:0;
  font-family:var(--sans);font-size:.84rem;font-weight:400;letter-spacing:normal;text-transform:none;
  color:var(--muted);line-height:1.4}
/* the number is the live value: a visually hidden native control can still back it */
.stepper input[type="number"]{width:4.5ch;text-align:center;font-variant-numeric:tabular-nums;font-weight:700;padding:.6rem .2rem}

/* ---------- festival pass card ---------- */
/* White card in both themes: the QR has to stay dark on light to scan. */
.passgrid{display:grid;grid-template-columns:auto minmax(0,1fr);gap:24px;align-items:start}
@media(max-width:720px){.passgrid{grid-template-columns:1fr}}
.passcard{background:#FFFFFF;color:#26163F;border:1px solid #E6DCC8;border-radius:18px;
  padding:14px 18px 16px;width:286px;max-width:100%;box-shadow:0 18px 40px -22px rgba(0,0,0,.55);overflow:hidden}
.passcard .ph{display:flex;gap:10px;align-items:center;margin:12px 0}
.passcard .ph>div{min-width:0}
.passcard .ph img{width:40px;height:42px;object-fit:contain;flex:none}
.passcard .ph strong{font-family:var(--serif);font-size:1.05rem;display:block;line-height:1.25;color:#26163F}
.passcard .ph span{display:block;font-size:.72rem;color:#6B5E80;font-family:var(--mono);letter-spacing:.08em;
  text-transform:uppercase;margin-top:.2rem}
.qrbox{background:#FFFFFF;border:1px solid #E6DCC8;border-radius:12px;padding:8px}
.qrbox svg,.qrbox canvas,.qrbox img{display:block;width:100%;height:auto;max-width:100%}
.qrbox canvas{image-rendering:pixelated}
.pcode{font-family:var(--code);font-size:.8rem;letter-spacing:.05em;text-align:center;margin:10px 0 6px;
  color:#26163F;word-break:break-all;font-weight:600}
.pline{font-size:.86rem;text-align:center;color:#26163F}
.pline.revoked{color:#B3413B;font-weight:600}
/* .passcard is 286px with overflow:hidden, so one long unbroken name, city or plate would be
   cut off at the card edge on the console's QR dialog. Wrap it anywhere instead. */
.passcard .pline,.passcard .pline strong{overflow-wrap:anywhere}
.pfoot{font-family:var(--mono);font-size:.58rem;letter-spacing:.08em;text-align:center;color:#6B5E80;
  margin-top:10px;border-top:1px dashed #E6DCC8;padding-top:9px;line-height:1.7}
.passcard .toran{width:100%;border-radius:4px}
.passnote strong{font-size:1.06rem}
.btnrow{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}
/* Save to Photos, and the state after it worked. The saved state is not a toast: the
   visitor has just left the page for the share sheet and comes back needing to know it
   landed, so the button itself says so and stays said. --ok on its own 16% tint measures
   5.03 on --surface and 4.62 on --surface-2 at night, 5.08 and 4.70 on cream; the border is
   --ok itself (4.62 worst against the tint) because a 55% mix measured 2.18. */
.btn.saved,.btn.saved:hover,.btn.saved:active{background:color-mix(in srgb,var(--ok) 16%,var(--surface));
  color:var(--ok);border-color:var(--ok);box-shadow:none;transform:none;cursor:default}
.btn.saved .ic{width:17px;height:17px;flex:none}
.savednote{display:flex;gap:.45rem;align-items:flex-start;margin:10px 0 0;font-size:.86rem;color:var(--ok);line-height:1.5}
.savednote svg{width:16px;height:16px;flex:none;margin-top:.2em}
/* the console's compact pass container (.pass family) in festival colour */
.pass{max-width:400px;border-radius:var(--radius);overflow:hidden;border:1px solid var(--line);background:var(--surface)}
.pass-top{padding:22px;background:linear-gradient(150deg,var(--purple-700),var(--purple-900));color:var(--on-dark)}
.pass-top b{font-family:var(--serif);font-size:1.4rem;letter-spacing:-.01em;display:block}
.pass-top span{font-family:var(--mono);font-size:.62rem;font-weight:600;letter-spacing:.2em;text-transform:uppercase;color:var(--gold);display:block;margin-top:9px}
.pass-body{padding:22px;text-align:center}
.qr{width:186px;height:186px;margin:0 auto 16px;image-rendering:pixelated;border-radius:12px;background:#fff;padding:10px}
.qr svg{display:block;width:100%;height:100%}
.pass-body code{font-family:var(--code);font-weight:600;font-size:.95rem;letter-spacing:.12em;color:var(--text)}
.pass-rows{border-top:1px dashed var(--line-strong);padding:16px 22px;display:grid;gap:10px}
.pass-row{display:flex;justify-content:space-between;font-size:.88rem;gap:14px}
.pass-row span{color:var(--muted)}
/* .pass-list and .pass-item went with the "passes issued on this device" list: one pass
   per account now, restored by signing in, so there is no list to render and no other
   surface used either class. The matching .pass-item>span rule near the bottom of this
   file went with them. */

/* ---------- meters ----------
   These two rules used to be the second halves of a shared pair that lived inside the
   section v2 deletes. The .meter half is what gives the ops Overview per-night check-in
   bars (src/assets/js/ops.js:499, inside .barrow) their height, radius, track and gradient
   fill. Deleting that block whole would have made those bars invisible with no thrown
   error and no failing gate, which is why these two survive alone. */
.meter{display:block;height:7px;border-radius:999px;background:color-mix(in srgb,var(--line) 70%,transparent);
  overflow:hidden;margin-top:.8rem;min-width:110px}
.meter>i{display:block;height:100%;background:linear-gradient(90deg,var(--jade),var(--saffron))}
/* Also removed with the sections they dressed, each grepped across src/ first: the whole
   capacity-grid family and its descendants, .addr (the venue address panel) and the
   .pchips / .pchip partner chips. */

/* ---------- dialog ---------- */
dialog{border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);color:var(--text);
  max-width:580px;width:calc(100% - 32px);padding:0;max-height:85dvh;overflow:auto;box-shadow:var(--shadow)}
dialog::backdrop{background:rgba(12,6,22,.74);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}
.dlg-in{padding:24px}
.dlg-in .close{float:right;margin-left:12px}
.facts{display:grid;grid-template-columns:1fr 1fr;gap:4px 20px;margin:0 0 14px}
@media(max-width:520px){.facts{grid-template-columns:1fr}}
.facts>div{display:flex;justify-content:space-between;gap:10px;border-bottom:1px dashed var(--line);
  padding:8px 2px;min-width:0}
.facts dt{color:var(--muted);font-size:.84rem}
.facts dd{margin:0;font-weight:600;font-size:.88rem;text-align:right}
/* .freecall was already unreferenced in v1 (zero class="freecall" in src/) and .success
   does the same job, so it goes rather than ship twice. */

/* ---------- footer ---------- */
.site-footer,.ftr{background:var(--purple-900);color:#E7DDF3;padding-block:clamp(50px,7vw,80px) 1.8rem}
:root[data-theme="light"] .site-footer,:root[data-theme="light"] .ftr{background:#231049}
/* Three regions stay dark ink in BOTH themes, so the day theme's dark focus ring would be
   invisible on them. Saffron is re-asserted locally, the way the console rail does. */
.hero,.demo-ribbon,.site-footer,.ftr,.side,.pass-top{--focus:var(--saffron)}
.footer-grid{display:grid;gap:2.2rem;grid-template-columns:1fr}
@media(min-width:800px){.footer-grid{grid-template-columns:1.5fr 1fr 1fr}}
.footer-grid>*{min-width:0}
.site-footer h3,.ftr h3{font-family:var(--mono);font-size:.68rem;letter-spacing:.18em;text-transform:uppercase;
  color:var(--purple-200);margin:0 0 1rem;font-weight:600}
.site-footer p,.ftr p{color:var(--on-dark-3);font-size:.92rem}
.site-footer a,.ftr a{color:#D9CBEF;transition:color .15s}
.site-footer a:hover,.ftr a:hover{color:#fff}
.footer-brand{display:flex;gap:.85rem;align-items:center;margin-bottom:1rem}
.footer-brand img{height:52px;width:auto;flex:none}
.footer-brand .fbt b{font-family:var(--serif);font-size:1.15rem;color:#fff;display:block;line-height:1.25}
.footer-brand .fbt>span{display:block;font-family:var(--mono);font-size:.58rem;letter-spacing:.16em;
  text-transform:uppercase;color:var(--gold);margin-top:.25rem}
.footer-links{list-style:none;padding:0;margin:0}
.footer-links li{margin-bottom:0}
/* The desk number sits on ink in the footer and on paper everywhere else (the verify page
   foot, the Buddy refusal). Gold measured 1.56:1 on the day theme's paper-2, so it is the
   footer's colour only; elsewhere the number is accent-ink, the small-text token held to
   4.5:1 on every paper ground. */
.tel{color:var(--accent-ink);font-family:var(--mono);text-decoration:underline dotted;text-underline-offset:.22em;text-decoration-thickness:1px}
.site-footer .tel,.site-footer .tel:hover{color:var(--gold)}
/* Inline text links get a vertical hit area of at least 24px (WCAG 2.5.8): the footer list,
   the verify page foot, the credit line, the venue facts and the demo code button. */
.footer-links a,.footer-bottom a,.pf a,.cfoot a,.facts dd a,.tel{display:inline-block;padding-block:.45rem}
.linkish{padding:.5rem .2rem}
/* The standalone controls among those, which are pressed and not merely read, get the full
   44px (WCAG 2.5.5): the festival desk phone and email in the footer, the back link and the
   demo pass-code button on verify.html, and the venue phone in the facts list. padding-block
   alone leaves three of them at 43.5px, so the height is set and the label centred in it.
   The two colophon lines are deliberately NOT here: they sit inside running prose, where
   44px tall inline-flex links would break the line box. */
.footer-links a,.pf .back,.pf #footEmail,.pf #footPhone,.facts dd .tel,.linkish{min-height:44px;display:inline-flex;align-items:center}
/* The honesty disclosure has to be the most readable small print on the page, not the
   least: letterspaced all caps prose is unreadable at this size. The dashed border
   already says "notice", so the capitals are not doing any work. */
.site-footer .demochip{font-family:var(--sans);font-size:.8rem;letter-spacing:0;line-height:1.65;
  border:1px dashed rgba(255,255,255,.22);border-radius:12px;padding:12px 14px;color:var(--on-dark-3);margin-top:1.4rem}
.colophon{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:18px}
.colophon .mark{background:var(--cream);border:1px solid rgba(38,22,63,.14);border-radius:10px;
  padding:7px 12px;line-height:0}
.colophon .mark img{height:26px;width:auto;mix-blend-mode:multiply}
.footer-bottom{border-top:1px solid rgba(255,255,255,.14);margin-top:2.4rem;padding-top:1.5rem;
  display:flex;flex-wrap:wrap;gap:1rem;justify-content:space-between;font-size:.82rem;color:var(--purple-200)}
.footer-bottom a{color:var(--gold)}
/* v3.0.1 review: verify.html carries the fixed Buddy button (16px up, 56px tall) and the
   footer's last row is the eVamb credit, which sat under the button at full scroll from 320
   to 768 wide. The footer ends 88px clear of the viewport bottom so the credit finishes above
   it. Offline and the 404s share the rule; the console has no .site-footer. */
.site-footer{padding-bottom:calc(88px + env(safe-area-inset-bottom,0px))}
/* v3.0.3 (V302-SPEC 9 A2): and below 900px the footer's last row keeps the Buddy button's
   column clear on the right (16px inset, 56px button, 8px of air), so however WebKit wraps
   the credit it never shares a pixel with the button. */
@media (max-width:899px){.site-footer .footer-bottom,.site-footer .meta{padding-right:calc(80px + env(safe-area-inset-right,0px))}}
/* the standing credit line as a single centred row (console gate, verify page) */
.cfoot{margin:22px 0 0;text-align:center;font-size:.8rem;color:var(--muted)}
.cfoot a{color:var(--accent-ink);text-decoration:underline;text-underline-offset:2px}

/* ---------- reveal: visible by default, html.reveal-ready opts in ---------- */
.reveal{opacity:1;transform:none}
html.reveal-ready .reveal{opacity:0;transform:translateY(20px);transition:opacity .65s ease,transform .65s ease}
html.reveal-ready .reveal.in{opacity:1;transform:none}
html.reveal-ready .reveal.d1{transition-delay:.08s}
@media (prefers-reduced-motion:reduce){
  html.reveal-ready .reveal{opacity:1;transform:none;transition:none}
}

/* ---------- utilities ---------- */
.skip{position:fixed;left:-9999px;top:8px;z-index:200;background:var(--brand);color:var(--on-brand);
  padding:.6rem 1rem;border-radius:999px;font-weight:600}
.skip:focus{left:8px}
.glow{position:fixed;inset:0;z-index:-1;pointer-events:none;background:
  radial-gradient(760px 420px at 50% -6%,color-mix(in srgb,var(--saffron) 15%,transparent),transparent 66%),
  radial-gradient(900px 620px at 50% 112%,color-mix(in srgb,var(--purple-500) 22%,transparent),transparent 72%)}
:root[data-theme="light"] .glow{opacity:.45}
.center{text-align:center}
.nw{white-space:nowrap}

/* ============================================================
   VERIFY PAGE: the verdict is the hero of that page
   ============================================================ */
.stage{min-height:100dvh;display:grid;place-items:center;align-content:center;justify-items:center;
  padding:clamp(20px,4vw,38px) clamp(14px,4vw,24px) 34px}
.vcard{width:min(580px,100%);background:var(--surface);border:1px solid var(--line);
  border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow)}
.vcard .in{padding:16px clamp(18px,5vw,26px) 26px}
.pres{display:flex;align-items:center;gap:12px;padding-bottom:14px;margin-bottom:16px;
  border-bottom:1px solid color-mix(in srgb,var(--gold) 26%,var(--line))}
.pres .mark{flex:none;width:48px;height:52px;background:url("../img/osf/logo-112.webp") 50%/contain no-repeat}
/* v3.0.1: the verify card shows the client logo mark (tools/make_mark.py) in both themes */
.pres .mark,:root[data-theme="light"] .pres .mark{width:48px;height:48px;border-radius:50%;background:#ebedee center/cover no-repeat url("../img/osf/osf-mark.webp")}
.pres img{flex:none;width:48px;height:52px;object-fit:contain}
.pres-t{flex:1;min-width:0;margin:0;font-family:var(--mono);font-size:.7rem;line-height:1.5;
  letter-spacing:.2em;text-transform:uppercase;font-weight:600;color:var(--text)}
.pres-t b{display:block;font-weight:400;letter-spacing:.3em;color:var(--muted)}
.vhead{display:flex;align-items:center;gap:13px}
.vhead .tt{flex:1;min-width:0}
.vhead h1{font-family:var(--serif);font-weight:600;font-size:clamp(1.55rem,5vw,2rem);line-height:1.08;letter-spacing:-.012em;margin:0}
.season{margin:.4rem 0 0;font-family:var(--serif);font-weight:500;font-style:italic;font-size:1.08rem;color:var(--brand)}
.where{margin:.7rem 0 0;font-size:.88rem;color:var(--muted)}
.vout:empty{display:none}
.vout{margin-top:17px}
.verdict{position:relative;border:1px solid;border-radius:var(--radius-sm);
  padding:24px 18px 18px;overflow:hidden;animation:vIn .32s ease both;margin-top:14px}
/* every state splits its fill colour from its ink colour, because a colour that reads on a
   dark purple card is not the colour that reads on a cream one. Four states, two spellings:
   the gate's (.ok .dup .revoked .unknown) and the verify page's (.valid .warn .bad). */
.verdict.valid,.verdict.ok{--vc:var(--jade);  --vci:#5FD3AC; --vgi:#07271E}
.verdict.bad,.verdict.revoked{--vc:var(--maple); --vci:#FF9A92; --vgi:#FFF1EE}
.verdict.warn,.verdict.dup{--vc:var(--saffron);--vci:#F3C77A; --vgi:#241104}
.verdict.unknown{--vc:var(--purple-300);--vci:var(--text);--vgi:#FFFFFF}
:root[data-theme="light"] .verdict.valid,:root[data-theme="light"] .verdict.ok{--vci:#14624C}
:root[data-theme="light"] .verdict.bad,:root[data-theme="light"] .verdict.revoked{--vci:#96302B}
:root[data-theme="light"] .verdict.warn,:root[data-theme="light"] .verdict.dup{--vci:#7A4A00}
:root[data-theme="light"] .verdict.unknown{--vci:#41266A}
.verdict{border-color:color-mix(in srgb,var(--vc) 55%,var(--line));
  background:color-mix(in srgb,var(--vc) 13%,var(--surface));
  box-shadow:0 24px 54px -30px color-mix(in srgb,var(--vc) 60%,transparent)}
.verdict::before{content:"";position:absolute;left:0;right:0;top:0;height:5px;
  background:linear-gradient(90deg,var(--vc),color-mix(in srgb,var(--vc) 45%,var(--gold)))}
@keyframes vIn{from{opacity:0;transform:translateY(10px) scale(.99)}}
.vh{display:flex;gap:16px;align-items:center}
.glyph{flex:none;width:66px;height:66px;border-radius:50%;display:grid;place-items:center;
  font-size:2.1rem;line-height:1;font-weight:700;background:var(--vc);color:var(--vgi);
  box-shadow:0 0 0 7px color-mix(in srgb,var(--vc) 15%,transparent)}
.verdict .big{font-family:var(--serif);font-weight:600;font-size:clamp(1.85rem,6.6vw,2.6rem);
  line-height:1.04;letter-spacing:-.014em;color:var(--vci)}
.verdict .sub{font-size:.96rem;margin-top:.55rem}
.verdict .sub,.verdict .det .k,.verdict .vfoot{color:color-mix(in srgb,var(--muted) 76%,var(--text))}
@media(max-width:460px){.vh{flex-direction:column;align-items:flex-start;gap:13px}}
.det{display:grid;grid-template-columns:max-content 1fr;gap:9px 16px;margin-top:17px;
  padding-top:15px;border-top:1px dashed color-mix(in srgb,var(--vc) 36%,var(--line));font-size:.98rem}
.det>span{min-width:0;overflow-wrap:anywhere}
.det .k{font-family:var(--mono);font-size:.68rem;font-weight:600;letter-spacing:.16em;
  text-transform:uppercase;color:var(--muted);padding-top:.26rem}
.vfoot{margin-top:14px;padding-top:12px;
  border-top:1px dashed color-mix(in srgb,var(--vc) 36%,var(--line));
  font-size:.72rem;letter-spacing:.05em}
.again{margin:17px 0 0}
/* day theme: fill the glyph with the display ink so the cream glyph sits on it at 7:1 */
:root[data-theme="light"] .glyph{background:var(--vci);color:#FFFFFF}
.vrow{display:flex;gap:10px}
.vrow input{flex:1;min-width:0;font-family:var(--code);letter-spacing:.04em;text-transform:uppercase}
.vrow input::placeholder{text-transform:none;letter-spacing:0}
@media(max-width:460px){.vrow{flex-direction:column}.again .btn{width:100%}}
.nine{display:flex;gap:7px;justify-content:center;margin:0 0 14px;min-height:9px}
.nine i{width:9px;height:9px;border-radius:50%;box-shadow:inset 0 0 0 1px var(--swatch-ring)}
.pf{margin-top:26px;text-align:center;color:var(--muted);font-size:.85rem;max-width:580px}
.pf p{margin:.32rem 0}
.pf .fine{font-family:var(--mono);font-size:.66rem;font-weight:500;letter-spacing:.08em;text-transform:uppercase;line-height:1.75;margin-top:1rem}
/* a text underline, not a border: the link box now carries padding for its tap height, and a
   border would sit 7px under the words */
.pf .back{font-weight:600;text-decoration:underline dotted var(--accent-ink);text-underline-offset:.22em;text-decoration-thickness:1px}
@media(max-width:430px){.pres-t{font-size:.64rem;letter-spacing:.14em}}

/* ============================================================
   AUTH GATE (operator console sign in)
   ============================================================ */
.auth-shell{min-height:100dvh;display:grid;place-items:center;
  padding:clamp(20px,5vw,48px);padding-top:calc(clamp(20px,5vw,48px) + env(safe-area-inset-top));position:relative}
.auth-shell::before{content:"";position:absolute;inset:0;background:var(--stage);pointer-events:none}
.auth-card{width:min(100%,470px);position:relative;z-index:2;background:var(--surface);border:1px solid var(--line);
  border-radius:var(--radius);padding:clamp(24px,4vw,36px);box-shadow:var(--shadow)}
.auth-card h1{font-family:var(--serif);font-size:clamp(1.55rem,4vw,2rem);font-weight:600;margin:16px 0 8px;letter-spacing:-.012em}
.auth-card .fest{font-family:var(--serif);font-weight:600;font-size:1.22rem;line-height:1.14;letter-spacing:-.01em}
.oslock{display:flex;align-items:center;gap:13px}
.oslogo{display:block;height:50px;width:auto;flex:none}
/* day theme: the white wordmark of logo.png vanishes on the white card, so the light variant
   is painted in front of the hidden img (see the band rule for why not content:url) */
:root[data-theme="light"] .oslock::before{content:"";width:46px;height:50px;flex:none;
  background:url("../img/osf/ohm-shanti-logo-light.webp") 50%/contain no-repeat}
:root[data-theme="light"] .auth-card .oslogo{display:none}
.role-list{display:flex;flex-direction:column;gap:9px;margin:20px 0 4px}
.role-opt{display:flex;gap:14px;align-items:center;padding:14px 16px;border:1px solid var(--line-strong);border-radius:var(--radius-sm);
  background:transparent;cursor:pointer;text-align:left;width:100%;color:var(--text);transition:border-color .15s,background .15s}
.role-opt:hover{border-color:var(--brand);background:var(--surface-2)}
.role-opt[aria-pressed="true"]{border-color:var(--accent);background:color-mix(in srgb,var(--saffron) 12%,transparent)}
.role-opt .ico{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;background:var(--surface-2);color:var(--accent-ink);flex:none}
.role-opt b{display:block;font-size:.95rem;letter-spacing:-.01em}
.role-opt small{display:block;color:var(--muted);font-size:.8rem;margin-top:2px;line-height:1.4}
.role-opt .r{font-family:var(--mono);font-size:.7rem;letter-spacing:.12em;text-transform:uppercase;color:var(--accent-ink);font-weight:600;margin-left:auto}

/* ============================================================
   CONSOLE SHELL (operator console)
   The rail stays ink in both themes, the way the festival console has it, so its
   own text tokens never flip and saffron is the focus ring there.
   ============================================================ */
.console{display:grid;grid-template-columns:246px minmax(0,1fr);min-height:100dvh}
.side-scrim{position:fixed;inset:0;background:rgba(14,7,24,.66);z-index:145;display:none}
.side-scrim.on{display:block}
.side{background:var(--purple-900);color:var(--aside-idle);display:flex;flex-direction:column;
  position:sticky;top:0;height:100dvh;overflow-y:auto;
  /* LIVE FIX. The drawer offered the TOP inset and not the bottom one, so on an iPhone
     12/13/14/15 portrait Sign out ended 14px off the screen edge and 20 of its 44px lay
     inside the 34px home indicator band. The global box-sizing:border-box means the
     bottom padding shrinks the content box, so a long nav (admin) scrolls instead of
     hiding its foot. It belongs here and not on .side-foot: padding there leaves .side's
     own content box the full 100dvh and the foot still ends flush with the screen edge
     whenever the nav list is short. .toasts, .install, .buddy-fab and .buddy all already
     carry this inset. */
  padding-top:env(safe-area-inset-top);padding-bottom:env(safe-area-inset-bottom,0px);min-width:0}
.side .brand{color:#fff}
.side .brand .bn{color:#fff}
.side .brand .bs,.side .brand small{color:var(--aside-sub)}
.side-head{padding:19px 18px;border-bottom:1px dashed color-mix(in srgb,var(--saffron) 34%,transparent)}
.side-nav{padding:12px 10px;display:flex;flex-direction:column;gap:3px;flex:1}
.side-nav button,.side-nav a{display:flex;align-items:center;gap:11px;padding:.6rem .85rem;border-radius:999px;border:1px solid transparent;
  background:transparent;cursor:pointer;color:var(--aside-idle);font-weight:500;font-size:.92rem;text-align:left;width:100%;
  transition:background .15s,color .15s,border-color .15s}
.side-nav button:hover,.side-nav a:hover{background:color-mix(in srgb,var(--saffron) 14%,transparent);color:var(--aside-live)}
.side-nav button[aria-current="true"],.side-nav a[aria-current="true"],.side-nav .on{background:color-mix(in srgb,var(--saffron) 20%,transparent);
  color:var(--aside-live);border-color:color-mix(in srgb,var(--saffron) 52%,transparent)}
.side-nav svg{width:18px;height:18px;flex:none}
.side-nav .sec{font-family:var(--mono);font-size:.6rem;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--aside-sub);padding:16px 13px 7px}
.side-foot{padding:14px;border-top:1px dashed color-mix(in srgb,var(--saffron) 34%,transparent)}
.who{display:flex;gap:11px;align-items:center;margin-bottom:11px}
.who .av{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--marigold),var(--saffron));display:grid;place-items:center;
  font-family:var(--sans);font-weight:700;font-size:.9rem;color:var(--on-accent);flex:none}
.who b{display:block;font-size:.88rem;color:#fff;letter-spacing:-.01em}
.who small{display:block;font-family:var(--mono);font-size:.62rem;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--aside-sub);margin-top:4px}
.side .btn.ghost,.side .btn-ghost{color:var(--aside-idle);border-color:rgba(255,255,255,.26)}
.side .btn.ghost:hover,.side .btn-ghost:hover{background:color-mix(in srgb,var(--saffron) 14%,transparent);border-color:var(--saffron);color:var(--aside-live)}
.main{min-width:0;display:flex;flex-direction:column}
.topbar{position:sticky;top:0;z-index:40;display:flex;align-items:center;gap:14px;padding:14px clamp(16px,3vw,30px);
  padding-top:calc(14px + env(safe-area-inset-top));flex-wrap:wrap;
  border-bottom:1px solid var(--line);background:color-mix(in srgb,var(--paper) 88%,transparent);
  -webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}
.topbar h1{font-family:var(--serif);font-size:1.45rem;font-weight:600;letter-spacing:-.01em;line-height:1.1}
.topbar .burger{display:none}
/* On a phone the mode chip and theme button used to wrap under the title and the sticky bar
   became 131px, a fifth of a 640px screen pinned over every view. One row: the title gives
   way with an ellipsis, the chip truncates (ops sets its title to the full label). */
@media (max-width:600px){
  .topbar{flex-wrap:nowrap;gap:10px;padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px))}
  .topbar h1{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:1.25rem}
  .topbar .row{flex:none;flex-wrap:nowrap}
  .topbar .chip-mode{max-width:92px;overflow:hidden;text-overflow:ellipsis}
}
.chip-mode{border:1px solid var(--line-strong);background:var(--surface);border-radius:999px;padding:.26rem .8rem;font-family:var(--mono);
  font-size:.7rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:600;white-space:nowrap}
.chip-mode.live{border-color:color-mix(in srgb,var(--ok) 62%,transparent);color:var(--ok)}
.view{padding:clamp(18px,3vw,30px);padding-left:max(clamp(18px,3vw,30px),env(safe-area-inset-left,0px));padding-right:max(clamp(18px,3vw,30px),env(safe-area-inset-right,0px));
  display:none;max-width:1240px;width:100%}
.view.on{display:block}
.view h2{font-family:var(--sans);font-size:1.02rem;font-weight:600;letter-spacing:0}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin:0 0 20px}
.kpi{border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface);padding:14px 16px;box-shadow:var(--shadow-sm);min-width:0}
.kpi b,.kpi .n{display:block;font-family:var(--sans);font-weight:700;font-size:1.72rem;letter-spacing:-.02em;line-height:1.1;font-variant-numeric:tabular-nums;color:var(--text)}
.kpi span,.kpi .l{display:block;margin-top:6px;font-size:.8rem;color:var(--muted)}
.kpi em{font-style:normal;font-size:.78rem;font-weight:600;margin-left:8px}
.kpi em.up{color:var(--ok)} .kpi em.down{color:var(--danger)}
.kpi.accent b{color:var(--accent)}
/* per night check in bars on the overview */
.bars{display:grid;gap:8px}
.barrow{display:grid;grid-template-columns:minmax(0,1fr) 110px 48px;gap:10px;align-items:center;font-size:.86rem}
.barrow .meter{margin-top:0;min-width:0}
.barrow .n{font-family:var(--mono);font-variant-numeric:tabular-nums;text-align:right;color:var(--muted)}
@media(max-width:480px){.barrow{grid-template-columns:minmax(0,1fr) 72px 44px}}

/* tables */
.tbl-wrap{border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;background:var(--surface);box-shadow:var(--shadow-sm)}
.tbl-head{display:flex;align-items:center;gap:12px;padding:14px 17px;border-bottom:1px solid var(--line);flex-wrap:wrap}
.tbl-head h2{font-family:var(--sans);font-size:1.02rem;font-weight:600}
.tbl-scroll{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:.9rem}
.tbl-scroll table{min-width:620px}
th{font-family:var(--mono);font-size:.67rem;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);font-weight:600;text-align:left;
  padding:.6rem .8rem;border-bottom:1px solid var(--line-strong);white-space:nowrap;background:var(--surface-2)}
td{padding:.6rem .8rem;border-bottom:1px solid color-mix(in srgb,var(--line) 70%,transparent);vertical-align:middle}
tbody tr:last-child td{border-bottom:0}
tbody tr:hover td{background:color-mix(in srgb,var(--purple-500) 12%,transparent)}
td .who-cell{display:flex;flex-direction:column;gap:3px}
td .who-cell b{font-size:.9rem;font-weight:600}
td .who-cell small{color:var(--muted);font-size:.78rem}
td.code,td .code{font-family:var(--code);letter-spacing:.04em;font-variant-numeric:tabular-nums}
.empty{padding:44px 20px;text-align:center;color:var(--muted);font-size:.9rem}
/* V302-SPEC 10 A2: the Team view says why a list is short, with its own Refresh */
.view-notice{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:12px 17px;border-bottom:1px solid color-mix(in srgb,var(--danger) 40%,transparent);
  background:color-mix(in srgb,var(--danger) 8%,var(--paper));color:var(--ink);font-size:.9rem;line-height:1.45}
.view-notice p{margin:0;flex:1 1 260px;min-width:0;overflow-wrap:anywhere}
.view-notice .btn{flex:none}
/* V302-SPEC 10a A4: the Authentication login versus staff record sentence, always above the Team table */
.view-hint{margin:0;padding:12px 17px;border-bottom:1px solid var(--line-strong);color:var(--muted);font-size:.88rem;line-height:1.5;overflow-wrap:anywhere}
.view-hint b{color:var(--ink)}
.list{display:flex;flex-direction:column;gap:6px;font-size:.86rem}
.list .it{display:flex;gap:10px;justify-content:space-between;border-bottom:1px dashed var(--line-strong);padding:.35rem 0}
/* v3.1.0 (group 19, 320x568): the action text shrinks and breaks inside long codes, the time
   stamp keeps its one line; a flex child's default min-width:auto let the nowrap stamp push
   the row 30 px past the viewport on the overview's Latest activity list */
.list .it>span:first-child{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}
.list .it>.note{flex:0 0 auto}
.list .it:last-child{border-bottom:0}

/* search and filters */
.searchbar{display:flex;gap:9px;align-items:center;flex:1;min-width:200px}
.searchbar input{flex:1;padding:.6rem .95rem;border-radius:999px;font-size:max(16px,1em)}
select.mini{width:auto;padding:.55rem .9rem;border-radius:999px;font-size:max(16px,.9em);cursor:pointer}
.filters{display:flex;gap:9px;align-items:center;flex-wrap:wrap}
/* sub-16px form controls trigger iOS focus zoom on touch devices. The Buddy chip rule lives
   with the Buddy block below: an earlier copy of it here lost to .chip on source order. */
@media (pointer:coarse){select.mini,.buddy-lang{font-size:16px}}
select.mini{min-height:44px}
/* Team page hardening (v3.2.0): a staff record whose role this console does not know shows
   "Unknown role"; Save arms only on a pick */
.role-unknown{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.role-unknown select.mini{min-width:0;max-width:100%}

/* rbac matrix */
.matrix td,.matrix th{text-align:center}
.matrix td:first-child,.matrix th:first-child{text-align:left}
.matrix td.y,.yes{color:var(--ok);font-weight:600}
.matrix td.n,.no{color:var(--muted)}
.matrix td.s,.scoped{color:var(--warn-ink);font-weight:600;font-size:.78rem;letter-spacing:.04em}

/* toasts */
/* stacked above the Buddy button (16 + 56 + 12), not on it, and clear of a landscape notch */
.toasts{position:fixed;right:calc(16px + env(safe-area-inset-right,0px));bottom:calc(84px + env(safe-area-inset-bottom,0px));z-index:300;display:flex;flex-direction:column;gap:9px;pointer-events:none;max-width:calc(100vw - 32px)}
.toast{background:var(--purple-900);color:#F4ECDF;border:1px solid color-mix(in srgb,var(--saffron) 38%,transparent);border-left:3px solid var(--saffron);
  border-radius:var(--radius-sm);padding:12px 16px;font-size:.88rem;box-shadow:var(--shadow);max-width:330px;animation:tin .28s ease}
.toast.ok{border-left-color:var(--jade)} .toast.bad{border-left-color:var(--maple)}
@keyframes tin{from{opacity:0;transform:translateY(10px)}}

/* offline / queue banner */
.netbar{display:none;padding:9px 16px;background:color-mix(in srgb,var(--danger) 8%,var(--paper));border-bottom:1px solid color-mix(in srgb,var(--danger) 40%,transparent);
  font-family:var(--mono);font-weight:600;font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;color:var(--danger);text-align:center}
.netbar.on{display:block}
.queued{opacity:.72}

/* install pill */
/* LIVE FIX. v1 shipped `left:50%;transform:translateX(-50%)` with NO width. A fixed box
   with only a left offset gets viewport-minus-left, i.e. HALF the viewport, to shrink to
   fit in; the transform is applied after layout and comes too late to give it any more.
   Measured on the live origin at six phone widths it rendered 213x181 at every one of them,
   a translucent blob with its label stacked one word per line, sitting over the register
   form: at 360 a centre tap on "Your name" opened Chrome's install sheet instead of the
   keyboard, and at 390 the tap did nothing at all. It also covered the gate console's Start
   camera button and its manual code input.
   left+right+margin-inline:auto centres it with the FULL viewport as its containing block,
   and width:max-content lets it be the one line pill it was drawn as (336x67 at 360: the 44px
   .btn.sm floor plus 10px padding top and bottom plus the border, so 66 is the minimum whatever
   the label does; below about 400px max-width:calc(100% - 24px) clamps under max-content and the
   label takes two lines, which costs nothing because .tucked handles every overlap). */
.install{position:fixed;left:12px;right:12px;margin-inline:auto;width:max-content;
  bottom:calc(18px + env(safe-area-inset-bottom));z-index:120;
  display:none;gap:11px;align-items:center;padding:10px 12px 10px 17px;border-radius:999px;background:var(--surface);
  border:1px solid var(--line-strong);box-shadow:var(--shadow);font-size:.86rem;font-weight:600;
  max-width:calc(100% - 24px);transition:opacity .15s}
.install.on{display:flex}
/* Same reason as .buddy-fab.tucked: a fixed bottom overlay must not take the tap meant for a
   control under it. Unconditional, not inside a media query, because core.js measures the
   overlap and a desktop window can be short enough to produce one. */
.install.tucked{opacity:0;pointer-events:none}
@media (prefers-reduced-motion:reduce){.install{transition:none}}
@media (max-width:860px){
  /* Stacked above the Buddy FAB (16 + 56 + 12), the way .toasts already is, for two
     reasons: a 336px wide pill centred at 360 would otherwise run under the 56px FAB at
     the right, and the FAB's z-index 130 beats the pill's 120 so the FAB would take the
     tap meant for Install; and the very bottom of a phone screen is where a full width
     submit button lands once the keyboard is up. */
  .install{bottom:calc(84px + env(safe-area-inset-bottom))}
  /* While focus is anywhere inside the page body the pill gets out of the way, the same
     deal .buddy-fab.tucked already has. .install is a SIBLING of #main on the attendee
     page and of #shell on the console, and it comes after both in the document, so the
     sibling combinator reaches it with no script at all. This covers the keyboard-up case
     with no JS; .install.tucked above is the hook core.js uses for the overlap it has to
     measure, for the controls a visitor has not focused yet. */
  #main:focus-within~.install,#shell:focus-within~.install{opacity:0;pointer-events:none}
}

/* ============================================================
   GATE SCANNER
   A 1:1 camera frame with a corner reticle, a manual entry row, and a verdict panel
   painted in solid theme-independent fills so it reads in daylight at a mall door.
   ============================================================ */
.scan{display:grid;gap:16px;grid-template-columns:minmax(0,1fr)}
@media(min-width:900px){.scan{grid-template-columns:minmax(280px,420px) minmax(0,1fr);align-items:start}}
.scan-frame{position:relative;aspect-ratio:1/1;width:100%;max-width:100%;border-radius:var(--radius);overflow:hidden;background:#000;
  border:1px solid var(--line-strong);box-shadow:var(--shadow-sm)}
.scan-frame video,.scan-frame canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;background:#000}
.scan-frame .scan-idle{position:absolute;inset:0;display:grid;place-items:center;text-align:center;padding:24px;color:#EADFF6;font-size:.9rem;
  background:linear-gradient(135deg,#160C2A,#2C1656)}
.scan-frame.live .scan-idle{display:none}
.scan-reticle{position:absolute;inset:12%;pointer-events:none;z-index:2;
  --rc:var(--saffron);
  background:
    linear-gradient(var(--rc),var(--rc)) top left/28px 3px no-repeat,
    linear-gradient(var(--rc),var(--rc)) top left/3px 28px no-repeat,
    linear-gradient(var(--rc),var(--rc)) top right/28px 3px no-repeat,
    linear-gradient(var(--rc),var(--rc)) top right/3px 28px no-repeat,
    linear-gradient(var(--rc),var(--rc)) bottom left/28px 3px no-repeat,
    linear-gradient(var(--rc),var(--rc)) bottom left/3px 28px no-repeat,
    linear-gradient(var(--rc),var(--rc)) bottom right/28px 3px no-repeat,
    linear-gradient(var(--rc),var(--rc)) bottom right/3px 28px no-repeat;
  filter:drop-shadow(0 0 6px rgba(0,0,0,.6))}
.scan-frame.ok .scan-reticle{--rc:var(--v-ok)}
.scan-frame.dup .scan-reticle{--rc:var(--v-dup)}
.scan-frame.revoked .scan-reticle{--rc:var(--v-revoked)}
.scan-frame.unknown .scan-reticle{--rc:#C9ABEC}
/* forged and down are grounds of their own, not borrowed ones. forged shares red with revoked
   on purpose (both mean refuse), but down has its own blue: "Cannot check, let them in" is
   the opposite instruction to dup's "Already checked in, no second entry", and they used to
   share one amber. The class attribute has to say which state it is: ops.js writes the state
   token into the audit row and the Last-scans label, and a gate assertion that reads the class
   must not see 'revoked' on a forgery. These rules are what let ops.js stop emitting the
   borrowed class. */
.scan-frame.forged .scan-reticle{--rc:var(--v-revoked)}
.scan-frame.down .scan-reticle{--rc:var(--v-down)}
.scan-frame .scan-line{position:absolute;left:12%;right:12%;top:12%;height:2px;background:color-mix(in srgb,var(--saffron) 85%,transparent);
  box-shadow:0 0 12px var(--saffron);z-index:2;pointer-events:none;display:none}
html.anim .scan-frame.live .scan-line{display:block;animation:scanline 2.2s ease-in-out infinite}
@keyframes scanline{0%,100%{top:12%}50%{top:88%}}
@media (prefers-reduced-motion:reduce){.scan-frame .scan-line{display:none!important}}
.scan-tools{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:10px}
.scan-in{font-family:var(--code);font-size:max(16px,1.05em);letter-spacing:.06em;padding:.75rem .85rem;font-variant-numeric:tabular-nums;text-transform:uppercase}
.scan-in::placeholder{text-transform:none;letter-spacing:0}
/* the sunlight verdict: solid fill, big serif word, dark or light ink per state */
.scan .verdict,.verdict.solid{border:0;padding:clamp(20px,4vw,30px) clamp(18px,3vw,26px);background:var(--v-unknown);color:var(--v-unknown-ink);
  box-shadow:var(--shadow);margin-top:0;min-height:160px;display:flex;flex-direction:column;gap:6px;justify-content:center}
.scan .verdict::before,.verdict.solid::before{display:none}
.scan .verdict .big,.verdict.solid .big{color:inherit;font-size:clamp(2rem,7vw,3.4rem);line-height:1}
.scan .verdict .sub,.verdict.solid .sub{color:inherit;opacity:.92;font-size:1.05rem;font-weight:500}
.scan .verdict .holder,.verdict.solid .holder{font-family:var(--sans);font-weight:600;font-size:clamp(1.15rem,3vw,1.5rem);color:inherit;margin-top:.3rem}
.scan .verdict .party,.verdict.solid .party{font-family:var(--mono);font-size:.84rem;letter-spacing:.1em;text-transform:uppercase;color:inherit;opacity:.9}
.scan .verdict .det,.verdict.solid .det{border-top-color:color-mix(in srgb,currentColor 35%,transparent)}
.scan .verdict .det .k,.verdict.solid .det .k,.scan .verdict .vfoot,.verdict.solid .vfoot{color:inherit;opacity:.85}
.scan .verdict.ok,.verdict.solid.ok,.scan .verdict.valid,.verdict.solid.valid{background:var(--v-ok);color:var(--v-ok-ink)}
.scan .verdict.dup,.verdict.solid.dup,.scan .verdict.warn,.verdict.solid.warn{background:var(--v-dup);color:var(--v-dup-ink)}
.scan .verdict.revoked,.verdict.solid.revoked,.scan .verdict.bad,.verdict.solid.bad{background:var(--v-revoked);color:var(--v-revoked-ink)}
.scan .verdict.unknown,.verdict.solid.unknown{background:var(--v-unknown);color:var(--v-unknown-ink)}
.scan .verdict.forged,.verdict.solid.forged{background:var(--v-revoked);color:var(--v-revoked-ink)}
.scan .verdict.down,.verdict.solid.down{background:var(--v-down);color:var(--v-down-ink)}
.scan .verdict.idle,.verdict.solid.idle{background:var(--surface);color:var(--muted);border:1px dashed var(--line-strong);box-shadow:none}
.scan .verdict .glyph,.verdict.solid .glyph{background:color-mix(in srgb,currentColor 18%,transparent);color:inherit;box-shadow:none;width:56px;height:56px;font-size:1.8rem}
/* Phones: the verdict comes BEFORE the camera. Stacked under frame, tools, form and note it
   sat below the fold while the camera was live, and under the keyboard on typed entry, so the
   operator had to scroll away from the viewfinder to read Valid or Already checked in. The two
   columns dissolve into the grid and the verdict is ordered first and tightened. This block
   sits AFTER the verdict family on purpose: equal specificity, so source order decides. */
@media (max-width:899px){
  .scan>div{display:contents}
  .scan .verdict{order:-2;min-height:0;padding:16px 18px}
  /* v3.0.3 (V302-SPEC 9 A1): the typed entry row sits DIRECTLY under the verdict and above the
     camera frame, so the field and Verify are on screen after every verdict on every phone
     without the frame giving its height away (the v3.0.2 fit shrank the frame to 72px on a
     320x568 or 360x640 phone with a party of five, which left the camera unusable, and on
     375x667 and 667x375 the row still sat under the fold before that fit ran). The frame,
     Start camera and the night note follow the row. */
  .scan #scForm{order:-1;margin-top:0}
  .scan .verdict .sub{font-size:.95rem;line-height:1.4}
  .scan .verdict .glyph{width:48px;height:48px;font-size:1.5rem}
  /* The floor, not the cap, is what a phone reads: 6vw never beats it under 534px wide, so a
     1.7rem floor rendered 27.2px on every phone we measured and the gate verdict is the one
     line a volunteer reads at arm's length in the dark. 2rem restores the 32px the designer
     set outside this block; the panel has the room (332x235 at 360). */
  .scan .verdict .big{font-size:clamp(2rem,6vw,2.4rem)}
  /* The volunteer scans the next pass straight after a verdict, so the verdict, the frame,
     Start camera, the code field and Verify must all fit one screen with no scrolling. A full
     width square frame did not: at 390x844 it pushed Verify 35px below the fold. The frame keeps
     its width and is capped by height; decoding reads the video's own frame, so the crop is
     cosmetic, exactly as with the 4:3 rule below. */
  .scan-frame{max-height:32dvh}
}
/* A short phone (360x640 class) cannot hold a scanned verdict and a square frame at once; the
   frame drops to 4:3 there. Decoding reads the video's own frame, so the crop is cosmetic.
   Measured with the camera live: verdict and frame both 100% in view in all four states. */
@media (max-width:899px) and (max-height:720px){.scan-frame{aspect-ratio:4/3}}
/* v3.0.2: the party at the door (ops.js verdictNames). The holder and one more name show,
   clamped to two lines; the rest sit behind "+N more", so the code field and Verify keep
   their place on a 390x844 phone after a verdict. */
.verdict .party-names{display:flex;align-items:center;gap:10px;margin-top:.45rem;padding-top:.4rem;
  border-top:1px solid color-mix(in srgb,currentColor 30%,transparent)}
.verdict .pn-list{flex:1;min-width:0;margin:0;font-size:1rem;line-height:1.35;overflow-wrap:anywhere;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden}
.verdict .party-names.open .pn-list{display:block;-webkit-line-clamp:unset;line-clamp:none;overflow:visible}
.verdict .party-more{flex:none;min-height:44px;min-width:44px;padding:0 14px;border-radius:999px;
  border:1px solid color-mix(in srgb,currentColor 55%,transparent);background:transparent;color:inherit;
  font:600 .9rem/1 var(--sans);cursor:pointer;white-space:nowrap}
.verdict .party-more:focus-visible{outline:3px solid currentColor;outline-offset:2px}
td .who-cell small.with{max-width:36ch;overflow-wrap:anywhere}
/* The names block is about 50px taller than a verdict without it, so on a phone the camera
   frame gives that height back while names are shown (decoding reads the video's own frame,
   so the crop is cosmetic), and gives more back while the full list is open. */
@media (max-width:899px){
  .scan:has(.verdict .party-names) .scan-frame{max-height:28dvh}
  .scan:has(.verdict .party-names.open) .scan-frame{max-height:20dvh}
}
/* A phone on its side (667x375, 844x390) has no height for a verdict stacked above the camera
   and the field: the headline scrolled off the top. Landscape keeps the desktop's two columns,
   camera and field on the left, the verdict on the right, both under the top bar. */
@media (max-width:899px) and (orientation:landscape) and (max-height:520px){
  .scan{grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:start}
  .scan>div{display:block;min-width:0}
  /* v3.0.3 (A1): the left column is a flex column with the typed entry row FIRST, level with
     the verdict's headline, then the frame and Start camera. A 667x375 or 844x390 phone holds
     a party of five verdict beside the row, the frame and the camera button in one screen. */
  .scan>div:first-child{display:flex;flex-direction:column}
  .scan>div:first-child #scForm{order:-1;margin-top:0}
  .scan>div:first-child .scan-frame{margin-top:10px}
}
/* last scans list */
.scans{display:grid;gap:6px;margin-top:14px}
.scanrow{display:grid;grid-template-columns:10px minmax(0,1fr) auto;gap:10px;align-items:center;padding:.45rem .2rem;border-bottom:1px dashed var(--line);font-size:.86rem}
.scanrow:last-child{border-bottom:0}
.scanrow i{width:10px;height:10px;border-radius:50%;background:var(--v-unknown)}
.scanrow.ok i{background:var(--v-ok)} .scanrow.dup i{background:var(--v-dup)} .scanrow.revoked i{background:var(--v-revoked)}
.scanrow.forged i{background:var(--v-revoked)} .scanrow.down i{background:var(--v-down)}
.scanrow .code{font-family:var(--code);font-weight:600;letter-spacing:.04em}
.scanrow time{font-family:var(--mono);font-variant-numeric:tabular-nums;color:var(--muted);font-size:.78rem}

/* ============================================================
   GARBA BUDDY (shared widget, mounted by buddy.js)
   ============================================================ */
.buddy-fab{position:fixed;right:calc(16px + env(safe-area-inset-right,0px));bottom:calc(16px + env(safe-area-inset-bottom,0px));z-index:130;
  width:56px;height:56px;border-radius:50%;border:0;cursor:pointer;display:grid;place-items:center;
  background:linear-gradient(135deg,var(--marigold),var(--saffron));color:var(--on-accent);
  box-shadow:0 14px 34px -12px rgba(246,169,59,.8);transition:transform .18s,opacity .15s}
.buddy-fab:hover{transform:scale(1.06)}
/* Phone keyboard up: the button sat exactly where a full width submit lands once the viewport
   shrinks and the user scrolls to it, and took the tap at the button's right end. While a field
   outside the Buddy has focus the button fades and lets taps through; it is back on blur.
   tucked = hidden by buddy.js while a form control has focus (and for a beat after it blurs, so
   the tap that moves focus to Submit cannot land on the button here) and whenever a control
   would sit under the FAB. At every width, not only phones: a tablet keyboard and a short
   desktop window produce the same overlap, and register-live.css already does the same. */
.buddy-fab.tucked{opacity:0;pointer-events:none}
.buddy-fab svg{width:26px;height:26px}
.buddy-fab .badge{position:absolute;top:-3px;right:-3px;min-width:20px;height:20px;border-radius:999px;background:var(--purple-700);
  color:#fff;font-family:var(--mono);font-weight:600;font-size:.7rem;line-height:20px;text-align:center;padding:0 5px;border:1px solid rgba(255,255,255,.35)}
.buddy{position:fixed;right:calc(14px + env(safe-area-inset-right,0px));z-index:140;width:min(calc(100vw - 28px),392px);
  bottom:calc(14px + env(safe-area-inset-bottom,0px));
  height:min(calc(100dvh - 96px),604px);
  display:none;flex-direction:column;background:var(--surface);border:1px solid var(--line-strong);
  border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden}
.buddy.open{display:flex}
.buddy-head{display:flex;align-items:center;gap:11px;padding:14px 15px;border-bottom:1px solid var(--line);background:var(--purple-900);color:var(--on-dark);--focus:var(--saffron)}
.buddy-head .av{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--marigold),var(--saffron));
  display:grid;place-items:center;color:var(--on-accent);flex:none;font-weight:700}
.buddy-head .av img{width:30px;height:30px;object-fit:contain}
.buddy-head b{display:block;font-family:var(--serif);font-size:1rem;letter-spacing:-.01em;color:#fff}
.buddy-head small{display:block;font-family:var(--mono);font-size:.6rem;font-weight:600;letter-spacing:.15em;text-transform:uppercase;color:var(--gold);margin-top:4px}
/* .15em tracking pulls apart an Indic grapheme cluster and uppercase means nothing there, so
   the translated strap drops both, the way .kicker .guj already does above. buddy.js
   sets lang on the strap's host element from STRINGS[c].html. */
.buddy-head small:lang(gu),.buddy-head small:lang(hi){letter-spacing:0;text-transform:none}
/* the close button keeps the 44px box every other icon button has: it was the one control
   under 44 in both dimensions */
.buddy-head .icon-btn{background:transparent;color:var(--on-dark);border-color:rgba(255,255,255,.3);width:44px;height:44px}
.buddy-head .icon-btn:hover{border-color:var(--saffron);color:#fff}
.buddy-log{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:11px;overscroll-behavior:contain;background:var(--paper)}
.msg{max-width:88%;padding:11px 14px;border-radius:15px;font-size:.92rem;line-height:1.55;white-space:pre-line;overflow-wrap:anywhere}
.msg.bot{background:var(--surface);border:1px solid var(--line);border-bottom-left-radius:5px;align-self:flex-start;color:var(--text)}
.msg.me{background:linear-gradient(135deg,var(--marigold),var(--saffron));color:var(--on-accent);border-bottom-right-radius:5px;align-self:flex-end;font-weight:500}
.msg.sys{align-self:center;background:transparent;color:var(--muted);font-size:.78rem;font-family:var(--mono);letter-spacing:.06em;text-transform:uppercase}
.msg a{color:var(--accent-ink);text-decoration:underline;font-weight:600}
.msg.me a{color:var(--on-accent)}
.chips{display:flex;flex-wrap:wrap;gap:7px;padding:0 16px 12px;background:var(--paper)}
.chip{padding:.45rem .9rem;border-radius:999px;border:1px solid var(--line-strong);background:var(--surface);cursor:pointer;
  font-size:.82rem;font-weight:600;color:var(--brand);transition:border-color .14s,color .14s,background .14s;min-height:44px}
.chip:hover{border-color:var(--brand);background:var(--surface-2)}
/* after .chip on purpose: same specificity, so source order decides, and the touch size
   rule has to win */
@media (pointer:coarse){.chip{font-size:16px}}
.buddy-form{display:flex;gap:8px;padding:12px;border-top:1px solid var(--line);background:var(--surface-2)}
.buddy-form input{flex:1;min-width:0;padding:.7rem .95rem;border-radius:999px;background:var(--surface);font-size:max(16px,1em)}
.buddy-form button{width:44px;height:44px;border-radius:50%;border:0;background:linear-gradient(135deg,var(--marigold),var(--saffron));color:var(--on-accent);
  cursor:pointer;display:grid;place-items:center;flex:none;padding:0}
.buddy-form button svg{width:18px;height:18px}
.buddy-lang{margin-left:auto;width:auto;min-height:44px;padding:.3rem .6rem;border-radius:999px;border:1px solid rgba(255,255,255,.3);background:transparent;color:var(--on-dark);
  font-size:max(16px,.9em);cursor:pointer;font-family:var(--guj)}
.buddy-lang option{color:#26163F;background:#fff}
.typing{display:flex;gap:4px;padding:13px 15px}
.typing i{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:blink 1.3s infinite}
.typing i:nth-child(2){animation-delay:.18s} .typing i:nth-child(3){animation-delay:.36s}
@keyframes blink{0%,60%,100%{opacity:.28}30%{opacity:1}}
/* the ops Buddy view: gaps and curated answers */
.gap{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:center;padding:.6rem 0;border-bottom:1px dashed var(--line)}
.gap q{quotes:none;font-weight:500}
.gap small{display:block;color:var(--muted);font-family:var(--mono);font-size:.72rem;letter-spacing:.06em;margin-top:2px}

/* ---------- responsive: console chassis ---------- */
@media (max-width:1080px){
  .g4{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:900px){
  /* collapse before overflow, not at phone width */
  .console{grid-template-columns:1fr}
  /* The drawer is fixed at z-index 150 and its first 262px cover the burger that opened it
     (LIVE-FINDINGS 11). The obvious fix, `.topbar .burger{position:relative;z-index:160}`,
     DOES NOT WORK and must not be re-added: `.topbar` is `position:sticky;z-index:40`, which
     makes it a stacking context, so the burger's z-index only competes with its own siblings
     inside the topbar and can never rise above the drawer. Measured with the drawer open: the
     element at the burger's centre is the drawer's brand block either way. Raising `.topbar`
     itself instead would paint the full width translucent bar over the drawer's head and bury
     #sideClose, which is a worse bug than the one being fixed.
     What actually resolves it is in markup and JS, and both are in place: the drawer head is
     no longer a link out to the public site (it is a <span class="brand">, so that tap cannot
     navigate a volunteer away mid-queue), and ops.js makes #sideHead a redundant close hit
     area, so the returning tap closes the drawer wherever it lands in that head. */
  .topbar .burger{display:flex}
  .side{position:fixed;left:0;top:0;bottom:0;width:262px;height:100dvh;z-index:150;transform:translateX(-100%);transition:transform .24s ease;box-shadow:var(--shadow)}
  .side.open{transform:none}
}
@media (max-width:720px){
  .g2,.g3,.g4{grid-template-columns:1fr}
  .buddy{width:calc(100vw - 20px - env(safe-area-inset-right,0px) - env(safe-area-inset-left,0px));right:calc(10px + env(safe-area-inset-right,0px));height:min(calc(100dvh - 76px),100%)}
  .hdr-cta .btn-ghost,.nav-actions .btn.ghost{display:none}
}
@media (max-width:480px){
  .hdr-cta .btn-primary,.nav-actions .btn:not(.ghost):not(.icon-btn){display:none}
  .nav,.hdr-in{gap:10px}
  .hdr-cta,.nav-actions{gap:7px}
  .view{padding:14px}
  .kpis{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:360px){.kpis{grid-template-columns:1fr}}

/* ---------- reduced motion ---------- */
/* ---------- console odds and ends (absorbed from the ops shell) ---------- */
:root[data-sg-mode="demo"] [data-live-only]{display:none}
.auth-card .oslock{margin-bottom:6px}
/* the lockup and the theme button share the first row at every width: the festival name
   wraps inside the lockup instead of pushing the button onto a second line */
.auth-card>.row.between:first-child{flex-wrap:nowrap;align-items:flex-start}
.oslock{min-width:0;flex:1}
.oslock>div{min-width:0}
/* the three way-out links on the sign in card are tap targets, not status tags */
.auth-card .pill{min-height:44px;padding:.6em 1em;font-size:.78rem}
.auth-card .presents{color:var(--muted);margin:0 0 .3rem}
.auth-card .divider{margin:14px 0 6px}
.auth-card .cfoot{margin-top:20px}
.scan-tools .scan-in{flex:1;min-width:200px}
.who-cell .code{font-size:.84rem}
.view h2.h3{margin:0 0 10px}
.nowrap{white-space:nowrap}
.r-end{text-align:right;white-space:nowrap}
.passcard.in-dlg{margin:0 auto}
dialog .dlg-in h2{font-family:var(--serif);font-size:1.35rem;margin:0 0 14px}
.gap .row{gap:6px}
.sr-link{font-size:.8rem}
.chip-link{text-decoration:none}
.tbl-foot{padding:14px 17px;border-top:1px solid var(--line)}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.001ms !important}
  html.reveal-ready .reveal{opacity:1 !important;transform:none !important}
  .side{transition:none}
}

/* ---------- print: isolate the festival pass ----------
   LIVE FIX. v1 hid the page with `body *{visibility:hidden}`. visibility:hidden keeps every
   hidden element's LAYOUT BOX, so under print emulation the document was still 7583px tall
   and the fixed pass card repeated on every page of it: a print or a save-as-PDF from the
   live site produced EIGHT identical Letter pages (seven on A4), each a complete,
   independently scannable copy of the same pass. Measured on the live origin with
   Chromium's own print engine: /Count 8, all eight pages carrying the same 201 characters
   and the same 10318 non-white pixels.
   The fix removes the boxes instead of merely blanking them, and leaves the card in normal
   flow so there is exactly one page of content to paginate. Two paths, because the card is
   printed from two surfaces: #main>#pass on the attendee page, and dialog#opsDlg on the
   console, where ops.js renders .passcard.in-dlg.qr-print inside .dlg-in. Both #main and
   #opsDlg are spared at the body level, so neither surface needs to know about the other. */
@media print{
  @page{size:auto;margin:12mm}
  :root,:root[data-theme="dark"]{
    --paper:#FFFFFF;--paper-2:#F6F1EA;--surface:#FFFFFF;--surface-2:#F6F1EA;
    --text:#26163F;--muted:#4A3F5E;--line:rgba(65,30,100,.2);--line-strong:rgba(65,30,100,.4);
    --stage:none;--shadow:none;--shadow-sm:none;color-scheme:light
  }
  html,body{height:auto;min-height:0;background:#fff;color:#26163F;overflow:visible}
  /* #pass is a <section hidden> until a pass exists, so nothing here forces it visible:
     :not() spares it, [hidden]{display:none !important} still wins when there is no pass. */
  body>*:not(#main):not(#opsDlg){display:none !important}
  #main>*:not(#pass){display:none !important}
  #main,#pass,#passOut{padding:0 !important;margin:0 !important;background:none !important;
    border:0 !important;box-shadow:none !important;min-height:0 !important}
  #opsDlg{position:static;max-width:none;width:auto;max-height:none;overflow:visible;
    border:0;background:none;box-shadow:none;padding:0}
  #opsDlg::backdrop{display:none}
  #opsDlg .dlg-in{padding:0}
  /* v3.0.2: the card is NOT a child of .dlg-in, it sits inside #dlgBody. Hiding every child
     but #passCard hid #dlgBody and the card with it, so Print pass printed a blank page.
     #dlgBody is spared, and inside it only the pass card prints. */
  #opsDlg .dlg-in>*:not(#dlgBody){display:none !important}
  #opsDlg #dlgBody>*:not(.qr-print){display:none !important}
  /* the card and nothing beside it: .passnote is the save / verify / install row, which is
     three dead buttons and a paragraph about this phone once it is on paper */
  .passgrid{display:block}
  #passCard,.qr-print{position:static;left:auto;top:auto;transform:none;box-shadow:none;
    margin:0 auto;float:none;break-inside:avoid;page-break-inside:avoid}
  /* #pass .kicker is the role="status" line the page announces the issue with ("Pass
     issued"); on paper it is a screen artefact above the card, so it goes too */
  #pass .kicker,.passnote,.btnrow,.site-header,.hdr,.demo-ribbon,.buddy-fab,.buddy,.install,.side,.side-scrim,.topbar,.netbar,.toasts,.scan-frame{display:none !important}
  .console{grid-template-columns:1fr}
  /* v3.0.2: verify.html has no #pass, so the rule above hid its whole page and Ctrl+P printed
     blank. Only verify.html carries data-sg-surface="register" with app.css. */
  html[data-sg-surface="register"] #main>*:not(#pass):not([hidden]){display:revert !important}
}

/* v3.2.0 (V320-SPEC 3 and 6): the gate counts arrivals on a shared QR.
   .v-count is "k of N in tonight" under the holder; .v-admit holds Admit the rest and
   Admit 1 more, 44 px or taller, drawn in the verdict's own ink so they read on the green
   fill at the fill/ink pair's measured 5.84:1 (the filled button reverses the pair). */
.verdict .v-count{font-family:var(--sans);font-weight:700;font-size:clamp(1.05rem,2.6vw,1.3rem);margin-top:.35rem;color:inherit}
.verdict .v-admit{display:flex;flex-wrap:wrap;gap:8px;margin-top:.7rem}
.verdict .v-admit .btn{min-height:44px;min-width:44px;padding:.55em 1.1em;font-size:.95rem;box-shadow:none;flex:1 1 auto;max-width:100%;white-space:normal}
.scan .verdict.ok .v-admit .btn.v-rest{background:var(--v-ok-ink);color:var(--v-ok);border-color:var(--v-ok-ink)}
.scan .verdict.ok .v-admit .btn.ghost{background:transparent;color:inherit;border-color:currentColor}
.scan .verdict .v-admit .btn:focus-visible{outline:3px solid currentColor;outline-offset:2px}
@media (max-width:899px){
  .scan .verdict .v-count{font-size:1.05rem;margin-top:.2rem;line-height:1.25}
  /* integrator, V302-SPEC 9 A1 at 320x568 with a party of five: the two admit buttons share
     one row (each 44 px or taller, the label may wrap inside), and the plain "Checked in for"
     sentence folds away under a counted Valid, so the code field and Verify stay on screen
     under the verdict. */
  .scan .verdict .v-admit{margin-top:.4rem;flex-wrap:nowrap}
  .scan .verdict .v-admit .btn{flex:1 1 0;min-width:0;padding:.2em .55em;font-size:.92rem;line-height:1.2}
  .scan .verdict.counted .sub.sub-plain{display:none}
}
@media (max-width:460px){.scan .verdict.counted .vh{gap:8px}}
/* the passes table: "2 of 4 in" and the first admission time under it */
td .tag.in-count{white-space:nowrap}
/* the count reads as written, "2 of 4 in", never shouted in capitals (passes table and verify) */
.tag.in-count{text-transform:none;letter-spacing:.02em;font-size:.76rem}
td small.in-at{display:block;margin-top:3px;font-size:.78rem;color:var(--muted);white-space:nowrap}
/* the desk issue: one row per attendee, First and Last side by side, Email full width;
   one column on a phone */
#iMembers .att-row{border:1px solid var(--line);border-radius:var(--radius-sm);padding:10px 12px 0;margin:0 0 12px;
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);column-gap:12px;min-width:0}
#iMembers .att-row>legend{padding:0 6px;font-family:var(--mono);font-size:.72rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
#iMembers .att-row .att-email{grid-column:1/-1}
#iMembers .att-row .field{min-width:0}
#iMembers input{font-size:16px;min-height:44px}
#iMembers .att-note{margin:0 0 10px}
@media (max-width:560px){#iMembers .att-row{grid-template-columns:minmax(0,1fr)}}
@media print{.in-tonight{display:none !important}}
