/* ==========================================================================
   VALDER ADS — PLAYER STYLESHEET
   ==========================================================================

   Everything here targets one job: a TV screen, seen from ten feet away,
   running for weeks without a human touching it.

   Two rules shape almost every decision in this file:

   1. NO PIXELS FOR TYPE OR SPACING.
      The same file has to look right on a 1920x1080 TV and a 3840x2160 TV.
      A 48px headline is large on 1080p and tiny on 4K. So sizes are written
      in `vmin` / `vw` / `vh` — viewport-relative units that are a percentage
      of the screen itself. 6vmin is 6% of the screen's shorter side, which
      is the same physical size on both TVs. Borders and hairlines are the
      only exception, and even those use vmin where they'd otherwise vanish.

   2. NOTHING MOVES EXCEPT THE CROSSFADE.
      No scrollbars, no cursor, no text selection, no tap highlights,
      no bounce. A signage screen that can be scrolled is a signage screen
      that will eventually be found scrolled.

   Section map:
     1. Reset & kiosk lockdown
     2. Type scale + colour tokens
     3. Stage and crossfade mechanics
     4. Image slides
     5. Special (daily special) slides
     6. Standby screen
     ========================================================================= */


/* ==========================================================================
   1. RESET & KIOSK LOCKDOWN
   ========================================================================== */

/* Border-box everywhere: padding never makes an element wider than we asked.
   Without this, a box set to 80% width with padding overflows and creates a
   scrollbar — the exact thing we cannot have on a TV. */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  width: 100%;
  height: 100%;

  /* The screen is black so that a letterboxed image blends into the bezel. */
  background: #000;

  /* `overflow: hidden` is the single most important kiosk rule here.
     It guarantees no scrollbar can ever appear, even if a slide is
     accidentally sized 1px too large. */
  overflow: hidden;

  /* Hide the mouse pointer. The TV stick may have a mouse-emulating remote;
     if it does, an idle arrow would otherwise sit burned into the ad. */
  cursor: none;

  /* Nobody is reading this with a keyboard. Kill selection highlights so a
     stray remote click can't leave blue-highlighted text on screen. */
  -webkit-user-select: none;
  user-select: none;

  /* Android WebView paints a translucent grey box on tap. Not on our TV. */
  -webkit-tap-highlight-color: transparent;

  /* Android TV browsers sometimes "helpfully" resize text. Turning this off
     keeps our vmin scale honest. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;

  /* FONT STACK — deliberately 100% local.
     There is no @import, no <link> to Google Fonts, no CDN. If the store's
     internet is down the player must still render perfectly from cache, and
     a webfont request that hangs would leave the screen blank while it waits.
     These names cover Android TV (Roboto), plus desktop fallbacks for when
     you preview on your own machine. */
  font-family: "Roboto", "Helvetica Neue", Helvetica, Arial,
               system-ui, -apple-system, "Segoe UI", sans-serif;

  /* Anti-aliasing hints. Large light-on-dark type looks chunky without them. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}


/* ==========================================================================
   2. TYPE SCALE + COLOUR TOKENS
   --------------------------------------------------------------------------
   Custom properties (CSS variables) live on :root so every value used more
   than once has exactly one place to change. If a client ever wants a
   different accent colour, it is a one-line edit here — not a hunt through
   the file.
   ========================================================================== */

:root {
  /* --- Colour ---------------------------------------------------------- */
  --ink:          #ffffff;   /* primary text — pure white for max contrast   */
  --ink-soft:     #d8d4cc;   /* body copy — slightly warm so it reads as
                                intentional rather than washed out           */
  --ink-faint:    #8f8a80;   /* footnotes, fine print                        */
  --accent:       #e8b64c;   /* warm gold: reads as "premium signage" and
                                stays visible on cheap TV panels             */
  --accent-deep:  #b8862a;   /* darker gold for gradients and rules          */
  --bg-deep:      #0d0f14;   /* near-black base of the special template      */
  --bg-lift:      #191d26;   /* lifted corner of the background gradient     */

  /* --- Crossfade -------------------------------------------------------- */
  /* One second, as specified. Kept as a variable because player.js is told
     the same number (SLIDE_FADE_MS). If you change one, change the other. */
  --fade-duration: 1000ms;

  /* `ease-in-out` (rather than linear) makes the fade read as a deliberate
     dissolve instead of a mechanical wipe. */
  --fade-easing: cubic-bezier(0.4, 0.0, 0.2, 1);
}


