/* Thaagam v5 — Sunlit Beige design system (from Thaagam Delivery App.dc.html / DESIGN.md)
   One file, cached for every screen. Keep it small: tokens + components only. */

:root {
  --white:#FFFFFF;
  --ink: #15110D;
  --beige: #F1E7E2 ; /* #F4EEE2 */; 
  --beige-raised: #F1EADC;
  --border: #E2D8C6;
  --border-strong: #D9CDB6;
  --red: #C8271E;
  --red-deep: #8f1a13;
  --red-tint: #FBE9E7;
  --blue: #1E8FB5;
  --blue-bright: #2FA3C7;
  --blue-on-dark: #56C4E0;
  --blue-tint: #DCEFF5;
  --blue-tint-text: #16708F;
  --cream: #EFE7D8;
  --selected-bg: #FDF9F2;
  --ink-45: rgba(21, 17, 13, .45);
  --ink-55: rgba(21, 17, 13, .55);
  --ink-65: rgba(21, 17, 13, .65);
  --cream-60: rgba(244, 238, 226, .6);
  --cream-75: rgba(244, 238, 226, .75);
  --font-ui: Archivo, system-ui, sans-serif;
  --font-display: "Bricolage Grotesque", Archivo, system-ui, sans-serif;
}

* { box-sizing: border-box; }
html { height: 100%; }
body {
  margin: 0;
  background: #EBE6DC;
  font-family: var(--font-ui);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

/* ---- App shell: full-viewport mobile screen, centered column on desktop ---- */
.screen {
  min-height: 100dvh;
  max-width: 430px;
  margin: 0 auto;
  background: var(--beige);
  display: flex;
  flex-direction: column;
  /* The narrowest phone we serve is 320px wide - 288px of usable width inside
     the 16px gutters - and one unshrinkable thing (a native date input, a long
     unbreakable name) is all it takes to push a row past the card edge. `clip`
     rather than `hidden`: hidden on one axis turns the other into a scroll
     container, which would break the occasion panel and every sticky bar.
     This is the net, not the fix - the fixes are on the rows themselves. */
  overflow-x: clip;
}
@media (min-width: 431px) {
  .screen { box-shadow: 0 0 40px rgba(0, 0, 0, .12); }
}
.screen--dark { background: var(--ink); color: var(--beige); }

.pad { padding-left: 22px; padding-right: 22px; }

/* ---- Type ---- */
h1, h2, h3 { margin: 0; font-family: var(--font-display); }
.h-hero { font-size: 38px; font-weight: 800; line-height: 1.05; }
.h-success { font-size: 31px; font-weight: 800; line-height: 1.1; }
.h-title { font-size: 24px; font-weight: 800; }
.eyebrow {
  font: 700 11px var(--font-ui);
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: black;
}
.eyebrow--red { color: var(--red); letter-spacing: 1.6px; }
.muted { color: var(--ink-55); }
.mono { font-family: ui-monospace, Menlo, monospace; }
a { color: inherit; text-decoration: none; }

/* ---- Cards ---- */
/* ---- Card ----
   THE card. Every detail screen uses this one - the cause page, both campaign
   pages, healthcare and both orphanage pages - which is why it is here and not
   copied into each of them. It was five near-identical copies (.lbox, .obox and
   this) with three different borders; the hardcoded #a19379 here is what made
   the cause page's border read darker than everything else. */
.card {
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 15px 16px;
}
.card__ttl { font: 800 14.5px var(--font-display); letter-spacing: -.1px; }
.card__sub { margin-top: 3px; font: 400 11.5px/1.55 var(--font-ui); color: var(--ink-45); }
.card__p   { margin: 9px 0 0; font: 400 12.5px/1.7 var(--font-ui); color: var(--ink-55); white-space: pre-line; }
.callout-blue {
  background: var(--blue-tint);
  border-radius: 12px;
  padding: 10px 14px;
  font: 500 11.5px/1.5 var(--font-ui);
  color: var(--blue-tint-text);
}
.callout-warm {
  background: var(--red-tint);
  border-radius: 14px;
  padding: 12px 16px;
  font: 400 12.5px/1.6 var(--font-ui);
  color: var(--ink);
}

/* ---- Chips ---- */
.chip {
  display: inline-block;
  padding: 11px 18px;
  border-radius: 24px;
  background: #fff;
  border: 1.5px solid var(--border-strong);
  font: 700 13.5px var(--font-ui);
  color: var(--ink);
  cursor: pointer;
}
.chip--on { background: var(--ink); border-color: var(--ink); color: var(--beige); }
.chip--dashed { border-style: dashed; color: var(--ink-45); font-weight: 600; cursor: default; }

/* ---- Buttons ---- */
.cta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  background: var(--red);
  border: 0;
  border-radius: 18px;
  padding: 17px 18px;
  width: 100%;
  cursor: pointer;
  color: #fff;
}
.cta__label { font: 800 16px var(--font-ui); color: #fff; }
.cta--center { justify-content: center; }
.btn-secondary {
  display: block;
  background: #fff;
  border: 1.5px solid var(--border-strong);
  border-radius: 16px;
  padding: 15px;
  text-align: center;
  font: 800 13.5px var(--font-ui);
  color: var(--ink);
  cursor: pointer;
}
.screen--dark .btn-secondary {
  background: rgba(244, 238, 226, .1);
  border-color: rgba(244, 238, 226, .25);
  color: var(--beige);
}
.sticky-cta { padding: 12px 22px 14px; flex: none; }

/* ---- Photo placeholder stripes (until real field photos land) ---- */
.stripe {
  background: repeating-linear-gradient(45deg, #EAE2D2, #EAE2D2 10px, var(--beige-raised) 10px, var(--beige-raised) 20px);
  display: flex;
  align-items: center;
  justify-content: center;
}
.stripe__note { font: 500 10px ui-monospace, monospace; color: var(--ink-45); }
.screen--dark .stripe { background: repeating-linear-gradient(45deg, #26221E, #26221E 10px, #2E2924 10px, #2E2924 20px); }

/* ---- Proof stamp ---- */
.stamp {
  background: var(--blue);
  color: #fff;
  border-radius: 7px;
  padding: 4px 8px;
  font: 600 9.5px ui-monospace, monospace;
}

/* ---- Bill rows ---- */
.billrow { display: flex; justify-content: space-between; font: 500 12.5px var(--font-ui); color: var(--ink-65); }
.billrow[hidden] { display: none; }
.bill-total {
  border-top: 1px dashed var(--border);
  padding-top: 8px;
  display: flex;
  justify-content: space-between;
  font: 800 15px var(--font-ui);
  color: var(--ink);
}

/* ---- Bottom nav ---- */
/* FIXED, not sticky. As a sticky last flex-child the bar sat at the END OF THE
   CONTENT, so filtering the deck down (search, category) pulled it up the
   screen. Fixed keeps it on the viewport edge whatever the deck is showing.
   It leaves the flow, so .screen--nav below reserves the space it used to take. */
.navbar {
  background: #fff;
  border-top: 1px solid var(--border);
  /* five items now, so the 30px gutters that suited four would squeeze
     'Volunteer' off the end on a 320px phone */
  padding: 10px 10px calc(10px + env(safe-area-inset-bottom));
  display: flex;
  justify-content: space-between;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  /* stay inside the phone column on desktop, like .screen */
  max-width: 430px;
  margin: 0 auto;
  z-index: 5;
}
/* Set by v5_base whenever a navbar is rendered, so pages WITHOUT one keep
   their full height and gain no dead space at the bottom. */
.screen--nav { padding-bottom: calc(66px + env(safe-area-inset-bottom)); }

.navbar a {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 5px 2px 3px;
  font: 600 10px var(--font-ui);
  color: var(--ink-65);
  transition: color .18s ease;
}
/* The active pill is a pseudo-element rather than a background on the icon:
   padding on the icon itself would resize its box and shunt the row. This
   sits behind it and costs no layout at all. */
.navbar a::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 50%;
  width: 46px;
  height: 28px;
  max-width: 100%;
  transform: translateX(-50%) scale(.8);
  border-radius: 999px;
  background: transparent;
  opacity: 0;
  transition: background .18s ease, opacity .18s ease, transform .18s ease;
}
.navbar a.on::before {
  background: var(--red-tint);
  opacity: 1;
  transform: translateX(-50%) scale(1);
}
.navbar a:active::before { background: var(--cream); opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .navbar a, .navbar a::before { transition: none; }
}
/* Lucide icons (lucide.dev, ISC/MIT) - inline SVG, not a text glyph.
   Their house style is exactly this app's: 24x24, no fill, 2px round stroke. */
/* ---- Line icons ----
   One definition, shared. This lived inside v5_track.html's <style>, so every
   other screen that wanted the same arrow had to re-declare it under its own
   name - .cico, .pico, .fico, .qico - all with identical geometry and only the
   pixel size differing. app.css is loaded by v5_base.html on every v5 page, so
   the class works anywhere and there is one place to change it. */
.tico {
  width: 20px; height: 20px; display: block; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.tico--sm { width: 12px; height: 12px; stroke-width: 3; }
.tico--fill { fill: currentColor; stroke: none; }

.navbar a .ico {
  position: relative;   /* above the pill */
  display: block;
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: rgba(21, 17, 13, .35);
}
.navbar a.on .ico { stroke-width: 2.3; }
.navbar a.on, .navbar a.on .ico { color: var(--red); font-weight: 700; }

/* ---- Quantity stepper ---- */
.step-btn {
  width: 44px; height: 44px;
  border-radius: 14px;
  border: 1.5px solid var(--border);
  background: #fff;
  font: 700 20px var(--font-ui);
  color: var(--ink);
  cursor: pointer;
}
.step-btn--plus { background: var(--red); border-color: var(--red); color: #fff; }
/* One row of picks - amounts on the campaign screens, counts on the cause
   screen. flex-basis:0 with min-width:0 so however many there are share the
   row instead of the last one wrapping onto a line of its own. */
.presets { display: flex; flex-wrap: nowrap; gap: 6px; margin-top: 12px; }
.preset {
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
  padding: 11px 3px;
  border-radius: 12px;
  border: 1.5px solid var(--border);
  background: #fff;
  font: 800 13px var(--font-ui);
  color: var(--ink-55);
  cursor: pointer;
  transition: .15s;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.preset--on { background: var(--selected-bg); border-color: var(--red); color: var(--red); }
.preset--custom { font: 700 12px var(--font-ui); }
@media (max-width: 360px) {
  .presets { gap: 5px; }
  .preset { padding: 10px 2px; font-size: 11.5px; }
  .preset--custom { font-size: 11px; }
}

/* ---- Form fields ----
   A small uppercase label above a bordered input. This was .lfield on the three
   campaign screens, .ofield on the two orphanage screens and a DIFFERENT
   component here (label inside the box), which is why the same "Name on parcel"
   field looked like two different controls depending on the screen. One
   definition now; the markup is <div class="field"><label>+<input>, which is
   what all six already had. */
.field { display: flex; flex-direction: column; gap: 5px; margin-top: 10px; }
.field label {
  font: 700 10px var(--font-ui); letter-spacing: 1.1px;
  color: var(--ink-45); text-transform: uppercase;
}
.field input {
  width: 100%; outline: 0;
  border: 1px solid var(--border); border-radius: 12px; background: #fff;
  padding: 11px 13px; font: 500 13.5px var(--font-ui); color: var(--ink);
}
.field input:focus { border-color: var(--red); }
.field input::placeholder { color: rgba(21, 17, 13, .35); }
/* Two names for one state: the campaign and orphanage scripts mark the INPUT
   (.is-bad), app.js marks the FIELD (.field--err). Both mean "this one". */
.field input.is-bad,
.field--err input { border-color: var(--red); background: #FFF7F6; }
/* required / optional tags that ride on a field label */
.field-req { font: 800 8.5px var(--font-ui); letter-spacing: 1px; color: var(--red); }
/* ---- Occasion picker ----
   A button + panel rather than a <select>, because a select's popup is always
   one column and the occasion list is long enough that one column runs off the
   bottom of a phone. The trigger is styled as a .field input with a caret, and
   room is reserved for that caret so a long occasion never runs under it - one
   rule, rather than the four near-identical per-page overrides this had while
   every section styled its own fields. */
.occ { position: relative; }
.occ__btn {
  display: block; width: 100%; text-align: left; outline: 0;
  border: 1px solid var(--border); border-radius: 12px;
  padding: 11px 30px 11px 13px;
  font: 500 13.5px var(--font-ui); color: var(--ink);
  background: #fff no-repeat right 12px center / 9px auto;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2315110D' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
  cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.occ__btn:focus-visible { border-color: var(--red); }
/* columns, not a grid: the options then flow DOWN column one and on into
   column two, which is the reading order asked for, and the browser balances
   the two columns itself however many occasions the server sends.
   Right-anchored because this field is the right half of its row - a
   left-anchored panel this wide would hang off the screen edge. */
.occ__panel {
  position: absolute; z-index: 40;
  top: calc(100% + 7px); right: -1.5px;
  width: min(330px, 82vw);
  max-height: 54vh; overflow-y: auto;
  columns: 2; column-gap: 4px;
  padding: 7px;
  border: 1.5px solid var(--border-strong);
  border-radius: 12px;
  background: #fff;
  box-shadow: 0 12px 28px rgba(21, 17, 13, .16);
}
.occ__panel[hidden] { display: none; }
.occ__opt {
  display: block; width: 100%; text-align: left;
  break-inside: avoid; /* never split one occasion across the column break */
  border: 0; background: transparent; cursor: pointer;
  padding: 7px 8px; border-radius: 8px;
  font: 700 13.5px var(--font-ui); color: var(--ink);
}
.occ__opt:hover, .occ__opt:focus-visible { background: var(--selected-bg); outline: 0; }
.occ__opt--on { background: var(--red-tint); color: var(--red); }

/* Inline validation message, sitting directly ABOVE its field so the error is
   next to the thing that is wrong rather than in one shared line under the CTA. */
.field-err {
  display: none;
  margin: 0 0 4px;
  font: 700 10.5px var(--font-ui);
  letter-spacing: .2px;
  color: var(--red);
}
.field-err.on { display: block; }
.field-opt { font: 600 8.5px var(--font-ui); letter-spacing: 1px; color: var(--ink-45); }

/* ---- Detail screens: hero, body, sticky pay bar ----
   The campaign screens (education, livelihood, healthcare) and the orphanage
   screens were carrying five copies of these between them - .lhero/.ohero,
   .lbody/.obody, .lbar/.obar, .lbtn/.obtn, .lerr/.oerr - identical apart from
   a hero 30px shorter here and a `gap` there. One set now. */
.dhero { position: relative; height: 240px; flex: none; background: var(--cream); }
.dhero img { width: 100%; height: 100%; object-fit: cover; }
.dhero__ph {
  width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
  font: 500 12px var(--font-ui); color: var(--ink-45);
}
.dhero__shade {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(21,17,13,.4) 0 20%, transparent 44%, rgba(21,17,13,.6));
}
.dhero__back {
  position: absolute; left: 16px; top: calc(14px + env(safe-area-inset-top));
  width: 40px; height: 40px; border-radius: 13px; background: rgba(255,255,255,.94);
  display: flex; align-items: center; justify-content: center; font-size: 17px;
  box-shadow: 0 2px 8px rgba(21,17,13,.2);
}
.dhero__foot { position: absolute; left: 16px; right: 16px; bottom: 12px; color: #fff; }
.dhero__name {
  font: 800 22px/1.2 var(--font-display); text-shadow: 0 1px 6px rgba(0,0,0,.35);
  overflow-wrap: anywhere;
}
.dhero__meta { margin-top: 5px; display: flex; flex-wrap: wrap; gap: 6px; }
.dhero__pill {
  background: rgba(21,17,13,.6); backdrop-filter: blur(4px); border-radius: 6px;
  padding: 3px 8px; font: 700 10.5px var(--font-ui);
}

.dbody { flex: 1; display: flex; flex-direction: column; gap: 12px; padding: 14px 16px 0; }

/* Total on the left, one button on the right. (The cause screen's own pay bar
   is .sticky-cta - a different control that carries its own proof line - and is
   deliberately left alone.) */
.paybar {
  position: sticky; bottom: 0; z-index: 11; flex: none;
  display: flex; align-items: center; gap: 12px;
  background: #fff; border-top: 1px solid var(--border);
  padding: 11px 16px calc(12px + env(safe-area-inset-bottom));
  box-shadow: 0 -4px 16px rgba(21,17,13,.08); margin-top: 14px;
}
.paybar__sum { flex: 1; min-width: 0; }
.paybar__amt { font: 800 18px var(--font-display); letter-spacing: -.3px; }
.paybar__note { margin-top: 1px; font: 500 10.5px var(--font-ui); color: var(--ink-45); }
.paybar__btn {
  flex: none; min-width: 150px; height: 46px; border: 0; border-radius: 13px;
  background: var(--red); color: #fff; font: 800 15px var(--font-ui); cursor: pointer;
  display: flex; align-items: center; justify-content: center; gap: 6px;
}
.paybar__btn:disabled { opacity: .5; cursor: default; }

/* The page-level failure line above the pay bar (payment and network errors -
   field errors go in .field-err, next to the field). */
.formerr {
  margin: 0 16px; background: var(--red-tint); border: 1px solid rgba(200,39,30,.28);
  border-radius: 12px; padding: 10px 13px; font: 600 12px var(--font-ui); color: var(--red);
}
.formerr[hidden] { display: none; }

/* ---- Photo viewer ----
   Tap a proof photo and it opens IN PLACE, with arrows, rather than dumping the
   donor on a raw image URL in a new tab. Fixed to the viewport, not to .screen:
   .screen is a 430px column on desktop and a field photo needs the whole window.

   Markup is v5_photo_viewer.html and the behaviour is in app.js, so the
   campaign screens, the proof page and the tracking page share one viewer
   instead of the two copies this started as. */
.pv{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center}
.pv[hidden]{display:none}
.pv__scrim{position:absolute;inset:0;background:rgba(21,17,13,.88);animation:pvfade .18s ease}
@keyframes pvfade{from{opacity:0}to{opacity:1}}
.pv__box{position:relative;margin:0;max-width:min(94vw,900px);max-height:86vh;
  display:flex;flex-direction:column;align-items:center;gap:10px}
.pv__img{max-width:100%;max-height:76vh;width:auto;height:auto;border-radius:10px;
  background:#fff;object-fit:contain}
.pv__cap{display:flex;align-items:baseline;gap:8px;
  font:700 12.5px var(--font-ui);color:rgba(255,255,255,.92);text-align:center}
.pv__cap b{font:600 11px var(--font-ui);color:rgba(255,255,255,.5)}
/* Dark fill and a rim, not 14% white: over a bright field photo - a white
   parcel, a sunlit street - a translucent white circle disappears completely,
   which is why the arrows read as "not there". These sit ON the photo, so they
   have to carry their own contrast whatever is behind them. 44px is also the
   smallest comfortable touch target. */
.pv__x, .pv__nav{
  position:absolute;z-index:2;
  border:1px solid rgba(255,255,255,.45);border-radius:50%;
  background:rgba(21,17,13,.62);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  color:#fff;cursor:pointer;
  box-shadow:0 2px 12px rgba(0,0,0,.45);
}
.pv__x:active, .pv__nav:active{background:rgba(21,17,13,.85)}
.pv__x{top:calc(10px + env(safe-area-inset-top));right:12px;
  width:44px;height:44px;font:700 16px var(--font-ui)}
.pv__nav{top:50%;transform:translateY(-50%);
  width:44px;height:44px;font:700 26px var(--font-ui);line-height:1}
.pv__nav--prev{left:10px}
.pv__nav--next{right:10px}
.pv__nav[hidden]{display:none}
/* A field photo is large and a phone connection is not: dim the last photo and
   spin until the next one is in. The fade-in waits .15s, so a photo already in
   cache swaps without a flicker. app.js sets the attribute. */
.pv__box[aria-busy="true"]{min-width:120px;min-height:120px}
.pv__box[aria-busy="true"] .pv__img{opacity:.35}
.pv__box[aria-busy="true"]::after{
  content:"";position:absolute;top:50%;left:50%;
  width:34px;height:34px;margin:-17px 0 0 -17px;
  border-radius:50%;border:3px solid rgba(255,255,255,.9);border-right-color:transparent;
  animation:v5spin .7s linear infinite, pvfade .2s .15s both;
}
@media (prefers-reduced-motion:reduce){
  .pv__scrim{animation:none}
}

/* ---- Parcel photo ----
   The optional picture printed on the parcel, offered by five screens now (the
   cause page, both campaign pages and both orphanage pages), so it lives here
   rather than in any one of them. app.js drives the toggle, the file checks and
   the FormData switch off the data- attributes; these are only its clothes.
   Written in app.css tokens on purpose - the campaign and orphanage templates
   re-map --line/--text for themselves, but every token used here is one of the
   originals, so the block looks the same wherever it is dropped in. */
.pphoto{margin-top:10px;border:1px solid var(--border);border-radius:12px;background:var(--cream)}
.pphoto__head{display:flex;align-items:center;gap:9px;margin:0;padding:10px 12px;cursor:pointer}
.pphoto__check{width:16px;height:16px;flex:none;accent-color:var(--red);margin:0;cursor:pointer}
.pphoto__ttl{flex:1;min-width:0;font:700 12.5px var(--font-ui);color:var(--ink)}
.pphoto__opt{font-weight:500;color:var(--ink-45)}
.pphoto__rate{flex:none;font:800 12.5px var(--font-ui);color:var(--red)}
.pphoto__rate small{font-size:9.5px;font-weight:700}
.pphoto__body{padding:0 12px 10px}
.pphoto__file{
  width:100%;font:500 11.5px var(--font-ui);color:var(--ink-65);
}
.pphoto__file::file-selector-button{
  padding:5px 10px;margin-right:9px;border:1px solid var(--border-strong);border-radius:8px;
  background:#fff;color:var(--ink);font:700 11px var(--font-ui);cursor:pointer;
}
.pphoto__hint{margin:6px 0 0;font:500 10.5px var(--font-ui);color:var(--ink-45)}


.otp-box {
  flex: 1 1 0;
  min-width: 0;
  width: auto;
  max-width: 69px;
  aspect-ratio: 1 / 1;
  height: auto;
  min-height: 56px;        /* stays a comfortable tap target when it shrinks */
  border-radius: 17px;
  background: #fff;
  border: 1.5px solid var(--border);
  text-align: center;
  font: 800 clamp(22px, 7.4vw, 30px) var(--font-ui);
  color: var(--ink);
  outline: none;
  transition: border-color .15s ease, box-shadow .15s ease;
}
/* A filled box reads as done without waiting for focus to leave it - with only
   four, how many are left is something the donor takes in at a glance. */
.otp-box:not(:placeholder-shown) { border-color: var(--border-strong); }
.otp-box:focus {
  border: 2px solid var(--red);
  box-shadow: 0 0 0 4px rgba(200, 39, 30, .10);   /* --red at 10% */
}

/* ---- Misc ---- */
.round-btn {
  width: 36px; height: 36px;
  border-radius: 18px;
  background: #fff;
  border: 1px solid var(--border);
  display: flex; align-items: flex-start; justify-content: center;
  font-size: 25px;
  color: var(--ink);
}
.ticket-notch { position: absolute; top: -10px; width: 18px; height: 18px; border-radius: 9px; }
.avatar {
  border-radius: 50%;
  background: var(--ink);
  color: var(--beige);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display);
  font-weight: 800;
}
.hide-scroll { scrollbar-width: none; }
.hide-scroll::-webkit-scrollbar { display: none; }

.h-hero { font-size: clamp(22px, 8.2vw, 36px); }
.h-success { font-size: clamp(24px, 7.6vw, 31px); }
.h-title { font-size: clamp(19px, 5.8vw, 24px); }

.safe-top { padding-top: env(safe-area-inset-top); }

@media (orientation: landscape) and (max-height: 460px) {
  .screen { min-height: auto; }
}
 
@media (max-width: 359px) {
  .pad { padding-left: 16px; padding-right: 16px; }
  .navbar { padding-left: 14px; padding-right: 14px; }
  .navbar a { font-size: 9.5px; }
}

/* ---- page-to-page: cross-fade instead of a white flash; the bottom nav stays put ---- */
@view-transition { navigation: auto; }
.navbar { view-transition-name: navbar; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}

/* ---- Loading ----
   One look for "please wait" on every screen, instead of each screen dimming,
   relabelling or doing nothing in its own way.

   .v5-loadbar - the bar across the top while the next page is on its way.
   Markup and v5Loading live in v5_base.html; app.js starts it for every
   navigation.

   button[aria-busy="true"] - set with v5Busy(button, true). A button that
   leads with an icon swaps the icon for the spinner and keeps its words
   ("Locating…", "SMS"); any other button hides its label under a centred
   spinner, so its width never changes mid-request. pointer-events:none is what
   stops a second tap sending the same request again. */
.v5-loadbar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  /* inside the phone column on desktop, like .navbar */
  max-width: 430px; margin: 0 auto;
  height: 3px; overflow: hidden; pointer-events: none;
  background: var(--red-tint);
}
.v5-loadbar[hidden] { display: none; }
.v5-loadbar::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 40%;
  border-radius: 0 3px 3px 0; background: var(--red);
  animation: v5bar 1.1s ease-in-out infinite;
}
@keyframes v5bar { from { transform: translateX(-100%); } to { transform: translateX(250%); } }
@keyframes v5spin { to { transform: rotate(360deg); } }

button[aria-busy="true"] { cursor: progress; pointer-events: none; }
/* opacity: a busy button is usually :disabled too, and several screens fade
   disabled buttons - the spinner has to stay readable */
button[aria-busy="true"]:not(:has(> svg:first-child)) {
  position: relative; opacity: 1;
  -webkit-text-fill-color: transparent;   /* the label, bare text nodes included */
}
button[aria-busy="true"]:not(:has(> svg:first-child)) > * { visibility: hidden; }
button[aria-busy="true"]:not(:has(> svg:first-child))::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 20px; height: 20px; margin: -10px 0 0 -10px;
  border-radius: 50%; border: 2.5px solid currentColor; border-right-color: transparent;
  animation: v5spin .7s linear infinite;
}
button[aria-busy="true"]:has(> svg:first-child) { opacity: 1; }
button[aria-busy="true"]:has(> svg:first-child) > svg:first-child { display: none; }
button[aria-busy="true"]:has(> svg:first-child)::before {
  content: ""; flex: none; display: inline-block; vertical-align: middle;
  width: min(1em, 17px); height: min(1em, 17px);
  border-radius: 50%; border: 2px solid currentColor; border-right-color: transparent;
  animation: v5spin .7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  /* slower rather than stopped: a spinner that holds still reads as a hang */
  .v5-loadbar::before { animation-duration: 2.4s !important; }
  button[aria-busy="true"]::before,
  button[aria-busy="true"]::after,
  .pv__box[aria-busy="true"]::after { animation-duration: 1.6s !important; }
}
