:root {
  /* Party-sign typography: a heavy geometric for headlines, a script for the
     flourishes. System faces only - the slideshow must not wait on a font
     download at a party, and a webfont that fails to load reflows the whole
     screen mid-show. */
  --display: "Futura", "Avenir Next", "Century Gothic", "Poppins",
             ui-rounded, "SF Pro Rounded", system-ui, sans-serif;
  /* Hand-PRINTED, not cursive: marker and pen faces where the letters stay
     separate. Ordered macOS first, then Windows, both ending in something
     casual rather than a formal script - a name signed on a photo should look
     written, not engraved. */
  --hand: "Marker Felt", "Chalkboard SE", "Segoe Print", "Ink Free",
          "Bradley Hand", "Comic Sans MS", cursive;

  --bg: #0e0d13;
  --surface: #191822;
  --surface-2: #21202c;
  --border: #2c2a38;
  --text: #f4f2ef;
  --muted: #9d99ac;
  --accent: #f0b642;
  --accent-ink: #2a1e05;
  --danger: #e5675f;
  --radius: 14px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: ui-rounded, "SF Pro Rounded", -apple-system, BlinkMacSystemFont,
               "Segoe UI", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

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

.wrap {
  max-width: 40rem;
  margin: 0 auto;
  padding: 2rem 1.25rem 5rem;
}

.wrap.narrow { max-width: 34rem; }

h1 { font-size: 2rem; line-height: 1.15; margin: 0 0 .5rem; letter-spacing: -.02em; }
h2 { font-size: 1.15rem; margin: 2.5rem 0 .75rem; }

.tagline { color: var(--accent); font-style: italic; margin: 0; }

.muted { color: var(--muted); font-size: .9rem; }

header { margin-bottom: 2rem; }
header.center { text-align: center; }

/* ---------- topbar ---------- */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--border);
}

.topbar .brand { font-weight: 700; letter-spacing: -.01em; }
.topbar .who { font-size: .85rem; color: var(--muted); }

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

label { display: block; font-size: .85rem; color: var(--muted); margin: 1rem 0 .4rem; }

input[type="text"], input[type="date"], select {
  width: 100%;
  padding: .85rem 1rem;
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

select { appearance: none; cursor: pointer; }

input[type="text"]:focus, input[type="date"]:focus, select:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

input[type="file"] { display: none; }

.btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  width: 100%;
  margin-top: 1rem;
  padding: 1.1rem;
  font: inherit;
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--accent-ink);
  background: var(--accent);
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  text-decoration: none;
}

.btn.secondary {
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  font-size: 1rem;
  font-weight: 500;
}

.btn.danger { color: var(--danger); background: transparent; border: 1px solid var(--border); font-size: .9rem; }
.btn.inline { width: auto; padding: .6rem 1rem; font-size: .9rem; margin: 0; }

.btn:active { transform: scale(.985); }
.btn:disabled { opacity: .5; cursor: default; transform: none; }

/* ---------- tabs ---------- */

.tabs {
  display: flex;
  gap: .25rem;
  margin: 2rem 0 0;
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
  scrollbar-width: none;
}

.tabs::-webkit-scrollbar { display: none; }

.tab {
  flex: 0 0 auto;
  padding: .8rem 1rem;
  font: inherit;
  font-size: .95rem;
  font-weight: 600;
  white-space: nowrap;
  color: var(--muted);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;      /* sit the indicator on the container's border */
  cursor: pointer;
}

.tab:hover { color: var(--text); }

.tab.is-active {
  color: var(--text);
  border-bottom-color: var(--accent);
}

.tab span { font-weight: 400; opacity: .6; }

.tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -4px;
  border-radius: 6px;
}

.tabpanel { padding-top: 1.25rem; }

/* The tab already names the section; a heading inside would repeat it. Only
   Danger zone keeps one, as a deliberate speed bump. */
.tabpanel h2:first-child { margin-top: 0; }

/* ---------- color picker + preview ---------- */

.swatches { display: flex; gap: .6rem; align-items: flex-start; }

.swatch {
  flex: 1;
  min-width: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: .35rem;
  cursor: pointer;
}

.swatch-label {
  font-size: .78rem;
  font-weight: 600;
  color: var(--text);
}

.swatch-note {
  font-size: .7rem;
  line-height: 1.3;
  color: var(--muted);
}

.swatches input[type="color"] {
  width: 100%;
  height: 3rem;
  padding: 0;
  background: none;
  border: 1px solid var(--border);
  border-radius: 10px;
  cursor: pointer;
}

/* Chrome/Safari draw the swatch inside a padded wrapper; strip it so the color
   fills the control and reads as a real swatch. */
.swatches input[type="color"]::-webkit-color-swatch-wrapper { padding: 3px; }
.swatches input[type="color"]::-webkit-color-swatch { border: none; border-radius: 7px; }
.swatches input[type="color"]::-moz-color-swatch { border: none; border-radius: 7px; }

/* Scopes the derived palette to itself via CSS custom properties, so it shows
   the real result rather than an approximation of it.
   pointer-events:none because the sample chip reads as a real button - people
   try to click it. It's a swatch, not a control. */
.palette-preview {
  margin-top: .85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  pointer-events: none;
  user-select: none;
}

.palette-preview .pp-label {
  padding: .4rem .75rem;
  font-size: .65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--muted);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.palette-preview .pp-body {
  padding: 1.5rem 1.25rem;
  background: var(--bg);
  text-align: center;
}

.palette-preview .pp-body { display: grid; justify-items: center; }

/* The photo's mat and border, which is where surface and border actually show
   up on a television - the biggest area of derived colour on the screen. */
.palette-preview .pp-photo {
  width: min(15rem, 100%);
  margin-top: .3rem;
  padding: .55rem .55rem .4rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .35);
}

/* Stands in for a photograph. Deliberately neutral: a coloured placeholder
   would be read as part of the palette. */
.palette-preview .pp-shot {
  height: 5.5rem;
  border-radius: 3px;
  background:
    linear-gradient(135deg, #6b6f76 0%, #9aa0a8 45%, #55595f 100%);
}

/* The uploader credit, in the same hand font the slideshow writes it in. */
.palette-preview .pp-by {
  margin-top: .3rem;
  font-family: var(--hand);
  font-size: .8rem;
  text-align: left;
  color: var(--text);
  opacity: .85;
}

/* The corner card that runs for the whole show. */
.palette-preview .pp-corner {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-top: .9rem;
  padding: .4rem .5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
}

.palette-preview .pp-corner-label {
  font-family: var(--display);
  font-size: .72rem;
  font-weight: 800;
  letter-spacing: .02em;
  color: var(--accent);
}

/* A QR stands in as a plain white square - it is white on every theme, and
   showing it stops a host choosing a background that swallows it. */
.palette-preview .pp-qr {
  width: 1.6rem;
  height: 1.6rem;
  background: #fff;
  border-radius: 3px;
}

/* The banner shrunk to preview scale. Its flags alternate accent / accent /
   second, which is the only place the third swatch is visible. */
.pp-bunting {
  position: relative;
  height: 46px;
  margin-bottom: .5rem;
}

/* The text banner fills the strip so it can centre across the whole preview;
   the pennant keeps its natural, flag-sized width. */
.pp-bunting.is-text { width: 100%; }

/* At preview scale the flag letters need to be a touch bigger to read, and sit
   lower so they land in the body of the pennant rather than up by the string. */
.palette-preview .bunting i {
  font-size: calc(var(--flag-w) * .52);
  padding-top: calc(var(--flag-w) * .42);
}

.pp-bunting .bunting {
  --flag-w: 15px;
  height: 46px;
  max-width: none;
}

.row { display: flex; gap: .75rem; }
.row > * { flex: 1; }

#status, .status {
  min-height: 1.4rem;
  margin-top: 1.25rem;
  text-align: center;
  color: var(--muted);
  font-size: .95rem;
}

#status.error, .status.error { color: var(--danger); }

/* ---------- event list ---------- */

.card {
  display: block;
  padding: 1.1rem 1.25rem;
  margin-bottom: .75rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-decoration: none;
  color: inherit;
}

.card:hover { background: var(--surface-2); }
.card .title { font-weight: 600; font-size: 1.05rem; }
.card .meta { margin-top: .3rem; font-size: .85rem; color: var(--muted); }

.empty {
  padding: 2.5rem 1.5rem;
  text-align: center;
  color: var(--muted);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
}

/* ---------- qr panel ---------- */

.qr-panel {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 1.25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.qr-panel .qr {
  flex: 0 0 auto;
  width: 128px;
  padding: 8px;
  background: #fff;
  border-radius: 8px;
}

.qr-panel .qr svg { display: block; width: 100%; height: auto; }

/* qrSvg is written by a Cloud Function a beat after the event is created -
   until it lands, don't render an empty white square. */
.qr:empty { display: none; }

.join-url {
  font-weight: 700;
  color: var(--accent);
  word-break: break-all;
  font-size: 1rem;
}

/* ---------- toggle ---------- */

.toggle {
  display: flex;
  align-items: center;
  gap: .75rem;
  margin: 1rem 0 .5rem;
  padding: .9rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  color: var(--text);
  font-size: .95rem;
}

/* A sliding switch rather than a checkbox - reads as on/off at a glance. */
.toggle input {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 2.6rem;
  height: 1.5rem;
  border-radius: 1rem;
  background: var(--border);
  position: relative;
  cursor: pointer;
  transition: background .15s ease;
}
.toggle input::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 3px;
  width: calc(1.5rem - 6px);
  height: calc(1.5rem - 6px);
  transform: translateY(-50%);
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
  transition: transform .15s ease;
}
.toggle input:checked { background: var(--accent); }
.toggle input:checked::before { transform: translate(1.1rem, -50%); }
.toggle input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---------- banner list ---------- */

/* Input takes the space, button takes what it needs. `.row` would split them
   evenly and leave a comically wide Add button. */
.add-row { display: flex; gap: .6rem; align-items: stretch; }
.add-row input { flex: 1; min-width: 0; }
.add-row .btn { margin-top: 0; flex: 0 0 auto; }

.banner-list { list-style: none; margin: 1rem 0 0; padding: 0; }

.banner-list li {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .6rem .85rem;
  margin-bottom: .4rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: .95rem;
}

.banner-list li span { flex: 1; }

.banner-list li button {
  flex: 0 0 auto;
  padding: .2rem .5rem;
  font: inherit;
  font-size: .85rem;
  color: var(--danger);
  background: none;
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
}

/* ---------- photo grid ---------- */

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: .5rem;
  margin-top: 1.25rem;
}

.grid figure {
  position: relative;
  margin: 0;
  aspect-ratio: 1;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface);
}

.grid img { width: 100%; height: 100%; object-fit: cover; }
.grid figure.pending img { opacity: .35; filter: blur(1px); }