/* ==========================================================================
   3. STAGE AND CROSSFADE MECHANICS
   --------------------------------------------------------------------------
   HOW THE CROSSFADE WORKS
   -----------------------
   Every slide is absolutely positioned to fill the whole stage, so all
   slides sit stacked on top of each other like sheets of paper.

   Fading is done with `opacity` only — never `display`, never `visibility`.
   That matters because opacity is one of the two properties (with transform)
   that a browser can animate on the GPU without re-laying-out the page.
   On a cheap TV stick that is the difference between a smooth dissolve and
   a stuttering one.

   player.js does this dance:
     1. build the new slide with class "slide"           -> opacity 0
     2. append it to the stage                           -> invisible, stacked on top
     3. next animation frame, add class "is-visible"     -> opacity 1, browser animates
     4. remove the old slide's "is-visible"              -> it fades out underneath
     5. after the fade finishes, delete the old element  -> stage holds one slide again
   ========================================================================== */

#stage {
  position: fixed;   /* fixed, not absolute: immune to any layout weirdness  */
  inset: 0;          /* shorthand for top/right/bottom/left: 0               */
  overflow: hidden;
}

.slide {
  position: absolute;
  inset: 0;

  /* The starting state. Every slide is born invisible and is faded in. */
  opacity: 0;

  /* The animation itself. Because .is-visible only flips opacity, this one
     line is the entire crossfade. */
  transition: opacity var(--fade-duration) var(--fade-easing);

  /* Promotes the slide to its own GPU layer *before* the animation starts,
     so the first frame of the fade isn't dropped while the browser sets up.
     This is the main reason transitions look smooth on weak hardware. */
  will-change: opacity;

  /* Slides never capture input — there is no input to capture. */
  pointer-events: none;
}

/* The one class player.js toggles. Everything above exists to make this
   single line produce a one-second dissolve. */
.slide.is-visible {
  opacity: 1;
}


/* ==========================================================================
   4. IMAGE SLIDES
   --------------------------------------------------------------------------
   A full-bleed photographic ad. The image is `object-fit: contain`, meaning
   the whole image is always visible and the black background letterboxes it
   if the aspect ratio doesn't match the TV.

   Why contain and not cover?
   `cover` fills the screen but crops — and a cropped ad can lose the price,
   the logo, or the phone number. For paid advertising, showing the whole
   creative matters more than filling every pixel. The black bars read as
   intentional on a black-bezel TV.
   ========================================================================== */

.slide--image {
  background: #000;
}

.slide__img {
  width: 100%;
  height: 100%;
  object-fit: contain;

  /* An <img> is inline by default, which leaves a few pixels of baseline gap
     under it. `display: block` removes that. */
  display: block;
}


/* ==========================================================================
   5. SPECIAL SLIDES — THE DAILY SPECIAL TEMPLATE
   --------------------------------------------------------------------------
   This is the piece that lets a client post an offer with zero design work:
   they type five short fields into playlist.json and get finished signage.

   THE COMPOSITION, AND WHY IT IS CENTRED
   --------------------------------------
   A 16:9 TV is a very wide canvas. Left-aligned text on it strands a third
   of the screen empty on the right, which reads as "unfinished web page"
   rather than "sign". Centring the stack fills the width at any content
   length — a two-word title and a two-line title both stay balanced — and
   it is what real specials boards do. So the template is a symmetrical
   poster, stacked top to bottom:

       ┌──────────────────────────────────────────────┐
       │                                              │
       │                 T H U R S D A Y     eyebrow  │  small, gold, tracked
       │                  ──────────           rule   │  fades at both ends
       │                                              │
       │              Prime Rib Night        title    │  the biggest element
       │                                              │
       │      12oz cut, garlic mash and      body     │  supporting copy
       │           seasonal vegetables                │
       │                                              │
       │                    $28              price    │  gold, unmissable
       │                                              │
       │    Dine-in only. While supplies last.  fine  │  quiet fine print
       │                                              │
       └──────────────────────────────────────────────┘

   The entire stack — footnote included — is vertically centred as ONE unit.
   An earlier version pinned the footnote to the bottom edge with
   `margin-top: auto`, which looked reasonable in the CSS and wrong on the
   screen: an auto margin absorbs all the free space in a flex column, so it
   silently cancels `justify-content: center` and leaves a large empty band
   between the price and the fine print. Keeping the group together reads as
   composed; spreading it to the edges reads as broken.

   Every field except the title is optional — if a client omits `footnote`,
   player.js never creates that element and the stack simply closes up.

   ---------------------------------------------------------------------------
   THE --fit VARIABLE: WHY EVERY SIZE BELOW IS A calc()
   ---------------------------------------------------------------------------
   Look at any size in this section and you will see the same shape:

       font-size: calc(12.5vmin * var(--fit, 1));

   `--fit` is a scale factor, normally 1, that player.js turns DOWN when a
   particular special is too tall for the screen.

   The problem it solves is real and was caught by testing rather than by
   reading the CSS. Fixed sizes are fine for the copy YOU write, because you
   keep it short. But in Phase 2 clients type their own, and a slide with a
   three-line title AND a four-line description AND a three-line footnote
   overflowed a 1080p screen — silently clipping the top of the day label and
   the bottom of the fine print. No error, no warning; just a sign missing
   its terms and conditions.

   Tightening the margins only made the overflow smaller, not impossible, so
   the fix is proportional instead: after building the slide, player.js
   measures it and, if it doesn't fit, steps `--fit` down (0.95, 0.90, ...)
   until it does. Because the multiplier is applied to the type sizes AND the
   gaps between them, the whole composition shrinks together and keeps its
   proportions — it reads as a slightly smaller poster, never as squashed
   text. See `fitSpecialSlide()` in js/player.js.

   The `, 1` in `var(--fit, 1)` is the fallback: if the property is never
   set, everything renders at full size. So the template is correct on its
   own and the JavaScript only intervenes in the rare case that needs it.
   ========================================================================== */

