/*
 * Brother 1816 -- the Parkview brand face. Subset to Latin + Latin Extended
 * (Spanish and French need it; Arabic has no Brother glyphs and falls back to
 * the system face on its own, which is why --sans-system exists below).
 *
 * Regenerate from the church's licensed OTFs with:
 *   python3 -m fontTools.subset Brother-1816-Regular.otf --flavor=woff2 \
 *     --unicodes=U+0000-00FF,U+0100-024F,U+2000-206F --layout-features=kern,liga,calt \
 *     --output-file=public/fonts/brother1816-400.woff2
 *
 * Weights are 400 / 500 / 700 / 800 only, plus 400 italic for <em>. Brother
 * has no SemiBold, so never write 600: it renders as Bold once the face loads
 * but as the system semibold before it, and the page visibly changes weight.
 *
 * font-display: swap -- on church wifi a phone shows the fallback immediately
 * and swaps when the face lands, rather than holding a blank screen.
 */
@font-face {
  font-family: "Brother 1816";
  src: url("/fonts/brother1816-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Brother 1816";
  src: url("/fonts/brother1816-400-italic.woff2") format("woff2");
  font-weight: 400; font-style: italic; font-display: swap;
}
@font-face {
  font-family: "Brother 1816";
  src: url("/fonts/brother1816-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Brother 1816";
  src: url("/fonts/brother1816-700.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Brother 1816";
  src: url("/fonts/brother1816-800.woff2") format("woff2");
  font-weight: 800; font-style: normal; font-display: swap;
}

/*
 * Two themes, one stylesheet. Both dark, for the same reason.
 *
 * Listener pages are DARK because the room is dark when this app is being used:
 * a bright phone screen in an auditorium is glare for the person holding it and
 * a distraction for everyone sitting behind them.
 *
 * They are dark in the BRAND colours rather than in black -- Parkview slate
 * #1b2e3b, with gold #f3b629 and teal #105d73 mixed in. Slate costs a little
 * more light than black does on an OLED phone, and buys a page that belongs to
 * the church rather than to no one. The two accents have separate jobs and
 * should keep them:
 *
 *   gold #f3b629  the one primary action, the selection ring, accent text.
 *                 It is the brightest thing on the page, so it has to be rare.
 *   teal #105d73  structure -- surfaces, hairlines, the fill behind a chosen
 *                 option. Never text: at this value it is dark-on-dark.
 *
 * This is a deliberate break from the light church website the pages embed
 * in. The seam shows where the frame starts; type and corners still match.
 *
 * The booth console runs on the same palette. It keeps the `booth` class, but
 * that now marks a LAYOUT difference and nothing else: its headings stay
 * left-aligned where the eye starts, because it is a working tool rather than
 * a page anyone reads. Colour is no longer part of the distinction.
 *
 * Every rule below reads semantic tokens, so the palette is the token block
 * above and nothing else. Add a colour by adding a token, not a hex to a rule.
 */
:root {
  --brand-950: #172534;
  --brand-900: #233a4f;
  --brand-800: #26435d;
  --brand-700: #2a4f6f;
  --brand-500: #4279a4;
  --brand-300: #9bbcd6;
  --brand-100: #e7eef5;
  --brand-50: #f4f7fb;
  --accent-500: #d99a3f;
  --accent-400: #e6b566;
  --accent-600: #bf7e26;

  --sans-system: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --sans: "Brother 1816", var(--sans-system);

  /* --- the listener, in a dark room, in Parkview colours --- */
  --slate: #1b2e3b;    /* ground */
  --gold: #f3b629;     /* action */
  --teal: #105d73;     /* structure */

  --page: var(--slate);
  --surface: #22404f;      /* the ground, lifted toward the teal */
  --surface-2: #2a5062;    /* pressed or hovered */
  --line: #2b4f60;
  --ink: #eaf1f5;      /* off-white: pure white glares on a dark ground */
  --ink-2: #9db4c1;
  --ink-3: #6f8b9a;
  --brand: var(--gold);
  --brand-ink: var(--slate);
  --brand-soft: var(--teal);   /* the fill behind a chosen option */
  --accent-ink: var(--gold);
  --bar: rgb(27 46 59 / 0.94);
  --ok: #4ade80;
  --warn: #fbbf24;
  --bad: #f87171;
  --ok-glow: rgb(74 222 128 / 0.15);
  --warn-bg: #2a2113;
  --warn-line: var(--accent-600);
  --warn-ink: #f0e2c8;
  --bad-bg: #2a1414;
  --qr-bg: #ffffff;    /* a QR code needs its quiet zone white to scan */
  /* One per campus on the /overview recap chart, in campuses.json order. Not
     brand colours on purpose: gold means "the action" and teal is too dark to
     read as a line here. Checked as a set against --surface for colour-blind
     separation; the orange is under 3:1, which the direct labels and the table
     beneath the chart cover. */
  --series-1: #3987e5;
  --series-2: #d95926;
  --series-3: #199e70;
  --radius: 16px;
  --scrim: rgb(13 23 30 / 0.72);   /* behind a popup: the page, dimmed, still there */
}

* { box-sizing: border-box; }

/* Every page here toggles visibility with the hidden property, and a class that
   sets display (.langs is a grid, .notice is a flex row) quietly outranks the
   UA rule for [hidden]. Without this the language picker and the empty music
   notice both stay on screen after the code has hidden them. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: var(--sans);
  background: var(--page);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  min-height: 100dvh;
}

.wrap {
  max-width: 720px;
  margin: 0 auto;
  padding-inline: 20px;
  padding-block: 24px;
}

/* The app has no serif anywhere. Heavy and tightly tracked is what makes a
   heading read as part of it. */
h1 {
  font-size: 30px;
  font-weight: 800;
  letter-spacing: -0.025em;
  margin: 0 0 6px;
}
/* Listener pages only: the title is the page's masthead and sits centred. The
   admin console is a working tool and its heading stays where the eye starts --
   but larger, because on a dense console the title is what tells a volunteer at
   a glance which of the two sides of this app they are looking at. */
body:not(.booth) h1 { text-align: center; }
body.booth h1 {
  font-size: 40px;
  letter-spacing: -0.03em;
  margin-bottom: 10px;
}
@media (max-width: 480px) {
  body.booth h1 { font-size: 32px; }
}

/*
 * The screens a listener reads BEFORE the service are centred -- masthead,
 * headings, notes, the lot. Two deliberate exceptions:
 *
 *   Buttons keep their own alignment. A button's text is its label, not page
 *   copy: the option cards stay start-aligned so a column of language or campus
 *   names reads down a single edge, and the solid buttons stay centred.
 *
 *   The captions view is NOT centred (see #live, untouched below). It is
 *   long-form reading for forty-five minutes, and centring wrapped paragraphs
 *   makes the eye hunt for the start of every line.
 */
body:not(.booth) #setup,
body:not(.booth) #stepLanguage,
body:not(.booth) #stepCampus {
  text-align: center;
}

body:not(.booth) .brand img { margin-inline: auto; }

/* Centred, these two stop being a row with an icon on the left and become a
   centred paragraph -- so the icon has to flow with the text rather than float
   off at the margin, orphaned from the words it belongs to. */
body:not(.booth) .wifi-note,
body:not(.booth) .headphones {
  display: block;
}
body:not(.booth) .wifi-note::before,
body:not(.booth) .headphones::before {
  display: inline-block;
  vertical-align: -4px;
  margin-top: 0;
  margin-inline-end: 8px;
}

.sub { color: var(--ink-2); margin: 0 0 28px; font-size: 15px; }

/* Form controls do not inherit the page face by default in any browser. */
button, input, select, textarea { font-family: inherit; }

/* --- buttons --- */
button {
  font-family: inherit;
  font-size: 16px;
  border-radius: var(--radius);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
}
button:disabled { opacity: 0.45; cursor: default; }
/* A faded gold slab reads as muddy brown rather than as "not yet". Give the
   disabled primary the surface instead, so it looks switched off on purpose. */
.primary:disabled {
  opacity: 1;
  background: var(--surface);
  color: var(--ink-3);
}

.primary {
  width: 100%;
  padding: 19px;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.01em;
  background: var(--brand);
  color: var(--brand-ink);
}
.primary:not(:disabled):hover { background: #f6c754; }

.ghost {
  padding: 11px 16px;
  background: transparent;
  border-color: var(--line);
  color: var(--ink);
  font-weight: 500;
}
.ghost:not(:disabled):hover { border-color: var(--ink-3); }

/* --- option cards: languages, campuses --- */
/* One shape for everything a listener taps: a soft filled tile, no border, the
   corners the app's tiles use. Selection is a ring drawn inside, so choosing
   one never nudges the layout. */
.langs, .campuses { display: grid; gap: 10px; margin-bottom: 24px; }

.lang, .campus {
  background: var(--surface);
  border: 0;
  color: var(--ink);
  text-align: start;
  border-radius: var(--radius);
}
.lang:hover, .campus:hover { background: var(--surface-2); }

/* A name row, then a line saying what choosing it gets you. */
.lang {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 16px 20px;
}
.lang-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}
.lang-sub {
  font-size: 13px;
  font-weight: 400;
  line-height: 1.4;
  color: var(--ink-2);
}
.lang[aria-pressed="true"] .lang-sub { color: var(--ink); }
.lang[aria-pressed="true"] {
  background: var(--brand-soft);
  box-shadow: inset 0 0 0 2px var(--brand);
}
.lang .native { font-size: 19px; font-weight: 700; letter-spacing: -0.01em; }
.lang .latin { font-size: 14px; color: var(--ink-2); }

/* --- status pill --- */
.status {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--ink-2);
  margin-bottom: 16px;
}
.dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--ink-3); flex: none;
}
.dot.live { background: var(--ok); box-shadow: 0 0 0 4px var(--ok-glow); }
.dot.wait { background: var(--warn); }
.dot.bad  { background: var(--bad); }

