﻿/* ══════════════════════════════════════════════════════════════════════════
   SHOPCORE GUEST DISPLAY
   ══════════════════════════════════════════════════════════════════════════

   Designed as a broadcast channel, because that is the only structure that
   fits the problem: several unrelated things shown to an audience that is not
   watching, arrives mid-loop, and needs one specific thing the instant it
   happens.

   ── Four rules ────────────────────────────────────────────────────────────

   1. The frame never moves. Venue, section, weather, clock. A guest who looks
      up for one second has still been told something.

   2. Nothing scrolls that a guest might need to read. A menu board that
      scrolls is one where the thing you wanted has just gone past. Content is
      fitted or paginated, never scrolled.

   3. Colour is structural. The accent is the venue's and appears on section
      headers, the live pip and the called number — nowhere else. Everything
      that is merely present is grey.

   4. Every effect is drawn. Gradients, masks and transforms; no images, no
      library. A television playing a looping MP4 background burns a decoder
      all day and the file has to be shipped, cached and replaced at rebrand.
      These cost a compositor layer and re-tint themselves from one token.

   ── Sizing ────────────────────────────────────────────────────────────────

   Everything is in vmin-based clamps rather than pixels, because this runs on
   a 15" counter screen and a 4K wall from the same file. Type that is right on
   one and wrong on the other is the single most common fault in digital
   signage, and a fixed pixel size guarantees it.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Tokens ─────────────────────────────────────────────────────────────── */

:root {
    /* Set per channel from the snapshot, so a venue's accent runs through every
       scene without any of them naming a colour. */
    --accent: #f2704f;
    --accent-ink: #ffffff;
    --accent-soft: rgba(242, 112, 79, 0.22);
    --accent-glow: rgba(242, 112, 79, 0.45);

    /* The type dial. The channel's TextScalePercent lands here and resizes the
       whole broadcast from one number in the back office. */
    --scale: 1;

    --bg: #0a0f14;
    --bg-soft: #111a23;
    --card: #16212c;
    --card-hi: #1d2b38;
    --edge: #223140;
    --edge-soft: #1a2632;

    --ink: #f4f8fc;
    --ink-strong: #ffffff;
    --ink-muted: #9db0c2;
    --ink-faint: #71879b;

    --ready: #17b26a;
    --ready-soft: #0e2a1d;
    --prep: #f5a524;
    --prep-soft: #2a2313;

    --radius: clamp(8px, 0.9vmin, 18px);
    --radius-lg: clamp(12px, 1.4vmin, 26px);
    --gap: clamp(10px, 1.3vmin, 26px);
    --pad: clamp(14px, 2vmin, 44px);

    /* The three bands the scene has to live between. Declared once so the
       scene area can be sized off them rather than guessed at. */
    --frame-h: clamp(44px, 6.2vmin, 96px);
    --rail-h: clamp(38px, 5.4vmin, 84px);
    --ticker-h: clamp(30px, 4.2vmin, 64px);

    /* One dissolve length, so scene changes, slide changes and the call-out all
       feel like the same instrument. */
    --fade: 620ms;
    --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

:root[data-theme="light"] {
    --bg: #eef2f6;
    --bg-soft: #ffffff;
    --card: #ffffff;
    --card-hi: #f5f8fb;
    --edge: #d7dee6;
    --edge-soft: #e6ebf0;

    --ink: #0f1a25;
    --ink-strong: #000000;
    --ink-muted: #52667a;
    --ink-faint: #7a8c9e;

    --ready-soft: #e7f8ef;
    --prep-soft: #fff6e4;
}

* { box-sizing: border-box; }

html, body {
    margin: 0;
    height: 100%;
    background: var(--bg);
    color: var(--ink);
    font-family: "Segoe UI Variable Display", "Segoe UI", system-ui, -apple-system, Roboto, Arial, sans-serif;

    /* A screen nobody is meant to touch. Without this, one passing sleeve leaves
       half the menu board highlighted in blue for the afternoon. */
    overflow: hidden;
    -webkit-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    -webkit-font-smoothing: antialiased;
}

[hidden] { display: none !important; }

/* ══ Pairing ═══════════════════════════════════════════════════════════════ */

.pairing {
    height: 100%;
    display: grid;
    place-items: center;
    padding: 24px;
    background: radial-gradient(120% 90% at 50% 0%, var(--bg-soft), var(--bg));
}

.pairing__card {
    width: min(520px, 100%);
    text-align: center;
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: 20px;
    padding: 40px 36px;
}

.pairing__mark { margin-bottom: 18px; border-radius: 16px; }
.pairing__title { margin: 0 0 10px; font-size: 30px; font-weight: 700; letter-spacing: -0.02em; }
.pairing__lead { margin: 0 0 28px; color: var(--ink-muted); font-size: 16px; line-height: 1.55; }
.pairing__lead strong { color: var(--ink); font-weight: 600; }
.pairing__form { display: grid; gap: 8px; text-align: left; }

.pairing__label {
    font-size: 12px; font-weight: 600; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--ink-faint); margin-top: 6px;
}

.pairing__code, .pairing__name {
    width: 100%;
    background: var(--bg-soft);
    border: 1px solid var(--edge);
    border-radius: 12px;
    color: var(--ink);
    font-size: 18px;
    padding: 14px 16px;
    outline: none;
    transition: border-color 0.2s ease;
}

.pairing__code:focus, .pairing__name:focus { border-color: var(--accent); }

/* Typed once, on a remote control, by somebody squinting at a laptop across the
   room. Big, spaced and unambiguous. */
.pairing__code {
    font-size: 34px; font-weight: 700; letter-spacing: 0.34em;
    text-align: center; text-transform: uppercase;
    font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
}

.pairing__go {
    margin-top: 18px; padding: 16px;
    background: var(--accent); color: var(--accent-ink);
    border: 0; border-radius: 12px;
    font-size: 17px; font-weight: 700; cursor: pointer;
    transition: filter 0.15s ease, transform 0.08s ease;
}

.pairing__go:hover { filter: brightness(1.08); }
.pairing__go:active { transform: translateY(1px); }
.pairing__go:disabled { opacity: 0.5; cursor: default; }

.pairing__error {
    margin: 12px 0 0; padding: 12px 14px;
    background: #2c1517; border: 1px solid #7f1d1d; border-radius: 10px;
    color: #ffb3ad; font-size: 14px;
}

.pairing__foot { margin: 26px 0 0; color: var(--ink-faint); font-size: 13px; }

/* ══ The stage ═════════════════════════════════════════════════════════════
   A grid, not a stack of absolutely positioned guesses. The scene gets exactly
   the room the three bands leave it, which is what makes "everything fits"
   true rather than aspirational. */

.stage {
    position: relative;
    height: 100%;
    font-size: calc(1rem * var(--scale));
    overflow: hidden;
    display: grid;
    grid-template-rows: var(--frame-h) 1fr auto auto;
}

/* ── The backdrop ───────────────────────────────────────────────────────────
   Two enormous, very soft accent clouds drifting in opposite directions at
   different speeds, so they never repeat a composition. Nobody looks at this;
   they would notice its absence, because a flat dark panel behind flat dark
   cards is exactly what a powered-down screen looks like. */

.stage::before,
.stage::after {
    content: "";
    position: absolute;
    z-index: 0;
    border-radius: 50%;
    pointer-events: none;
    background: radial-gradient(circle at center, var(--accent-soft), transparent 62%);
    filter: blur(46px);
    will-change: transform;
}

.stage::before {
    width: 92vw; height: 92vw;
    top: -36vw; left: -24vw;
    animation: aurora 52s var(--ease) infinite alternate;
}

.stage::after {
    width: 76vw; height: 76vw;
    right: -28vw; bottom: -32vw;
    opacity: 0.66;
    animation: aurora 67s var(--ease) infinite alternate-reverse;
}

/* Full-bleed scenes cover it anyway, and compositing two blurred layers under a
   playing video is where a cheap television's dissolves start to stutter. */
