/* Candy Wars board.

   Art direction: a pumpkin patch at dusk with string lights, and the monster boxes the kids
   already know, because the competition is called Candy Wars and the boxes are called monster
   boxes. Every class's row is its monster standing on a pile of candy, and the length of the
   pile is how much that class has collected.

   Two rules the whole file answers to. A kid at the back of the cafeteria has to read their
   class's weight, so the art never wins an argument with a number. And this is on screen eight
   hours a day for three weeks, so one thing moves, not six. */

/* Palette. Valparaiso Community Schools is a green school and Halloween defaults to orange and
   purple; the two overlap on green, and almost nobody leads a Halloween thing with green. So
   deep monster green leads, burnt orange and warm gold answer it, and the whole board sits on
   deep plum going to midnight navy, which is what the sky over Indiana actually looks like at
   six in the evening in October.

   We share a colour with the district and nothing else. No Viking, no seal, no wordmark. */
:root {
  --plum: #1b0f2c;
  --plum-deep: #120a1f;
  --navy: #101a2e;
  --green: #1f8a45;
  --green-deep: #0e4a25;
  --green-bright: #8ce39b;
  --pumpkin: #f97a18;
  --pumpkin-deep: #cf4f0c;
  --gold: #ffd166;
  --kraft: #cda36a;
  --cream: #fff4e6;

  --panel: rgba(140, 227, 155, 0.085);
  --panel-line: rgba(199, 240, 210, 0.19);
  --ink: var(--cream);
  --muted: #a9bdb0;
  --orange: var(--pumpkin);
  --leaf: var(--green-bright);
  --row-h: 5.5rem;
  --monster-size: 1.9rem;
  --radius: 14px;
  color-scheme: dark;
}

* { box-sizing: border-box; }

html { font-size: 17px; }

body {
  margin: 0;
  min-height: 100vh;
  font-family: ui-rounded, "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  color: var(--ink);
  background:
    radial-gradient(78% 52% at 50% -10%, rgba(34, 156, 77, 0.5), transparent 64%),
    radial-gradient(60% 40% at 50% 104%, rgba(31, 138, 69, 0.16), transparent 70%),
    linear-gradient(174deg, var(--plum) 0%, #18123a 38%, var(--navy) 100%);
  background-repeat: no-repeat;
  -webkit-font-smoothing: antialiased;
}

/* A quiet row of string lights across the top. Pure CSS, no assets to lose. */
.lights {
  height: 10px;
  background-image: radial-gradient(circle at 10px 5px, var(--gold) 0 3px, transparent 3.5px);
  background-size: 34px 10px;
  opacity: 0.8;
  filter: drop-shadow(0 0 7px rgba(255, 209, 102, 0.8));
}

a { color: var(--gold); }

.page {
  max-width: 62rem;
  margin: 0 auto;
  padding: 1.6rem 1.15rem 4rem;
}

h1 { font-size: 2.1rem; margin: 0 0 .2rem; letter-spacing: -0.01em; }
h2 { font-size: 1.25rem; margin: 0 0 .7rem; }
p  { line-height: 1.55; }

.sub { color: var(--muted); margin: 0 0 1.4rem; font-size: 1.05rem; }

.masthead { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }

.sample-banner {
  background: rgba(255, 209, 102, 0.13);
  border: 1px solid rgba(255, 209, 102, 0.48);
  border-radius: var(--radius);
  padding: .75rem 1rem;
  margin: 0 0 1.3rem;
  font-size: 1rem;
}
.sample-banner strong { color: var(--gold); }

/* ------------------------------------------------------------------ standings */

.band { margin: 0 0 2.1rem; }
.band-title { color: var(--green-bright); text-transform: none; font-size: 1.2rem; }

.rows {
  list-style: none;
  margin: 0;
  padding: 0;
  position: relative;
  height: calc(var(--rows) * var(--row-h));
}

.row {
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: var(--row-h);
  /* translateY in percent resolves against the row's own height, so one rule covers both the
     fixed-height phone layout and the TV layout where rows divide the screen. */
  transform: translateY(calc(var(--pos) * 100%));
  transition: transform .75s cubic-bezier(.22,.9,.24,1);
  display: grid;
  grid-template-columns: 2.1rem minmax(0, 1fr) auto;
  grid-template-areas:
    "rank who   lbs"
    "rank track gain";
  align-items: center;
  column-gap: .7rem;
  padding: .45rem .8rem .45rem .3rem;
}

.row::after {
  content: "";
  position: absolute;
  left: 2.6rem; right: 0; bottom: 0;
  border-bottom: 1px solid var(--panel-line);
}

.rank {
  grid-area: rank;
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--muted);
  text-align: center;
}
.row[style*="--pos:0"] .rank { color: var(--gold); }