/* --- text size control --- */
/* One button in the fixed bottom bar opens four A's drawn at the sizes they
   set -- the only control on this page that needs no translation, which is
   why it is not a word. It used to sit at the top of the live view, where it
   scrolled away with the captions. */
/* The status bar: pinned to the top of the live view so "Live" stays in sight
   while the captions scroll. Flush with the top edge (the negative margin eats
   .wrap's padding) and full-bleed past the 720px column via the shadow. Kept
   slim: it is a signal, not a header. */
.live-head {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
  margin: -24px -20px 16px;
  padding: calc(11px + env(safe-area-inset-top)) 20px 11px;
  /* Solid, not the bottom bar's 94% frost: captions scroll right under this
     one, and at 94% the line beneath stays readable through it. */
  /* Full-bleed: painted as a border-image pushed 100vmax out to each side,
     the ground with a 1px rule at its foot. An outset never widens the page,
     so this can't cause sideways scrolling the way a wide child would. */
  border-image: linear-gradient(var(--page) calc(100% - 1px), var(--line) 0) fill 0 / / 0 100vmax;
}
.live-head .status { margin-bottom: 0; font-size: 14px; }
.live-head.is-live .status { color: var(--ink); font-weight: 700; }
.live-campus {
  font-size: 14px; font-weight: 500; color: var(--ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}

.sizer { position: relative; flex: none; }
.sizer-btn {
  display: inline-flex; align-items: baseline; justify-content: center;
  gap: 1px; min-width: 48px; font-weight: 700; line-height: 1;
}
.sizer-btn .sm { font-size: 13px; }
.sizer-btn .lg { font-size: 19px; }
.sizer-btn[aria-expanded="true"] {
  border-color: var(--brand);
  box-shadow: inset 0 0 0 1px var(--brand);
}
.textsize {
  position: absolute; bottom: calc(100% + 12px); left: 0;
  display: flex; align-items: center; gap: 6px;
  padding: 8px; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--line);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
}
.textsize[hidden] { display: none; }
.tsize {
  font-family: inherit; font-weight: 700; line-height: 1;
  min-width: 38px; height: 38px; padding: 0 8px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 10px;
  background: var(--surface); color: var(--ink-2);
  border: 1px solid var(--line);
  cursor: pointer;
}
.tsize.s0 { font-size: 13px; }
.tsize.s1 { font-size: 16px; }
.tsize.s2 { font-size: 19px; }
.tsize.s3 { font-size: 23px; }
.tsize[aria-pressed="true"] {
  background: var(--brand-soft);
  color: var(--ink);
  box-shadow: inset 0 0 0 2px var(--brand);
}

/* --- captions --- */
/* Caption size is a base times a scale the listener sets, rather than one
   number. The base is what the room gets by default -- and a phone gets less
   than a laptop because the line is narrower, not because the eye is younger.
   The scale is the Aa control in the bottom bar, remembered per phone.
   Nothing else on the page scales with it: the captions are what is being
   read, and the fixed bottom bar has to stay where the thumb expects it. */