.stage[data-scene="Reel"]::before,
.stage[data-scene="Reel"]::after,
.stage[data-scene="Stream"]::before,
.stage[data-scene="Stream"]::after { animation-play-state: paused; opacity: 0; }

/* ══ The frame ═════════════════════════════════════════════════════════════
   Row one. Never animates out. */

.frame {
    position: relative;
    z-index: 3;
    display: flex;
    align-items: center;
    gap: clamp(10px, 1.6vmin, 30px);
    padding: 0 var(--pad);
    background: linear-gradient(180deg, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.18));
    border-bottom: 1px solid var(--edge-soft);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
}

:root[data-theme="light"] .frame {
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.7));
}

.frame__id { display: flex; align-items: center; gap: clamp(8px, 1vmin, 18px); min-width: 0; }

/* The venue's own mark, given a rule rather than a plate: at this size a plate
   reads as a button. Never stretched — `contain` inside a fixed height is what
   lets a wide wordmark and a square badge both arrive intact. */
.frame__logo {
    height: clamp(22px, 3.4vmin, 54px);
    width: auto;
    max-width: clamp(90px, 14vmin, 220px);
    object-fit: contain;
    padding-right: clamp(8px, 1vmin, 18px);
    border-right: 2px solid var(--accent);
}

.frame__names { display: flex; flex-direction: column; min-width: 0; line-height: 1.15; }