.who { grid-area: who; min-width: 0; display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }
.name { font-size: 1.25rem; font-weight: 700; white-space: nowrap; }
.grade { font-size: .82rem; color: var(--muted); white-space: nowrap; }

/* Not a progress bar. A pile of candy that the class's monster is standing on top of, and the
   length of it is how much that monster has eaten. Same geometry, same numbers, different
   feeling, and it costs one gradient and one image. */
.track {
  grid-area: track;
  position: relative;
  display: block;
  height: .95rem;
  border-radius: 999px;
  background: rgba(8, 20, 14, 0.45);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
  min-width: 0;
}
.bar {
  display: block;
  height: 100%;
  width: calc(var(--pct, 0) * 1%);
  border-radius: 999px;
  background:
    repeating-linear-gradient(112deg, rgba(255, 255, 255, .085) 0 7px, rgba(255, 255, 255, 0) 7px 19px),
    radial-gradient(circle at 16% 130%, rgba(255, 243, 214, .3), transparent 48%),
    linear-gradient(90deg, var(--pumpkin-deep), var(--pumpkin) 52%, var(--gold));
  box-shadow: inset 0 -2px 3px rgba(120, 40, 0, .35), inset 0 2px 2px rgba(255, 240, 200, .28);
  transition: width 1.1s cubic-bezier(.22,.9,.24,1);
}

/* The monster rides the top of its own candy pile. It is clamped to the track so a class that
   has barely started does not have half a monster hanging over the teacher's name. */
.monster {
  position: absolute;
  width: var(--monster-size);
  height: var(--monster-size);
  bottom: -0.1rem;
  left: clamp(
    calc(var(--monster-size) / 2),
    calc(var(--pct, 0) * 1%),
    calc(100% - var(--monster-size) / 2)
  );
  transform: translateX(-50%);
  object-fit: contain;
  object-position: bottom center;
  filter: drop-shadow(0 3px 5px rgba(0, 0, 0, 0.55));
  transition: left 1.1s cubic-bezier(.22,.9,.24,1);
  pointer-events: none;
}

.spark { display: none; }
.spark polyline { fill: none; stroke: var(--green-bright); stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; opacity: .8; }

.lbs { grid-area: lbs; text-align: right; white-space: nowrap; align-self: end; }
.lbs b { font-size: 1.45rem; font-variant-numeric: tabular-nums; }
.lbs i { font-style: normal; font-size: .8rem; color: var(--muted); margin-left: .15rem; }

.gain {
  grid-area: gain;
  text-align: right;
  font-size: .85rem;
  font-weight: 700;
  color: var(--green-bright);
  white-space: nowrap;
  min-width: 3rem;
  align-self: start;
}
/* Weighed, but the pile did not grow. Quiet and grey rather than a green number or a blank,
   so it never reads as a class being called out and never reads as missing data either. */
.gain.flat {
  color: var(--muted);
  font-weight: 600;
  font-size: .72em;
  letter-spacing: .01em;
}