:root {
  --caption-base: 26px;
  --caption-scale: 1;
}
.captions {
  font-size: calc(var(--caption-base) * var(--caption-scale));
  line-height: 1.5;
  letter-spacing: -0.01em;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding-bottom: 40vh; /* keep the newest line clear of the bottom bar */
}
.captions p { margin: 0; }
/* While listening, the captions sit at the bottom of their frame, so every
   downward swipe hit the end and carried on into the website page around it,
   sliding the captions off the phone until a refresh. Contained to the frame
   only once listening starts: on the splash a swipe should still reach the
   website. Leave reloads the page, which drops the class. */
html.listening { overscroll-behavior-y: contain; }
.captions .partial { color: var(--ink-2); }
.captions .settled { color: var(--ink); }
.captions .old { color: var(--ink-3); }

.empty {
  color: var(--ink-2);
  font-size: 16px;
  text-align: center;
  padding: 48px 0;
}

/* --- bottom bar --- */
.bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  background: var(--bar);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--line);
  padding: 12px 20px calc(12px + env(safe-area-inset-bottom));
}
.bar-inner {
  max-width: 720px; margin: 0 auto;
  display: flex; align-items: center; gap: 12px;
}
.bar .spacer { flex: 1; }

input[type="range"] { width: 130px; accent-color: var(--brand); }
/* Mute, Aa, volume and Leave have to share a 320px phone; the slider gives. */
.bar input[type="range"] { flex: 0 1 130px; min-width: 60px; }

/* --- booth console --- */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 20px;
  margin-bottom: 16px;
}
.panel h2 {
  font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-2); margin: 0 0 14px; font-weight: 700;
}
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
label { font-size: 14px; color: var(--ink-2); display: block; margin-bottom: 6px; }
/* Native controls -- the select popup, the date picker -- should come up dark
   like everything else rather than flashing a white panel in a dark booth. */
:root { color-scheme: dark; }

select, input[type="password"], input[type="text"], input[type="datetime-local"] {
  font-family: inherit; font-size: 15px;
  padding: 10px 12px; border-radius: 10px;
  background: var(--page); color: var(--ink);
  border: 1px solid var(--line); width: 100%;
}

/* The admin board's small per-campus level bar. */
.meter {
  height: 10px; border-radius: 5px; overflow: hidden;
  background: var(--page); border: 1px solid var(--line);
}
.meter span {
  display: block; height: 100%; width: 0%;
  background: linear-gradient(90deg, var(--ok), var(--warn) 78%, var(--bad));
  transition: width 60ms linear;
}

/* Booth peak meter, dBFS -60..0. The track carries the zones at fixed
   positions (-18 dB = 70%, -6 dB = 90%) and a cover in the page colour hides
   whatever is above the current level, as a board's LED ladder does. */
.dbmeter { margin-top: 4px; }
.dbm-track {
  position: relative; height: 14px; border-radius: 4px; overflow: hidden;
  border: 1px solid var(--line);
  background: linear-gradient(90deg,
    var(--ok) 0 70%, var(--warn) 70% 90%, var(--bad) 90% 100%);
}
.dbm-cover {
  position: absolute; top: 0; right: 0; bottom: 0; width: 100%;
  background: var(--page);
  transition: width 60ms linear;
}
.dbm-hold {
  position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px;
  background: var(--ink);
}
.dbm-grid { display: flex; gap: 8px; }
.dbm-body { flex: 1; min-width: 0; }
.dbm-labels { flex: none; display: flex; flex-direction: column; width: 52px; }
.dbm-grrow { margin-bottom: 10px; }
.dbm-grrow .dbm-labels { justify-content: flex-end; }
.dbm-scale-top { margin: 0 0 3px; }
.dbm-grtrack {
  position: relative; height: 10px; border-radius: 3px; overflow: hidden;
  background: var(--page); border: 1px solid var(--line);
}
.dbm-grfill {
  position: absolute; top: 0; bottom: 0; right: 0; width: 0%;
  background: var(--warn); transition: width 60ms linear;
}
.dbm-grhold {
  position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px;
  background: var(--ink);
}
.dbm-grrow .dbm-lab { height: 12px; line-height: 12px; }
.dbm-lab {
  font-size: 10px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-2); cursor: help;
  height: 16px; line-height: 16px;
}
.dbm-lab-gate { height: 10px; line-height: 10px; margin-top: 4px; }
.dbm-lab[hidden] { display: none; }
.dbm-bars { position: relative; }
.dbm-gtrack {
  position: relative; height: 8px; margin-top: 4px; border-radius: 3px;
  overflow: hidden; background: var(--page); border: 1px solid var(--line);
}
.dbm-gfill {
  position: absolute; top: 0; bottom: 0; left: 0; width: 0%;
  background: var(--ink-3); transition: width 60ms linear;
}
.dbm-gfill.open { background: var(--ok); }
/* The threshold spans both bars. The visible line is 2px; the hit area is
   wide enough to grab on a touchscreen. */
.dbm-thresh {
  position: absolute; top: -4px; bottom: -4px; width: 20px; margin-left: -10px;
  cursor: ew-resize; touch-action: none; z-index: 2;
}
.dbm-thresh::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 9px; width: 2px;
  background: var(--accent-ink);
}
.dbm-thresh::after {
  content: ""; position: absolute; bottom: -4px; left: 4px;
  border: 6px solid transparent; border-bottom-color: var(--accent-ink);
}
.dbm-thresh:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.dbm-ceiling {
  position: absolute; top: -1px; bottom: -1px; width: 2px; margin-left: -1px;
  background: var(--ink); opacity: 0.55;
}
.dbm-limit.on { color: var(--warn); }
.dbm-scale {
  position: relative; height: 16px; margin-top: 4px;
  font-size: 11px; color: var(--ink-3); font-variant-numeric: tabular-nums;
}
.dbm-scale span { position: absolute; top: 0; transform: translateX(-50%); }
.dbm-scale span:first-child { transform: none; }
.dbm-scale span:last-child { transform: translateX(-100%); }
.dbm-read {
  display: flex; flex-wrap: wrap; gap: 4px 18px; margin-top: 6px;
  font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums;
}
.dbm-read > span { white-space: nowrap; }
.dbm-read b { color: var(--ink); display: inline-block; min-width: 76px; }

.counts { display: flex; gap: 20px; flex-wrap: wrap; }
.count .n { font-size: 28px; font-weight: 700; }
.count .k { font-size: 12px; color: var(--ink-2); text-transform: uppercase; letter-spacing: 0.06em; }