.slide--special {
  /* A two-stop radial gradient rather than flat black. On a large panel a
     perfectly flat background looks like a dead signal; a subtle lift behind
     the headline reads as lighting, and as "designed". */
  background:
    radial-gradient(115% 90% at 50% 12%, var(--bg-lift) 0%, var(--bg-deep) 68%),
    var(--bg-deep);

  /* The outer box only centres; the inner box does the stacking. Splitting
     the two is what lets player.js measure the content's true height
     (`.special__inner`) against the space available (`.slide--special`). */
  display: flex;
  align-items: center;
  justify-content: center;

  /* Generous margins. Signage needs air; also, TVs overscan — some panels
     crop 2–3% off every edge. This padding is the safe zone that guarantees
     nothing important is ever cut off by the screen itself. */
  padding: 7vmin 10vmin;

  /* Belt and braces. --fit should mean nothing ever overflows, but if some
     content still did, clipping it is better than letting it spill over the
     frame. */
  overflow: hidden;
}

/* The measured box. Everything the client typed lives in here. */
.special__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  width: 100%;
}

/* --- The thin gold frame ---------------------------------------------------
   A 1-pixel border would be faint at ten feet and nearly invisible on 4K.
   This is a vmin-based inset border drawn as a pseudo-element, so it scales
   with the screen and never affects the layout of the text inside it.
   Deliberately NOT scaled by --fit: the frame belongs to the screen, not to
   the content, so it should stay put even when the type shrinks. */
.slide--special::after {
  content: "";
  position: absolute;
  inset: 3.2vmin;
  border: 0.22vmin solid rgba(232, 182, 76, 0.28);
  pointer-events: none;
}

/* --- Eyebrow: the day label ("THURSDAY", "LUNCH SPECIAL") ------------------
   Small, uppercase, widely letter-spaced. Wide tracking is what separates
   signage typography from web typography — it signals "label", and it holds
   up across a room. */
.special__day {
  font-size: calc(3.2vmin * var(--fit, 1));
  font-weight: 700;
  letter-spacing: 0.42em;   /* em, so tracking grows with the font size */
  text-transform: uppercase;
  color: var(--accent);
  line-height: 1;

  /* letter-spacing also adds a gap AFTER the final letter, which makes the
     word's box wider than the ink and pushes it visually right of centre.
     A negative right margin of exactly one tracking unit removes that
     phantom space and re-centres the word optically. Classic typographic
     correction; the difference is small but visible at this size.
     It is in `em`, so it tracks the font size automatically. */
  margin-right: -0.42em;

  margin-bottom: calc(2.6vmin * var(--fit, 1));
}

/* --- The gold rule under the eyebrow --------------------------------------
   A short horizontal bar that separates label from headline and anchors the
   symmetry. It is a gradient that fades to transparent at BOTH ends, which
   suits a centred composition — a hard-stopped bar would look like a cut-off
   underline. */
.special__rule {
  width: calc(22vmin * var(--fit, 1));
  height: calc(0.5vmin * var(--fit, 1));
  background: linear-gradient(
    90deg,
    rgba(232, 182, 76, 0) 0%,
    var(--accent) 50%,
    rgba(232, 182, 76, 0) 100%
  );
  margin-bottom: calc(4.5vmin * var(--fit, 1));

  /* flex: none stops flexbox from squashing the bar's height when the stack
     is tall (a long title plus a long footnote). */
  flex: none;
}