@media (min-width: 46rem) {
  :root { --row-h: 4.9rem; --monster-size: 2.5rem; }
  .row {
    grid-template-columns: 2.4rem minmax(9rem, 13rem) minmax(0, 1fr) auto 5rem 4.2rem;
    grid-template-areas: "rank who track spark lbs gain";
    align-items: center;
  }
  .spark { grid-area: spark; display: block; width: 4.6rem; height: 1.4rem; }
  .who { flex-direction: column; gap: 0; align-items: flex-start; }
  .lbs, .gain { align-self: center; }
}

/* ------------------------------------------------------------------ cards */

.cards { display: grid; gap: 1rem; grid-template-columns: 1fr; margin: 0 0 2rem; }
@media (min-width: 46rem) { .cards { grid-template-columns: 1.4fr 1fr 1fr; } }

.total-card, .climber-card, .countdown-card {
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: var(--radius);
  padding: 1.1rem 1.2rem;
}
.total-label, .climber-label, .countdown-line { margin: 0; color: var(--muted); font-size: .95rem; }
.total-num { margin: .1rem 0 .3rem; font-size: 2.6rem; font-weight: 800; line-height: 1; }
.total-num i { font-style: normal; font-size: 1.1rem; color: var(--muted); font-weight: 600; }
body.tv .shared .total-num { display: flex; flex-direction: column; gap: .2rem; }
.total-compare { margin: 0; color: var(--gold); font-size: 1rem; }
.climber-card.has-monster { display: flex; align-items: center; gap: 1rem; }
.climber-monster { width: 4.4rem; height: 4.4rem; object-fit: contain; flex: none;
  filter: drop-shadow(0 4px 7px rgba(0, 0, 0, 0.5)); }
.climber-name { margin: .15rem 0 .1rem; font-size: 1.5rem; font-weight: 800; }
.climber-grade { margin: 0 0 .25rem; color: var(--muted); font-size: .95rem; }
.climber-gain { margin: 0; color: var(--green-bright); font-weight: 700; }
.countdown-num { margin: .1rem 0 .1rem; font-size: 2.6rem; font-weight: 800; line-height: 1; color: var(--gold); }