.grid figure.pending::after {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  width: 20px; height: 20px;
  margin: -10px 0 0 -10px;
  border: 2px solid rgba(255,255,255,.25);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .7s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- managed photo tiles (host) ---------- */

.grid figure.tile img { transition: opacity .2s ease, filter .2s ease; }

.grid figure.tile.is-hidden img { opacity: .3; filter: grayscale(1); }

.tile-actions {
  position: absolute;
  left: 4px; right: 4px; bottom: 4px;
  display: flex;
  gap: 4px;
}

/* Always visible rather than hover-only - this has to work on a phone, which
   is where a host is most likely to be when they need something gone fast. */
.tile-btn {
  flex: 1;
  padding: .35rem .2rem;
  font: inherit;
  font-size: .7rem;
  font-weight: 600;
  /* Fixed light ink, not var(--text). These sit on their own dark scrim over a
     photograph, not on the page background, so they must not follow the page's
     ink colour. They were written for the slideshow, where --text is near
     white; in the host app it is navy, which on this scrim is unreadable. */
  color: #f4f2ef;
  background: rgba(14, 13, 19, .82);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, .16);
  border-radius: 7px;
  cursor: pointer;
  line-height: 1.2;
}

.tile-btn:hover { background: rgba(14, 13, 19, .94); }
.tile-btn:active { transform: scale(.94); }

/* Same reasoning: a light red that reads on the scrim, rather than the page's
   danger colour, which is dark by design because it normally sits on white. */
.tile-btn.danger { flex: 0 0 30px; color: #ff9c94; }

/* Sits opposite the "Hidden" badge so a hidden video shows both. */
.tile-badge.video {
  top: auto;
  bottom: 26px;
  left: auto;
  right: 4px;
  color: #f4f2ef;
  background: rgba(14, 13, 19, .85);
  border: 1px solid rgba(255, 255, 255, .18);
}

.tile-badge {
  position: absolute;
  top: 4px; left: 4px;
  padding: .15rem .4rem;
  font-size: .65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--accent-ink);
  background: var(--accent);
  border-radius: 5px;
}

footer {
  margin-top: 2.5rem;
  text-align: center;
  font-size: .8rem;
  color: var(--muted);
  line-height: 1.6;
}

[hidden] { display: none !important; }

/* ================= slideshow ================= */

body.tv { overflow: hidden; height: 100vh; background: #000; cursor: none; }
body.tv.pointer { cursor: default; }

#stage { position: fixed; inset: 0; }

/* One slide = blurred backdrop + theme tint + matted photo. Crossfades as a
   unit so the frame never appears before the image it holds. */
.slide {
  position: absolute;
  inset: 0;
  z-index: 0;                 /* below the show-wide confetti layer */
  opacity: 0;
  transition: opacity 1.6s ease;
}

.slide.on { opacity: 1; }

/* The photo's own colors, heavily blurred - fills the screen for any aspect
   ratio instead of leaving dead black bars. */
.slide-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(48px) brightness(.42);
  transform: scale(1.15);
}

/* Pulls that backdrop toward the event's palette so every slide reads as part
   of the same set, whatever the photo happens to look like. */
.slide-tint {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 90% at 50% 45%, transparent 20%, var(--bg) 100%),
    var(--bg);
  opacity: .62;
}