.log {
  font-size: 13px; line-height: 1.55; max-height: 260px; overflow-y: auto;
  display: flex; flex-direction: column-reverse; gap: 8px;
}
.log div { color: var(--ink); }
.log .src { color: var(--ink-3); font-size: 12px; }
.log .err { color: var(--bad); }

.qr { background: var(--qr-bg); padding: 12px; border-radius: 12px; width: 180px; }
.qr img { display: block; width: 100%; }

@media (max-width: 480px) {
  :root { --caption-base: 22px; }
  h1 { font-size: 27px; }
}

/* --- wake-lock warning --- */
.warn {
  background: var(--warn-bg);
  border: 1px solid var(--warn-line);
  border-inline-start-width: 4px;
  border-radius: 12px;
  padding: 14px 16px;
  margin-bottom: 18px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--warn-ink);
}
.warn strong { color: var(--accent-ink); }

.err-banner {
  background: var(--bad-bg);
  border-color: var(--bad);
}
.err-banner strong { color: var(--bad); display: block; margin-bottom: 4px; }

/* --- picker section labels --- */
.pick-label {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-2);
  font-weight: 700;
  margin: 0 0 10px;
}
#voiceSection { margin-bottom: 24px; }

/* --- quiet-screen notice (music, or a closed mic) --- */
.notice {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin-bottom: 18px;
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
  display: flex;
  gap: 12px;
  align-items: flex-start;
}
.notice::before {
  content: "♪";
  color: var(--accent-ink);
  font-size: 20px;
  line-height: 1.2;
  flex: none;
}
/* A music note beside "translation will resume once speaking resumes" is the
   exact confusion this notice exists to end -- on the mic-only feed there is no
   music on the wire, just a closed mic. An ellipsis says "hold on" and renders
   in every font we fall back to. */
.notice[data-mode="silence"]::before { content: "…"; }

/* --- segmented choice (the booth's speaker voice) --- */
/* Two or three options, all visible, one tap to change. A dropdown hid the
   choice behind a tap and made a six-item list out of three languages. */
.choices { display: flex; gap: 8px; flex-wrap: wrap; }
.choice {
  flex: 1 1 0;
  min-width: 130px;
  padding: 13px 16px;
  font-weight: 700;
  /* These sit inside a .panel, which is already --surface, so an unselected
     option on --surface is invisible. Page colour plus a hairline reads in
     both themes. */
  background: var(--page);
  color: var(--ink);
  border: 1px solid var(--line);
}
.choice:hover { background: var(--surface-2); }
.choice[aria-pressed="true"] {
  background: var(--brand-soft);
  box-shadow: inset 0 0 0 2px var(--brand);
  color: var(--ink);
}

/* --- campus picker --- */
.campus {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 4px 14px;
  padding: 16px 18px;
  align-items: baseline;
}
.campus[aria-pressed="true"] {
  background: var(--brand-soft);
  box-shadow: inset 0 0 0 2px var(--brand);
}
.campus .name { font-size: 19px; font-weight: 700; letter-spacing: -0.01em; }

/* Staff picker only. Starting the wrong campus is the one mistake here that
   costs a service, and it is made by a volunteer scanning quickly before 9am --
   so the campus name is the largest thing on the row, and the chosen one is
   filled and ringed rather than merely tinted. */
body.booth .campus { padding: 22px 22px; gap: 6px 14px; }
body.booth .campus .name { font-size: 28px; font-weight: 800; letter-spacing: -0.02em; }
body.booth .campus[aria-pressed="true"] {
  box-shadow: inset 0 0 0 3px var(--brand);
}
body.booth .campus[aria-pressed="true"] .name { color: #fff; }
.campus .state {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13px; color: var(--ink-2); justify-self: end;
}
.campus .meta { font-size: 13px; line-height: 1.45; color: var(--ink-2); min-height: 1.45em; }
.campus .path { font-size: 13px; color: var(--ink-3); justify-self: end; }

/* --- campus badge on the booth console --- */
/* The campus badge on the admin console.
   This is the answer to "which campus am I about to put on the air?", and it
   used to be a line of small gold text under a big heading -- the wrong way
   round. It is now a filled, ringed pill: at a glance from across a booth it
   is the campus name that reads, not the words "Admin Console". */
.campus-bar {
  display: flex; align-items: center; gap: 14px;
  flex-wrap: wrap; margin: 0 0 14px;
}
.campus-bar .who {
  font-size: 24px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: #fff;
  background: var(--brand-soft);
  box-shadow: inset 0 0 0 2px var(--brand);
  border-radius: 14px;
  padding: 10px 20px;
}
.campus-bar a { color: var(--ink-2); font-size: 14px; }

@media (max-width: 480px) {
  .campus-bar .who { font-size: 20px; padding: 9px 16px; }
  body.booth .campus .name { font-size: 24px; }
}

/* --- campus overview (/admin) --- */
/* Three campuses side by side for staff who are not in any booth. It is read
   only, so nothing here is an action and nothing takes the gold: a live campus
   is ringed in the status green the dots already use, and the eye finds the
   running services without a second vocabulary of colour.

   Wider than the rest of the app because it is the only page that is read on a
   desk rather than in a hand. */
.wrap.wide { max-width: 1000px; }

/* Two tabs on that page: the read-only board, and the Names & Phrases editor
   that used to live at /glossary. Understated on purpose -- they sit under the
   h1 and must not read as the primary action, which on every staff page is
   gold and there is only ever one of. */
.tabs {
  display: flex; gap: 4px; flex-wrap: wrap;
  margin: 0 0 22px; border-bottom: 1px solid var(--line);
}
.tab {
  width: auto; margin-bottom: -1px; padding: 10px 16px;
  font: inherit; font-size: 15px; font-weight: 700;
  background: none; color: var(--ink-2);
  border: 0; border-bottom: 2px solid transparent; border-radius: 0;
  cursor: pointer;
}
.tab:hover { color: var(--ink); }
.tab.on { color: var(--ink); border-bottom-color: var(--brand); }
/* Sign-out, pushed to the far end so it never reads as a fifth section. */
.tab-lock { margin-left: auto; font-weight: 500; color: var(--ink-3); }
/* Signed in with Microsoft: the server checks the sign-in, so no key boxes. */
body.sso .keygate { display: none !important; }

.ov-top {
  display: flex; align-items: baseline; gap: 8px 20px;
  flex-wrap: wrap; margin: 0 0 16px;
}
.ov-tally { font-size: 17px; font-weight: 700; letter-spacing: -0.01em; color: var(--ink-2); }
.ov-tally.on { color: var(--ink); }
.ov-tally.warn { color: var(--warn); }

/* The booth's clip warning, beside the gain readout. */
.clip { color: var(--bad); font-weight: 700; margin-left: 8px; }
.ov-fresh { font-size: 13px; color: var(--ink-3); }
.ov-fresh.bad { color: var(--bad); }
.ov-link, .ov-foot a { color: var(--accent-ink); font-size: 13px; font-weight: 700; }
/* The only links in running text anywhere in the staff pages. */
.sub a { color: var(--accent-ink); font-weight: 700; }

.ov-grid {
  display: grid; gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
}
.ov-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 18px 20px;
  display: flex; flex-direction: column; gap: 16px;
}
.ov-card.is-live { box-shadow: inset 0 0 0 2px var(--ok); }
/* Live, with nothing arriving. Findable from across a room. */
.ov-card.is-alarm { box-shadow: inset 0 0 0 2px var(--bad); }