.total-sub { margin: .35rem 0 0; color: var(--muted); font-size: .95rem; }
.goal-track {
  height: .7rem;
  border-radius: 999px;
  background: rgba(8, 20, 14, 0.5);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .5);
  overflow: hidden;
  margin: .5rem 0 .55rem;
}
.goal-fill {
  display: block;
  height: 100%;
  width: calc(var(--goal, 0) * 1%);
  border-radius: 999px;
  background: linear-gradient(90deg, var(--green), #35c169 60%, var(--green-bright));
  transition: width 1.1s cubic-bezier(.22,.9,.24,1);
}

.updated { color: var(--muted); font-size: .85rem; margin: 1.5rem 0 0; }
.dot { display: inline-block; width: .5rem; height: .5rem; border-radius: 50%; background: var(--green-bright); margin-right: .4rem; vertical-align: baseline; }
body.stale .dot { background: var(--muted); }

/* ------------------------------------------------------------------ forms */

.card {
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: var(--radius);
  padding: 1.1rem 1.2rem;
  margin: 0 0 1.2rem;
}

label { display: block; font-weight: 700; margin-bottom: .35rem; }

input[type="text"], input[type="password"], input[type="number"], input[type="date"], select, textarea {
  width: 100%;
  font: inherit;
  font-size: 1.15rem;
  padding: .7rem .8rem;
  border-radius: 10px;
  border: 1px solid var(--panel-line);
  background: rgba(0, 0, 0, 0.3);
  color: var(--ink);
}
input:focus-visible, select:focus-visible, button:focus-visible, a:focus-visible {
  outline: 3px solid var(--green-bright);
  outline-offset: 2px;
}

.entry-grid { display: grid; gap: .8rem; grid-template-columns: 1fr; }
@media (min-width: 40rem) { .entry-grid { grid-template-columns: 1fr 1fr; } }

.entry-row { display: grid; grid-template-columns: 1fr 9.5rem; gap: .7rem; align-items: center; }
.entry-row .who-line { min-width: 0; }
.entry-row .who-line b { display: block; font-size: 1.15rem; }
.entry-row .who-line span { color: var(--muted); font-size: .85rem; }
.entry-row input { font-size: 1.35rem; text-align: right; padding: .85rem .7rem; }
.entry-row input::placeholder { color: rgba(255, 244, 230, 0.32); }

button, .btn {
  font: inherit;
  font-weight: 700;
  font-size: 1.05rem;
  padding: .85rem 1.4rem;
  border-radius: 11px;
  border: 0;
  background: linear-gradient(180deg, #2aa155, var(--green));
  color: #f2fff5;
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}
.btn.quiet, button.quiet {
  background: transparent;
  border: 1px solid var(--panel-line);
  color: var(--ink);
}
.sticky-save {
  position: sticky;
  bottom: 0;
  background: linear-gradient(180deg, rgba(16, 26, 46, 0) 0%, var(--navy) 38%, var(--navy) 100%);
  padding: 1.4rem 0 1.2rem;
  margin-top: .5rem;
}
.sticky-save button { width: 100%; font-size: 1.2rem; padding: 1.05rem; }

.notice { border-radius: var(--radius); padding: .85rem 1rem; margin: 0 0 1.2rem; border: 1px solid; }
.notice.good { background: rgba(140, 227, 155, 0.13); border-color: rgba(140, 227, 155, 0.45); }
.notice.warn { background: rgba(255, 209, 102, 0.13); border-color: rgba(255, 209, 102, 0.5); }
.notice ul { margin: .4rem 0 0; padding-left: 1.2rem; }

.nav { display: flex; gap: 1rem; flex-wrap: wrap; margin: 0 0 1.4rem; font-size: .95rem; }

table { width: 100%; border-collapse: collapse; }
.monster-cell { display: flex; align-items: center; gap: .5rem; }
.monster-cell img { width: 2.4rem; height: 2.4rem; object-fit: contain; flex: none; }
.monster-cell select { width: 4.2rem; font-size: .95rem; padding: .4rem; }
th, td { text-align: left; padding: .5rem .4rem; border-bottom: 1px solid var(--panel-line); font-size: .95rem; }

/* ------------------------------------------------------------------ TV mode */

body.tv {
  --row-h: 6.2rem;
  /* Consumer TVs often overscan by two to five percent, which would quietly clip the header
     and the last class in the bracket with nobody standing there to notice. Keep a safe area
     inside the panel. Dial these two down once somebody has seen the actual screen. */
  --tv-safe-y: 2.5vh;
  --tv-safe-x: 2vw;
  height: 100vh;
  padding: var(--tv-safe-y) var(--tv-safe-x);
  overflow: hidden;
  cursor: none;
  display: flex;
  flex-direction: column;
}
body.tv .lights { flex: none; }
body.tv .tv-wrap { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 1.4rem 2.6rem 1.8rem; }

body.tv .tv-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 2rem;
  padding-bottom: .9rem;
  border-bottom: 1px solid var(--panel-line);
}
body.tv .tv-title { font-size: 2.8rem; font-weight: 800; margin: 0; line-height: 1.05; letter-spacing: -0.015em; }
body.tv .wordmark { display: block; height: 6.6rem; width: auto; }
.wordmark { display: block; height: 3.2rem; width: auto; max-width: 100%; object-fit: contain; object-position: left center; }
.brand { margin: 0 0 .35rem; }
.tv-brand { display: flex; flex-direction: column; gap: .3rem; }
body.tv .tv-sub { font-size: 1.4rem; color: var(--gold); margin: .15rem 0 0; }
body.tv .tv-meta { text-align: right; color: var(--muted); font-size: 1.1rem; margin: 0; line-height: 1.4; }

body.tv .stage { position: relative; flex: 1; margin-top: 1rem; }
body.tv .card-slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: translateY(1.2rem);
  transition: opacity .6s ease, transform .6s ease;
  pointer-events: none;
}
body.tv .card-slide.on { opacity: 1; transform: none; }