.slide-center {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Photos tuck under the lower half of the banner but never higher, so the
   lettering always stays readable. Kept in sync with BANNER_INSET_VMIN in
   tv.js, which the collage planner also subtracts from its available height. */
body.banner-space .slide-center { padding-top: 6vmin; }
/* The collage's own padding plus the banner's reserve. Reading the variable
   matters: this used to hard-code 3.5vmin, which is the default frame's
   padding, so Mosaic - whose padding is zero - reserved 3.5vmin more at the top
   than the planner had subtracted, and pushed the bottom row off screen. */
body.banner-space .collage {
  padding-top: calc(var(--collage-pad, 3.5vmin) + 6vmin);
}

/* The mat hugs whatever the image turns out to be: the img is capped in vw/vh
   and the frame shrink-wraps it, so portrait, landscape, square and panorama
   all get the same treatment without measuring anything. */
.mat {
  position: relative;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 1.1vmin;
  padding: var(--mat-pad, 1.5vmin);
  background: var(--mat-bg, var(--surface));
  border: var(--mat-border, .32vmin solid var(--accent));
  border-radius: var(--mat-radius, 1.2vmin);
  box-shadow: var(--mat-shadow,
    0 0 8vmin -2vmin var(--accent),
    0 2.5vmin 7vmin rgba(0, 0, 0, .65));
}

/* Sized from the photo's real aspect ratio rather than capped by it.
   max-width/max-height only ever shrink - and since upload downscales but never
   upscales, a small original rendered at natural size is a stamp in the middle
   of a 4K screen. Deriving the width from --ar makes every photo fill the frame
   whatever it arrived as. */
/* The guest's name, bottom-left, in a hand-printed face - a signature on the
   picture rather than a caption describing it. */
.mat figcaption {
  align-self: flex-start;
  padding: 0 .6vmin;
  font-family: var(--hand);
  font-size: 2.5vmin;
  font-weight: 400;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* Edge-to-edge has no mat, so the name lands on the photo itself. */
.frame-edge figcaption {
  padding-top: 1vmin;
  color: #fff;
  text-shadow: 0 .2vmin 1.5vmin rgba(0, 0, 0, .9);
}

.mat img,
.mat video {
  display: block;
  width: min(84vw, calc(72vh * var(--ar, 1.5)));
  height: auto;
  max-height: 72vh;
  border-radius: .5vmin;
}

/* ---------- frame styles ---------- */

/* Every frame dimension is a multiple of --fs, so the same classes render
   full-screen on the slideshow and thumbnail-sized in the editor preview.
   Hard-coded vmin would make a frame that looks right on a TV enormous inside
   a 40rem panel. */
body.tv { --fs: 1vmin; }
.palette-preview { --fs: 2.4px; }

/* Frames set custom properties; the mat rules below consume them with
   fallbacks. They deliberately do NOT restyle .mat directly - `.collage .mat-sm`
   is a two-class selector and would beat a one-class `.frame-x` on specificity
   AND source order, which silently made every frame render as Classic inside
   collages. Only frame classes ever set these variables, so nothing competes. */

.frame-gallery {
  --mat-bg: var(--surface);
  --mat-border: calc(.12 * var(--fs)) solid var(--accent);
  --mat-radius: 0;
  --mat-pad: calc(3.2 * var(--fs));
  --mat-pad-sm: calc(1.6 * var(--fs));
  --mat-shadow: 0 calc(1.5 * var(--fs)) calc(5 * var(--fs)) rgba(0, 0, 0, .5);
}

.frame-edge {
  --mat-bg: none;
  --mat-border: 0;
  --mat-radius: calc(.8 * var(--fs));
  --mat-pad: 0;
  --mat-pad-sm: 0;
  --mat-shadow: 0 calc(2.5 * var(--fs)) calc(7 * var(--fs)) rgba(0, 0, 0, .7);
}

.frame-film {
  position: relative;
  --mat-bg: #0b0b0c;
  --mat-border: 0;
  --mat-radius: calc(.3 * var(--fs));
  --mat-pad: calc(2.6 * var(--fs)) calc(1.1 * var(--fs));
  --mat-pad-sm: calc(1.8 * var(--fs)) calc(.7 * var(--fs));
  --mat-shadow: 0 calc(2 * var(--fs)) calc(6 * var(--fs)) rgba(0, 0, 0, .65);
}

/* Edge to edge. No mat, no padding, hairline gutters - the whole screen is
   photographs, which is what a wall of them on a television should look like.
   The gap is not zero on purpose: butted together, two similarly-lit photos
   read as one confusing image. */
.frame-mosaic {
  --mat-bg: none;
  --mat-border: 0;
  --mat-radius: 0;
  --mat-pad: 0;
  --mat-pad-sm: 0;
  --mat-shadow: none;
  --collage-gap: .4vmin;
  --collage-pad: 0vmin;
  --collage-pad-bottom: 5vmin;
}

/* Prints laid on a table. The angles come from tv.js, which gives every tile
   its own rather than only the pairs. */
.frame-scrapbook {
  --mat-bg: #fbf9f4;
  --mat-border: 0;
  --mat-radius: calc(.2 * var(--fs));
  --mat-pad: calc(1.4 * var(--fs));
  --mat-pad-sm: calc(.9 * var(--fs));
  --mat-shadow: 0 calc(1.2 * var(--fs)) calc(4 * var(--fs)) rgba(0, 0, 0, .55);
  --collage-gap: 2.4vmin;
  --collage-pad: 4vmin;
}

/* Its caption sits on the white border, not on the photograph. */
.frame-scrapbook figcaption { color: #2b2b2b; text-shadow: none; }

/* Sprocket holes, top and bottom. Sized off --fs so they stay in proportion at
   any scale rather than turning into specks on a TV or blobs in the preview. */
.frame-film::before,
.frame-film::after {
  content: "";
  position: absolute;
  left: calc(.6 * var(--fs));
  right: calc(.6 * var(--fs));
  height: calc(2.6 * var(--fs));
  background-image: radial-gradient(circle, #d8d5cd 0 calc(.38 * var(--fs)), transparent calc(.4 * var(--fs)));
  background-size: calc(2.2 * var(--fs)) calc(2.6 * var(--fs));
  background-position: center;
  opacity: .9;
}

.frame-film::before { top: 0; }
.frame-film::after { bottom: 0; }

.mat-sm.frame-film::before,
.mat-sm.frame-film::after {
  height: calc(1.8 * var(--fs));
  background-size: calc(1.5 * var(--fs)) calc(1.8 * var(--fs));
}

/* ---------- collage slides (large albums) ---------- */

/* Rows of equal-height tiles, sized in JS from each photo's real aspect ratio
   (see collage.js). CSS only centres them - it can't know the shapes. */
.collage {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* Mirrored in tv.js, which needs the same numbers to plan the tiles. A frame
     class on this container is what changes them. */
  gap: var(--collage-gap, 1.8vmin);
  padding: var(--collage-pad, 3.5vmin);
  /* Separate, because Mosaic runs to all four edges except this one - the
     corner QR card lives down there, and a television's overscan eats the
     last few percent of the picture. */
  padding-bottom: var(--collage-pad-bottom, var(--collage-pad, 3.5vmin));
}

.crow {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--collage-gap, 1.8vmin);
}

.collage .mat-sm {
  margin: 0;
  flex: 0 0 auto;
  display: flex;
  padding: var(--mat-pad-sm, .9vmin);
  background: var(--mat-bg, var(--surface));
  border: var(--mat-border, .28vmin solid var(--accent));
  border-radius: var(--mat-radius, 1vmin);
  box-shadow: var(--mat-shadow,
    0 0 6vmin -2vmin var(--accent),
    0 1.5vmin 4vmin rgba(0, 0, 0, .6));
}

.collage .mat-sm { position: relative; }

/* Overlaid on the tile so the name costs the photo no height. Sits directly on
   the photograph, so it needs to survive a bright one. */
.collage .mat-sm figcaption {
  position: absolute;
  left: 0;
  bottom: 0;
  max-width: 92%;
  padding: .3vmin .8vmin;
  font-family: var(--hand);
  font-size: 1.9vmin;
  font-weight: 400;
  color: #fff;
  text-shadow: 0 .15vmin 1vmin rgba(0, 0, 0, .95), 0 0 .4vmin rgba(0, 0, 0, .8);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* `cover` rather than `contain`: in a grid, contain leaves every tile padded
   with dead background and the photos end up small - which is the whole
   complaint. Edge cropping is the normal trade for a collage. */
.collage .mat-sm img {
  display: block;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  object-fit: cover;
  border-radius: .4vmin;
}

/* Applies to every tile; --tilt is 0 unless the row holds exactly two, so only
   side-by-side pairs actually lean (see tiltFor in motifs.js). Scaled down so a
   rotated corner can't overlap its neighbour. */
.collage .mat-sm {
  transform: rotate(var(--tilt, 0deg)) scale(.97);
}


/* Parked at top:0 and translated up by a full 100% of its own height, so it is
   guaranteed off-screen at any font size or viewport. The previous version sat
   at top:4vh and moved up only 140% of its height - on a 1080p screen that left
   roughly 20px of orange permanently peeking over the top of the slideshow. */


/* ---------- party-sign furniture ---------- */

/* Lettered pennant banner. Flags are absolutely placed along the same
   parabola the SVG string draws, so they hang ON the line rather than near it.
   Positions and angles come from JS (see bunting in motifs.js) because CSS
   can't compute a curve. */
.bunting {
  position: absolute;
  top: 0;
  /* Sized to its own letters and centred, rather than stretched edge to edge.
     Spreading the flags to fill the screen made "CHEERS" and a 20-character
     long phrase sit at wildly different spacings - a real banner has fixed flags and
     dead space either side. */
  left: 50%;
  transform: translateX(-50%);
  width: calc(var(--flag-w) * var(--w-units, 18));
  max-width: 92vw;
  height: 16vmin;
  color: var(--text);
  z-index: 1;
  pointer-events: none;
}

.bunting-string {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity .4s ease;
}

.bunting.on .bunting-string { opacity: .55; }

/* Shield shape: a rectangle notched to a V at the bottom - the party-shop
   pennant, not a plain triangle. */
.bunting {
  /* Fixed flag size. The min() is only an overflow guard for phrases long
     enough that fixed-size flags wouldn't fit the screen at all. */
  /* --w-units is the full string length in flag-widths, tails included, set by
     bunting() in motifs.js. The min() is only an overflow guard. */
  --flag-w: min(5.2vmin, calc(92vw / var(--w-units, 18)));
}

.bunting i {
  position: absolute;
  /* `top` - not translateY - because this percentage must resolve against the
     banner's height, which is what the string's sag is expressed in. */
  top: var(--drop);
  width: var(--flag-w);
  height: calc(var(--flag-w) * 1.35);
  margin-left: calc(var(--flag-w) / -2);   /* centre on its point on the curve */
  /* Flags drop onto the string one after another, each on its own delay, so a
     change reads as the banner being re-hung rather than swapped. */
  opacity: 0;
  transform: rotate(var(--lean)) translateY(-1.6vmin) scale(.86);
  transform-origin: top center;
  transition:
    opacity .42s ease var(--delay, 0ms),
    transform .52s cubic-bezier(.2, .9, .3, 1.25) var(--delay, 0ms);
  background: var(--c);
  clip-path: polygon(0 0, 100% 0, 100% 66%, 50% 100%, 0 66%);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: calc(var(--flag-w) * .16);
  font-family: var(--display);
  font-size: calc(var(--flag-w) * .58);
  font-weight: 800;
  line-height: 1;
  color: var(--flag-ink, var(--accent-ink));
  font-style: normal;
  box-shadow: 0 .6vmin 2vmin rgba(0, 0, 0, .35);
}

/* Word breaks hang a star instead of a letter, so the string has no holes. */
.bunting i.star { font-size: calc(var(--flag-w) * .66); }

/* Hung in front of everything, including the full-screen cards, so it reads as
   a banner across the room rather than part of any one screen. */
.bunting.persistent { position: fixed; z-index: 4; }

/* Visibility is carried by the flags themselves, so the whole banner doesn't
   fade as one block on top of the per-letter animation. */
.bunting.on i { opacity: 1; transform: rotate(var(--lean)) scale(1); }

@media (prefers-reduced-motion: reduce) {
  .bunting i { transition-duration: .01ms; transition-delay: 0ms; }
}

/* Big-text banner: the alternative to the pennant string. Sized in vmin like
   everything else on the TV, so it holds from a laptop to a projector. */
.marquee {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  padding: 3.4vmin 4vmin 2.2vmin;
  text-align: center;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  transform: translateY(-1.4vmin);
  transition: opacity .42s ease, transform .5s cubic-bezier(.2, .9, .3, 1.25);
}

.marquee.on { opacity: 1; transform: translateY(0); }

.marquee span {
  display: inline-block;
  max-width: 92vw;
  font-family: var(--display);
  font-weight: 800;
  font-size: 6vmin;
  line-height: 1.05;
  letter-spacing: .03em;
  color: var(--accent);
  /* The banner floats over photographs, so it needs to survive a bright one. */
  text-shadow: 0 .35vmin 1.8vmin rgba(0, 0, 0, .55);
}

@media (prefers-reduced-motion: reduce) {
  .marquee { transition-duration: .01ms; }
}

/* Background-text banner: one phrase blown up to fill the whole screen, sitting
   behind the photos as the actual background. z-index -1 puts it under the
   matted photo (z-index 0) but above the body fill. It is one of the three
   banner styles, so it never shares the screen with the top-strip banners. */
.backdrop {
  position: fixed;
  inset: 0;
  /* z-index 0, NOT -1: a negative z-index would sit behind the body's own
     background fill (the solid palette colour in bg-text mode) and vanish. It is
     instead inserted before #stage in the DOM (see tv.js), so the photos still
     stack above it while it sits above the body fill. */
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  /* Row height unit. On the full-screen show this is 1vh (the box IS the
     viewport); the app preview overrides it to 1cqh so the same rows fill its
     small box. Kept as a variable rather than cqh everywhere because
     container-type on a fixed element can resolve cqh to zero - font-size 0,
     invisible text - which is exactly why this never showed on the slideshow. */
  --u: 1vh;
  opacity: 0;
  transition: opacity .5s ease;
}
.backdrop.on { opacity: .15; }
/* The tilted, oversized layer. Bigger than the outer on every side so a 30deg
   rotation leaves no bare corner; the outer clips it back to the screen. */
.backdrop-inner {
  position: absolute;
  inset: -50%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--u);
  transform: rotate(-30deg);
}
.backdrop-row {
  white-space: nowrap;
  text-align: center;
  font-family: var(--display);
  font-weight: 800;
  font-size: calc(8 * var(--u));
  line-height: 1;
  letter-spacing: .04em;
  color: var(--accent);
  /* Per-row phase offset set in motifs.js, so the lines never line up. */
  transform: translateX(var(--shift, 0));
}

/* In this mode the stretched phrase is the background, so the per-slide blurred
   fill and its tint drop out and the body wears the palette colour instead. */
body.tv.bg-text { background: var(--bg); }
body.tv.bg-text .slide-bg,
body.tv.bg-text .slide-tint { display: none; }

@media (prefers-reduced-motion: reduce) {
  .backdrop { transition-duration: .01ms; }
}

/* Sunburst rays behind a headline. Masked to a soft circle so the rays fade
   out instead of ending in a hard edge at the screen border. */
.starburst {
  position: absolute;
  inset: -25%;
  background: repeating-conic-gradient(
    from 0deg, var(--accent) 0deg 5deg, transparent 5deg 12deg);
  opacity: .12;
  -webkit-mask-image: radial-gradient(closest-side, #000 25%, transparent 72%);
  mask-image: radial-gradient(closest-side, #000 25%, transparent 72%);
  z-index: 0;
  pointer-events: none;
}

/* Corner QR. Sized in vmin so it keys off the SMALLER viewport dimension -
   the same physical proportion on a 24" monitor and a 75" TV, no breakpoints. */
#corner {
  position: fixed;
  right: 2.5vmin; bottom: 2.5vmin;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .9vmin;
  padding: 1.2vmin;
  background: rgba(14,13,19,.72);
  backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,.1);
  border-radius: 1.4vmin;
  opacity: 0;
  transition: opacity .8s ease;
}

#corner.on { opacity: 1; }
/* The corner code is a standing reminder, not the one people cross a room to
   scan - that's the full-screen card. It only needs to be big enough to read
   from a phone that's already pointed at the screen. */
#corner .qr { width: 9.5vmin; padding: .8vmin; background: #fff; border-radius: .8vmin; }
#corner .qr svg { display: block; width: 100%; height: auto; }

/* Now the whole call to action, so it carries the weight the URL used to. */
/* Wraps to two lines instead of forcing the panel as wide as the sentence.
   Uppercase display lettering is much wider than the sentence case it replaced,
   which is what pushed this whole card oversized. */
#corner .label {
  font-family: var(--display);
  font-size: 1.05vmin;
  font-weight: 700;
  line-height: 1.2;
  /* Title case, not uppercase - the all-caps treatment belongs to the banner
     and the headline cards, where it reads as signage. Down here it just made
     a small label shout and forced the panel wider. */
  letter-spacing: .02em;
  text-align: center;
  max-width: 9.5vmin;
  color: var(--accent);
}

/* Full-screen QR interstitial + idle splash share this layout. */
.fullcard {
  position: fixed;
  inset: 0;
  /* Small, because these cards never carry a banner: setBannerAllowed(false)
     runs whenever one is shown. It used to reserve 16vmin for a banner that is
     not there, which on a landscape screen is a sixth of the whole height. */
  padding: 3vmin 4vmin;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.8vmin;
  text-align: center;
  background: var(--bg);
  opacity: 0;
  pointer-events: none;
  transition: opacity .8s ease;
  z-index: 3;
}

.fullcard.on { opacity: 1; }

.fullcard .brand {
  font-family: var(--display);
  font-size: 7vmin;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .02em;
  line-height: 1;
}

#splash .brand { font-size: 8.5vmin; }

/* ---------- theme decoration ---------- */

.fullcard .year {
  font-size: 9vmin;
  font-weight: 800;
  line-height: .9;
  letter-spacing: -.04em;
  font-family: var(--display);
  letter-spacing: .06em;
  color: var(--accent);
  opacity: .95;
}

.motifs {
  display: flex;
  gap: 3vmin;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  opacity: .8;
}

.motifs svg {
  width: 7vmin;
  height: 7vmin;
  fill: none;
  stroke: currentColor;
  stroke-width: 3.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.confetti {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

/* The show-wide layer: sits above photo slides but below the full-screen cards,
   which are opaque and carry their own. Held back in opacity because it drifts
   over people's photographs, where the card version has only flat colour
   behind it. */
.confetti.ambient {
  position: fixed;
  z-index: 2;
}

.confetti.ambient i { opacity: .38; }

/* Keep card content above the drifting pieces. */
.fullcard > *:not(.confetti):not(.bubbles):not(.sparkles) { position: relative; z-index: 1; }

.confetti i {
  position: absolute;
  top: -10vmin;
  width: 1.1vmin;
  height: 2.2vmin;
  background: var(--c);
  border-radius: .2vmin;
  opacity: .8;
  animation-name: confetti-fall;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.confetti i.round { border-radius: 50%; height: 1.1vmin; }

@keyframes confetti-fall {
  to { transform: translate3d(var(--drift, 0), 115vh, 0) rotate(720deg); }
}

/* Rising bubbles: the same layer contract as confetti (absolute in a box,
   fixed and show-wide when ambient), only drifting upward. Pieces are round,
   translucent, and lit from the upper-left so they read as bubbles rather than
   dots. --size is unitless so the same value scales to vmin here and px in the
   app preview. */
.bubbles {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.bubbles.ambient { position: fixed; z-index: 2; }
.bubbles i {
  position: absolute;
  bottom: -14vmin;
  width: calc(var(--size, 2) * 1vmin);
  height: calc(var(--size, 2) * 1vmin);
  background: radial-gradient(circle at 32% 28%, rgba(255, 255, 255, .6), var(--c) 65%);
  border-radius: 50%;
  opacity: .4;
  animation-name: bubble-rise;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.bubbles.ambient i { opacity: .32; }

@keyframes bubble-rise {
  to { transform: translate3d(var(--drift, 0), -125vh, 0); }
}

/* Sparkles: scattered four-point stars that twinkle on and off. Unlike confetti
   and bubbles there is no travel - each stays put and flashes. The cycle is long
   but the lit slice (see the keyframes) is short, so with the staggered delays
   only one or two glint at a time. Shape is a clip-path star, no image needed. */
.sparkles {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.sparkles.ambient { position: fixed; z-index: 2; }
.sparkles i {
  position: absolute;
  width: calc(var(--size, 2) * 1vmin);
  height: calc(var(--size, 2) * 1vmin);
  /* A bright white core fading to gold arms, for a bit of dimension. The same
     on every background - gold reads on dark and light alike, and the white
     centre keeps the glint. */
  background: radial-gradient(circle at 50% 45%, #fff 0%, #fff 24%, #b8860b 82%);
  clip-path: polygon(50% 0%, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0% 50%, 40% 40%);
  opacity: 0;
  animation-name: sparkle-twinkle;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.sparkles.ambient i { --peak: .85; }

/* A brief flash near the very start of the cycle, dark for the long rest - a
   quick glint and a scale-and-turn, gone almost as soon as it lands. */
@keyframes sparkle-twinkle {
  0%, 100% { opacity: 0; transform: scale(.2) rotate(0deg); }
  4%       { opacity: var(--peak, 1); transform: scale(1.1) rotate(45deg); }
  10%      { opacity: 0; transform: scale(.2) rotate(90deg); }
}

/* A slideshow runs for hours; motion sensitivity deserves a real opt-out. */
@media (prefers-reduced-motion: reduce) {
  .confetti, .bubbles, .sparkles { display: none; }
}
.fullcard .tagline { font-size: 2.4vmin; }
/* The QR is the only thing on this card with a job. Everything else supports
   it, so it gets the white plate, the size, and the lift. */
.fullcard .qr {
  width: 30vmin;
  padding: 2vmin;
  background: #fff;
  border-radius: 2vmin;
  box-shadow: 0 3vmin 8vmin rgba(0, 0, 0, .55);
}

.fullcard .qr svg { display: block; width: 100%; height: auto; }

/* States the purpose. Without it the card is a barcode with no verb - people
   glance at it, don't know what it does, and look away. */
/* The instruction, set like sign lettering rather than app copy. */
.fullcard .kicker {
  font-family: var(--display);
  font-size: 4vmin;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--text);
}

.fullcard .sub {
  font-size: 2.4vmin;
  font-weight: 500;
  color: var(--muted);
}

.fullcard .tagline {
  font-family: var(--hand);
  font-size: 3vmin;
  font-style: normal;
  opacity: .85;
}

/* On the QR card the tagline is a headline element, not a footnote - it sits
   directly under the brand and has to hold its own against the code below it. */
#qrcard .tagline { font-size: 3.4vmin; opacity: .95; margin-bottom: 1.4vmin; }
#qrcard .brand { margin-bottom: -.5vmin; }

/* The logo, above the wordmark. Sized in vmin like everything else on this
   screen, and lifted above the scattered-photo backdrop, which is absolutely
   positioned at z-index 0 and would otherwise sit over it. */
.fullcard .card-mark {
  position: relative;
  z-index: 1;
  width: 8vmin;
  height: 8vmin;
  margin-bottom: -1vmin;
}

/* The mark is navy with bright blades - legible on a light theme, but on a
   near-black background its outer ring disappears into the screen. A faint
   halo keeps its edge without putting a hard box around it. */
.fullcard .card-mark {
  filter: drop-shadow(0 0 1.4vmin rgba(255, 255, 255, .18));
}

/* Photos from the album, scattered behind the card. Held well back in opacity
   and saturation - this is texture, and it must never compete with the code
   a camera has to lock onto. */
.qr-stack {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

.qr-stack img {
  position: absolute;
  width: 26vmin;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border: .7vmin solid var(--surface-2);
  border-radius: 1vmin;
  box-shadow: 0 2vmin 5vmin rgba(0, 0, 0, .6);
  opacity: .3;
  filter: saturate(.65);
  transform: rotate(var(--tilt, 0deg));
}

/* Fixed positions rather than random: they must stay clear of the centre where
   the QR sits, and randomness eventually drops one across it. */
.qr-stack img:nth-child(1) { left: -4vmin;  top: 14vh; }
.qr-stack img:nth-child(2) { right: -4vmin; top: 9vh; }
.qr-stack img:nth-child(3) { left: 5vmin;   bottom: 7vh; }
.qr-stack img:nth-child(4) { right: 6vmin;  bottom: 11vh; }
.qr-stack img:nth-child(5) { left: 50%; margin-left: -13vmin; top: -7vmin; }


/* Only visible on mouse movement, so it never shows up in photos of the TV. */
#back {
  position: fixed;
  top: 2.5vmin; left: 2.5vmin;
  padding: 1.2vmin 2vmin;
  font: inherit;
  font-size: 1.9vmin;
  color: var(--text);
  background: rgba(14,13,19,.8);
  backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,.14);
  border-radius: 1vmin;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity .35s ease;
  z-index: 10;
}

#back.on { opacity: 1; pointer-events: auto; }

/* Opposite corner to the back button, on the same idle timer. */
#controls {
  position: fixed;
  top: 2.5vmin;
  right: 2.5vmin;
  display: flex;
  gap: .8vmin;
  opacity: 0;
  pointer-events: none;
  transition: opacity .35s ease;
  z-index: 10;
}

#controls.on { opacity: 1; pointer-events: auto; }

/* Sized for a room, not a desk. */
.tv-btn {
  width: 6vmin;
  height: 6vmin;
  display: grid;
  place-items: center;
  font: inherit;
  font-size: 2.6vmin;
  line-height: 1;
  color: #f4f2ef;
  background: rgba(14, 13, 19, .8);
  backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: 1vmin;
  cursor: pointer;
}

.tv-btn:hover { background: rgba(14, 13, 19, .95); }
.tv-btn:active { transform: scale(.93); }
.tv-btn.is-live { border-color: var(--accent); color: var(--accent); }


/* ---------- bright site theme (landing, host app, guest upload) ------------
   The slideshow keeps the dark :root palette above - it is a stage for
   photographs, and a light background washes them out. The pages people
   actually touch get the logo's colours instead: navy ink, balloon sky / sun /
   pop, paper-white cards with hard offset shadows, printed the way party
   stationery is.

   All of it is scoped to body.site, so none of it can reach the TV.
*/

/* Tokens live on <html>, not <body>. `html, body { background: var(--bg) }` at
   the top of this file means the root element paints the canvas - so with the
   light theme on body only, html kept painting :root's near-black wherever the
   body ran short, and the fixed confetti drifting over it made the strip look
   alive. It also means an inline palette from applyPalette(…, documentElement)
   still beats this block, which is how the upload page gets the event's colors. */
html.site {
  --bg: #eef3fa;
  --surface: #ffffff;
  --surface-2: #dbe8f6;
  --border: #c9d9ea;
  --text: #1e3a5f;
  --muted: #556e8b;
  --accent: #d4145a;
  --accent-ink: #ffffff;
  --danger: #c02a3d;
  --radius: 18px;

  /* Straight off the logo. --pop is the undarkened magenta: it never carries
     text, so it keeps the brand's full brightness in places only the eye has
     to read. --accent is the same hue darkened until white type clears 4.5:1
     on it, because that one does carry text. */
  --navy: #1e3a5f;
  --sky:  #4db8e8;
  --sun:  #ffc93c;
  --pop:  #ec1e63;

  /* Soft depth, tinted with the brand navy rather than neutral grey, so
     shadows read as part of the palette instead of dirt on the page. */
  --lift: 0 2px 10px rgba(30, 58, 95, .09);
  --lift-hi: 0 6px 18px rgba(30, 58, 95, .15);
}

body.site .card,
body.site .toggle,
body.site .qr-panel,
body.site .step,
body.site .banner-list li {
  border: 1px solid var(--border);
  box-shadow: var(--lift);
}

body.site .btn {
  font-weight: 700;
  box-shadow: var(--lift);
  transition: box-shadow .15s ease, transform .1s ease, filter .15s ease;
}

body.site .btn:hover:not(:disabled) { box-shadow: var(--lift-hi); filter: brightness(1.04); }
body.site .btn:active:not(:disabled) { transform: translateY(1px); box-shadow: var(--lift); }
body.site .btn:disabled { opacity: .55; box-shadow: none; }

body.site .card { transition: box-shadow .15s ease, transform .1s ease; }
body.site .card:hover { box-shadow: var(--lift-hi); transform: translateY(-2px); }

body.site h1, body.site h2, body.site .wordmark {
  font-family: var(--display);
  font-weight: 800;
  letter-spacing: -.015em;
}

body.site .tagline {
  font-family: var(--hand);
  font-style: normal;
  font-size: 1.05rem;
  color: var(--accent);
}

/* Ambient confetti sits behind the whole page rather than one box. */
body.site .confetti { position: fixed; opacity: .55; z-index: 0; }
body.site main, body.site .topbar { position: relative; z-index: 1; }

/* ---------- landing hero ---------- */

.hero { position: relative; text-align: center; padding-top: .5rem; }

/* The bunting is absolutely positioned against its own box, so it needs one. */
.hero-banner {
  position: relative;
  height: 17vmin;
  min-height: 92px;
  margin-bottom: .5rem;
}

.hero .mark { width: 76px; height: 76px; display: block; margin: 0 auto .75rem; }

.wordmark {
  font-size: clamp(1.9rem, 8.5vw, 3rem);
  line-height: 1.05;
  color: var(--navy);
  margin: 0;
}

.wordmark .dot { color: var(--muted); font-weight: 600; }

.hero .lead {
  max-width: 26rem;
  margin: 1rem auto 0;
  line-height: 1.65;
  color: var(--muted);
}

/* ---------- how it works ---------- */

.steps {
  display: grid;
  gap: 1.1rem;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  margin: 2.5rem 0 1rem;
  list-style: none;
  padding: 0;
}

.step {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 1.35rem 1rem 1.15rem;
  text-align: center;
  font-size: .92rem;
  line-height: 1.5;
}

/* A hair of rotation, alternating, so the row reads as things laid on a table
   rather than a grid. Fixed by position - random signs look like misalignment. */
.step:nth-child(odd)  { transform: rotate(-1.3deg); }
.step:nth-child(even) { transform: rotate(1.3deg); }

.step .n {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  margin: 0 auto .7rem;
  border-radius: 50%;
  background: var(--tint, var(--sky));
  color: var(--navy);
  font-family: var(--display);
  font-weight: 800;
  font-size: 1.15rem;
}

.step:nth-child(1) { --tint: var(--sky); }
.step:nth-child(2) { --tint: var(--sun); }
.step:nth-child(3) { --tint: var(--pop); }
.step:nth-child(3) .n { color: #fff; }

.step b { display: block; font-size: 1rem; margin-bottom: .2rem; color: var(--text); }

/* ---------- host app flourishes ---------- */

body.site .topbar {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--lift);
}

body.site .topbar .brand {
  display: flex;
  align-items: center;
  gap: .5rem;
  font-family: var(--display);
  font-weight: 800;
  font-size: 1.05rem;
  color: var(--navy);
}

body.site .topbar .brand img { width: 26px; height: 26px; display: block; }

/* Each event gets a colour off the logo, cycling. Keyed to position so a host's
   list stays stable and they can find "the yellow one" twice running. */
body.site .card { border-left: 8px solid var(--tint, var(--sky)); }
body.site .card:nth-child(4n+1) { --tint: var(--sky); }
body.site .card:nth-child(4n+2) { --tint: var(--sun); }
body.site .card:nth-child(4n+3) { --tint: var(--pop); }
body.site .card:nth-child(4n+4) { --tint: var(--navy); }

body.site .tab.is-active { border-bottom-width: 3px; margin-bottom: -2px; }

body.site .empty { border: 1px dashed var(--border); }

/* ---------- guest upload ---------- */

/* The one action that matters on a phone at a party, sized accordingly. */
body.site .btn.big {
  padding: 1.5rem 1.1rem;
  font-size: 1.25rem;
}

body.site .btn.big .emoji { font-size: 1.5rem; }

/* ---------- logo lockup ---------- */

.logo-lockup {
  display: block;
  width: min(100%, 21rem);
  height: auto;
  margin: 0 auto;
}

/* The wordmark is live text in the SVG, so it renders in the same face as the
   rest of the site. textLength on the <text> pins its width, which keeps the
   lockup identical whether Futura, Poppins or the system rounded face wins. */
.logo-lockup text {
  font-family: var(--display);
  font-weight: 800;
  letter-spacing: -.01em;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- photographic backdrop ---------- */

/* A real party behind the interface, turned right down. Opt-in per page via
   `photo` on <html>: the guest upload page deliberately never loads it, since
   every guest at an event fetches that page over whatever wifi the venue has.

   Painted as a fixed pseudo-element rather than `background-attachment: fixed`,
   which stutters badly on iOS. It sits at z-index 0, under the z-index 1 the
   site theme already gives main and the topbar. */
html.photo body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  background: url('/party.jpg?v=1') center / cover no-repeat;
  opacity: .09;
  pointer-events: none;
}

/* Phones show a sliver of a 16:9 photo, which reads as noise rather than a
   room full of people - and it is the one place the bytes are least welcome. */
@media (max-width: 34rem) {
  html.photo body::before { opacity: .06; }
}

/* ---------- lettering pairs ---------- */

/* Each class swaps the two font tokens the slideshow reads: --display for
   banners and headings, --hand for the guest's credit on a photo. Written as
   plain classes rather than body-scoped, so the host app can put one on its
   preview box and show the choice without a page of its own.

   System faces only. macOS candidates first, then Windows, then a generic -
   nothing here can trigger a download. */

.font-rounded {
  --display: "Futura", "Avenir Next", "Century Gothic", "Poppins",
             ui-rounded, "SF Pro Rounded", system-ui, sans-serif;
  --hand: "Marker Felt", "Chalkboard SE", "Segoe Print", "Ink Free",
          "Bradley Hand", "Comic Sans MS", cursive;
}

.font-classic {
  --display: "Didot", "Bodoni 72", "Hoefler Text", "Baskerville",
             "Palatino Linotype", Georgia, "Times New Roman", serif;
  /* The one stack that is genuinely cursive. That is wrong for a default - a
     signature nobody can read is not a credit - but chosen deliberately for a
     wedding it is exactly right. */
  --hand: "Snell Roundhand", "Apple Chancery", "Segoe Script",
          "Brush Script MT", cursive;
}

.font-modern {
  --display: "Helvetica Neue", "Inter", "Segoe UI", Roboto, Arial,
             system-ui, sans-serif;
  /* Deliberately the same face as the banner: this is the option for someone
     who wants no handwriting anywhere. */
  --hand: "Helvetica Neue", "Inter", "Segoe UI", Roboto, Arial,
          system-ui, sans-serif;
}

.font-typewriter {
  --display: "American Typewriter", "Courier New", Consolas,
             ui-monospace, monospace;
  --hand: "American Typewriter", "Courier New", Consolas,
          ui-monospace, monospace;
}

.font-marker {
  --display: "Marker Felt", "Chalkboard SE", "Segoe Print", "Ink Free",
             "Bradley Hand", "Comic Sans MS", cursive;
  --hand: "Marker Felt", "Chalkboard SE", "Segoe Print", "Ink Free",
          "Bradley Hand", "Comic Sans MS", cursive;
}

/* Monospace runs narrow at the same size, and the pennant flags are sized from
   the flag width rather than the text - so without this the letters rattle
   around inside them. */
.font-typewriter .bunting i { font-size: calc(var(--flag-w) * .5); }
.font-marker .bunting i { font-size: calc(var(--flag-w) * .54); }

/* ---------- lettering picker ---------- */

.font-picker {
  display: grid;
  gap: .6rem;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  margin-top: .4rem;
}

.font-card {
  display: block;
  margin: 0;
  padding: .9rem .8rem .7rem;
  text-align: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  cursor: pointer;
}

/* The radio still exists and still owns the state - it is only moved out of
   sight, so keyboard and screen-reader users get a real radio group. */
.font-card input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.font-card:has(input:checked) {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.font-card:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The two samples are the point: each is drawn with the token the slideshow
   reads, so the card is a rehearsal rather than a description. */
.font-card .fc-banner {
  display: block;
  font-family: var(--display);
  font-weight: 800;
  font-size: 1.05rem;
  line-height: 1.15;
  color: var(--text);
  overflow-wrap: anywhere;
}

.font-card .fc-credit {
  display: block;
  margin-top: .2rem;
  font-family: var(--hand);
  font-size: .85rem;
  color: var(--muted);
}

/* Deliberately NOT in the sampled face - this is the name of the option, and
   it has to stay readable when the option itself is a flowing script. */
.font-card .fc-label {
  display: block;
  margin-top: .6rem;
  font-size: .68rem;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Tabs share the width instead of bunching at the left edge. flex-basis 0 makes
   the share equal, while the default min-width:auto on a flex item stops any
   tab shrinking below its own label - so "Look & Feel" never clips, and on a
   narrow phone the row simply scrolls as it did before. */
body.site .tabs { gap: 0; }

body.site .tab {
  flex: 1 1 0;
  text-align: center;
}

/* ---------- guest album ---------- */

#album { margin-top: 2.25rem; }
#album h2 { margin-bottom: .25rem; }

.grid.album { margin-top: 1rem; }

.grid.album .tile {
  position: relative;
  display: block;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 10px;
  background: var(--surface);
  border: 1px solid var(--border);
}

.grid.album .tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Marks a video without needing a second element per tile. */
.grid.album .tile.is-video::after {
  content: '▶';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 2.2rem;
  height: 2.2rem;
  display: grid;
  place-items: center;
  font-size: .9rem;
  color: #fff;
  background: rgba(0, 0, 0, .55);
  border-radius: 50%;
}

.grid.album .tile-by {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: .5rem .45rem .3rem;
  font-family: var(--hand);
  font-size: .75rem;
  color: #fff;
  background: linear-gradient(transparent, rgba(0, 0, 0, .6));
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#album-more { margin-top: 1rem; }

/* ---------- guest page topbar ---------- */

/* This bar sits on the event's palette, not the brand's, so its colours are
   whatever the host chose. Everything below reads from the derived tokens for
   that reason - a fixed navy would be invisible on a dark party theme. */
.guest-bar {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.guest-bar .brand,
.guest-bar .bar-cta {
  text-decoration: none;
}

/* Beats `body.site .topbar .brand`, which paints the brand navy for the host
   app - correct there, unreadable here the moment a host picks a dark theme. */
body.site .topbar.guest-bar .brand {
  color: var(--text);
}

.guest-bar .bar-cta {
  flex: 0 0 auto;
  padding: .45rem .8rem;
  font-size: .85rem;
  font-weight: 600;
  color: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 999px;
  white-space: nowrap;
}

/* The bar is the brand, not a toolbar - on a narrow phone the invitation can
   go rather than squeeze the name it is inviting people to. */
@media (max-width: 24rem) {
  .guest-bar .bar-cta { display: none; }
}

/* ---------- colour presets + hex entry ---------- */

/* One row, equal-spaced, whatever the screen width - the chips share the space
   rather than wrapping to a ragged second line. */
.palettes {
  display: flex;
  flex-wrap: nowrap;
  gap: .25rem;
  margin: .4rem 0 1rem;
}
.palettes .palette-chip {
  flex: 1 1 0;
  width: auto;
  min-width: 0;
  border-radius: 5px;
}

/* Each chip shows all three roles in order, so the choice is legible before
   it is made rather than after. */
.palette-chip {
  display: flex;
  width: 3.6rem;
  height: 1.9rem;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 7px;
  cursor: pointer;
  background: none;
}

.palette-chip span { flex: 1; }
.palette-chip span:first-child { flex: 1.6; }   /* the background, weighted as it appears */

.palette-chip:hover { transform: translateY(-1px); }
.palette-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.picker-head .hex {
  width: 6.5rem;
  padding: .35rem .4rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .8rem;
  text-align: center;
  text-transform: lowercase;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.picker-head .hex:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---------- colour area picker ---------- */

.picker { margin: 0 0 1rem; }

.picker-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  margin-bottom: .5rem;
  font-size: .85rem;
  color: var(--muted);
}

.picker-head .btn { margin-top: 0; width: auto; padding: .35rem .7rem; font-size: .8rem; }

.cp { display: grid; gap: .55rem; }

/* Two stacked gradients: white-to-hue across, transparent-to-black down. That
   is exactly the HSV square, with no canvas and nothing to repaint. */
.cp-area {
  position: relative;
  height: 9rem;
  border-radius: 10px;
  border: 1px solid var(--border);
  cursor: crosshair;
  touch-action: none;              /* or a drag scrolls the page on a phone */
  background:
    linear-gradient(to top, #000, rgba(0, 0, 0, 0)),
    linear-gradient(to right, #fff, var(--cp-hue, #f00));
}

.cp-hue {
  position: relative;
  height: 1.1rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  cursor: ew-resize;
  touch-action: none;
  background: linear-gradient(to right,
    #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}

.cp-dot {
  position: absolute;
  width: 15px;
  height: 15px;
  margin: -8px 0 0 -8px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .55), 0 1px 3px rgba(0, 0, 0, .4);
  pointer-events: none;
}

.cp-hue .cp-dot { top: 50%; }

.cp-area:focus-visible, .cp-hue:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The swatch is now a button that selects which role the area edits. */
.swatch-chip {
  display: block;
  width: 100%;
  height: 2.4rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
}

.swatch.is-target .swatch-chip {
  box-shadow: 0 0 0 2px var(--accent);
}

/* ---------- landing demo links ---------- */

.demo-links { margin-top: 2.5rem; text-align: center; }
.demo-links h2 { margin-bottom: .25rem; }
.demo-links .row { display: flex; gap: .6rem; margin-top: 1rem; }
.demo-links .row .btn { margin-top: 0; }

@media (max-width: 26rem) {
  .demo-links .row { flex-direction: column; }
}

/* ---------- the sample event ---------- */

.sample-card .meta { line-height: 1.5; }

body.site .sample-card {
  border-left-color: var(--sun);
  background: linear-gradient(180deg, var(--surface), var(--surface-2));
}

.demo-note {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 1rem 0 0;
  padding: .85rem 1rem;
  font-size: .9rem;
  line-height: 1.5;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 8px solid var(--sun, var(--accent));
  border-radius: var(--radius);
}

.demo-note .btn { flex: 0 0 auto; margin-top: 0; width: auto; padding: .45rem .9rem; }

/* Nothing on this page writes to the shared album, so the controls that would
   are not merely disabled, they are gone. */
body.is-demo .tile-actions { display: none; }

/* ---------- guest photo viewer ---------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  padding: .75rem;
  background: rgba(8, 10, 16, .93);
  backdrop-filter: blur(6px);
}

/* Deliberately not `--text`: this sits on its own near-black backdrop, not on
   the event's background, so the palette's ink could be the wrong colour here. */
.lightbox-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex: 0 0 auto;
  color: #f4f2ef;
}

.lightbox-by {
  font-family: var(--hand);
  font-size: .95rem;
  opacity: .85;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.lightbox-actions { display: flex; align-items: center; gap: .5rem; flex: 0 0 auto; }
.lightbox-actions .btn { margin-top: 0; width: auto; padding: .5rem .9rem; font-size: .9rem; }

.lightbox-close {
  width: 2.4rem;
  height: 2.4rem;
  font: inherit;
  font-size: 1.1rem;
  color: #f4f2ef;
  background: rgba(255, 255, 255, .12);
  border: 1px solid rgba(255, 255, 255, .2);
  border-radius: 50%;
  cursor: pointer;
}

.lightbox-close:hover { background: rgba(255, 255, 255, .2); }

/* min-height:0 lets the flex child actually shrink, without which the image
   overflows the viewport instead of fitting it. */
.lightbox-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  /* Explicit tracks, not the default auto: an auto track is sized to the image,
     so the image's own max-height:100% resolves against itself and never caps.
     minmax(0, 1fr) makes the single cell the size of the stage instead, which is
     what the image is then bounded to - otherwise a tall photo overflows the
     bottom on a wide screen. */
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
  place-items: center;
  padding: .75rem 0;
}

.lightbox-stage img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 8px;
}

/* On-screen prev/next arrows. Pointer devices only - phones and tablets use the
   swipe instead, so these are hidden on anything without a fine hovering pointer. */
.lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  font-size: 2rem;
  line-height: 1;
  color: #f4f2ef;
  background: rgba(255, 255, 255, .12);
  border: 1px solid rgba(255, 255, 255, .2);
  border-radius: 50%;
  cursor: pointer;
}
.lightbox-prev { left: .5rem; }
.lightbox-next { right: .5rem; }
.lightbox-nav:hover { background: rgba(255, 255, 255, .22); }
.lightbox-nav:disabled { opacity: .25; cursor: default; }

@media (hover: none), (pointer: coarse) {
  .lightbox-nav { display: none; }
}

.lightbox .status { flex: 0 0 auto; text-align: center; color: #d8d5e2; }
.lightbox .status.error { color: #ff9c94; }

/* The album must not scroll behind the viewer on a phone. */
body.is-locked { overflow: hidden; }

/* ---------- access code, deliberately out of the way ---------- */

.code-entry { margin-top: .75rem; text-align: center; }
.code-entry .add-row { margin-top: .6rem; }
.code-entry .status { margin-top: .5rem; font-size: .8rem; }

.linkish {
  font: inherit;
  font-size: .8rem;
  color: var(--muted);
  background: none;
  border: 0;
  padding: .25rem;
  text-decoration: underline;
  cursor: pointer;
}

.linkish:hover { color: var(--text); }

/* ---------- pricing ---------- */

.tiers {
  display: grid;
  gap: 1.25rem;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  margin: 2rem 0 3rem;
}

.tier {
  display: flex;
  flex-direction: column;
  padding: 1.5rem 1.25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--lift);
}

.tier.is-featured {
  border-color: var(--accent);
  box-shadow: var(--lift-hi);
}

.tier h2 { margin: 0; font-size: 1.05rem; }

.tier-price {
  margin: .35rem 0 .25rem;
  font-family: var(--display);
  font-weight: 800;
  font-size: 2.4rem;
  line-height: 1;
  color: var(--navy);
}

.tier-price span { font-size: .9rem; font-weight: 600; color: var(--muted); }
.tier-note { margin: 0 0 1rem; line-height: 1.5; }

.tier-features {
  list-style: none;
  margin: 0 0 1.25rem;
  padding: 0;
  font-size: .9rem;
  line-height: 1.5;
  /* Pushes the button to the bottom so the cards' CTAs line up whatever the
     feature list length. */
  flex: 1 1 auto;
}

.tier-features li { padding-left: 1.5rem; position: relative; margin-bottom: .4rem; }
.tier-features li::before { position: absolute; left: 0; font-weight: 700; }
.tier-features li.yes::before { content: '✓'; color: var(--accent); }

/* What this tier ADDS over the one below reads bold, so the reason to step up
   is what stands out; inherited features stay in the regular weight. */
.tier-features li.added { font-weight: 700; color: var(--text); }

/* Excluded features stay listed rather than hidden - what a plan does NOT
   include is the reason to look at the next one along - but struck through and
   greyed so the difference between plans reads at a glance, not just a mark. */
.tier-features li.no { color: var(--muted); text-decoration: line-through; }
.tier-features li.no::before { content: '✕'; color: var(--muted); text-decoration: none; }

.tier .btn { margin-top: 0; }

.upgrading {
  margin: 0 0 -.5rem;
  text-align: center;
  font-size: .95rem;
  color: var(--muted);
}

/* The plan an event is already on. Marked rather than hidden - seeing where you
   are is what makes the row above it mean anything. */
.tier.is-current {
  border-color: var(--accent);
  background: var(--surface-2);
}

.tier.is-current::before {
  content: 'Current';
  align-self: flex-start;
  margin-bottom: .4rem;
  padding: .15rem .5rem;
  font-size: .65rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--accent-ink);
  background: var(--accent);
  border-radius: 999px;
}

/* ---------- landing price line ---------- */

.price-line { margin-top: 2.75rem; text-align: center; }
.price-line h2 { margin-bottom: .35rem; }
.price-line p { max-width: 26rem; margin: 0 auto; line-height: 1.6; }
.price-line .btn { max-width: 16rem; margin-left: auto; margin-right: auto; }

/* Who added a photo. A host scanning their own album needs to tell at a glance
   what came from the room and what they put there themselves, and the name is
   the only apology anyone gets for a picture they want taken down. */
.tile-who {
  position: absolute;
  top: 4px;
  right: 4px;
  max-width: calc(100% - 52px);
  padding: .15rem .4rem;
  font-size: .65rem;
  font-weight: 700;
  line-height: 1.4;
  border-radius: 5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A guest's photo is outlined, so the answer to "what did the room send me" is
   visible at grid scale rather than one tile at a time. */
.grid figure.tile.from-guest {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Guests stand out; your own uploads recede. The interesting case is the one
   you did not do yourself. */
.tile-who.is-guest {
  color: var(--accent-ink);
  background: var(--accent);
}

.tile-who.is-you {
  color: #d6d3e0;
  background: rgba(14, 13, 19, .8);
  font-weight: 600;
}

/* ---------- opening card reminder ---------- */

/* Sized to be read from a sofa, and bordered rather than tinted so it survives
   any event palette. It sits under the tagline, which is the last thing on the
   card, so it never pushes the QR off a small screen. */
.fullcard .reminder {
  max-width: 62vmin;
  margin-top: 0;
  padding: 1.2vmin 2vmin;
  border: 0.3vmin solid var(--accent);
  border-radius: 1.4vmin;
  text-align: center;
  line-height: 1.4;
}

.fullcard .reminder strong {
  display: block;
  font-family: var(--display);
  font-size: 2.2vmin;
  font-weight: 800;
  color: var(--accent);
}

.fullcard .reminder span {
  display: block;
  margin-top: .4vmin;
  font-size: 1.6vmin;
  color: var(--muted);
}

/* ---------- long-form pages ---------- */

.prose h2 { margin-top: 2rem; }
.prose p, .prose li { line-height: 1.65; color: var(--muted); }
.prose p strong, .prose li strong { color: var(--text); }
.prose ul { padding-left: 1.2rem; margin: .75rem 0; }
.prose li { margin-bottom: .4rem; }
.prose footer { margin-top: 3rem; }



/* Confirmation after returning from checkout. A soft success notice, not a
   slab of brand colour, and it fades itself out after a few seconds. */
.paid-banner {
  display: flex;
  align-items: center;
  gap: .55rem;
  margin: .75rem 0 0;
  padding: .65rem .9rem;
  font-size: .9rem;
  font-weight: 600;
  color: #14663a;
  background: #e7f4ec;
  border: 1px solid #bfe3cb;
  border-radius: var(--radius);
  transition: opacity .5s ease;
}

.paid-banner::before {
  content: '';
  flex: 0 0 auto;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  background: #2e9e5b;
  /* a white check, drawn so it needs no icon font */
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23000' d='M6.5 11.2 3.3 8l1.1-1.1 2.1 2.1 5-5L12.7 5z'/%3E%3C/svg%3E") center/1.1rem no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23000' d='M6.5 11.2 3.3 8l1.1-1.1 2.1 2.1 5-5L12.7 5z'/%3E%3C/svg%3E") center/1.1rem no-repeat;
}

.paid-banner.fading { opacity: 0; }

/* ---------- approval queue (host) ---------- */

#ev-pending {
  margin: 1rem 0;
  padding: 1rem;
  background: #fef6e6;
  border: 1px solid var(--sun, #ffc93c);
  border-radius: var(--radius);
}

.pending-head {
  margin: 0 0 .75rem;
  font-weight: 700;
  color: var(--text);
}

.tile.pending-tile { outline: 2px solid var(--sun, #ffc93c); outline-offset: -2px; }

/* Approve is affirmative, so it reads green rather than the neutral ink of the
   other tile buttons. Delete keeps the shared danger red. */
.tile-btn.approve {
  flex: 1;
  color: #eafff1;
  background: rgba(20, 102, 58, .9);
  border-color: rgba(255, 255, 255, .25);
}
.tile-btn.approve:hover { background: rgba(20, 102, 58, 1); }

/* Consent line beneath a sign-in or checkout action. */
.consent {
  margin: .6rem auto 0;
  max-width: 26rem;
  font-size: .78rem;
  line-height: 1.5;
  text-align: center;
  color: var(--muted);
}
.consent a { color: var(--muted); text-decoration: underline; }
.consent a:hover { color: var(--text); }

/* ---------- stats table scroll ---------- */
.stats-table-wrap { overflow-x: auto; }

/* Deletes-soon warning on the event page. Amber: urgent, not an error. */
.expiry-banner {
  margin: .75rem 0 0;
  padding: .7rem 1rem;
  font-size: .9rem;
  font-weight: 600;
  color: #7a4e00;
  background: #fdf1d6;
  border: 1px solid #efc873;
  border-radius: var(--radius);
}

/* ---------- stats dashboard ---------- */

.tally {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: .8rem;
  margin: 1.75rem 0 2.5rem;
}

.stat {
  position: relative;
  padding: 1.1rem 1.15rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: var(--lift);
  overflow: hidden;
}

/* a bold colour rail down the side, so each metric reads at a glance */
.stat::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 5px;
  background: var(--tone, var(--sky));
}

.stat .n {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(1.5rem, 4.6vw, 2.1rem);
  line-height: 1.05;
  letter-spacing: -.02em;
  color: var(--navy);
  font-variant-numeric: tabular-nums;
  /* a long value (revenue with commas and cents) shrinks and, in the extreme,
     wraps inside the card rather than spilling out of it */
  overflow-wrap: anywhere;
}

/* Revenue is the longest string, so it gets a slightly smaller number. */
.stat-money .n { font-size: clamp(1.35rem, 4.2vw, 1.85rem); }

.stat .k {
  margin-top: .55rem;
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
}

.stat-sky   { --tone: #4db8e8; }
.stat-sun   { --tone: #f0a500; }
.stat-med   { --tone: #2f7db5; }
.stat-low   { --tone: #2f8a5f; }
/* the money card gets the loudest treatment - filled, not just railed */
.stat-money { --tone: #2f8a5f; background: linear-gradient(180deg, #eaf6ef, var(--surface)); }
.stat-money .n { color: #1f7a4d; }
:root[data-theme="dark"] .stat-money,
@media (prefers-color-scheme: dark) { .stat-money { background: var(--surface); } }

.stats-table { width: 100%; border-collapse: collapse; font-size: .88rem; margin-top: .5rem; }
.stats-table th, .stats-table td {
  padding: .6rem .7rem; text-align: right; border-bottom: 1px solid var(--border);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.stats-table th {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); border-bottom: 2px solid var(--border);
}
.stats-table td:first-child, .stats-table th:first-child {
  text-align: left; font-weight: 600; white-space: normal;
}
.stats-table tbody tr:hover { background: var(--surface-2); }
.stats-table td:nth-child(n+5) { font-weight: 700; color: var(--navy); }
h2 { margin-top: 2.25rem; }

/* ---------- confirm dialog ---------- */
/* A styled stand-in for the browser's confirm(), which looks foreign against
   the app. Built from the theme tokens so it matches the light host pages and
   the dark slideshow alike; the scrim is always a dark wash so the card lifts
   off whatever is behind it. */
.confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.25rem;
  background: rgba(8, 10, 16, .55);
  backdrop-filter: blur(4px);
  animation: confirm-in .15s ease;
}
.confirm-card {
  width: 100%;
  max-width: 25rem;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 1.5rem 3.5rem rgba(0, 0, 0, .35);
  padding: 1.5rem;
  animation: confirm-pop .18s cubic-bezier(.2, .8, .3, 1);
}
.confirm-title {
  margin: 0 0 .5rem;
  font-size: 1.2rem;
  font-weight: 700;
  text-wrap: balance;
}
.confirm-msg {
  margin: 0 0 1.4rem;
  color: var(--muted);
  line-height: 1.5;
}
.confirm-actions {
  display: flex;
  gap: .6rem;
  justify-content: flex-end;
  flex-wrap: wrap;
}
.confirm-actions .btn {
  width: auto;
  margin-top: 0;
  padding: .65rem 1.15rem;
  font-size: 1rem;
}
.confirm-actions .confirm-ok {
  color: #fff;
  background: var(--danger);
  border: 0;
}
@keyframes confirm-in { from { opacity: 0 } to { opacity: 1 } }
@keyframes confirm-pop {
  from { opacity: 0; transform: translateY(.5rem) scale(.98) }
  to { opacity: 1; transform: none }
}
@media (prefers-reduced-motion: reduce) {
  .confirm-overlay, .confirm-card { animation: none }
}

/* ---------- photos-tab usage header ---------- */
/* Photos and video shown as at-a-glance bars, so "how full am I" reads without
   a sentence to parse. */
.usage {
  display: flex;
  flex-direction: column;
  gap: .6rem;
  margin: 0 0 1.25rem;
  padding: .9rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.usage:empty { display: none; }
.usage-row {
  display: grid;
  grid-template-columns: 3.25rem 1fr auto;
  align-items: center;
  gap: .8rem;
  font-size: .85rem;
}
.usage-label { color: var(--muted); }
.usage-track {
  height: .5rem;
  border-radius: 1rem;
  background: var(--surface-2);
  overflow: hidden;
}
.usage-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
  transition: width .3s ease;
}
.usage-num {
  color: var(--text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.usage-row.warn .usage-fill { background: var(--danger); }
.usage-row.warn .usage-num { color: var(--danger); }

.tips { margin: 0 0 1rem; }
.tips > summary {
  cursor: pointer;
  width: max-content;
  color: var(--accent);
  font-size: .9rem;
}
.tips > summary:hover { text-decoration: underline; }
.tips p { margin: .6rem 0 0; }

/* ---------- settings section headers ---------- */
/* Each h2 opens a group with a divider rule above it, so the settings read as
   sections rather than one long list. The first has no rule to sit under. */
#panel-settings h2 {
  margin: 2.25rem 0 1rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
  font-size: 1.15rem;
}
#panel-settings h2:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* The text-style banner inside the colour preview: laid in normal flow, always
   shown (the slideshow fades it in with .on), and shrunk from its TV vmin size
   to fit the small box. */
.palette-preview .marquee {
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 1;
  transform: none;
  padding: 0 .5rem;
}
.palette-preview .marquee span {
  font-size: 1.1rem;
  line-height: 1.1;
  max-width: 100%;
  text-shadow: none;
}

/* Confetti inside the colour preview. The host app makes .confetti fixed and
   page-wide with vmin pieces falling a screen-height; here it's contained to the
   preview box, shrunk to px, and drifting straight down so it stays in frame.
   It sits behind the banner, photo, and cards. */
.palette-preview .pp-body { position: relative; }
/* Out-specify `body.site .confetti { position: fixed }`, which otherwise wins
   and throws the confetti across the whole page instead of the preview. */
body.site .palette-preview .confetti {
  position: absolute;
  inset: 0;
  opacity: 1;
  /* Over the photo and cards, the way it falls on the real slideshow. */
  z-index: 2;
  /* So the fall distance below can be the box's own height, not a magic px. */
  container-type: size;
}
.palette-preview .pp-body > :not(.confetti):not(.bubbles):not(.sparkles):not(.backdrop) { position: relative; z-index: 1; }
.palette-preview .confetti i {
  top: -12px;
  width: 4px;
  height: 7px;
  animation-name: pp-confetti-fall;
}
.palette-preview .confetti i.round { height: 4px; }
@keyframes pp-confetti-fall {
  to { transform: translateY(115cqh) rotate(720deg); }
}

/* Bubbles in the preview - the mirror of the confetti rules above: contained to
   the box, sized in px, rising straight up so they stay in frame. */
body.site .palette-preview .bubbles {
  position: absolute;
  inset: 0;
  z-index: 2;
  container-type: size;
}
.palette-preview .bubbles i {
  bottom: -14px;
  width: calc(var(--size, 2) * 2.2px);
  height: calc(var(--size, 2) * 2.2px);
  opacity: .7;
  animation-name: pp-bubble-rise;
}
@keyframes pp-bubble-rise {
  to { transform: translateY(-130cqh); }
}

/* Sparkles in the preview - just the px sizing and a lift above the photo; the
   twinkle animation has no travel, so it needs no preview-specific keyframe. */
body.site .palette-preview .sparkles { z-index: 2; }
.palette-preview .sparkles i {
  width: calc(var(--size, 2) * 2.4px);
  height: calc(var(--size, 2) * 2.4px);
}

/* Background-text banner in the preview: the same tiled rows, filling the whole
   box behind the photo (which sits at z-index 1). Here the box is small, not the
   viewport, so switch the row unit to cqh and make this a size container. */
body.site .palette-preview .backdrop {
  position: absolute;
  inset: 0;
  z-index: 0;
  container-type: size;
  --u: 1cqh;
}

/* ---------- Look & Feel: pinned preview + sub-tabs ---------- */
.lf-preview {
  position: sticky;
  top: 0;
  z-index: 5;
  margin-bottom: 1rem;
}
/* The preview card is opaque on its own, so nothing needs to scroll-hide behind
   a solid fill. */
.lf-preview .palette-preview { margin-top: 0; }

/* Underlined text tabs, matching the main event tabs above. */
.lf-tabs {
  display: flex;
  gap: .25rem;
  margin: 0 0 1.1rem;
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
  scrollbar-width: none;
}
.lf-tabs::-webkit-scrollbar { display: none; }
.lf-tab {
  flex: 0 0 auto;
  padding: .65rem .9rem;
  font: inherit;
  font-size: .9rem;
  font-weight: 600;
  white-space: nowrap;
  color: var(--muted);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  cursor: pointer;
}
.lf-tab:hover { color: var(--text); }
.lf-tab.is-active { color: var(--text); border-bottom-color: var(--accent); }
body.site .lf-tab.is-active { border-bottom-width: 3px; margin-bottom: -2px; }

/* The chosen frame, shown on the preview photo. Single-photo frames read
   clearly here; mosaic and scrapbook are multi-photo layouts, so in a
   single-photo preview they just drop the mat to a plain, edge-to-edge photo. */
.palette-preview .pp-photo.frame-classic {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 6px 18px rgba(0, 0, 0, .4);
}
.palette-preview .pp-photo.frame-gallery {
  padding: .95rem .85rem;
  border-radius: 0;
}
.palette-preview .pp-photo.frame-edge,
.palette-preview .pp-photo.frame-mosaic,
.palette-preview .pp-photo.frame-scrapbook {
  padding: 0;
  background: none;
  border: 0;
}

/* ============================================================
   Pricing page redesign (body.pricing)
   The hero + tiers marketing layout. Scoped to body.pricing so nothing reaches
   the app, and built entirely on the html.site brand tokens (navy / magenta /
   sky / sun). These rules override the older global .tier block by specificity.
   ============================================================ */
.p-wrap { width: min(1120px, 92vw); margin: 0 auto; }
.pricing h1, .pricing h2, .pricing h3 {
  font-family: var(--display); font-weight: 800; letter-spacing: -.02em; text-wrap: balance; margin: 0;
}
.pricing .hl { color: var(--accent); }

/* Buttons: pill on the site's .btn base. Hero/closing sit inline; tier CTAs stay full width. */
.pricing .btn { border-radius: 999px; }
.p-hero .cta-row .btn, .p-closing .btn { width: auto; margin-top: 0; }
.pricing .p-btn-lg { padding: .8rem 1.5rem; font-size: 1rem; }

/* ---- hero ---- */
.p-hero { position: relative; overflow: hidden; padding: clamp(2rem,5vw,4rem) 0 clamp(1.5rem,3vw,3rem); }
.p-hero .p-wrap { display: grid; grid-template-columns: 1.05fr .95fr; gap: clamp(1.5rem,4vw,3.5rem);
  align-items: center; position: relative; z-index: 1; }
.p-hero-copy .eyebrow { display: inline-flex; align-items: center; gap: .5rem; font-weight: 700; font-size: .8rem;
  letter-spacing: .08em; text-transform: uppercase; color: var(--accent); margin-bottom: 1rem; }
.p-hero-copy .eyebrow::before { content: ""; width: 1.6rem; height: 2px; background: var(--accent); border-radius: 2px; }
.p-hero h1 { font-size: clamp(2.1rem,5vw,3.5rem); line-height: 1.02; }
.p-hero .lede { font-size: clamp(1.05rem,1.6vw,1.2rem); color: var(--muted); margin: 1.1rem 0 1.7rem; max-width: 34ch; }
.p-hero .cta-row { display: flex; flex-wrap: wrap; gap: .8rem; }
.p-hero .p-micro { margin-top: 1.1rem; font-size: .9rem; color: var(--muted); }
.p-hero .p-micro b { color: var(--text); }

.p-confetti { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; }
.p-confetti i { position: absolute; top: -12px; width: 9px; height: 13px; border-radius: 2px; opacity: .85; animation: p-fall linear infinite; }
@keyframes p-fall { to { transform: translateY(115vh) rotate(600deg); } }

.screen { position: relative; z-index: 2; }
.tv { background: #0e2138; border-radius: 20px; padding: 14px; box-shadow: var(--lift-hi); border: 1px solid rgba(255,255,255,.08); }
.carousel { position: relative; aspect-ratio: 16/9; border-radius: 12px; overflow: hidden; background: #0f2440; }
.cslide { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity .8s ease; }
.cslide.on { opacity: 1; }
.cdots { position: absolute; bottom: 9px; left: 0; right: 0; display: flex; justify-content: center; gap: 6px; z-index: 2; }
.cdots button { width: 7px; height: 7px; padding: 0; border: 0; border-radius: 50%; background: rgba(255,255,255,.55); cursor: pointer; transition: width .2s, background .2s; }
.cdots button[aria-current="true"] { width: 18px; border-radius: 4px; background: #fff; }
.float-tag { position: absolute; z-index: 4; display: inline-flex; align-items: center; gap: .5rem; white-space: nowrap;
  background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 999px; padding: .45rem .85rem;
  font-size: .82rem; font-weight: 700; box-shadow: var(--lift); }
.float-tag.a { top: -16px; right: 22px; }
.float-tag.b { bottom: 30px; left: -18px; }
.float-tag .fdot { width: .55rem; height: .55rem; border-radius: 50%; flex: none; }
.float-tag .fdot.pop { background: var(--pop); }
.float-tag .fdot.sun { background: var(--sun); }

/* ---- trust strip ---- */
.p-trust { border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); background: var(--surface-2); }
.p-trust .p-wrap { display: flex; flex-wrap: wrap; justify-content: center; gap: .6rem 2.2rem; padding: 1rem 0; }
.p-trust span { display: inline-flex; align-items: center; gap: .5rem; font-weight: 600; font-size: .95rem; }
.p-trust svg { width: 18px; height: 18px; color: var(--accent); flex: none; }

/* ---- sections ---- */
.p-block { padding: clamp(2.5rem,6vw,5rem) 0; }
.p-sec-head { text-align: center; max-width: 44ch; margin: 0 auto clamp(1.8rem,4vw,2.8rem); }
.p-sec-head h2 { font-size: clamp(1.7rem,3.2vw,2.4rem); }
.p-sec-head p { color: var(--muted); font-size: 1.05rem; margin: .6rem 0 0; }

/* ---- tiers ---- */
.pricing .tiers { display: grid; grid-template-columns: repeat(3,1fr); gap: 1.1rem; align-items: start; margin: 0; }
.pricing .tier { position: relative; display: flex; flex-direction: column; background: var(--surface);
  border: 1px solid var(--border); border-radius: 20px; padding: 1.7rem 1.5rem; box-shadow: var(--lift); }
.pricing .tier.pop { border-color: var(--accent); transform: translateY(-10px);
  box-shadow: 0 3px 8px rgba(30,58,95,.08), 0 24px 46px -22px color-mix(in srgb, var(--accent) 55%, transparent); }
.pricing .tier .badge { position: absolute; top: -13px; left: 50%; transform: translateX(-50%); background: var(--accent);
  color: var(--accent-ink); font-family: var(--display); font-weight: 800; font-size: .72rem; letter-spacing: .04em;
  text-transform: uppercase; padding: .35rem .85rem; border-radius: 999px; white-space: nowrap; box-shadow: 0 8px 18px -8px var(--accent); }
.pricing .tier .name { font-family: var(--display); font-weight: 800; font-size: 1.2rem; }
.pricing .tier .blurb { color: var(--muted); font-size: .95rem; margin: .25rem 0 1rem; min-height: 2.6em; }
.pricing .tier .price { font-family: var(--display); font-weight: 800; letter-spacing: -.03em; line-height: 1;
  display: flex; align-items: baseline; gap: .25rem; color: var(--navy); }
.pricing .tier .price .amt { font-size: 3rem; }
.pricing .tier .price .per { font-size: .95rem; font-weight: 700; color: var(--muted); }
.pricing .tier .rule { border: 0; border-top: 1px solid var(--border); margin: 1.3rem 0; }
.pricing .tier .feat { list-style: none; margin: 0 0 1.5rem; padding: 0; display: grid; gap: .6rem; flex: 1; }
.pricing .tier .feat li { display: flex; gap: .55rem; align-items: flex-start; font-size: .95rem; line-height: 1.4; }
.pricing .tier .feat li svg { width: 18px; height: 18px; flex: none; margin-top: 2px; color: var(--accent); }
.pricing .tier .feat li.no { color: var(--muted); }
.pricing .tier .feat li.no svg { color: var(--muted); opacity: .7; }
.pricing .tier .feat li.plus { font-weight: 700; }
.pricing .tier .feat .lead { display: block; font-weight: 700; color: var(--muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; }
.pricing .tier .btn { margin-top: auto; width: 100%; }
.pricing .tier.is-current { border-color: var(--accent); background: var(--surface-2); }
.pricing .tier.is-current::before { content: none; }
.pricing .upgrading { margin: 0 0 1rem; text-align: center; font-size: .95rem; color: var(--muted); }

/* ---- benefits ---- */
.p-benefits-block { background: var(--surface-2); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.p-benefits { display: grid; grid-template-columns: repeat(3,1fr); gap: 1.2rem; }
.p-benefit { background: var(--surface); border: 1px solid var(--border); border-radius: 18px; padding: 1.6rem; box-shadow: var(--lift); }
.p-benefit .ic { width: 46px; height: 46px; border-radius: 13px; display: grid; place-items: center; margin-bottom: 1rem;
  background: color-mix(in srgb, var(--accent) 12%, var(--surface)); color: var(--accent); }
.p-benefit .ic svg { width: 24px; height: 24px; }
.p-benefit h3 { font-size: 1.15rem; }
.p-benefit p { color: var(--muted); margin: .5rem 0 0; font-size: .96rem; }

/* ---- faq ---- */
.p-faq { max-width: 760px; margin: 0 auto; display: grid; gap: .7rem; }
.p-faq details { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 0 1.2rem; box-shadow: var(--lift); }
.p-faq summary { list-style: none; cursor: pointer; font-family: var(--display); font-weight: 700; font-size: 1.05rem;
  padding: 1.1rem 0; display: flex; justify-content: space-between; align-items: center; gap: 1rem; }
.p-faq summary::-webkit-details-marker { display: none; }
.p-faq summary::after { content: "+"; font-size: 1.5rem; color: var(--accent); font-weight: 700; transition: transform .2s ease; }
.p-faq details[open] summary::after { transform: rotate(45deg); }
.p-faq p { margin: 0 0 1.1rem; color: var(--muted); }

/* ---- closing ---- */
.p-closing { position: relative; overflow: hidden; text-align: center; color: #fff;
  background: linear-gradient(135deg, #ec1e63, #d4145a 48%, #f5a623); border-radius: 26px; padding: clamp(2.5rem,6vw,4rem) 1.5rem; }
.p-closing h2 { font-size: clamp(1.8rem,3.6vw,2.6rem); color: #fff; }
.p-closing p { color: rgba(255,255,255,.92); font-size: 1.1rem; margin: .8rem auto 1.6rem; max-width: 40ch; }
.pricing .p-closing .btn { background: #fff; color: #c0134f; box-shadow: 0 14px 30px -12px rgba(0,0,0,.35); }
.p-closing-small { margin-top: 1rem; font-size: .9rem; color: rgba(255,255,255,.85); }

.p-footer { text-align: center; color: var(--muted); font-size: .9rem; padding: 2.5rem 0 3rem; }

@media (max-width: 860px) {
  .p-hero .p-wrap { grid-template-columns: 1fr; }
  .p-hero .screen { order: -1; max-width: 460px; }
  .p-hero .lede { max-width: none; }
  .pricing .tiers { grid-template-columns: 1fr; max-width: 420px; margin: 0 auto; }
  .pricing .tier.pop { transform: none; }
  .p-benefits { grid-template-columns: 1fr; }
  .float-tag.b { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .p-confetti { display: none; }
  .cslide { transition: none; }
}

/* Dark mode, site-wide. Keyed off html.site so it overrides the html.site light
   tokens on every site page (events, stats, upload, pricing, landing) and paints
   the canvas navy. The magenta/sky/sun brand colours brighten a touch to hold up
   on the deep navy ground. The slideshow (body.tv) keeps its own :root palette. */
@media (prefers-color-scheme: dark) {
  html.site {
    --bg: #0f2440;
    --surface: #17324f;
    --surface-2: #1d3b5c;
    --border: #274766;
    --text: #eaf1fa;
    --muted: #9fb4cc;
    --accent: #ff5c88;
    --accent-ink: #ffffff;
    --navy: #dbe8f6;
    --sky: #5fc0ee;
    --sun: #ffce55;
    --pop: #ff4d7f;
    --lift: 0 2px 12px rgba(0, 0, 0, .4);
    --lift-hi: 0 10px 28px rgba(0, 0, 0, .55);
  }
}

/* Site-wide redesign carry-over: pill buttons on every site page (events, stats,
   upload), matching the redesigned marketing pages. The slideshow is untouched. */
body.site .btn { border-radius: 999px; }

/* Stats status pills and callouts carry hardcoded light backgrounds; give them
   dark-mode variants so the stats page doesn't show light chips on a navy page. */
@media (prefers-color-scheme: dark) {
  html.site .stat-good { color: #7fe0a6; background: rgba(46, 158, 91, .16); border-color: rgba(46, 158, 91, .4); }
  html.site .stat-warn { color: #f5c877; background: rgba(239, 200, 115, .14); border-color: rgba(239, 200, 115, .38); }
  html.site .stat-money .n { color: #7fe0a6; }
}