/* The name never breaks across lines -- a campus called "Orland Park" reading
   as two lines is how this looked first, and it made the cards hard to scan.
   The status moves under it instead when the card is too narrow for both. */
.ov-head { display: flex; align-items: baseline; gap: 4px 12px; flex-wrap: wrap; }
.ov-name { font-size: 24px; font-weight: 800; letter-spacing: -0.02em; white-space: nowrap; }
.ov-state {
  display: inline-flex; align-items: center; gap: 7px; margin-inline-start: auto;
  font-size: 13px; color: var(--ink-2);
}

.ov-block { display: block; }
.ov-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-3); margin: 0 0 8px; font-weight: 700;
}
.ov-audio {
  display: flex; align-items: center; gap: 7px;
  margin: 10px 0 0; font-size: 13px; color: var(--ink-2);
}
.ov-audio.warn { color: var(--warn); }
.ov-audio.bad { color: var(--bad); }

.ov-total { display: flex; align-items: baseline; gap: 10px; }
.ov-total .n { font-size: 40px; font-weight: 800; letter-spacing: -0.03em; line-height: 1; }
.ov-total .k { font-size: 13px; color: var(--ink-2); }
.ov-langs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.ov-langs:empty { display: none; }
.ov-lang {
  font-size: 13px; color: var(--ink-2);
  background: var(--page); border: 1px solid var(--line);
  border-radius: 999px; padding: 5px 12px;
}
.ov-lang b { color: var(--ink); font-weight: 700; }

.ov-foot {
  border-top: 1px solid var(--line); padding-top: 12px; margin-top: auto;
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; font-size: 13px; color: var(--ink-3);
}

/* Numbers that have stopped arriving must not keep reading as now. */
.stale .ov-grid { opacity: 0.45; }

/* --- /overview recap: listeners per service, week on week --- */
.recap { margin-top: 36px; }
.recap-head { display: flex; align-items: baseline; gap: 8px 16px; flex-wrap: wrap; }
.recap-head h2 { margin: 0; font-size: 22px; font-weight: 800; letter-spacing: -0.02em; }
.recap-head .sub { margin: 0; flex: 1 1 260px; }
.recap-export {
  background: var(--surface); color: var(--ink); border: 1px solid var(--line);
  border-radius: 10px; padding: 9px 14px; font: inherit; font-size: 14px; font-weight: 700;
  text-decoration: none; white-space: nowrap;
}
.recap-export:hover, .recap-export[aria-expanded="true"] { background: var(--surface-2); cursor: pointer; }
.export { position: relative; }
.export-menu {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 3;
  min-width: 140px; padding: 6px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
  display: flex; flex-direction: column;
}
.export-menu a, .export-menu button {
  display: block; text-align: left; padding: 9px 12px; border-radius: 7px;
  background: none; border: 0; color: var(--ink); font: inherit; font-size: 14px; font-weight: 500;
  text-decoration: none; cursor: pointer;
}
.export-menu a:hover, .export-menu button:hover,
.export-menu a:focus-visible, .export-menu button:focus-visible { background: var(--surface-2); outline: none; }

/* --- /overview printed (Export → PDF): the recap alone, on white paper ---
   The screen palette is dark slate; on paper that is a page of ink and the
   chart's dark-surface steps. So the tokens flip to paper values here, and the
   series take the light-surface steps of the same three hues. */
.print-only { display: none; }
@media print {
  :root {
    --page: #ffffff;
    --surface: #ffffff;
    --line: #d7e0e6;
    --ink: #1b2e3b;
    --ink-2: #4d6272;
    --ink-3: #6f8b9a;
    --series-1: #2a78d6;
    --series-2: #eb6834;
    --series-3: #1baf7a;
  }
  @page { margin: 0.6in; }
  body { min-height: 0; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  body.overview .brand, body.overview h1, body.overview > main > .sub,
  body.overview #board, body.overview #errBanner, .export, .recap-tip { display: none !important; }
  body.overview .wrap { max-width: none; padding: 0; }
  .recap { margin-top: 0; }
  .recap-head h2 { font-size: 20pt; }
  .print-only { display: block; }
  .print-meta { color: var(--ink-2); font-size: 10pt; margin: 4px 0 0; }
  .recap-card { border-color: var(--line); break-inside: avoid; }
  .recap-table-wrap { max-height: none; overflow: visible; }
  .recap-table th { position: static; }
  .recap-table tr { break-inside: avoid; }
}
.recap-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
  padding: 16px 18px; margin-top: 14px;
}
.recap-legend { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 13px; color: var(--ink-2); margin-bottom: 8px; }
.recap-legend span { display: inline-flex; align-items: center; gap: 7px; }
.recap-legend i { width: 14px; height: 2px; border-radius: 1px; }
.recap-chart { position: relative; }
.recap-chart svg { display: block; width: 100%; height: auto; overflow: visible; }
.recap-chart .grid { stroke: var(--line); stroke-width: 1; }
.recap-chart .axis { fill: var(--ink-3); font-size: 11px; }
.recap-chart .end { fill: var(--ink-2); font-size: 12px; font-weight: 500; }
.recap-chart .line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.recap-chart .pt { stroke: var(--surface); stroke-width: 2; }
.recap-chart .cross { stroke: var(--ink-3); stroke-width: 1; }
.recap-tip {
  position: absolute; top: 0; pointer-events: none; z-index: 1;
  background: var(--page); border: 1px solid var(--line); border-radius: 10px;
  padding: 8px 10px; font-size: 13px; color: var(--ink); min-width: 150px;
  box-shadow: 0 6px 18px rgb(0 0 0 / 0.3);
}
.recap-tip b { display: block; margin-bottom: 4px; }
.recap-tip div { display: flex; align-items: center; gap: 7px; color: var(--ink-2); }
.recap-tip div span:last-child { margin-inline-start: auto; color: var(--ink); font-weight: 700; padding-inline-start: 12px; }
.recap-tip i { width: 8px; height: 8px; border-radius: 50%; }
.recap-tip .tot { border-top: 1px solid var(--line); margin-top: 4px; padding-top: 4px; }
.recap-empty { color: var(--ink-2); font-size: 14px; margin: 6px 0; }
.recap-table-wrap { max-height: 380px; overflow: auto; }
.recap-table { width: 100%; border-collapse: collapse; font-size: 14px; font-variant-numeric: tabular-nums; }
.recap-table th, .recap-table td { padding: 8px 10px; border-bottom: 1px solid var(--line); text-align: right; white-space: nowrap; }
.recap-table th:first-child, .recap-table td:first-child { text-align: left; }
.recap-table th { position: sticky; top: 0; background: var(--surface); color: var(--ink-2); font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
.recap-table td.none { color: var(--ink-3); }
.recap-table td.total { font-weight: 700; }

code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em; color: var(--ink);
}