/* On the TV the rows share out whatever height is left after the header, so the board always
   fills the screen instead of leaving a band of dead space under the last class. */
body.tv .band { height: 100%; display: flex; flex-direction: column; margin: 0; }
body.tv .rows { flex: 1; height: auto; }
body.tv .row { height: calc(100% / var(--rows)); }
body.tv .band-title { font-size: 1.7rem; margin-bottom: .6rem; flex: none; }
body.tv .rank { font-size: 2rem; }
body.tv .name { font-size: 2.5rem; line-height: 1.15; }
body.tv .grade { font-size: 1.15rem; }
body.tv .track { height: 2rem; }
body.tv .lbs b { font-size: 3rem; }
body.tv .lbs i { font-size: 1.2rem; }
body.tv .gain { font-size: 1.45rem; min-width: 5.5rem; }
body.tv .gain.flat { font-size: 1.05rem; }
body.tv .monster { --monster-size: 5.2rem; }
body.tv .climber-monster { width: 8.5rem; height: 8.5rem; }
body.tv .goal-track { height: 2rem; margin: .4rem 0 .2rem; }
body.tv .total-sub { font-size: 1.9rem; margin-top: .3rem; }
/* The name column is sized to hold the longest name on the roster without wrapping or
   ellipsis. A teacher's name truncated on a cafeteria TV is worse than a narrower bar. */
body.tv .row {
  grid-template-columns: 3.2rem 21rem minmax(0, 1fr) 7.6rem 6rem;
  grid-template-areas: "rank who track lbs gain";
  padding: .4rem 0;
  column-gap: 1rem;
}
body.tv .who { flex-direction: column; gap: 0; align-items: flex-start; }
body.tv .row::after { left: 3.6rem; }

body.tv .shared {
  height: 100%;
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  grid-template-rows: 1.3fr 1fr;
  gap: 1.6rem;
}
/* This card owns a whole half of a 1080p screen. Spreading the content down it instead of
   centring a band in the middle lets the number that matters be the size it deserves. */
body.tv .shared .total-card {
  grid-row: 1 / span 2;
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
  padding: 2.4rem 2.6rem;
}
body.tv .shared .total-label { font-size: 2.1rem; }
body.tv .shared .total-num { font-size: 13rem; line-height: .92; }
body.tv .shared .total-num i { font-size: 3.1rem; }
body.tv .shared .total-compare { font-size: 2.5rem; margin-top: .4rem; }
body.tv .shared .climber-card, body.tv .shared .countdown-card { display: flex; flex-direction: column; justify-content: center; }
body.tv .shared .climber-card.has-monster { flex-direction: row; align-items: center; gap: 1.6rem; }
body.tv .shared .climber-label { font-size: 1.5rem; }
body.tv .shared .climber-name { font-size: 3.2rem; }
body.tv .shared .climber-grade { font-size: 1.5rem; }
body.tv .shared .climber-gain { font-size: 1.9rem; }
body.tv .shared .countdown-num { font-size: 8.6rem; line-height: .95; }
body.tv .shared .countdown-line { font-size: 2.1rem; }

body.tv .sample-banner { font-size: 1.3rem; text-align: center; margin: .6rem 0 0; padding: .45rem 1rem; }

body.tv .tv-empty { display: flex; height: 100%; align-items: center; justify-content: center; text-align: center; font-size: 2.4rem; color: var(--muted); }

/* Scale the whole TV layout off the viewport so 1080p, 720p and a 4K panel all land. */
/* The whole TV layout is sized in rem off one root value, so 720p, 1080p and a 4K panel all
   land with the same proportions. 1920 wide resolves to about 17px. */
html:has(body.tv) { font-size: clamp(11px, 0.885vw, 24px); }

/* ------------------------------------------------------------------ Fall Fest theme */