/* --- Title: the single biggest element on the screen ----------------------- */
.special__title {
  font-size: calc(12.5vmin * var(--fit, 1));
  font-weight: 800;
  line-height: 0.98;        /* tight leading — big type needs less space */
  letter-spacing: -0.02em;  /* big type also needs slightly negative
                               tracking, or it reads as loose            */
  color: var(--ink);

  /* Caps the headline at roughly 16 characters per line, so a long title
     breaks into a strong two-line stack instead of one thin ribbon of text
     stretched across a 4K screen. `ch` is relative to the font size, so this
     measure survives --fit shrinking without needing its own calc(). */
  max-width: 16ch;

  /* Stops a single very long word (an unbroken product name) from running
     off the edge of the screen. */
  overflow-wrap: break-word;

  margin-bottom: calc(3.2vmin * var(--fit, 1));
}

/* --- Description: supporting copy ------------------------------------------ */
.special__description {
  font-size: calc(4.3vmin * var(--fit, 1));
  font-weight: 400;
  line-height: 1.34;
  color: var(--ink-soft);

  /* ~32 characters per line. Long measures are hard to read at distance
     because the eye loses its place on the return to the next line. */
  max-width: 32ch;

  margin-bottom: calc(4.5vmin * var(--fit, 1));
}

/* --- Price: the call to action --------------------------------------------- */
.special__price {
  font-size: calc(11vmin * var(--fit, 1));
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--accent);

  /* A soft gold glow. On a TV panel this lifts the price off the background
     the way a lit sign does, without looking like a web effect. */
  text-shadow: 0 0 4vmin rgba(232, 182, 76, 0.22);
}

/* --- Footnote: fine print --------------------------------------------------
   Part of the centred group (see the note at the top of this section), just
   held away from the price by a deliberate gap so it reads as a separate
   register rather than as another line of copy. */
.special__footnote {
  font-size: calc(2.5vmin * var(--fit, 1));
  font-weight: 400;
  line-height: 1.45;
  color: var(--ink-faint);
  letter-spacing: 0.05em;
  max-width: 46ch;
  margin-top: calc(4.5vmin * var(--fit, 1));
}


/* ==========================================================================
   6. STANDBY SCREEN
   --------------------------------------------------------------------------
   Shown when zero slides pass the schedule filter, or before the first
   playlist arrives. The goal is a screen that looks *finished* rather than
   broken — a store owner glancing at it should read "between showings",
   not "the TV died".

   It fades with the same timing as slides so switching to and from standby
   feels like part of the same system.
   ========================================================================== */

#standby {
  position: fixed;
  inset: 0;

  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;

  background:
    radial-gradient(120% 100% at 50% 40%, var(--bg-lift) 0%, var(--bg-deep) 70%),
    var(--bg-deep);

  opacity: 0;
  transition: opacity var(--fade-duration) var(--fade-easing);

  /* Sits above the stage so it cleanly covers whatever was last drawn. */
  z-index: 10;

  /* When hidden it must not block anything; opacity 0 alone still leaves an
     element "present", so pointer-events is switched off too. */
  pointer-events: none;
}

/* The class player.js toggles, mirroring `.slide.is-visible`. */
#standby.is-visible {
  opacity: 1;
}

/* A small gold diamond. A geometric mark instead of a logo keeps the player
   client-agnostic — no image file to load, nothing to go missing offline. */
.standby__mark {
  width: 4.5vmin;
  height: 4.5vmin;
  background: linear-gradient(135deg, var(--accent) 0%, var(--accent-deep) 100%);
  transform: rotate(45deg);
  margin-bottom: 5vmin;

  /* Slow, gentle pulse. Movement tells a passer-by the screen is alive and
     not frozen — the main thing that makes a static standby look broken.
     8 seconds is deliberately slow enough to be calm rather than blinky. */
  animation: standby-pulse 8s ease-in-out infinite;
}

.standby__text {
  font-size: 3vmin;
  font-weight: 600;
  letter-spacing: 0.36em;
  margin-right: -0.36em;   /* same trailing-space correction as the eyebrow */
  text-transform: uppercase;
  color: var(--ink-faint);
}

@keyframes standby-pulse {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 1;    }
}

/* --- Motion safety ---------------------------------------------------------
   If the device is ever configured to reduce motion, drop the pulse. The
   crossfade stays: a dissolve between ads is content, not decoration. */
@media (prefers-reduced-motion: reduce) {
  .standby__mark {
    animation: none;
    opacity: 0.8;
  }
}