/* Campus name on the listener page. Stays visible in embed mode -- inside the
   website the page has no other clue which campus's stream it is. */
.campus-label {
  margin: 0 0 16px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--brand);
}

/* --- embed mode: the website page already has a title, so drop ours --- */
body.embed h1,
body.embed .sub.page-intro { display: none; }
body.embed .wrap { padding-block-start: 18px; }
body.embed .live-head { margin-top: -18px; }

/* --- setup notes, directly above the start button --- */
/* Wi-fi first (the thing most likely to make this look broken), then
   headphones (the one instruction that is about everyone else in the row).
   Full ink, no box: these are instructions, not warnings, and should not have
   to compete with the service note above them.

   Icons are masked SVG rather than emoji, so both are the same weight and take
   the text colour instead of whichever palette the phone paints emoji in. */
.wifi-note,
.headphones {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  margin: 0 0 14px;
  color: var(--ink);
  font-size: 15px;
  font-weight: 500;
  line-height: 1.5;
}
.headphones { margin-bottom: 18px; }

.wifi-note::before,
.headphones::before {
  content: "";
  flex: none;
  width: 20px;
  height: 20px;
  margin-top: 1px;
  background: currentColor;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}
.wifi-note::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M2 8.6a16 16 0 0 1 20 0'/%3E%3Cpath d='M5.2 12.4a11 11 0 0 1 13.6 0'/%3E%3Cpath d='M8.6 16.2a6 6 0 0 1 6.8 0'/%3E%3Ccircle cx='12' cy='19.9' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M2 8.6a16 16 0 0 1 20 0'/%3E%3Cpath d='M5.2 12.4a11 11 0 0 1 13.6 0'/%3E%3Cpath d='M8.6 16.2a6 6 0 0 1 6.8 0'/%3E%3Ccircle cx='12' cy='19.9' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E");
}
.headphones::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 15.5v-3.5a8 8 0 0 1 16 0v3.5'/%3E%3Cpath d='M4.2 14.6h2.1a.8.8 0 0 1 .8.8v3.8a.8.8 0 0 1-.8.8H5.1a.9.9 0 0 1-.9-.9z'/%3E%3Cpath d='M19.8 14.6h-2.1a.8.8 0 0 0-.8.8v3.8a.8.8 0 0 0 .8.8h1.2a.9.9 0 0 0 .9-.9z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 15.5v-3.5a8 8 0 0 1 16 0v3.5'/%3E%3Cpath d='M4.2 14.6h2.1a.8.8 0 0 1 .8.8v3.8a.8.8 0 0 1-.8.8H5.1a.9.9 0 0 1-.9-.9z'/%3E%3Cpath d='M19.8 14.6h-2.1a.8.8 0 0 0-.8.8v3.8a.8.8 0 0 0 .8.8h1.2a.9.9 0 0 0 .9-.9z'/%3E%3C/svg%3E");
}

/* --- AI disclaimer, foot of every listener page --- */
/* Quiet by design. It has to be there and it has to be findable when a
   translation goes wrong, but it is not what anyone came for. */
.disclaimer {
  display: block;
  margin-top: 28px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  color: var(--ink-2);
  font-size: 12.5px;
  line-height: 1.55;
}
.disclaimer a {
  color: var(--accent-ink);
  font-weight: 500;
  white-space: nowrap;
}

/* --- "only at the 9:00am service" --- */
/* Gold, because it is the one thing on the page that can save someone sitting
   through a service waiting for audio that was never coming. Its own element,
   never part of the heading: embed mode hides headings, and inside the website
   this line would be the first thing to disappear. */
.service-note {
  margin: 0 0 16px;
  padding: 11px 14px;
  border-radius: 12px;
  background: var(--surface);
  border-inline-start: 4px solid var(--brand);
  color: var(--ink);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.45;
}

/* --- brand mark --- */
/* Shown on the screens a listener reads before the service starts. It is left
   out of the captions view on purpose: that screen is for one thing. */
.brand { margin: 0 0 20px; }
.brand img { display: block; width: 168px; height: auto; }
body.embed .brand { margin-bottom: 16px; }
/* Smaller on the booth: the console is dense, and the mark is there to say
   whose tool this is, not to take the top of the screen. */
body.booth .brand { margin-bottom: 14px; }
body.booth .brand img { width: 132px; }

/* --- listener splash: language, then campus --- */
/* Only reached from a bare URL (the app's shared tab). Someone with their own
   campus's link or QR code never sees it, so each step is one screen, one tap. */
.steplabel { margin: 0 0 18px; }

.backlink {
  background: none;
  border: 0;
  padding: 0;
  font-size: 15px;
  font-weight: 700;
  color: var(--brand);
}
.backlink::before {
  content: "\2039";       /* a single chevron, flipped for RTL below */
  margin-inline-end: 6px;
  display: inline-block;
}
:root[dir="rtl"] .backlink::before { content: "\203A"; }

.ask {
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--ink);
  margin: 0 0 14px;
}

