/* ===========================================================================
   NAMA Spring Trip Raffle — student-facing site
   Ported from mock-site/index.html (inline styles) and kept intentionally
   unchanged in palette, type and layout. See docs/DESIGN.md D17/D19/D20.
   Palette: a darkened hall before the downbeat — stage dark, chalk, NAMA
   scarlet (sampled from the logo), warm stage light, and steel.

   Removed from the mock on purpose (production, not a prototype):
     - the director/demo transport bar and everything scoped to it
     - the "viewing as" picker
     - anything tied to campaign.js's simulated timeline
   =========================================================================== */

/* ------------------------------------------------------------- self-hosted
   fonts. HOSTING.md ("Operations"): bundle Barlow rather than loading Google
   Fonts, for one less third-party request from a minor's browser. Files are
   the Google Fonts "latin" subset only — our content is English-only; if that
   ever changes, re-add the latin-ext/vietnamese subsets from
   fonts.googleapis.com/css2?family=Barlow... */
@font-face {
  font-family: 'Barlow';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('assets/fonts/barlow-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('assets/fonts/barlow-500.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('assets/fonts/barlow-600.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('assets/fonts/barlow-700.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow Condensed';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('assets/fonts/barlow-condensed-500.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow Condensed';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('assets/fonts/barlow-condensed-600.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow Condensed';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('assets/fonts/barlow-condensed-700.woff2') format('woff2');
}

:root {
  --stage:    #12161A;   /* darkened hall — cool, not a tinted black */
  --stage-2:  #1B2229;
  --chalk:    #F2F4F3;   /* cool off-white, deliberately not warm cream */
  --paper:    #FFFFFF;
  --scarlet:  #CA0727;
  --scarlet-d:#9C0520;
  --gold:     #F2A916;   /* warm stage light */
  --steel:    #5A646E;
  --steel-l:  #98A2AC;
  --ink:      #101418;
  --ink-2:    #3D464F;
  --hair:     #DDE2E1;
  --hair-d:   rgba(255,255,255,.14);
  --good:     #0C8F4E;

  --cond: 'Barlow Condensed', 'Arial Narrow', 'Franklin Gothic Condensed', sans-serif;
  --sans: 'Barlow', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono: ui-monospace, 'Cascadia Mono', 'Consolas', monospace;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--sans);
  background: var(--chalk);
  color: var(--ink);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
h1, h2, h3, .fig { font-family: var(--cond); font-weight: 600; letter-spacing: .01em; }
h1 { font-size: 40px; line-height: 1.04; margin: 0; }
h2 { font-size: 26px; line-height: 1.1; margin: 0 0 4px; }
h3 { font-size: 19px; line-height: 1.15; margin: 0 0 3px; }
p  { margin: 0 0 10px; max-width: 68ch; }
a  { color: var(--scarlet); }
:focus-visible { outline: 2px solid var(--scarlet); outline-offset: 2px; border-radius: 3px; }
button:focus-visible, input:focus-visible, a:focus-visible, select:focus-visible {
  outline: 2px solid var(--scarlet); outline-offset: 2px;
}

.wrap { max-width: 1120px; margin: 0 auto; padding: 0 22px; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* ------------------------------------------------------------------ topbar */
.topbar {
  background: var(--paper);
  border-bottom: 1px solid var(--hair);
  position: sticky; top: 0; z-index: 30;
}
.topbar .wrap { display: flex; align-items: center; gap: 18px; height: 74px; }
/* The bar height tracks the logo — at 45px a 62px bar left about 8px above and
   below, which reads as cramped rather than deliberate. */
.topbar img { height: 45px; }
.topbar .brand { display: flex; align-items: center; }
nav { display: flex; gap: 2px; margin-left: auto; }
/* The buy page hides Sign in: it is district-email only, so to the parent or
   grandparent that page is written for it is a door that cannot open. Set as
   a body attribute by views/buy.js so it does not fight the auth-driven
   .hidden toggling in app.js. */
body[data-hide-signin] #navSignin { display: none; }
/* Admin chrome. Sign in is a buyer's door; My dashboard, Standings and Prizes
   are a student's. An admin has no student row, so My dashboard would hand
   them a registration form. */
body[data-admin-chrome] #navSignin,
body[data-admin-chrome] nav [data-nav="dashboard"],
body[data-admin-chrome] nav [data-nav="standings"],
body[data-admin-chrome] nav [data-nav="prizes"] { display: none; }
/* The goal banner too. It reads "now earns money toward your trip" — written
   to a student, and wrong on a console whose whole job is watching the
   campaign from outside it. The crescendo above the tabs already shows the
   committee where the goal stands. An id beats .banner.on, so this holds when
   renderSwell() turns the banner on. */
body[data-admin-chrome] #goalBanner { display: none; }
/* The signed-in admin, sitting in the topbar beside Sign out. Quiet — it is
   provenance, not navigation — and it must not push the nav into wrapping on
   a phone, hence the truncation. */
.navwho {
  font-size: 13px; color: var(--steel); align-self: center;
  padding: 0 4px 0 8px; max-width: 34ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-width: 760px) {
  /* On a narrow nav the address is the first thing to give up its room. */
  .navwho { max-width: 100%; padding-left: 0; order: 99; flex-basis: 100%; }
}
nav button, nav a {
  font: inherit; font-weight: 500; font-size: 14.5px;
  background: none; border: none; cursor: pointer; color: var(--ink-2);
  padding: 7px 13px; border-radius: 6px; text-decoration: none;
}
nav button:hover, nav a:hover { background: var(--chalk); color: var(--ink); }
nav button[aria-current="page"], nav a[aria-current="page"] { background: var(--ink); color: var(--paper); }

/* ------------------------------------------------------------- the swell --
   The one bold element: the group goal drawn as a crescendo. The wedge grows
   toward the goal the way any ensemble grows as more voices come in — a figure
   every group here shares. Everything else on the page stays quiet. */
.swell {
  background: var(--stage);
  color: var(--chalk);
  background-image:
    radial-gradient(120% 140% at 50% -40%, rgba(242,169,22,.13), transparent 62%);
}
.swell .wrap { padding-top: 26px; padding-bottom: 24px; }
.swell-head { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.swell-head h2 { color: var(--chalk); font-size: 22px; }
.swell-head .sub { color: var(--steel-l); font-size: 14px; }

.crescwrap { position: relative; margin: 18px 0 8px; padding-right: 3px; }
.cresc {
  position: relative; height: 80px;
  /* the crescendo hairpin: hairline at the left, full height at the goal */
  clip-path: polygon(0 47.5%, 100% 0, 100% 100%, 0 52.5%);
  background: rgba(255,255,255,.055);
}
.cfill {
  position: absolute; left: 0; top: 0; bottom: 0; width: 0;
  background: linear-gradient(90deg, rgba(202,7,39,.45), var(--scarlet));
  /* Matches the 700ms ease-out the readout NUMBERS beside this bar animate
     with (web/ui.js, animateNumber) — a filling bar and a counting-up figure
     that finish at different speeds reads as two unrelated things moving,
     not one. prefers-reduced-motion is handled globally, below. */
  transition: width .7s ease-out;
}
.swell.scored .cfill { background: linear-gradient(90deg, var(--scarlet), var(--gold)); }

.ticks { position: absolute; inset: 0; display: flex; pointer-events: none; }
.ticks i { flex: 1; border-right: 1px solid rgba(255,255,255,.09); font-style: normal; }
.ticks i.major { border-right-color: rgba(255,255,255,.22); }
.ticks i:last-child { border-right: none; }

.ticklabels { display: flex; margin-top: 7px; }
.ticklabels i {
  flex: 1; font-family: var(--cond); font-size: 12px; color: var(--steel);
  text-align: right; font-style: normal;
}
.ticklabels i:last-child { color: var(--gold); }

.nowmark {
  position: absolute; top: -8px; height: 96px; width: 2px; left: 0;
  background: var(--chalk); transition: left .7s ease-out;
}
.nowmark::after {
  content: ''; position: absolute; top: -4px; left: -3px;
  width: 8px; height: 8px; background: var(--chalk); border-radius: 50%;
}
.goalmark { position: absolute; right: 0; top: -8px; height: 96px; width: 3px;
            background: var(--gold); }
.goaltag {
  position: absolute; right: 8px; top: -30px; font-family: var(--cond);
  font-size: 13px; color: var(--gold); letter-spacing: .06em;
  opacity: 0; transform: translateY(6px);
}
.swell.scored .goaltag { opacity: 1; transform: none; transition: all .5s ease-out; }

.swell-readout { display: flex; gap: 30px; flex-wrap: wrap; align-items: flex-end;
                 margin-top: 26px; }
.readout .k { font-size: 12.5px; color: var(--steel-l); }
.readout .v { font-family: var(--cond); font-size: 30px; line-height: 1.05;
              color: var(--chalk); }
.readout .v.gold { color: var(--gold); }
.readout.lead .v { font-size: 46px; }

.locknote {
  margin-top: 16px; font-size: 13.5px; color: var(--steel-l);
  border-left: 2px solid var(--scarlet); padding-left: 11px;
}
.swell.scored .locknote { border-left-color: var(--gold); color: #D8DEE3; }

/* Simpler hero variant used on the sign-in page — dark band, headline only,
   no crescendo elements. */
.hero-band .wrap { padding-top: 34px; padding-bottom: 30px; }

/* ------------------------------------------------------------------- cards */
.grid { display: grid; gap: 16px; }
.card {
  background: var(--paper); border: 1px solid var(--hair);
  border-radius: 6px; padding: 18px 20px;
}
.card.hero { border-left: 3px solid var(--scarlet); }
.label { font-size: 12.5px; color: var(--steel); margin-bottom: 2px; }
.fig { font-size: 38px; line-height: 1.02; }
.fig.sm { font-size: 27px; }
.muted { color: var(--steel); }
.small { font-size: 13px; }

section.pad { padding: 30px 0; }

/* stat strip */
.stats { display: grid; grid-template-columns: repeat(auto-fit,minmax(128px,1fr));
         border: 1px solid var(--hair); border-radius: 6px; overflow: hidden;
         background: var(--hair); gap: 1px; }
.stat { background: var(--paper); padding: 13px 15px; }
.stat .k { font-size: 12px; color: var(--steel); }
.stat .v { font-family: var(--cond); font-size: 25px; line-height: 1.1; }

/* Metric rows inside the earnings card. A vertical list reads better in a
   narrow column than five squeezed tiles, and it fills the height the card
   already occupies. */
.statlist { margin: 16px 0 0; padding: 14px 0 0; border-top: 1px solid var(--hair); }
.statlist div {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 9px 0; border-bottom: 1px solid var(--hair);
}
.statlist div:last-child { border-bottom: none; padding-bottom: 0; }
.statlist dt { font-size: 13.5px; color: var(--steel); }
.statlist dd {
  margin: 0; font-family: var(--cond); font-size: 21px; line-height: 1;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* ------------------------------------------------------------- share block */
.sharebox {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  background: var(--chalk); border: 1px solid var(--hair);
  border-radius: 6px; padding: 11px 13px; margin: 10px 0 12px;
}
.sharebox .url {
  font-family: var(--mono); font-size: 14px; color: var(--ink);
  overflow-wrap: anywhere; flex: 1 1 240px;
}
/* Applies to <button> and to <a class="btn"> alike. The anchors need
   text-decoration and display resetting: the base `a` rule leaves the
   underline on, and an inline <a> ignores vertical padding, so without
   inline-block the anchor buttons sit shorter than the real ones. */
.btn {
  font: inherit; font-weight: 600; font-size: 14px; cursor: pointer;
  padding: 8px 15px; border-radius: 6px; border: 1px solid var(--ink);
  background: var(--ink); color: var(--paper);
  text-decoration: none; display: inline-block; text-align: center;
}
.btn:hover { background: #000; }
.btn:disabled { opacity: .55; cursor: not-allowed; }
.btn.primary { background: var(--scarlet); border-color: var(--scarlet); }
.btn.primary:hover { background: var(--scarlet-d); border-color: var(--scarlet-d); }
.btn.ghost { background: none; color: var(--ink); border-color: var(--hair); }
.btn.ghost:hover { background: var(--chalk); border-color: var(--steel-l); }
.btn.sm { padding: 6px 11px; font-size: 13px; }
.btnrow { display: flex; gap: 8px; flex-wrap: wrap; }

/* share */
.btn.share { display: inline-flex; align-items: center; gap: 7px; }
.btn.wide { width: 100%; justify-content: center; }

/* QR modal */
.modal {
  position: fixed; inset: 0; z-index: 80; display: grid; place-items: center;
  background: rgba(16,20,24,.55); padding: 20px;
}
.modal[hidden] { display: none; }
.sheet {
  background: var(--paper); border-radius: 10px; padding: 22px 24px;
  max-width: 340px; width: 100%; text-align: center;
  box-shadow: 0 16px 48px rgba(16,20,24,.28);
}
.sheet h3 { margin-bottom: 2px; }
.sheet p { margin-left: auto; margin-right: auto; }
/* The canvas is rendered at exactly the size it is shown at, so it must not be
   resized here. Any CSS scaling resamples the modules and breaks scanning. */
.qrbox { margin: 14px 0 10px; min-height: 40px; }
.qrbox canvas { display: block; margin: 0 auto; }
.qrlink { font-family: var(--mono); font-size: 12px; color: var(--steel);
          overflow-wrap: anywhere; }
.sheet .btnrow { justify-content: center; }

/* meter toward trip cost */
.meter { height: 9px; background: var(--hair); border-radius: 99px; overflow: hidden;
         margin: 9px 0 6px; }
.meter i { display: block; height: 100%; background: var(--scarlet); border-radius: 99px;
           transition: width .7s ease-out; }
.meter i.done { background: var(--good); }

/* --------------------------------------------------------------- standings */
table { width: 100%; border-collapse: collapse; }
th {
  text-align: right; font-size: 12.5px; font-weight: 600; color: var(--steel);
  padding: 0 10px 7px 0; border-bottom: 1px solid var(--hair); white-space: nowrap;
}
/* Numeric columns right-align so figures line up for scanning; the rank and
   name columns stay left. The :nth-child rule outranks both bare th and td. */
td { text-align: right; padding: 8px 10px 8px 0; border-bottom: 1px solid var(--hair);
     font-variant-numeric: tabular-nums; }
th:nth-child(-n+2), td:nth-child(-n+2) { text-align: left; }
tr:last-child td { border-bottom: none; }
tr.me td { background: #FFF4F1; }
tr.me td:first-child { box-shadow: inset 3px 0 0 var(--scarlet); }
.rank { font-family: var(--cond); font-size: 19px; color: var(--steel); width: 46px; }
tr.me .rank { color: var(--scarlet); }
/* Tied ranks repeat the number, so mark them rather than leaving a reader to
   wonder whether the board is broken. */
.tie { font-size: 12px; color: var(--steel); margin-right: 1px; }
.who { font-weight: 600; }
.chip {
  display: inline-block; font-size: 11.5px; font-weight: 600; padding: 1px 7px;
  border-radius: 99px; background: #EAF3EC; color: var(--good);
}

/* ------------------------------------------------------------ activity feed */
.feed { list-style: none; margin: 0; padding: 0; }
.feed li { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--hair);
           font-size: 14px; align-items: baseline; }
.feed li:last-child { border-bottom: none; }
.feed .when { font-size: 12px; color: var(--steel); min-width: 54px; }
/* The buyer's name, when they chose to be named. It is the variable-width
   thing in a row that also has to fit a ticket count and an amount, so it is
   the one allowed to truncate — min-width:0 because a flex item will not
   shrink below its content otherwise, and the amount would be pushed off. */
.feed .who {
  font-weight: 600; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.feed .qty { white-space: nowrap; }
.feed .amt { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 600; }

/* ---------------------------------------------------------- sign-in / steps */
.steps { counter-reset: s; display: grid; gap: 12px; }
.step { display: grid; grid-template-columns: 30px 1fr; gap: 13px; align-items: start; }
.step .n {
  font-family: var(--cond); font-size: 16px; width: 27px; height: 27px;
  border-radius: 50%; display: grid; place-items: center;
  background: var(--ink); color: var(--paper);
}
.step.done .n { background: var(--good); }
.step.off { opacity: .42; }
.step.off .n { background: var(--hair); color: var(--steel); }
label.field-label { display: block; font-size: 13px; color: var(--steel); margin-bottom: 4px; font-weight: 500; }
/* Every text-like type, not three of them. A `number` or `search` box left
   off this list falls back to the browser's own control, which sits next to
   the styled ones looking like it belongs to a different page. */
input[type=text], input[type=email], input[type=tel],
input[type=number], input[type=search], input[type=url] {
  font: inherit; font-size: 15px; padding: 9px 11px; width: 100%;
  border: 1px solid var(--hair); border-radius: 6px; background: var(--paper);
}
input.bad { border-color: var(--scarlet); }
.err { color: var(--scarlet); font-size: 13px; margin-top: 5px; }
.hint { color: var(--steel); font-size: 13px; margin-top: 5px; }

/* ------------------------------------------------------------ buy page */
.qty { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0; }
.qty button {
  font: inherit; cursor: pointer; border: 1px solid var(--hair); background: var(--paper);
  border-radius: 6px; padding: 11px 16px; text-align: center; min-width: 78px;
}
.qty button .n { font-family: var(--cond); font-size: 23px; display: block; line-height: 1; }
.qty button .p { font-size: 12.5px; color: var(--steel); display: block; }
.qty button[aria-pressed=true] { border-color: var(--scarlet); background: #FFF4F1; }
.qty button .best { font-size: 11px; color: var(--scarlet); font-weight: 600;
                    display: block; margin-top: 2px; }
.checkrow { display: flex; gap: 9px; align-items: flex-start; margin: 12px 0; font-size: 14px; }
/* `flex: none` and the !important width are load-bearing: .buyer input sets
   width:100% for the text fields, and a .checkrow that ever lands inside that
   block would otherwise render a checkbox stretched across the whole form.
   Same specificity, later rule wins, and it is not obvious from either one. */
.checkrow input { width: 16px !important; height: 16px; flex: none;
                  margin-top: 2px; accent-color: var(--scarlet); }
.checkrow label { font-size: 14px; line-height: 1.4; color: var(--ink-2); cursor: pointer; }

/* visibility choice — opt-in, since these are minors' names (or a buyer's own
   name) on a public page */
.opts { border: 1px solid var(--hair); border-radius: 6px; padding: 9px 12px 11px;
        margin: 11px 0 0; }
.opts legend { padding: 0 5px; }
.opts label { display: flex; gap: 9px; align-items: flex-start; padding: 5px 0;
              cursor: pointer; }
.opts input { width: 16px; height: 16px; margin-top: 2px; accent-color: var(--scarlet);
              flex: none; }
.opts span { display: block; font-size: 14px; line-height: 1.35; }
.opts b { font-weight: 600; }
.opts i { display: block; font-style: normal; font-size: 12.5px; color: var(--steel); }
.opts[disabled] { opacity: .5; }

/* ------------------------------------------------------------ buyer block */
.buyer { margin-top: 16px; }
.buyer h4 { margin: 0 0 2px; font-family: var(--cond); font-size: 17px; font-weight: 600; }
.f2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
@media (max-width: 620px) { .f2 { grid-template-columns: 1fr; } }
.buyer label, .fone { display: block; font-size: 13px; color: var(--steel); }
.fone { margin-top: 10px; }
.buyer input { width: 100%; margin-top: 3px; }

/* --------------------------------------------------------- prizes/winners */
.prizes, .winners { list-style: none; margin: 10px 0 0; padding: 0; }
.prizes li, .winners li {
  display: flex; gap: 10px; align-items: baseline; padding: 7px 0;
  border-bottom: 1px solid var(--hair); font-size: 14px;
}
.prizes li:last-child, .winners li:last-child { border-bottom: none; }
.prizes .day, .winners .day {
  font-family: var(--cond); font-size: 13px; color: var(--steel);
  min-width: 52px; flex: none;
}
.prizes .what { font-weight: 550; }
.prizes .who, .winners .who { color: var(--steel); font-size: 13px; }
/* .name/.anon started as a `.winners`-only rule for the public buy page's
   separate winners list. The signed-in Prizes page (views/prizes.js) shows
   the same "who won" line inside a single `.prizes` list instead of a second
   one, so these are shared the same way `.day`/`.who` already are above. */
.prizes .name, .winners .name { font-weight: 600; }
.prizes .anon, .winners .anon { color: var(--steel); font-style: italic; font-weight: 400; }
.emptynote { color: var(--steel); font-size: 13.5px; padding: 8px 0; }

/* -------------------------------------------------------------------- misc */
.toast {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(8px);
  background: var(--ink); color: var(--paper); padding: 9px 16px; border-radius: 6px;
  font-size: 14px; opacity: 0; pointer-events: none; transition: all .18s; z-index: 70;
}
.toast.on { opacity: 1; transform: translateX(-50%); }

.banner {
  background: var(--gold); color: var(--stage); font-weight: 600;
  padding: 11px 0; display: none;
}
.banner.on { display: block; }
.banner .wrap { display: flex; align-items: center; gap: 12px; }

/* Dev/ops-facing notice, distinct from .banner so it isn't gated by the
   goal-reached on/off class convention — visibility is purely the native
   `hidden` attribute here. */
.notice-bar { background: var(--gold); color: var(--stage); font-weight: 600; padding: 9px 0; }
.notice-bar .wrap { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 13.5px; }

/* ------------------------------------------------------------- demo notice
   Dark, so it reads as chrome wrapped around the site rather than a message
   from inside it — the gold notice-bar above is the site talking, this is
   not. Neither brand colour is used: a scarlet bar would look designed, and
   looking designed is the opposite of the job. */
.demo-bar { background: var(--stage); color: var(--chalk); padding: 10px 0; }
.demo-bar .wrap {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  font-size: 13.5px; line-height: 1.45;
}
.demo-bar strong {
  font-family: var(--cond); font-size: 17px; font-weight: 600;
  letter-spacing: .01em; color: var(--gold); flex: none;
}
.demo-bar span { color: #C3CAD1; }

/* The travelling copy. Sits against paper, so it takes the opposite treatment
   to the bar: outlined rather than filled, to stay quiet beside the logo. */
.demo-pill {
  font-family: var(--cond); font-size: 13px; font-weight: 600;
  letter-spacing: .04em; color: var(--ink-2);
  border: 1px solid var(--hair); border-radius: 3px;
  padding: 2px 7px; margin-left: 10px; align-self: center;
  white-space: nowrap;
}

.view { display: none; }
.view.on { display: block; }

.two { display: grid; grid-template-columns: 1.25fr 1fr; gap: 16px; }
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

/* ----------------------------------------------------------- loading/error */
.skeleton {
  background: linear-gradient(90deg, var(--hair) 25%, #eef1f0 37%, var(--hair) 63%);
  background-size: 400% 100%; animation: shimmer 1.4s ease infinite;
  border-radius: 6px; color: transparent !important;
}
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
.error-box {
  border: 1px solid var(--scarlet); background: #FFF4F1; border-radius: 6px;
  padding: 14px 16px; margin: 12px 0;
}
.error-box p { margin: 0 0 8px; }
.spinner {
  display: inline-block; width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid var(--hair); border-top-color: var(--scarlet);
  animation: spin .7s linear infinite; vertical-align: -3px;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* --------------------------------------------------------------------- 404 */
.notfound { padding: 70px 0; text-align: center; }
.notfound .fig { font-size: 88px; color: var(--hair-d); color: var(--steel-l); }

/* ------------------------------------------------------------- responsive */
@media (max-width: 900px) {
  .two, .split { grid-template-columns: 1fr; }
  h1 { font-size: 32px; }
}

@media (max-width: 760px) {
  /* Topbar: let it wrap instead of forcing a minimum page width. The nav
     scrolls sideways rather than squeezing the labels. */
  .topbar .wrap { height: auto; padding-top: 9px; padding-bottom: 9px;
                  flex-wrap: wrap; gap: 10px; }
  .topbar img { height: 39px; }
  nav { margin-left: 0; width: 100%; overflow-x: auto; gap: 4px;
        scrollbar-width: none; -webkit-overflow-scrolling: touch; }
  nav::-webkit-scrollbar { display: none; }
  nav button, nav a { white-space: nowrap; font-size: 14px; padding: 7px 11px; }

  .wrap { padding: 0 16px; }
  section.pad { padding: 20px 0; }

  /* The swell readout is four figures; two columns beats a ragged wrap. */
  .swell .wrap { padding-top: 18px; padding-bottom: 18px; }
  .swell-readout { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 18px; }
  .readout.lead { grid-column: 1 / -1; }
  .readout.lead .v { font-size: 38px; }
  .readout .v { font-size: 24px; }
  .cresc { height: 56px; }
  .nowmark, .goalmark { height: 70px; }

  .fig { font-size: 32px; }
  .card { padding: 15px 16px; }
  .banner .wrap { display: block; }

  .qty button { min-width: 0; flex: 1 1 68px; }
}

/* Touch targets. Fingers aren't cursors — the desktop sizes are too small to
   hit reliably, and these are students on phones. Keyed on pointer type as
   well as width, because a tablet is a touch device at any width and a narrow
   desktop window is not. */
@media (max-width: 900px), (pointer: coarse) {
  nav button, nav a { padding: 9px 12px; }
  .btn.sm { padding: 9px 13px; font-size: 13.5px; }
  .btn { padding: 11px 16px; }
  .opts label { padding: 8px 0; }
}

@media (max-width: 420px) {
  h1 { font-size: 28px; }
  .readout.lead .v { font-size: 32px; }
  .sharebox .url { font-size: 12.5px; }
}
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* --- Who this purchase credits -------------------------------------------
   The single most important sentence on the buy page: a buyer who followed a
   student's link wants confirmation they got the right person, and a buyer
   who mistyped a code needs to be told before they pay, not after. Given its
   own block above the quantity picker rather than a quiet line of body text.

   Colour does not carry the meaning on its own — each state has its own
   wording, so this reads correctly in greyscale and to a screen reader. */
.supporting {
  margin: 14px 0 4px; padding: 11px 13px;
  border: 1px solid var(--hair); border-left-width: 3px;
  border-radius: 6px; background: var(--chalk);
}
.supporting p { margin: 0; font-size: 15px; }
.supporting p + p { margin-top: 6px; }
.supporting.is-student    { border-left-color: var(--good); }
.supporting.is-department { border-left-color: var(--steel); }
.supporting.is-warning    { border-left-color: var(--gold); background: #FEF8EA; }

.codeform { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.codeform input {
  flex: 1 1 11em; min-width: 0;
  font: inherit; font-size: 14px; font-family: var(--mono);
  padding: 7px 10px; border: 1px solid var(--hair); border-radius: 6px;
  background: var(--paper); color: var(--ink);
}
.codeform input:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }

/* Visible to screen readers, not to the eye. The code input has a visible
   placeholder but a placeholder is not a label. */
.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;
}

/* --- Admin console --------------------------------------------------------
   Deliberately plainer than the student side. This is a working tool for a
   handful of people reconciling money, so it favours density and legibility
   over any of the campaign's character. Nothing links here; see
   web/views/admin.js. */
.adminbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.adminbar h1 { margin: 0; }

.tabs { display: flex; gap: 4px; flex-wrap: wrap; margin: 16px 0 18px;
        border-bottom: 1px solid var(--hair); }
.tabs .tab {
  font: inherit; font-size: 14px; font-weight: 500; cursor: pointer;
  background: none; border: none; border-bottom: 2px solid transparent;
  color: var(--ink-2); padding: 8px 12px; margin-bottom: -1px;
}
.tabs .tab:hover { color: var(--ink); }
.tabs .tab[aria-current="page"] { color: var(--ink); border-bottom-color: var(--scarlet); font-weight: 600; }

/* The ledger has eight columns and a phone has room for about three. Scroll
   the table rather than wrapping every cell into an unreadable stack — a
   reconciliation table is read by scanning down a column, which wrapping
   destroys. `card` keeps its border; the scroll happens inside it. */
.tablewrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.adm { width: 100%; border-collapse: collapse; font-size: 13.5px; }
table.adm th {
  text-align: left; font-weight: 600; font-size: 12px; color: var(--steel);
  padding: 6px 10px; border-bottom: 1px solid var(--hair); white-space: nowrap;
  /* Bottom, to match the top-aligned cells below: a centred 12px header over
     a two-line 13.5px cell reads as sitting between two columns rather than
     over one. */
  vertical-align: bottom;
}
table.adm td { padding: 7px 10px; border-bottom: 1px solid var(--chalk); vertical-align: top; }
/* Deterministic columns. With auto layout the browser sizes each column from
   its content, so two tables with the same headers can lay out differently
   and a wide cell can drag its heading away from the data underneath it. */
table.adm th:first-child, table.adm td:first-child { padding-left: 13px; }
table.adm th:last-child,  table.adm td:last-child  { padding-right: 13px; }
table.adm tr:last-child td { border-bottom: none; }
table.adm .r, table.adm th.r { text-align: right; }
table.adm tr.clickable { cursor: pointer; }
table.adm tr.clickable:hover td { background: var(--chalk); }
/* A student whose sales go to the fund instead of to them (D22) — visibly
   different, because reading past one by accident is a payout error. */
table.adm tr.ineligible td { background: #FFF7F5; }
table.adm code { font-family: var(--mono); font-size: 12.5px; }

.notice {
  border: 1px solid var(--gold); background: #FEF8EA; border-radius: 6px;
  padding: 10px 13px; margin-bottom: 14px; font-size: 14px;
}

.ticketlist { list-style: none; margin: 10px 0 0; padding: 0;
              display: flex; flex-wrap: wrap; gap: 6px; }
.ticketlist li {
  border: 1px solid var(--hair); border-radius: 5px; padding: 4px 9px;
  font-size: 13px; background: var(--paper);
}
.tnum { font-family: var(--mono); }
.won { color: var(--good); font-weight: 600; }

/* --- Admin console on a phone -------------------------------------------- */
@media (max-width: 760px) {
  .adminbar { flex-direction: column; gap: 6px; }
  .adminbar h1 { font-size: 30px; }

  /* Tabs scroll sideways instead of stacking into six full-width rows that
     push the content off the first screen. */
  .tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
          -webkit-overflow-scrolling: touch; }
  .tabs::-webkit-scrollbar { display: none; }
  .tabs .tab { white-space: nowrap; flex: none; }

  /* Two readable columns beats four cramped ones. */
  .stats { grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); }
  .stat { padding: 10px 12px; }
  .stat .v { font-size: 21px; }

  table.adm { font-size: 13px; }
  table.adm th, table.adm td { padding: 6px 8px; }
  /* Nothing in a reconciliation table should wrap mid-value; the container
     scrolls instead. Buyer name and email are the exception — they are the
     widest cells and wrapping them saves the most horizontal room. */
  table.adm th, table.adm td { white-space: nowrap; }
  table.adm td:nth-child(2) { white-space: normal; min-width: 140px; }

  .ticketlist li { font-size: 12.5px; padding: 3px 7px; }
}

@media (max-width: 420px) {
  .stats { grid-template-columns: 1fr 1fr; }
  .tabs .tab { padding: 8px 10px; font-size: 13.5px; }
}

/* --- Admin: dialogs and toolbars ----------------------------------------- */
/* Native <dialog>: Escape, focus trapping and the backdrop come from the
   browser rather than from us reimplementing them slightly wrong. */
dialog.formdlg {
  border: 1px solid var(--hair); border-radius: 8px; padding: 0;
  max-width: 440px; width: calc(100% - 32px);
  background: var(--paper); color: var(--ink);
  box-shadow: 0 18px 44px rgba(16,20,24,.18);
}
dialog.formdlg::backdrop { background: rgba(16,20,24,.42); }
dialog.formdlg form { padding: 20px 22px 18px; overflow-x: hidden; }
dialog.formdlg h3 { margin: 0 0 4px; font-family: var(--cond); font-size: 21px; font-weight: 600; }
dialog.formdlg p { margin: 0 0 10px; }
dialog.formdlg .fone { display: block; margin-top: 12px; }
/* Text-like inputs only.
 *
 * As `dialog.formdlg input` this also caught radios and checkboxes, and beat
 * `.dlgradio input { width: 16px }` on specificity — one class plus two
 * elements against one class plus one. A 100%-wide radio renders its dot
 * centred and pushes its label off the right edge, which is exactly what it
 * looked like: two floating circles and a horizontal scrollbar. */
dialog.formdlg input:not([type="checkbox"]):not([type="radio"]) {
  width: 100%; margin-top: 3px;
}
dialog.formdlg .dlgrow { margin-top: 18px; justify-content: flex-end; }

/* A heading and its one action, side by side — replaces the always-visible
   "add a record" form that used to sit under each list competing with it. */
.toolbar { display: flex; align-items: flex-start; justify-content: space-between;
           gap: 16px; margin-bottom: 12px; }
.toolbar p { margin: 0; max-width: 62ch; }
.toolbar .btn { flex: none; }

/* A drawn prize is settled: the result has been published, so the row reads
   as a record rather than something still being edited. */
table.adm tr.drawn td { background: #F7FAF8; }
table.adm .nowrap { white-space: nowrap; }

@media (max-width: 760px) {
  .toolbar { flex-direction: column; gap: 10px; }
  .toolbar .btn { width: 100%; }
  dialog.formdlg { max-width: none; }
  dialog.formdlg .dlgrow { flex-direction: column-reverse; }
  dialog.formdlg .dlgrow .btn { width: 100%; }
}

/* A voided ticket is struck through rather than hidden — it still exists, it
   just cannot win, and the committee needs to see that it was deliberate. */
.ticketlist li.voided { opacity: .6; }
.ticketlist li.voided .tnum { text-decoration: line-through; }
.ticketlist.hist { flex-direction: column; gap: 4px; }
.ticketlist.hist li { width: 100%; }
/* An inline verb inside a list item, not a button-shaped thing: void/restore
   sit next to a ticket number and should read as an action on it. */
.linkish {
  font: inherit; font-size: 12px; background: none; border: none; padding: 0 0 0 6px;
  color: var(--scarlet); cursor: pointer; text-decoration: underline;
}
.linkish:hover { color: var(--scarlet-d); }

/* --- Admin filters -------------------------------------------------------- */
.filterbar { display: flex; align-items: center; gap: 10px; margin: 0 0 10px; }
.filterinput {
  flex: 1 1 260px; max-width: 420px; font: inherit; font-size: 14px;
  padding: 7px 11px; border: 1px solid var(--hair); border-radius: 6px;
  background: var(--paper); color: var(--ink);
}
.filterinput:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.filterbar .muted { white-space: nowrap; }

@media (max-width: 760px) {
  .filterbar { flex-wrap: wrap; }
  .filterinput { max-width: none; }
}

/* --- Picker dialog --------------------------------------------------------
   Deliberately not a bordered list inside a bordered box. The rows carry
   their own separation, so the dialog reads as a short answer to a question
   rather than a form control stuffed with a roster. */
.pickinput {
  width: 100%; margin-top: 12px; font: inherit; font-size: 15px;
  padding: 10px 12px; border: 1px solid var(--hair); border-radius: 6px;
  background: var(--paper); color: var(--ink);
}
.pickinput:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.pickinput::-webkit-search-cancel-button { cursor: pointer; }

.pickerlist { margin-top: 8px; max-height: 44vh; overflow-y: auto; }

.pickopt {
  display: block; width: 100%; text-align: left; cursor: pointer;
  font: inherit; background: none; border: none;
  border-left: 2px solid transparent; border-radius: 5px;
  padding: 8px 10px;
}
.pickopt + .pickopt { margin-top: 1px; }
.pickopt:hover, .pickopt:focus-visible {
  background: var(--chalk); border-left-color: var(--scarlet); outline: none;
}
.pickname { display: block; font-size: 14.5px; font-weight: 600; color: var(--ink); }
.picksub  { display: block; font-size: 12px; color: var(--steel); margin-top: 1px; }

/* The choice that is always valid, marked as a category rather than a person. */
.pickopt.pickalways .pickname { font-family: var(--cond); font-size: 16px; }
.pickopt.pickalways { background: var(--chalk); }
.pickopt.pickalways:hover { background: #E9EDEB; }

.pickhint { margin: 8px 2px 0; min-height: 1.2em; }
.pickempty { margin: 10px 2px; font-size: 14px; color: var(--steel); }

/* --- Dialog fields -------------------------------------------------------- */
.dlgradio { border: none; padding: 0; margin: 14px 0 0; }
.dlgradio legend { padding: 0; }
.dlgradio label {
  display: flex; gap: 9px; align-items: flex-start; padding: 7px 0; cursor: pointer;
}
.dlgradio input { width: 16px; height: 16px; margin-top: 2px; flex: none;
                  accent-color: var(--scarlet); }
.dlgradio label > span { min-width: 0; }
.dlgradio b { display: block; font-size: 14.5px; font-weight: 600; }
.dlgradio i { display: block; font-style: normal; font-size: 12.5px; color: var(--steel); }

.dlgcheck { margin-top: 14px; }
/* A conditional field is hidden until its option is chosen, rather than
   greyed out. Greyed out reads as "you may not have this", which is the wrong
   message for something you can have by clicking the radio above it. */
dialog.formdlg label[hidden] { display: none; }

/* The custom-amount tile. Same shape and weight as the presets — it is one of
   the choices, not a secondary control — but its placeholder state reads as a
   prompt rather than a price. */
.qty button.qtyother .p { font-size: 12px; }
.qty button.qtyother[aria-pressed="false"] .n { color: var(--steel-l); letter-spacing: .06em; }

/* --- The front door -------------------------------------------------------
   Two equal halves rather than .two's 1.25fr/1fr: neither arrival is the
   secondary one. A student who cannot find registration and a buyer who
   cannot find the buy button are the same failure. */
.entry { grid-template-columns: 1fr 1fr; align-items: start; }
.entry .card { height: 100%; }
/* The buyer side leads with the scarlet edge, since it is the side with a
   single obvious action; the student side leads with a form, which carries
   its own weight. */
.buyside h2 { margin-bottom: 6px; }

@media (max-width: 760px) {
  /* Students first on a phone. A buyer following a share link lands on /s/
     and never sees this page; a student typing the bare domain does. */
  .entry { grid-template-columns: 1fr; }
}