/* Kara sent the Save the Date and asked for the board to match it. This is that: her black
   night sky, her cream moon in the top left, her white signboard, her pumpkin patch. Every
   piece is cut from art generated with her flyer as the reference, so the continuity is real
   rather than approximate.

   Two measured facts drive the layout. The moon lands at x 0-330, y 0-358 on a 1080p screen,
   which is where the first standings row puts its rank and the start of a teacher's name, so
   the moon is its own layer and gets sized to stay above the rows. And the pumpkin patch is the
   bottom 22% of the plate, which six rows of standings would sit inside, so it is a footer band
   with its height reserved rather than a background the rows have to survive.

   Contrast, measured rather than assumed, against this theme's black: Kara's headline orange
   #f07a30 is 7.5:1, which clears even the body-text bar, so the headings are genuinely hers.
   Cream is 19.3:1, and that best-available contrast is spent where it matters most, on the
   weights, because a kid at the back of the cafeteria came for their class's number. */

body.theme-fallfest {
  --ff-orange: #f07a30;
  --ff-cream: #fff4e6;
  --ff-moon: #f8e9a8;

  --ink: var(--ff-cream);
  --muted: #c0b3a2;
  --gold: var(--ff-moon);
  --green-bright: #8ce39b;
  --panel: rgba(255, 244, 230, 0.07);
  --panel-line: rgba(255, 244, 230, 0.2);
  --patch-h: 15vh;

  background-color: #000;
  background-image: url("/img/fallfest-moon"), url("/img/fallfest-stars");
  background-position: left top, top center;
  background-size: auto 22vh, 100% auto;
  background-repeat: no-repeat, repeat-y;
}

/* The pumpkin patch, as a band whose height the board controls. The art is anchored to the top
   of the band so the pumpkins keep their proportions and get cropped at the bottom, which is
   what the flyer does at its own frame edge. */
.patch { display: none; }
body.theme-fallfest .patch {
  display: block;
  height: var(--patch-h);
  background-image: url("/img/fallfest-patch");
  background-position: top center;
  background-size: 100% auto;
  background-repeat: no-repeat;
  pointer-events: none;
}
body.tv.theme-fallfest .patch {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
}
/* Reserve the band. Rows then divide what is left instead of standing in the pumpkins. */
body.tv.theme-fallfest .tv-wrap { padding-bottom: calc(var(--patch-h) + 0.6rem); }

/* Her sky has no string lights in it. */
body.theme-fallfest .lights { display: none; }

/* The moon is 22vh tall, so about 220px wide on a 1080p screen, and the header has to start
   after it. Cream type on a cream moon is unreadable, and this is the composition her flyer
   uses anyway: moon alone in the corner, signboard to the right of it. */
body.tv.theme-fallfest .tv-head { padding-left: 15vw; }
/* The signboard is this theme's title, so it gets the size a title deserves. The phase line
   moves alongside it rather than under it, which buys the height back out of the header
   instead of out of the standings rows. */
body.tv.theme-fallfest .tv-brand { flex-direction: row; align-items: flex-end; gap: 1.4rem; }
body.tv.theme-fallfest .wordmark { height: 9.2rem; }
body.tv.theme-fallfest .tv-sub { padding-bottom: 1.2rem; }
/* On the family page the viewport is tall and narrow, so a moon sized in vh is enormous and a
   gutter sized in vw does not clear it. Both are in vw here, and both are clamped, so the moon
   and the space reserved for it scale together on a phone and on a laptop. */
body.theme-fallfest:not(.tv) {
  background-size: auto clamp(88px, 26vw, 250px), 100% auto;
}
body.theme-fallfest:not(.tv) .masthead,
body.theme-fallfest:not(.tv) .page > .brand { padding-left: clamp(104px, 31vw, 285px); }

/* The rows share a shorter stage in this theme because the pumpkin band takes the bottom of
   the screen, so the monsters come down to match. The numbers do not. */
body.tv.theme-fallfest .monster { --monster-size: 4.4rem; }