.campus.choose {
  grid-template-columns: 1fr auto;
  align-items: center;
  padding: 24px 20px;
  position: relative;
}
.campus.choose .name { font-size: 22px; font-weight: 800; letter-spacing: -0.02em; }
.campus.choose .tag {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  justify-self: end;
  font-size: 14px;
  color: var(--ink-2);
}
/* Last week's campus, nudged rather than chosen -- see welcome.js. A stripe
   rather than the ring a selection gets: nothing here is selected, and tapping
   goes straight through. Absolutely positioned so it does not become a grid
   item, and logical so it lands on the reading edge in Arabic too. */
/* The campus the phone says it is at -- see welcome.js. Under the name, on its
   own row, so the Live tag keeps its place beside the name. */
.campus.choose .here {
  grid-column: 1;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-2);
}
.campus.choose[data-last="true"]::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 5px;
  background: var(--brand);
  border-start-start-radius: var(--radius);
  border-end-start-radius: var(--radius);
}

.note { margin-top: 18px; font-size: 13px; line-height: 1.5; }

/* Non-breaking spaces around the separator: a plain space after a hex escape is
   eaten as the escape terminator, and the line reads "New Lenox \00B7Francais". */
#langNameText::before { content: "\00A0\00B7\00A0"; color: var(--ink-3); }

/* Change-campus link, sitting after the campus and language names. */
.campus-label .campus-change {
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0;
  color: var(--ink-2);
  margin-inline-start: 12px;
  white-space: nowrap;
}

/* Brother 1816 is a Latin face. Rather than let Arabic fall back glyph by glyph
   across two sets of metrics, hand the whole right-to-left page to the system
   stack, which has a real Arabic face on every phone we care about. */
:root[dir="rtl"] body,
:root[dir="rtl"] .captions,
:root[dir="rtl"] h1 { font-family: var(--sans-system); }

/* --- Names & Phrases (/glossary) ---------------------------------------------
   Two panes: the terms being edited, and what is being heard right now. The
   transcript is not decoration -- a glossary rule matches what the translator
   actually wrote, so it is the only place the wrong word can be got from. */

.gl-bar {
  display: flex; align-items: flex-end; gap: 14px; flex-wrap: wrap;
  margin-bottom: 20px;
}
.gl-field { display: flex; flex-direction: column; gap: 6px; min-width: 240px; }
.gl-label { font-size: 13px; color: var(--ink-2); font-weight: 700; }
.gl-inherit { font-size: 13px; color: var(--ink-3); padding-bottom: 12px; }
.gl-saved { font-size: 13px; font-weight: 700; color: var(--ink-3); padding-bottom: 12px; }
.gl-saved.ok { color: var(--ok); }
.gl-saved.warn { color: var(--warn); }
.gl-saved.bad { color: var(--bad); }

.gl-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start; }
@media (max-width: 860px) { .gl-cols { grid-template-columns: 1fr; } }

.gl-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: 12px;
}
.gl-head h2 { margin: 0; }
.gl-live { font-size: 13px; color: var(--ink-3); }
.gl-live.on { color: var(--ok); }
.gl-empty { font-size: 14px; color: var(--ink-3); line-height: 1.5; margin: 0; }

button.ghost {
  width: auto; padding: 8px 14px; font-size: 14px; font-weight: 700;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 10px; cursor: pointer;
}
button.ghost:hover { background: var(--surface-2); }

/* --- a term --- */
.gl-term {
  background: var(--surface); border-radius: var(--radius);
  padding: 14px 16px; margin-bottom: 12px;
  border: 1px solid var(--line);
}
.gl-term.flash { border-color: var(--brand); }
.gl-term-head { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.gl-term-name { flex: 1; min-width: 120px; font-weight: 700; }
.gl-check {
  display: flex; align-items: center; gap: 6px;
  font-size: 13px; color: var(--ink-2); white-space: nowrap; cursor: pointer;
}
.gl-check input { width: auto; accent-color: var(--brand); }
.gl-x {
  width: auto; padding: 4px 8px; font-size: 14px; line-height: 1;
  background: none; color: var(--ink-3);
  border: none; border-radius: 8px; cursor: pointer;
}
.gl-x:hover { color: var(--bad); background: var(--page); }

.gl-lang-row {
  display: grid; grid-template-columns: 96px minmax(0, 1fr);
  gap: 8px 10px; align-items: center; margin-top: 8px;
}
.gl-lang-name { font-size: 13px; color: var(--ink-2); font-weight: 700; }
.gl-say { font-size: 14px; }
.gl-chips { grid-column: 2; display: flex; flex-wrap: wrap; gap: 6px; }
.gl-chips:empty { display: none; }
.gl-chip {
  width: auto; padding: 4px 9px; font-size: 12px; font-weight: 700;
  background: var(--brand-soft); color: var(--ink);
  border: none; border-radius: 999px; cursor: pointer;
}
.gl-chip:hover { background: var(--surface-2); }

/* --- what is being heard --- */
.gl-heard-card {
  background: var(--surface); border-radius: var(--radius);
  padding: 12px 14px; margin-bottom: 10px;
}
.gl-heard-meta { font-size: 12px; color: var(--ink-3); margin-bottom: 6px; }
.gl-src { font-size: 15px; color: var(--ink-2); line-height: 1.5; }
.gl-out {
  font-size: 15px; color: var(--ink); line-height: 1.5;
  margin-top: 4px; display: flex; gap: 8px; align-items: baseline;
}
.gl-out-tag {
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-3); flex: none; padding-top: 2px;
}
.gl-word { cursor: pointer; border-radius: 4px; }
.gl-word:hover { background: var(--brand); color: var(--brand-ink); }

.gl-foot { margin-top: 24px; font-size: 13px; line-height: 1.6; }

/* --- the Languages tab -------------------------------------------------------

   A switchboard, read top to bottom: what the language is called in its own
   words first, what it is called in English second, and what turning it on
   means third. The switch is the only control on the row, and the only gold
   thing on the page besides Save -- gold means "this is on and it is costing
   something", which is exactly what the row is telling you. */

.lg-summary { font-size: 15px; font-weight: 700; color: var(--ink-2); padding-bottom: 12px; }
/* The confirmation here is a sentence, not a word ("French on, and the campus
   that is live takes it at its next Go Live"), so it gets its own line rather
   than elbowing Save onto one. */
.lg-bar .gl-saved { order: 9; flex-basis: 100%; text-align: right; padding-bottom: 0; }
.lg-summary.billed { color: var(--ink); }

.lg-list { display: flex; flex-direction: column; gap: 10px; }

.lg-row {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 16px 18px;
}
/* An enabled language is the exception, not the default, because every one of
   them is on the bill whether or not anybody selects it. */