.frame__venue {
    font-size: clamp(11px, 1.55vmin, 26px);
    font-weight: 700;
    letter-spacing: 0.01em;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.frame__branch {
    font-size: clamp(9px, 1.05vmin, 17px);
    color: var(--ink-muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* The segment label. Tells somebody arriving mid-loop what they are looking at
   before they have read any of it. */
.frame__section {
    margin-left: auto;
    display: flex; align-items: center; gap: 0.6em;
    padding: clamp(3px, 0.5vmin, 9px) clamp(8px, 1.1vmin, 20px);
    border-radius: 99px;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    font-size: clamp(9px, 1.15vmin, 19px);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    white-space: nowrap;
}

/* The one deliberate loop in the frame, and it is six pixels wide: a live
   indicator that does not blink is not read as live. */
.frame__dot {
    width: 0.6em; height: 0.6em; border-radius: 50%;
    background: var(--accent);
    animation: pip 2.4s ease-in-out infinite;
}

.frame__wx {
    display: flex; align-items: baseline; gap: 0.4em;
    font-size: clamp(11px, 1.5vmin, 25px);
    font-weight: 700;
    white-space: nowrap;
}

.frame__wx-glyph { font-size: 1.15em; }
.frame__wx-temp { font-variant-numeric: tabular-nums; }

.frame__clock {
    font-size: clamp(13px, 1.9vmin, 32px);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
    white-space: nowrap;
}

/* ══ Scenes ════════════════════════════════════════════════════════════════
   Row two. The scene gets exactly what the bands leave it. */

.scenes { position: relative; z-index: 1; min-height: 0; }

.scene {
    position: absolute;
    inset: 0;
    padding: var(--pad);
    opacity: 0;
    visibility: hidden;
    transform: scale(1.012);
    transition: opacity var(--fade) var(--ease),
                transform var(--fade) var(--ease),
                visibility 0s linear var(--fade);
    will-change: opacity, transform;
    display: flex;
    flex-direction: column;
    min-height: 0;
}

.scene.is-live {
    opacity: 1;
    visibility: visible;
    transform: scale(1);
    transition: opacity var(--fade) var(--ease),
                transform var(--fade) var(--ease),
                visibility 0s;
}

/* The children of a live scene arrive just after it does, so a scene assembles
   rather than appearing whole. One pass, no loop. */
.scene.is-live [data-enter] {
    animation: rise 600ms var(--ease-out) both;
    animation-delay: calc(var(--i, 0) * 55ms + 110ms);
}

.scene__title {
    margin: 0 0 clamp(8px, 1.2vmin, 24px);
    font-size: clamp(17px, 2.6vmin, 48px);
    font-weight: 800;
    letter-spacing: -0.01em;
    flex: none;
}

.scene__title::after {
    content: "";
    display: block;
    width: clamp(40px, 5.5vmin, 120px);
    height: clamp(3px, 0.35vmin, 6px);
    margin-top: clamp(5px, 0.6vmin, 12px);
    border-radius: 99px;
    background: linear-gradient(90deg, var(--accent), var(--accent-glow), var(--accent));
    background-size: 260% 100%;
}

.scene.is-live .scene__title::after { animation: sweep 2.6s var(--ease) both 280ms; }

/* A code in the corner of any scene — a menu, an offer, the wifi. White card,
   because a QR on a photograph is a QR nothing scans. */
.scene__qr {
    position: absolute;
    right: var(--pad); bottom: var(--pad);
    margin: 0;
    padding: clamp(5px, 0.6vmin, 12px);
    background: #fff;
    border-radius: clamp(6px, 0.7vmin, 14px);
    animation: rise 700ms var(--ease-out) both 400ms;
    text-align: center;
}

.scene__qr-img { display: block; width: clamp(70px, 9vmin, 190px); height: auto; }

.scene__qr-cap {
    margin-top: 4px;
    color: #101b26;
    font-size: clamp(8px, 0.85vmin, 14px);
    font-weight: 700;
}

/* ── The reel ───────────────────────────────────────────────────────────── */

.scene--reel { padding: 0; }

.reel__layer {
    position: absolute; inset: 0;
    opacity: 0;
    transition: opacity var(--fade) var(--ease);
}
.reel__layer.is-live { opacity: 1; }

.reel__layer img, .reel__layer video {
    width: 100%; height: 100%;
    /* Fills the frame. A promotion designed for one ratio and shown on another
       either has bars or is cropped, and bars look like a fault. */
    object-fit: cover;
    display: block;
}

/* Alternating stills push in and pull out. Every slide drifting the same way is
   a tic the eye picks up within three rotations; alternating reads as camera
   work. */
.reel__layer[data-motion="in"].is-live img { animation: driftIn 24s linear both; }
.reel__layer[data-motion="out"].is-live img { animation: driftOut 24s linear both; }

/* A vignette over the reel. Photographs from a dozen sources have a dozen
   exposures; this gives them all the same seated-in look and stops a
   white-cornered picture blowing out the caption. */
.scene--reel::after {
    content: "";
    position: absolute; inset: 0;
    pointer-events: none;
    background: radial-gradient(ellipse at center, transparent 50%, rgba(0, 0, 0, 0.46));
}

.reel__caption {
    position: absolute; left: 0; right: 0; bottom: 0;
    z-index: 2;
    padding: clamp(14px, 2.6vmin, 52px);
    font-size: clamp(16px, 2.7vmin, 50px);
    font-weight: 700; line-height: 1.15; color: #fff;
    text-shadow: 0 2px 28px rgba(0, 0, 0, 0.72);
    background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
    animation: rise 700ms var(--ease-out) both 240ms;
}

/* ══ The menu board ════════════════════════════════════════════════════════

   Built the way a real board above a counter is built: panels with a header
   bar, then rows. The eye finds the category and reads down, rather than
   scanning one column of forty things.

   It never scrolls. Script measures the content against the space and either
   tightens it or splits it into pages that cross-fade — a menu board that
   scrolls is one where the item you wanted has just gone past. */

.menu {
    position: relative;
    flex: 1;
    min-height: 0;
    display: grid;
    grid-template-columns: repeat(var(--cols, 3), 1fr);
    gap: var(--gap);
    align-content: start;

    /* Set by script once it knows how much has to fit. */
    font-size: calc(1em * var(--fit, 1));
}

/* One page of the board. All pages are built; one is shown. */
.menu__page {
    position: absolute;
    inset: 0;
    display: grid;
    grid-template-columns: repeat(var(--cols, 3), 1fr);
    gap: var(--gap);
    align-content: start;
    opacity: 0;
    visibility: hidden;
    transition: opacity 520ms var(--ease), visibility 0s linear 520ms;
}

.menu__page.is-live {
    opacity: 1;
    visibility: visible;
    transition: opacity 520ms var(--ease), visibility 0s;
}

.menu__cat {
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: var(--radius);
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

/* The header bar. This is the single element that makes the difference between
   "a list of food" and "a menu board" — it is what every board above every
   counter has, and it is why the eye can find the section it wants from the
   back of a queue. */
.menu__catname {
    margin: 0;
    padding: clamp(5px, 0.7vmin, 13px) clamp(8px, 1vmin, 18px);
    background: var(--accent);
    color: var(--accent-ink);
    font-size: clamp(11px, 1.5vmin, 26px);
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    line-height: 1.2;
}

.menu__rows { padding: clamp(5px, 0.7vmin, 14px) clamp(8px, 1vmin, 18px); }

.menu__row {
    display: flex;
    align-items: baseline;
    gap: 0.5em;
    padding: clamp(2px, 0.28vmin, 7px) 0;
}

.menu__item {
    font-size: clamp(10px, 1.3vmin, 23px);
    font-weight: 600;
    line-height: 1.25;
}

/* Leader dots drawn rather than typed, so the price stays hard right whatever
   the name does — the thing that makes a price list readable at four metres. */
.menu__lead {
    flex: 1;
    border-bottom: 1.5px dotted var(--edge);
    transform: translateY(-0.26em);
    min-width: 10px;
}

.menu__price {
    font-size: clamp(10px, 1.35vmin, 24px);
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Sold out stays on the board. A guest who cannot see it will ask for it; one
   who can see it struck through will order something else. */
/* One item, which may be two lines. */
.menu__entry { padding: clamp(2px, 0.3vmin, 8px) 0; }

/* The description, quieter and beneath. Chain boards read as calm because the
   second line is genuinely secondary rather than competing with the first. */
.menu__desc {
    font-size: clamp(8px, 0.98vmin, 17px);
    line-height: 1.3;
    color: var(--ink-faint);
    padding-right: 3.5em;
    margin-top: 0.05em;
}

/* The high-margin item, emphasised rather than decorated: a rule in the accent
   and a heavier name. Every board in the trade marks one thing per section and
   marking more than one marks nothing. */
.menu__entry.is-signature {
    margin: 0 calc(-1 * clamp(8px, 1vmin, 18px));
    padding-left: clamp(5px, 0.7vmin, 14px);
    padding-right: clamp(8px, 1vmin, 18px);
    border-left: clamp(2px, 0.3vmin, 5px) solid var(--accent);
    background: var(--accent-soft);
}

.menu__entry.is-signature .menu__item { font-weight: 800; }

.menu__entry.is-out .menu__item,
.menu__entry.is-out .menu__price {
    opacity: 0.4;
    text-decoration: line-through;
}

.menu__entry.is-out .menu__desc { opacity: 0.4; }

.menu__tag {
    padding: 0.05em 0.45em;
    border-radius: 99px;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    color: var(--ink);
    font-size: clamp(7px, 0.85vmin, 14px);
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    white-space: nowrap;
}

/* Which page of the board is showing. Only ever drawn when there is more than
   one, so its presence means something. */
.menu__pager {
    flex: none;
    display: flex;
    justify-content: center;
    gap: 0.5em;
    padding-top: clamp(6px, 0.8vmin, 14px);
}

.menu__pip {
    width: clamp(5px, 0.6vmin, 10px);
    height: clamp(5px, 0.6vmin, 10px);
    border-radius: 50%;
    background: var(--edge);
    transition: background 300ms ease, transform 300ms var(--ease);
}

.menu__pip.is-live { background: var(--accent); transform: scale(1.35); }


/* ── Adverts on the board ───────────────────────────────────────────────────

   A real menu board is not only a price list. Every one of them carries a deal
   panel next to the categories — the meal-deal tile, the combo, the takeaway
   price — because that is where the margin is and because a wall of prices with
   nothing to look at is a wall nobody looks at.

   So a promo is a tile in the same grid as the categories, sized in grid units
   rather than pinned somewhere, and the script decides how many of each fit. */

.menu__promo {
    position: relative;
    grid-row: span var(--rows, 1);
    border-radius: var(--radius);
    overflow: hidden;
    background: linear-gradient(145deg, var(--accent), color-mix(in srgb, var(--accent) 62%, #000));
    color: var(--accent-ink);
    border: 1px solid rgba(255, 255, 255, 0.22);
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* A promo with a photograph: the picture is the tile and the words sit over it,
   which is how every board in the reference images does it. */
.menu__promo-img {
    position: absolute;
    inset: 0;
    width: 100%; height: 100%;
    object-fit: cover;
    opacity: 0.55;
}

.menu__promo-body {
    position: relative;
    z-index: 1;
    padding: clamp(8px, 1.2vmin, 24px);
    display: flex;
    flex-direction: column;
    gap: 0.25em;
    height: 100%;
    background: linear-gradient(180deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.45));
}

.menu__promo-flag {
    align-self: flex-start;
    padding: 0.15em 0.7em;
    border-radius: 99px;
    background: rgba(0, 0, 0, 0.35);
    font-size: clamp(8px, 0.95vmin, 16px);
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.menu__promo-name {
    font-size: clamp(13px, 1.85vmin, 34px);
    font-weight: 900;
    line-height: 1.12;
    text-shadow: 0 2px 14px rgba(0, 0, 0, 0.4);
}

.menu__promo-desc {
    font-size: clamp(9px, 1.05vmin, 19px);
    line-height: 1.3;
    opacity: 0.92;
}

/* The price, bottom right and enormous — the "3.00" on the takeaway panel and
   the "5.95" on the wrap deal. It is the entire point of the tile. */
.menu__promo-price {
    margin-top: auto;
    align-self: flex-end;
    font-size: clamp(20px, 3.4vmin, 68px);
    font-weight: 900;
    line-height: 1;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    text-shadow: 0 3px 18px rgba(0, 0, 0, 0.45);
}

.menu__promo-price small {
    font-size: 0.45em;
    font-weight: 800;
    opacity: 0.85;
    margin-right: 0.15em;
}

/* ── A price matrix ─────────────────────────────────────────────────────────

   The Wild Bean coffee board: one row per drink, one column per cup size. A
   drink that comes in three sizes is one row with three prices, not three rows
   — which is both shorter and how anybody actually chooses. */

.menu__cat--matrix .menu__row { gap: 0; }

.menu__sizes {
    display: flex;
    gap: clamp(6px, 0.9vmin, 18px);
    padding: 0 clamp(8px, 1vmin, 18px) clamp(3px, 0.4vmin, 8px);
    justify-content: flex-end;
}

.menu__size {
    min-width: clamp(28px, 3.6vmin, 68px);
    text-align: right;
    font-size: clamp(8px, 0.9vmin, 15px);
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.menu__prices { display: flex; gap: clamp(6px, 0.9vmin, 18px); }

.menu__prices .menu__price {
    min-width: clamp(28px, 3.6vmin, 68px);
    text-align: right;
}

/* ══ The order board ═══════════════════════════════════════════════════════ */

.board { display: flex; flex-direction: column; gap: var(--gap); flex: 1; min-height: 0; }

.board__group {
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: var(--radius);
    padding: clamp(8px, 1.2vmin, 24px);
    min-height: 0;
    display: flex;
    flex-direction: column;
}

/* Ready gets the room: somebody waiting for a number reads one of these lists
   and glances at the other. */
#ready-group { flex: 2; }
#preparing-group { flex: 1; }

.board__heading {
    margin: 0 0 clamp(5px, 0.8vmin, 14px);
    display: flex; align-items: center; gap: 0.5em;
    font-size: clamp(11px, 1.5vmin, 26px);
    font-weight: 800;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    flex: none;
}

.board__heading--ready { color: var(--ready); }
.board__heading--prep { color: var(--prep); }

/* Two pixels of movement, and the only loop on the board: a live indicator that
   does not move is not read as live. */
.board__pip {
    width: 0.55em; height: 0.55em; border-radius: 50%;
    background: currentColor;
    animation: pip 2s ease-in-out infinite;
}

.board__list {
    margin: 0; padding: 0; list-style: none;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(clamp(90px, 13vmin, 260px), 1fr));
    gap: clamp(5px, 0.8vmin, 16px);
    align-content: start;
    min-height: 0;
    overflow: hidden;
}

.board__item {
    background: var(--ready-soft);
    border: 1px solid var(--ready);
    border-radius: var(--radius);
    padding: clamp(5px, 0.8vmin, 16px);
    text-align: center;
}

.board__number {
    display: block;
    font-size: clamp(19px, 3.1vmin, 62px);
    font-weight: 900;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.01em;
}

.board__name {
    display: block;
    margin-top: 0.2em;
    font-size: clamp(9px, 1.05vmin, 18px);
    color: var(--ink-muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* The freshest number carries the glow. One item, not the list: highlighting
   everything highlights nothing. */
.board__item.is-new {
    box-shadow: 0 0 0 2px var(--ready), 0 0 34px var(--accent-glow);
    animation: pop 620ms var(--ease-out) both;
}

.board__list--quiet .board__item { background: var(--prep-soft); border-color: var(--prep); }
.board__list--quiet .board__number { color: var(--prep); font-size: clamp(15px, 2.2vmin, 44px); }

.board__empty {
    margin: 0;
    color: var(--ink-faint);
    font-size: clamp(11px, 1.4vmin, 24px);
}

/* ══ Offers ════════════════════════════════════════════════════════════════ */

.offers {
    flex: 1;
    min-height: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(clamp(140px, 20vmin, 400px), 1fr));
    gap: var(--gap);
    align-content: start;
    overflow: hidden;
}

.offer {
    position: relative;
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: var(--radius);
    overflow: hidden;
    box-shadow: 0 18px 44px rgba(0, 0, 0, 0.28);
    display: flex;
    flex-direction: column;
}

:root[data-theme="light"] .offer { box-shadow: 0 14px 34px rgba(16, 27, 38, 0.12); }

.offer__img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; display: block; }
.offer__body { padding: clamp(8px, 1vmin, 22px); display: flex; flex-direction: column; gap: 0.3em; }

.offer__name {
    font-size: clamp(12px, 1.6vmin, 28px);
    font-weight: 800;
    line-height: 1.2;
}

.offer__desc {
    font-size: clamp(9px, 1.1vmin, 19px);
    color: var(--ink-muted);
    line-height: 1.35;
}

.offer__price {
    margin-top: auto;
    font-size: clamp(14px, 2vmin, 36px);
    font-weight: 900;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}

/* A corner flash, drawn rather than an image, so it costs nothing and scales. */
.offer__flag {
    position: absolute;
    top: clamp(6px, 0.8vmin, 14px); left: clamp(6px, 0.8vmin, 14px);
    z-index: 2;
    padding: 0.2em 0.7em;
    border-radius: 99px;
    background: var(--accent);
    color: var(--accent-ink);
    font-size: clamp(8px, 0.95vmin, 16px);
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* ══ A stream ══════════════════════════════════════════════════════════════ */

.scene--stream { padding: 0; }
.stream { position: absolute; inset: 0; background: #000; }
.stream iframe, .stream video { width: 100%; height: 100%; border: 0; display: block; }

/* ══ Weather ═══════════════════════════════════════════════════════════════
   The detailed version. The frame carries the one-glance version, which is why
   this scene is allowed to be a panel rather than a headline. */

.wx { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: var(--gap); }

.wx__now {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: clamp(10px, 2vmin, 40px);
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: var(--radius-lg);
    padding: clamp(10px, 1.6vmin, 34px);
}

.wx__glyph { font-size: clamp(44px, 9vmin, 180px); line-height: 1; }

.wx__temp {
    font-size: clamp(34px, 7vmin, 140px);
    font-weight: 900;
    line-height: 0.95;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
}

.wx__desc { font-size: clamp(14px, 2vmin, 40px); font-weight: 700; margin-top: 0.1em; }
.wx__place { font-size: clamp(10px, 1.2vmin, 22px); color: var(--ink-muted); margin-top: 0.2em; }

.wx__stats {
    display: grid;
    grid-template-columns: repeat(2, auto);
    gap: clamp(6px, 1vmin, 20px) clamp(12px, 2vmin, 44px);
}

.wx__stat { display: flex; flex-direction: column; align-items: flex-end; }
.wx__stat span { font-size: clamp(14px, 1.9vmin, 36px); font-weight: 800; font-variant-numeric: tabular-nums; }

.wx__stat small {
    font-size: clamp(8px, 0.9vmin, 15px);
    color: var(--ink-faint);
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* The next few hours — the horizon that decides whether anybody sits outside. */
.wx__hours {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: clamp(5px, 0.8vmin, 14px);
}

.wx__hour {
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: var(--radius);
    padding: clamp(5px, 0.8vmin, 16px) 4px;
    text-align: center;
}

.wx__hour b { display: block; font-size: clamp(11px, 1.5vmin, 26px); font-weight: 800; }
.wx__hour span { display: block; font-size: clamp(13px, 1.8vmin, 32px); }
.wx__hour small { display: block; font-size: clamp(8px, 0.9vmin, 15px); color: var(--ink-faint); }

.wx__days { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(5px, 0.8vmin, 14px); }

.wx__day {
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: var(--radius);
    padding: clamp(6px, 1vmin, 20px);
    display: flex;
    align-items: center;
    gap: 0.6em;
}

.wx__day-glyph { font-size: clamp(18px, 2.6vmin, 46px); }
.wx__day-label { font-size: clamp(10px, 1.25vmin, 22px); font-weight: 800; text-transform: uppercase; }
.wx__day-temps { margin-left: auto; font-size: clamp(11px, 1.4vmin, 25px); font-variant-numeric: tabular-nums; }
.wx__day-temps b { font-weight: 800; }
.wx__day-temps span { color: var(--ink-faint); }

.wx__stale { margin: 0; color: var(--ink-faint); font-size: clamp(9px, 1vmin, 17px); }

/* ══ Trending ══════════════════════════════════════════════════════════════ */

.news {
    flex: 1; min-height: 0;
    margin: 0; padding: 0; list-style: none;
    display: flex; flex-direction: column;
    gap: clamp(5px, 0.8vmin, 16px);
    overflow: hidden;
}

.news__item {
    background: var(--card);
    border: 1px solid var(--edge);
    border-left: clamp(3px, 0.4vmin, 7px) solid var(--accent);
    border-radius: var(--radius);
    padding: clamp(7px, 1vmin, 20px) clamp(9px, 1.3vmin, 26px);
}

.news__title {
    font-size: clamp(11px, 1.5vmin, 27px);
    font-weight: 700;
    line-height: 1.3;
}

.news__source {
    margin-top: 0.25em;
    display: flex;
    align-items: center;
    gap: 0.6em;
    font-size: clamp(8px, 0.95vmin, 16px);
    color: var(--ink-faint);
    letter-spacing: 0.05em;
    text-transform: uppercase;
}

/* Why this headline is near the top. Without it the ordering reads as
   arbitrary, which is worse than no ordering at all. */
.news__flag {
    padding: 0.15em 0.6em;
    border-radius: 99px;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    color: var(--ink);
    font-weight: 800;
    letter-spacing: 0.08em;
}

.news__flag.is-breaking {
    background: #d3352f;
    border-color: #d3352f;
    color: #fff;
    animation: pip 2.2s ease-in-out infinite;
}

/* ══ Welcome ═══════════════════════════════════════════════════════════════ */

.welcome {
    flex: 1;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    gap: clamp(8px, 1.4vmin, 28px);
    text-align: center;
}

/* The venue's brand on the venue's wall, so it gets real furniture: its own
   plate, a hairline in the accent, and a soft lift. Contained, never stretched
   — and removed in script rather than left as a broken-image glyph. */
.welcome__logo {
    max-width: min(40vw, 460px);
    max-height: 28vh;
    object-fit: contain;
    padding: clamp(10px, 1.4vmin, 30px) clamp(14px, 1.9vmin, 40px);
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: var(--radius-lg);
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.34), 0 0 0 6px var(--accent-soft);
    animation: logoIn 1100ms var(--ease-out) both;
}

/* A light-mode logo is usually dark ink on white, and a white plate on a white
   scene disappears. The plate stays; the shadow does the separating. */
:root[data-theme="light"] .welcome__logo {
    background: #fff;
    box-shadow: 0 18px 44px rgba(16, 27, 38, 0.14), 0 0 0 6px var(--accent-soft);
}

.welcome__name {
    font-size: clamp(26px, 5vmin, 120px);
    font-weight: 900;
    letter-spacing: -0.03em;
    line-height: 1;
    animation: rise 900ms var(--ease-out) both 240ms;
}

.welcome__tag {
    font-size: clamp(11px, 1.5vmin, 26px);
    letter-spacing: 0.34em;
    text-transform: uppercase;
    color: var(--accent);
    font-weight: 700;
    animation: rise 900ms var(--ease-out) both 380ms;
}

.welcome__clock {
    font-size: clamp(20px, 3.4vmin, 70px);
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--ink-muted);
    animation: rise 900ms var(--ease-out) both 500ms;
}

/* ══ The rail ══════════════════════════════════════════════════════════════
   Row three. On screen whatever scene is running. */

.rail {
    position: relative;
    z-index: 3;
    display: flex;
    align-items: center;
    gap: clamp(8px, 1.2vmin, 22px);
    height: var(--rail-h);
    padding: 0 var(--pad);
    background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.55));
    border-top: 1px solid var(--edge-soft);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    overflow: hidden;
}

:root[data-theme="light"] .rail {
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.92));
}

.rail__label {
    flex: none;
    padding: clamp(2px, 0.35vmin, 7px) clamp(7px, 1vmin, 18px);
    border-radius: 99px;
    background: var(--ready);
    color: #04150d;
    font-size: clamp(9px, 1.1vmin, 18px);
    font-weight: 800;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.rail__chips { display: flex; gap: clamp(6px, 1vmin, 18px); align-items: center; min-width: 0; }

.rail__chip {
    padding: clamp(2px, 0.35vmin, 7px) clamp(8px, 1.1vmin, 20px);
    border-radius: var(--radius);
    background: var(--ready-soft);
    border: 1px solid var(--ready);
    font-size: clamp(13px, 1.85vmin, 34px);
    font-weight: 900;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    animation: pop 520ms var(--ease-out) both;
    animation-delay: calc(var(--i, 0) * 80ms);
}

/* ══ The ticker ════════════════════════════════════════════════════════════
   Row four. Headlines and the venue's own message, on one line. */

.ticker {
    position: relative;
    z-index: 3;
    display: flex;
    align-items: center;
    height: var(--ticker-h);
    background: var(--bg-soft);
    border-top: 1px solid var(--edge-soft);
    overflow: hidden;
}

.ticker__flag {
    flex: none;
    align-self: stretch;
    display: grid;
    place-items: center;
    padding: 0 clamp(8px, 1.2vmin, 22px);
    background: var(--accent);
    color: var(--accent-ink);
    font-size: clamp(9px, 1.1vmin, 18px);
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    white-space: nowrap;
}

.ticker__window { flex: 1; overflow: hidden; }

.ticker__track {
    display: inline-block;
    white-space: nowrap;
    padding-left: 100%;
    font-size: clamp(11px, 1.4vmin, 25px);
    font-weight: 600;
    animation: marquee 40s linear infinite;
}

/* ══ Scene progress ════════════════════════════════════════════════════════ */

.progress {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    z-index: 5;
    height: clamp(2px, 0.3vmin, 5px);
    background: rgba(255, 255, 255, 0.08);
    pointer-events: none;
}

.progress span {
    display: block;
    height: 100%;
    width: 100%;
    transform-origin: left center;
    transform: scaleX(0);
    background: var(--accent);
}

/* ══ The ShopCore mark ═════════════════════════════════════════════════════ */

.brand {
    position: absolute;
    left: var(--pad);
    bottom: calc(var(--rail-h) + var(--ticker-h) + clamp(6px, 0.9vmin, 16px));
    z-index: 4;
    width: clamp(18px, 2.2vmin, 40px);
    height: auto;
    opacity: 0.3;
    pointer-events: none;
}

/* ══ The sting ═════════════════════════════════════════════════════════════
   The venue's logo, wiping across between scenes.

   Two elements moving in opposite directions: a band of the venue's accent that
   sweeps in from the left and out to the right, and the mark itself which rides
   it. The wipe is what makes it read as a transition rather than as a logo
   fading in — the same reason a broadcaster wipes rather than dissolves into an
   ident. */

.sting {
    position: absolute;
    inset: 0;
    z-index: 20;
    display: grid;
    place-items: center;
    pointer-events: none;
    overflow: hidden;
}

.sting__wipe {
    position: absolute;
    inset: -10% -30%;
    background: linear-gradient(100deg,
        transparent 0%,
        var(--accent) 22%,
        color-mix(in srgb, var(--accent) 78%, #000) 50%,
        var(--accent) 78%,
        transparent 100%);
    transform: translateX(-100%);
    animation: stingWipe 1100ms var(--ease) both;
}

.sting__logo {
    position: relative;
    max-width: min(34vw, 380px);
    max-height: 24vh;
    object-fit: contain;
    opacity: 0;
    animation: stingMark 1100ms var(--ease) both;
    filter: drop-shadow(0 10px 30px rgba(0, 0, 0, 0.35));
}

/* The band crosses the screen once. Deliberately quicker on the way out than on
   the way in: a transition that lingers on the way out reads as a stall. */
@keyframes stingWipe {
    0%   { transform: translateX(-105%); }
    45%  { transform: translateX(0); }
    55%  { transform: translateX(0); }
    100% { transform: translateX(105%); }
}

/* The mark rides the band: visible only while the colour is actually behind it,
   so it never appears floating over the scene it is covering. */
@keyframes stingMark {
    0%   { opacity: 0; transform: scale(0.92); }
    35%  { opacity: 1; transform: scale(1); }
    58%  { opacity: 1; transform: scale(1.01); }
    75%  { opacity: 0; transform: scale(1.03); }
    100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .sting { display: none; }
}

/* ══ The call ══════════════════════════════════════════════════════════════
   A lower third, over the running scene. Announces just as loudly as a takeover
   and lets everybody else carry on reading the menu. */

.callout {
    position: fixed;
    left: 0; right: 0;
    bottom: calc(var(--rail-h) + var(--ticker-h));
    z-index: 40;
    padding: 0 var(--pad) clamp(8px, 1.2vmin, 22px);
    animation: lowerThird 520ms var(--ease-out) both;
    pointer-events: none;
}

.callout__bar {
    display: flex;
    align-items: center;
    gap: clamp(12px, 2vmin, 40px);
    padding: clamp(8px, 1.4vmin, 30px) clamp(12px, 2vmin, 40px);
    border-radius: var(--radius-lg);
    background: linear-gradient(100deg, var(--accent), color-mix(in srgb, var(--accent) 70%, #000));
    color: var(--accent-ink);
    box-shadow: 0 24px 70px rgba(0, 0, 0, 0.5);
    border: 1px solid rgba(255, 255, 255, 0.25);
}

.callout__label {
    flex: none;
    display: flex; align-items: center; gap: 0.5em;
    font-size: clamp(10px, 1.25vmin, 22px);
    font-weight: 800;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    opacity: 0.92;
}

.callout__pulse {
    width: 0.7em; height: 0.7em; border-radius: 50%;
    background: currentColor;
    animation: pip 1.2s ease-in-out infinite;
}

.callout__id { display: flex; align-items: baseline; gap: clamp(8px, 1.4vmin, 26px); min-width: 0; }

.callout__number {
    font-size: clamp(30px, 6vmin, 120px);
    font-weight: 900;
    line-height: 1;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

.callout__name {
    font-size: clamp(13px, 1.8vmin, 34px);
    font-weight: 700;
    opacity: 0.9;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.callout__qrwrap {
    margin: 0 0 0 auto;
    flex: none;
    background: #fff;
    border-radius: var(--radius);
    padding: clamp(4px, 0.55vmin, 10px);
    text-align: center;
}

.callout__qr { display: block; width: clamp(46px, 6vmin, 120px); height: auto; }

.callout__qrcap {
    margin-top: 2px;
    color: #101b26;
    font-size: clamp(7px, 0.75vmin, 12px);
    font-weight: 700;
}

/* ══ Trouble ═══════════════════════════════════════════════════════════════ */

.trouble {
    position: fixed;
    left: 50%; transform: translateX(-50%);
    bottom: calc(var(--rail-h) + var(--ticker-h) + clamp(8px, 1.2vmin, 20px));
    z-index: 50;
    display: flex; align-items: center; gap: 0.6em;
    padding: 0.45em 1em;
    border-radius: 99px;
    background: rgba(10, 15, 20, 0.9);
    border: 1px solid var(--edge);
    color: var(--ink-muted);
    font-size: clamp(10px, 1.15vmin, 19px);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
}

.trouble__dot {
    width: 0.55em; height: 0.55em; border-radius: 50%;
    background: var(--prep);
    animation: pip 1.6s ease-in-out infinite;
}

/* ══ The way in ════════════════════════════════════════════════════════════ */

.settings-tap {
    position: fixed;
    right: 0; bottom: 0;
    z-index: 60;
    width: 56px; height: 56px;
    display: grid; place-items: center;
    border: 0; background: transparent; color: var(--ink-faint);
    opacity: 0; cursor: pointer; transition: opacity 0.3s ease;
}

.settings-tap.is-shown { opacity: 0.65; }

.panel {
    position: fixed;
    inset: 0;
    z-index: 70;
    display: grid;
    place-items: center;
    background: rgba(6, 10, 15, 0.82);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    animation: calloutIn 260ms ease both;
}

.panel__card {
    width: min(420px, 92vw);
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: 20px;
    padding: 28px;
    display: grid;
    gap: 10px;
}

.panel__title { margin: 0; font-size: 22px; font-weight: 800; }
.panel__meta { margin: 0 0 8px; color: var(--ink-faint); font-size: 13px; line-height: 1.5; }

.panel__btn {
    padding: 15px 18px;
    border-radius: 12px;
    border: 1px solid var(--edge);
    background: var(--card-hi);
    color: var(--ink);
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    text-align: left;
    transition: border-color 0.15s ease, transform 0.08s ease;
}

.panel__btn:hover { border-color: var(--accent); }
.panel__btn:active { transform: translateY(1px); }
.panel__btn--danger { color: #ff9c9c; border-color: #7f1d1d; }
.panel__btn--quiet { color: var(--ink-faint); background: transparent; }

/* ══ Motion ════════════════════════════════════════════════════════════════ */

@keyframes rise {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}

@keyframes pop {
    0%   { opacity: 0; transform: scale(0.86); }
    60%  { opacity: 1; transform: scale(1.03); }
    100% { opacity: 1; transform: scale(1); }
}

/* The call arriving. Slides up from under its own edge rather than fading in,
   because a band that fades is a band somebody misses. */
@keyframes lowerThird {
    from { opacity: 0; transform: translateY(110%); }
    to   { opacity: 1; transform: none; }
}

@keyframes calloutIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@keyframes pip {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.25; }
}

@keyframes marquee {
    from { transform: translateX(0); }
    to   { transform: translateX(-100%); }
}

/* The backdrop clouds. Long, slow, and never in the same place at the same time
   as each other. */
@keyframes aurora {
    from { transform: translate3d(0, 0, 0) scale(1); }
    to   { transform: translate3d(6vw, 8vh, 0) scale(1.18); }
}

/* A slow push in and pull out on a still. Invisible per second, unmistakable
   over ten — the thing that stops a static promotion reading as a frozen
   screen. */
@keyframes driftIn {
    from { transform: scale(1) translate3d(0, 0, 0); }
    to   { transform: scale(1.06) translate3d(-1.2%, -0.8%, 0); }
}

@keyframes driftOut {
    from { transform: scale(1.06) translate3d(1.2%, 0.8%, 0); }
    to   { transform: scale(1) translate3d(0, 0, 0); }
}

@keyframes sweep {
    from { background-position: 120% 0; }
    to   { background-position: -60% 0; }
}

@keyframes logoIn {
    0%   { opacity: 0; transform: translateY(18px) scale(0.94); }
    70%  { opacity: 1; transform: translateY(0) scale(1.015); }
    100% { opacity: 1; transform: none; }
}


/* ══ THE TRANSITION ENGINE ═════════════════════════════════════════════════

   How one scene becomes the next.

   A broadcast changes shot in a dozen different ways, and a screen that only
   ever dissolves reads as a slideshow however good the scenes are. Each of
   these is a different piece of grammar rather than a variation: a cut is
   abrupt on purpose, a wipe says "and now something else", a push says "the
   next in a series", a zoom says "look at this".

   Implemented as data attributes on the stage rather than classes on scenes, so
   the incoming and outgoing scenes animate as a pair without either needing to
   know about the other. Every one is transform and opacity only, which is the
   difference between a smooth change on a cheap television and a stutter. */

.scene {
    /* The default. Overridden per transition below. */
    transition: opacity var(--fade) var(--ease),
                transform var(--fade) var(--ease),
                clip-path var(--fade) var(--ease),
                visibility 0s linear var(--fade);
}

/* ── Cut ─────────────────────────────────────────────────────────────────
   No transition at all. Abrupt on purpose: the right grammar for the order
   board, which is urgent, and wrong for almost everything else. */
.stage[data-transition="Cut"] .scene,
.stage[data-transition="Cut"] .scene.is-live {
    transition: visibility 0s;
    transform: none;
}

/* ── Wipes ───────────────────────────────────────────────────────────────
   The new scene sweeps across. clip-path rather than a moving mask, because a
   clip is composited and a mask is not. */
.stage[data-transition="WipeLeft"] .scene { clip-path: inset(0 0 0 100%); opacity: 1; transform: none; }
.stage[data-transition="WipeLeft"] .scene.is-live { clip-path: inset(0 0 0 0); }

.stage[data-transition="WipeRight"] .scene { clip-path: inset(0 100% 0 0); opacity: 1; transform: none; }
.stage[data-transition="WipeRight"] .scene.is-live { clip-path: inset(0 0 0 0); }

/* ── Pushes ──────────────────────────────────────────────────────────────
   The new scene arrives from an edge. Reads as "the next in a series", which
   is exactly what a menu page or a second offers wall is. */
.stage[data-transition="PushUp"] .scene { opacity: 1; transform: translateY(100%); }
.stage[data-transition="PushUp"] .scene.is-live { transform: none; }

.stage[data-transition="PushDown"] .scene { opacity: 1; transform: translateY(-100%); }
.stage[data-transition="PushDown"] .scene.is-live { transform: none; }

.stage[data-transition="SlideOver"] .scene { opacity: 1; transform: translateX(100%); }
.stage[data-transition="SlideOver"] .scene.is-live { transform: none; }

/* ── Zooms ───────────────────────────────────────────────────────────────
   "Look at this" and "we are finished with that". A hero shot earns one. */
.stage[data-transition="ZoomIn"] .scene { opacity: 0; transform: scale(0.86); }
.stage[data-transition="ZoomIn"] .scene.is-live { opacity: 1; transform: scale(1); }

.stage[data-transition="ZoomOut"] .scene { opacity: 0; transform: scale(1.16); }
.stage[data-transition="ZoomOut"] .scene.is-live { opacity: 1; transform: scale(1); }

/* ── Iris ────────────────────────────────────────────────────────────────
   Opens from the centre. The oldest transition there is and still the most
   theatrical, which is why it is worth having and worth using rarely. */
.stage[data-transition="Iris"] .scene { clip-path: circle(0% at 50% 50%); opacity: 1; transform: none; }
.stage[data-transition="Iris"] .scene.is-live { clip-path: circle(75% at 50% 50%); }

/* ══ POP-UPS ═══════════════════════════════════════════════════════════════

   Information that arrives over whatever is playing, and leaves.

   A broadcast interrupts itself constantly and none of it stops the programme.
   The scene underneath never pauses here either, because the guest reading the
   menu did not ask to be interrupted. */

.pops {
    position: fixed;
    right: var(--pad);
    bottom: calc(var(--rail-h) + var(--ticker-h) + clamp(10px, 1.4vmin, 26px));
    z-index: 35;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: clamp(6px, 0.9vmin, 16px);
    pointer-events: none;
    max-width: min(46vw, 640px);
}

.pop {
    display: flex;
    align-items: center;
    gap: clamp(8px, 1.1vmin, 20px);
    padding: clamp(8px, 1.1vmin, 22px) clamp(11px, 1.5vmin, 30px);
    border-radius: var(--radius-lg);
    background: var(--card);
    border: 1px solid var(--edge);
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.42);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    animation: popIn 460ms var(--ease-out) both;
}

/* Leaving is its own animation, run by script before the node is removed —
   otherwise a pop vanishes mid-sentence, which reads as a glitch. */
.pop.is-going { animation: popOut 340ms var(--ease) both; }

.pop__mark {
    flex: none;
    display: grid; place-items: center;
    width: clamp(24px, 3vmin, 54px);
    height: clamp(24px, 3vmin, 54px);
    border-radius: var(--radius);
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    font-size: clamp(12px, 1.5vmin, 26px);
    font-weight: 800;
}

.pop__body { min-width: 0; }

.pop__title {
    font-size: clamp(11px, 1.4vmin, 25px);
    font-weight: 800;
    line-height: 1.2;
}

.pop__text {
    margin-top: 0.1em;
    font-size: clamp(9px, 1.1vmin, 19px);
    color: var(--ink-muted);
    line-height: 1.3;
}

/* The one that must not be missed. Accent-filled rather than carded, and it is
   the only pop allowed to pulse. */
.pop--urgent {
    background: linear-gradient(100deg, var(--accent), color-mix(in srgb, var(--accent) 70%, #000));
    color: var(--accent-ink);
    border-color: rgba(255, 255, 255, 0.28);
}

.pop--urgent .pop__text { color: rgba(255, 255, 255, 0.88); }
.pop--urgent .pop__mark { background: rgba(0, 0, 0, 0.22); border-color: rgba(255, 255, 255, 0.3); }

@keyframes popIn {
    from { opacity: 0; transform: translateX(24px) scale(0.96); }
    to   { opacity: 1; transform: none; }
}

@keyframes popOut {
    from { opacity: 1; transform: none; }
    to   { opacity: 0; transform: translateX(24px) scale(0.98); }
}

/* ══ THE WIDER SCENE LIBRARY ═══════════════════════════════════════════════ */

/* ── Hero ────────────────────────────────────────────────────────────────
   One dish, edge to edge, price enormous. The single most effective screen in
   the trade: a photograph that fills a television and one number. */

.scene--hero { padding: 0; }

.hero__img {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    object-fit: cover;
    animation: driftIn 26s linear both;
}

.hero__body {
    position: relative;
    z-index: 1;
    margin-top: auto;
    padding: var(--pad);
    background: linear-gradient(transparent, rgba(0, 0, 0, 0.82));
    color: #fff;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.15em;
}

.hero__flag {
    padding: 0.2em 0.8em;
    border-radius: 99px;
    background: var(--accent);
    color: var(--accent-ink);
    font-size: clamp(9px, 1.1vmin, 19px);
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    animation: rise 600ms var(--ease-out) both 120ms;
}

.hero__name {
    margin: 0.15em 0 0;
    font-size: clamp(26px, 5.4vmin, 116px);
    font-weight: 900;
    line-height: 1.02;
    letter-spacing: -0.03em;
    text-shadow: 0 3px 30px rgba(0, 0, 0, 0.55);
    animation: rise 700ms var(--ease-out) both 200ms;
}

.hero__desc {
    margin: 0.2em 0 0;
    max-width: 60%;
    font-size: clamp(11px, 1.5vmin, 27px);
    line-height: 1.35;
    opacity: 0.9;
    animation: rise 700ms var(--ease-out) both 300ms;
}

.hero__price {
    align-self: flex-end;
    margin-top: -0.4em;
    font-size: clamp(34px, 7.5vmin, 160px);
    font-weight: 900;
    line-height: 1;
    letter-spacing: -0.04em;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
    text-shadow: 0 4px 40px rgba(0, 0, 0, 0.6);
    animation: pop 700ms var(--ease-out) both 380ms;
}

/* ── Split ───────────────────────────────────────────────────────────────
   Prices on one half, something appetising on the other. What most boards
   above a counter actually are. */

.scene--split { padding: 0; }
.split { display: grid; grid-template-columns: 1.15fr 1fr; height: 100%; }
.split__menu { padding: var(--pad); overflow: hidden; display: flex; flex-direction: column; }
.split__media { position: relative; overflow: hidden; background: var(--bg-soft); }
.split__media img, .split__media video { width: 100%; height: 100%; object-fit: cover; display: block; }

.stage[data-orientation="Portrait"] .split { grid-template-columns: 1fr; grid-template-rows: 1.2fr 1fr; }

/* ── Combo ───────────────────────────────────────────────────────────────
   Items, a bracket, one price. The "Duo Deal" panel, given a whole screen. */

.combo {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: clamp(10px, 2vmin, 44px);
    flex-wrap: wrap;
}

.combo__item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4em;
    text-align: center;
    animation: rise 620ms var(--ease-out) both;
    animation-delay: calc(var(--i, 0) * 120ms + 140ms);
}

.combo__img {
    width: clamp(80px, 16vmin, 320px);
    height: clamp(80px, 16vmin, 320px);
    object-fit: cover;
    border-radius: var(--radius-lg);
    border: 1px solid var(--edge);
}

.combo__name { font-size: clamp(11px, 1.5vmin, 27px); font-weight: 700; max-width: 9em; }

.combo__plus {
    font-size: clamp(22px, 4vmin, 80px);
    font-weight: 300;
    color: var(--ink-faint);
    line-height: 1;
}

.combo__price {
    font-size: clamp(30px, 6vmin, 130px);
    font-weight: 900;
    line-height: 1;
    letter-spacing: -0.03em;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
    animation: pop 700ms var(--ease-out) both 460ms;
}

/* ── The card scenes ─────────────────────────────────────────────────────
   Wifi, hours, loyalty, social, allergens, wait. All the same shell: a panel,
   centred, with rows. One layout for six scenes is the reason a seventh costs
   twenty lines rather than a hundred. */

.scene--card { align-items: center; justify-content: center; }

.card {
    width: min(72%, 900px);
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: var(--radius-lg);
    padding: clamp(16px, 2.4vmin, 52px);
    text-align: center;
    box-shadow: 0 26px 70px rgba(0, 0, 0, 0.3);
    animation: rise 640ms var(--ease-out) both 100ms;
}

.card--wide { width: min(88%, 1200px); }
:root[data-theme="light"] .card { box-shadow: 0 20px 50px rgba(16, 27, 38, 0.12); }

.card__icon {
    font-size: clamp(15px, 2vmin, 34px);
    font-weight: 900;
    letter-spacing: 0.1em;
    color: var(--accent);
    margin-bottom: 0.3em;
}

.card__title {
    margin: 0 0 clamp(8px, 1.2vmin, 24px);
    font-size: clamp(18px, 2.8vmin, 56px);
    font-weight: 900;
    letter-spacing: -0.01em;
}

.card__lead {
    margin: clamp(8px, 1.2vmin, 22px) 0 0;
    font-size: clamp(11px, 1.4vmin, 25px);
    color: var(--ink-muted);
    line-height: 1.4;
}

.card__rows { display: grid; gap: clamp(5px, 0.7vmin, 14px); }

.card__row {
    display: flex;
    align-items: baseline;
    gap: 0.6em;
    padding: clamp(5px, 0.7vmin, 14px) clamp(8px, 1.1vmin, 22px);
    background: var(--card-hi);
    border-radius: var(--radius);
    text-align: left;
}

.card__key {
    flex: none;
    min-width: 6em;
    font-size: clamp(9px, 1.1vmin, 19px);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.card__val {
    flex: 1;
    font-size: clamp(12px, 1.6vmin, 30px);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    word-break: break-word;
}

/* Today's row, marked. On an opening-hours card it is the only row anybody is
   actually reading. */
.card__row.is-today {
    background: var(--accent-soft);
    border: 1px solid var(--accent);
}

.card__qr {
    display: block;
    margin: clamp(12px, 1.6vmin, 30px) auto 0;
    width: clamp(90px, 13vmin, 260px);
    height: auto;
    background: #fff;
    padding: clamp(5px, 0.6vmin, 12px);
    border-radius: var(--radius);
}

.wait__figure {
    font-size: clamp(40px, 8vmin, 170px);
    font-weight: 900;
    line-height: 1;
    letter-spacing: -0.04em;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}

/* ── Countdown ───────────────────────────────────────────────────────────
   A number that moves is a number people act on. */

.scene--countdown { align-items: center; justify-content: center; }
.countdown { text-align: center; }

.countdown__label {
    font-size: clamp(12px, 1.7vmin, 32px);
    font-weight: 800;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--accent);
}

.countdown__clock {
    margin-top: 0.1em;
    font-size: clamp(52px, 12vmin, 260px);
    font-weight: 900;
    line-height: 1;
    letter-spacing: -0.04em;
    font-variant-numeric: tabular-nums;
}

.countdown__note {
    margin-top: 0.3em;
    font-size: clamp(11px, 1.5vmin, 28px);
    color: var(--ink-muted);
}

/* ── Gallery ─────────────────────────────────────────────────────────────
   A photograph wall, and the team board on the same grid. */

.gallery {
    flex: 1;
    min-height: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(clamp(110px, 17vmin, 340px), 1fr));
    gap: var(--gap);
    align-content: center;
    overflow: hidden;
}

.gallery__cell {
    position: relative;
    border-radius: var(--radius);
    overflow: hidden;
    aspect-ratio: 4 / 3;
    background: var(--card);
    border: 1px solid var(--edge);
    animation: rise 620ms var(--ease-out) both;
    animation-delay: calc(var(--i, 0) * 70ms + 120ms);
}

.gallery__cell img { width: 100%; height: 100%; object-fit: cover; display: block; }

.gallery__cap {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    padding: clamp(5px, 0.8vmin, 16px);
    background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
    color: #fff;
    font-size: clamp(9px, 1.15vmin, 20px);
    font-weight: 700;
}

.gallery--team .gallery__cell { aspect-ratio: 1; border-radius: 50%; }

/* ── Quote, story, message ───────────────────────────────────────────────
   Three scenes, one layout: a paragraph, set as large as it can be. */

.scene--quote { align-items: center; justify-content: center; }

.quote { max-width: min(80%, 1400px); margin: 0; text-align: center; }

.quote__text {
    margin: 0;
    font-size: clamp(17px, 3vmin, 62px);
    font-weight: 700;
    line-height: 1.28;
    letter-spacing: -0.01em;
    animation: rise 720ms var(--ease-out) both 120ms;
}

/* A review is a quotation and is marked as one. */
.scene--quote blockquote .quote__text::before { content: "\201C"; }
.scene--quote blockquote .quote__text::after { content: "\201D"; }

.quote__text--plain {
    font-size: clamp(12px, 1.9vmin, 36px);
    font-weight: 500;
    line-height: 1.5;
    color: var(--ink-muted);
    text-align: left;
}

.quote__text--big { font-size: clamp(22px, 4.4vmin, 96px); font-weight: 900; }

.quote__by {
    margin-top: clamp(8px, 1.2vmin, 24px);
    font-size: clamp(11px, 1.4vmin, 25px);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--accent);
    animation: rise 720ms var(--ease-out) both 260ms;
}

/* ── Events ──────────────────────────────────────────────────────────────
   What is on this week. Same anatomy as the news list, different content. */

.events {
    flex: 1; min-height: 0;
    margin: 0; padding: 0; list-style: none;
    display: flex; flex-direction: column;
    gap: clamp(5px, 0.8vmin, 16px);
    overflow: hidden;
}

.event {
    display: flex;
    align-items: center;
    gap: clamp(10px, 1.4vmin, 28px);
    background: var(--card);
    border: 1px solid var(--edge);
    border-left: clamp(3px, 0.4vmin, 7px) solid var(--accent);
    border-radius: var(--radius);
    padding: clamp(7px, 1vmin, 20px) clamp(10px, 1.4vmin, 28px);
}

.event__when {
    flex: none;
    min-width: 7em;
    font-size: clamp(10px, 1.25vmin, 22px);
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--accent);
}

.event__what { font-size: clamp(11px, 1.5vmin, 28px); font-weight: 700; }

/* ── Clock ───────────────────────────────────────────────────────────────
   A bumper, and what to show when there is genuinely nothing to say. */

.scene--clockface { align-items: center; justify-content: center; }
.clockface { text-align: center; }

.clockface__time {
    font-size: clamp(60px, 15vmin, 340px);
    font-weight: 900;
    line-height: 0.9;
    letter-spacing: -0.05em;
    font-variant-numeric: tabular-nums;
}

.clockface__date {
    margin-top: 0.2em;
    font-size: clamp(13px, 2vmin, 40px);
    font-weight: 700;
    letter-spacing: 0.24em;
    text-transform: uppercase;
    color: var(--ink-muted);
}

@media (prefers-reduced-motion: reduce) {
    .pop, .pop.is-going,
    .hero__img, .hero__flag, .hero__name, .hero__desc, .hero__price,
    .combo__item, .combo__price, .card, .gallery__cell,
    .quote__text, .quote__by { animation: none; }

    .stage[data-transition] .scene,
    .stage[data-transition] .scene.is-live {
        transition: opacity 200ms linear, visibility 0s;
        transform: none;
        clip-path: none;
    }
}

/* ══ Portrait ══════════════════════════════════════════════════════════════
   A screen hung the tall way. The menu drops to one column and the weather
   stacks, because a three-column menu on a portrait screen is unreadable. */

.stage[data-orientation="Portrait"] .menu,
.stage[data-orientation="Portrait"] .menu__page { --cols: 1; }

.stage[data-orientation="Portrait"] .wx__now {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
}

.stage[data-orientation="Portrait"] .wx__stats { grid-template-columns: repeat(4, auto); }
.stage[data-orientation="Portrait"] .wx__hours { grid-template-columns: repeat(3, 1fr); }
.stage[data-orientation="Portrait"] .offers { grid-template-columns: 1fr; }

/* A narrow screen cannot carry the segment label and the weather and the clock.
   The label goes first: it is the one a guest can infer from what is on screen. */
@media (max-aspect-ratio: 3/4) {
    .frame__section { display: none; }
}

/* ══ Reduced motion ════════════════════════════════════════════════════════
   Honoured here too. Somebody set this on a display machine for a reason. The
   broadcast still changes scene — it just stops moving while it does. */

@media (prefers-reduced-motion: reduce) {
    .scene, .scene.is-live { transition: opacity 200ms linear, visibility 0s; transform: none; }
    .menu__page, .menu__page.is-live { transition: opacity 200ms linear, visibility 0s; }

    .scene.is-live [data-enter],
    .board__item, .rail__chip, .scene__qr, .reel__caption,
    .callout, .callout__pulse, .scene.is-live .scene__title::after,
    .reel__layer.is-live img,
    .reel__layer[data-motion="in"].is-live img,
    .reel__layer[data-motion="out"].is-live img,
    .welcome__logo, .welcome__name, .welcome__tag, .welcome__clock,
    .stage::before, .stage::after,
    .ticker__track, .board__pip, .frame__dot, .trouble__dot { animation: none; }

    /* The ticker still has to move or it is a truncated sentence, but it can do
       it slowly and without easing. */
    .ticker__track { animation: marquee 90s linear infinite; }
}