body.theme-fallfest .band-title { color: var(--ff-orange); }
body.theme-fallfest .total-label,
body.theme-fallfest .climber-label,
body.theme-fallfest .countdown-line { color: var(--ff-orange); }
body.theme-fallfest .total-compare { color: var(--ff-moon); }
body.theme-fallfest .countdown-num { color: var(--ff-moon); }
body.theme-fallfest .row[style*="--pos:0"] .rank { color: var(--ff-moon); }

/* The trough reads as a furrow in the dark rather than a grey pill. */
body.theme-fallfest .track {
  background: rgba(255, 244, 230, 0.1);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.75);
}
body.theme-fallfest .goal-track { background: rgba(255, 244, 230, 0.1); }
body.theme-fallfest .goal-fill {
  background: linear-gradient(90deg, #c2560f, var(--ff-orange) 55%, #ffc861);
}

body.theme-fallfest .card,
body.theme-fallfest .total-card,
body.theme-fallfest .climber-card,
body.theme-fallfest .countdown-card,
body.theme-fallfest .rows { backdrop-filter: blur(1px); }

body.theme-fallfest button, body.theme-fallfest .btn {
  background: linear-gradient(180deg, #ff9448, var(--ff-orange));
  color: #2a1204;
}
body.theme-fallfest .sample-banner {
  background: rgba(240, 122, 48, 0.16);
  border-color: rgba(255, 200, 97, 0.55);
}
body.theme-fallfest .sample-banner strong { color: #ffc861; }

/* The title card: her whole world in one frame, for the screens with nothing to count yet.
   It is a finished composition, so it gets the whole screen rather than a panel inside one. */
.tv-hero {
  height: 100%;
  background-image: var(--hero);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
/* A slide can ask for the chrome to step aside. "soft" keeps the moon and the pumpkin band,
   because the Fall Fest sign is standing in that same patch; "full" takes the whole screen. */
body.tv[data-bare] .tv-head,
body.tv[data-bare] .sample-banner { display: none; }
body.tv[data-bare] .stage { margin-top: 0; }
body.tv[data-bare="full"] { padding: 0; }
body.tv[data-bare="full"] .patch { display: none; }
body.tv[data-bare="full"] .tv-wrap { padding: 0; }

/* The family page is a scrolling document, so the patch caps the end of it rather than
   floating over the standings. */
body.theme-fallfest:not(.tv) .patch {
  margin-top: 1.5rem;
  height: 12vh;
  min-height: 96px;
  /* Fill the band rather than leaving black under a strip scaled to the page width. */
  background-size: cover;
  background-position: center top;
}

/* ------------------------------------------------------------------ Fall Fest card */

/* Kara's headline is a chunky dry-brush marker. Of the three candidates rendered side by side
   against her flyer, Permanent Marker is the match; Gochi Hand is lighter and rounder, and
   Caveat Brush is a different animal entirely. Self-hosted, because a board that runs eight
   hours a day for three weeks behind school wifi should not depend on someone else's server.

   The weight range is declared 400 to 900 even though the file is a single 400. That stops the
   browser synthesising a fake bold over the marker outlines, while still letting the fallback
   sans render bold if the font never arrives. */
@font-face {
  font-family: "Marker";
  src: url("/static/fonts/permanent-marker.woff2") format("woff2");
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
}

/* Kara's information, set for a room instead of for a hand.
   Live text over a blank signboard, never baked into the picture. */

.event-card {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* The frame carries the sign's aspect, so the text stays registered to the board whether the
   sign is limited by width or by height. */
/* Height-driven, because this card only ever appears on the TV, where the stage is always
   wider than it is tall. The frame then matches the sign exactly, which is the whole point:
   the text is positioned against the frame and has to stay registered to the board. */
.event-frame {
  position: relative;
  height: 100%;
  aspect-ratio: 1200 / 784;
  max-width: 100%;
}
.event-sign { display: block; width: 100%; height: 100%; object-fit: contain; }
/* Drop the whole sign so the post reaches the pumpkin band and tucks behind it, instead of
   ending in mid air and reading as something pasted onto the sky. The band is fixed and paints
   over the top, so the post genuinely goes behind the pumpkins. */
body.tv.theme-fallfest .event-card { transform: translateY(9vh); }

/* Placed from the measured board in identity/slice.py, which levels the sign and then prints
   these numbers: centre 49.7% x, 46.9% y of the cut image, board 90.0% by 62.9% of it. Re-run
   the slicer if the plate is ever regenerated. */
.event-text {
  position: absolute;
  left: 49.7%;
  top: 46.9%;
  width: 88%;
  height: 56%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .18em;
  text-align: center;
  /* Her orange, darkened, and only here.
     On black it is 7.5:1 and stays exactly hers, which is why the band titles and labels use
     it untouched. On this white signboard the same colour measures 2.79:1, which fails even
     the large-text bar; right in a hand at eighteen inches, the first thing to go at forty
     feet. This is the same hue taken down to 4.45:1, which clears the applicable bar by half
     again and still reads as her orange. */
  color: #c4570d;
}
.event-text p { margin: 0; line-height: 1.04; width: 100%; }

/* The type has to scale with the SIGN, not with the viewport width.
   The frame is height-driven, so on a 1280x720 panel the sign is two thirds the size it is at
   1080p while vw-sized type is only two thirds as wide: the headline wrapped to three lines and
   spilled off the board, top and bottom. Sizing in vh tracks the sign, and the vw term is the
   floor for the case where the frame gets clamped by width instead, such as a narrow window. */
/* The marker goes on the headline and nowhere else.
   It has no true lowercase, so a date set in it comes out as small caps, which is neither
   legible nor faithful: her own date line is a delicate lowercase pen face, not a marker. And
   at the activity line's size the marker measurably loses to the sans, which is the whole risk
   with a display face on a board read from forty feet. Headline only. */
.event-headline {
  font-family: "Marker", ui-rounded, "Segoe UI", system-ui, sans-serif;
  /* The marker sets far narrower than the sans did, so it gets the size back. Measured: at the
     old 4.0vw its two lines ran 624 and 609 inside an 1118 measure, leaving a third of the
     board empty. */
  font-size: min(9.2vh, 7.16vw);
  line-height: 1.08;
  text-wrap: balance;
  font-weight: 800;
  letter-spacing: 0.012em;
  text-transform: uppercase;
}
.event-when { font-size: min(5.1vh, 3.97vw); font-weight: 700; margin-top: .22em !important; }
.event-time { font-size: min(5.1vh, 3.97vw); font-weight: 700; }
/* Deliberately much larger than the flyer's version of this line. It is the first thing a
   parent scans and the first thing that fails at distance. */
.event-activities { font-size: min(2.85vh, 2.22vw); font-weight: 700; margin-top: .4em !important; }

/* The monster world gets the same information in its own idiom rather than a pumpkin sign
   dropped into a plum sky. */
/* The panel hugs its content and sits centred in the stage, rather than being stage-height
   with the text in a band across the middle. */
.event-card.plain { background: none; border: 0; }
.event-card.plain .event-text {
  position: static;
  transform: none;
  width: min(92%, 64rem);
  height: auto;
  color: var(--gold);
  gap: .1em;
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: var(--radius);
  padding: 3.4rem 3rem;
}
.event-card.plain .event-when,
.event-card.plain .event-time { color: var(--ink); }
.event-card.plain .event-activities { color: var(--green-bright); }

/* ------------------------------------------------------------------ print */

@media print {
  body { background: #fff; color: #111; }
  .lights, .nav, .btn, button { display: none !important; }
  .card, .total-card, .climber-card, .countdown-card { border-color: #bbb; background: #fff; }
  a { color: #111; }
  .page { max-width: none; padding: 0; }
  h1 { font-size: 1.6rem; }
  .muted, .sub, .grade { color: #555; }
}

@media (prefers-reduced-motion: reduce) {
  .row, .bar, .monster, .goal-fill, body.tv .card-slide { transition: none !important; }
}