.lg-row.on { border-color: var(--brand-soft); background: var(--surface-2); }

.lg-names { display: flex; align-items: baseline; gap: 10px; min-width: 190px; }
.lg-native { font-size: 19px; font-weight: 700; letter-spacing: -0.01em; }
.lg-en { font-size: 14px; color: var(--ink-3); }
.lg-note { font-size: 13px; color: var(--ink-2); flex: 1; min-width: 200px; }
.lg-locked {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ink-3); border: 1px solid var(--line); border-radius: 999px; padding: 6px 12px;
}

/* A campus mid-service cannot take a new target list, so the page says which
   ones are holding the change rather than letting it look landed. */
.lg-live { margin: 18px 0 0; }

/* --- the switch --- */
.sw { position: relative; display: inline-flex; flex: none; cursor: pointer; }
/* Hidden to a pixel, and deaf to the pointer. A checkbox stretched over its own
   label takes the click twice -- once as itself and once as the label passing it
   on -- which lands back where it started and reads as a switch that does not
   work. The label is the target; this stays only to hold the state and the
   keyboard focus. */
.sw input {
  position: absolute; width: 1px; height: 1px; margin: 0;
  opacity: 0; pointer-events: none;
}
.sw-track {
  width: 56px; height: 32px; border-radius: 999px;
  background: var(--page); border: 1px solid var(--line);
  transition: background 120ms ease, border-color 120ms ease;
}
.sw-track::after {
  content: ""; display: block;
  width: 24px; height: 24px; margin: 3px; border-radius: 50%;
  background: var(--ink-3);
  transition: transform 120ms ease, background 120ms ease;
}
.sw input:checked + .sw-track { background: var(--brand-soft); border-color: var(--brand); }
.sw input:checked + .sw-track::after { background: var(--brand); transform: translateX(24px); }
.sw input:focus-visible + .sw-track { outline: 2px solid var(--brand); outline-offset: 2px; }

/* --- the Transcripts tab -----------------------------------------------------

   A list of services, each one a row you act on: which service, how much of it
   there is, which pairing you want, and the thing to take away. The download is
   the only gold on the row because it is the only reason anyone opens this. */

.tx-list { display: flex; flex-direction: column; gap: 10px; }

.tx-row {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 18px;
}
.tx-head { display: flex; align-items: baseline; gap: 10px; min-width: 260px; }
.tx-when { font-size: 17px; font-weight: 700; letter-spacing: -0.01em; }
.tx-facts { font-size: 13px; color: var(--ink-2); flex: 1; min-width: 150px; }

.tx-badge {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ok); border: 1px solid var(--ok); border-radius: 999px; padding: 3px 9px;
}
/* A service with no end record: the tab closed or the machine slept. What was
   said up to that point is still in the file, so this is a note, not an error. */
.tx-badge.warn { color: var(--warn); border-color: var(--warn); }

.tx-lang { width: auto; min-width: 190px; }

.tx-dl {
  padding: 10px 18px; border-radius: 10px;
  font-size: 14px; font-weight: 700; text-decoration: none;
  background: var(--brand); color: var(--brand-ink);
}
.tx-dl.ghostish {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); font-weight: 700;
}
.tx-dl.ghostish:hover { background: var(--surface-2); }
/* A phone has room for the numbers, not for "ORLAND PARK" on one line. */
@media (max-width: 520px) {
  .recap-table { font-size: 13px; }
  .recap-table th, .recap-table td { padding: 8px 6px; }
  .recap-table th { white-space: normal; vertical-align: bottom; }
  .recap-card { padding: 14px 12px; }
}

/* --- booth console: an armed schedule must be obvious from across the booth --- */
#scheduleRow { border: 1px solid transparent; border-radius: 12px; padding: 0; transition: border-color .2s, background .2s, padding .2s; }
#scheduleRow.armed { border-color: var(--ok); background: var(--ok-glow); padding: 12px 14px; }
.armed-badge {
  display: inline-block; margin-left: 8px; padding: 2px 8px; border-radius: 999px;
  font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  background: var(--ok); color: var(--slate); vertical-align: middle;
}
button.ghost.armed { border-color: var(--ok); color: var(--ok); }

/* --- "not live yet" popup on the splash --- */
/* A native <dialog>: Escape, focus trapping and the backdrop come free. Sized
   for a phone first and centred; the countdown is the one thing to read. */
.notlive {
  width: min(420px, calc(100vw - 32px));
  padding: 26px 22px 18px;
  border: 0;
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  text-align: center;
}
.notlive::backdrop { background: var(--scrim); }
/* The same card, in the page: the listening page's "not live yet". */
.waitcard {
  margin: 0 0 18px;
  padding: 22px 20px 18px;
  border-radius: var(--radius);
  background: var(--surface);
  text-align: center;
}
.notlive h2, .waitcard h2 {
  font-size: 21px;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.25;
  margin: 0 0 14px;
}
.notlive .sub, .waitcard .sub { margin: 0 0 6px; }
.notlive .countdown, .waitcard .countdown {
  margin: 4px 0 8px;
  font-size: 34px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--accent-ink);
  font-variant-numeric: tabular-nums;
}
.notlive .primary { margin-top: 16px; }
.notlive .ghost, .waitcard .ghost {
  width: 100%;
  margin-top: 16px;
  padding: 13px 16px;
  font-size: 16px;
  font-weight: 700;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.notlive .primary + .ghost, .notlive .ghost + .ghost { margin-top: 10px; }
/* The reminder is an <a> (it opens a calendar file or Google), styled as the
   ghost button beside it. */
a.ghost { display: block; text-decoration: none; text-align: center; }

/* --- "Save a shortcut on your phone", under the campus list --- */
.shortcut-link { margin: 14px 0 0; text-align: center; }
.textlink {
  background: none;
  border: 0;
  padding: 6px 0;
  font-size: 14px;
  font-weight: 700;
  color: var(--accent-ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.shortcut-campuses { display: grid; gap: 8px; margin-top: 16px; }
.shortcut-campuses .campus {
  display: block;
  padding: 16px 18px;
  background: var(--surface-2);
  color: var(--ink);
  font-size: 18px;
  font-weight: 700;
  text-align: center;
  text-decoration: none;
}
.shortcut-campuses .campus:hover { background: var(--brand-soft); }

/* The listening page, opened from that link: how to finish the job. */
.shortcut-hint { text-align: start; }
.shortcut-hint strong { display: block; margin-bottom: 4px; }
