    * { box-sizing: border-box; }
    html { height: 100%; }
    body { margin: 0; height: 100dvh; background: #0b1220; color: #e7eefc;
      font: 15px system-ui, sans-serif; display: flex; flex-direction: column; overflow: hidden; }
    /* header is a flex row so the title sits left and the view tabs sit right */
    header { display: flex; align-items: center; justify-content: space-between; gap: 12px;
      padding: 10px max(14px, env(safe-area-inset-right)) 10px max(14px, env(safe-area-inset-left));
      background: #111a2e; border-bottom: 1px solid #1e2b47; }
    h1 { margin: 0; font-size: clamp(14px, 4vw, 16px); font-weight: 600; }
    /* live ↔ snapshot tabs (shared with snapshot.html) */
    .tabs { display: flex; gap: 6px; flex-shrink: 0; }
    .tab { text-decoration: none; padding: 5px 10px; border: 1px solid #1e2b47; border-radius: 7px;
      background: rgba(17,26,46,.85); color: #e7eefc; font: 700 12px system-ui; white-space: nowrap; }
    .tab:hover { background: #1e2b47; }
    .tab[aria-current="page"] { border-color: #38bdf8; color: #38bdf8; }
    main { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 12px; gap: 0; }
    .chrome-top, .chrome-bottom { display: flex; align-items: center; gap: 14px; width: 100%; max-width: 1280px; }
    .chrome-top:empty, .chrome-bottom:empty { display: none; }
    .wrap { position: relative; width: 100%; max-width: 1280px; aspect-ratio: 16/9; max-height: 100%;
      background: #000; border-radius: 12px; overflow: hidden; touch-action: none; }
    /* Framed (a chrome bar is populated): fitStage() in overlays.js sizes the video + bars to
       the video's DISPLAYED 16:9 width (given the height left between the bars) and sets that
       inline, so the bars track the video exactly — no overhang when it letterboxes at
       short/wide viewports. These overrides drop the box caps so the inline width takes effect
       and square the video's corners so the outer-rounded bars meet it as one rounded frame.
       (Pure CSS can't size this — the video's size depends on the bars' height and vice-versa.) */
    main:has(.chrome-top:not(:empty)) .wrap, main:has(.chrome-bottom:not(:empty)) .wrap {
      max-width: none; max-height: none; border-radius: 0; }
    video, #snap { width: 100%; height: 100%; object-fit: contain; background: #000;
      transform-origin: center center; transition: transform .05s linear; cursor: grab; }
    video.panning, #snap.panning { cursor: grabbing; transition: none; }
    /* #snap (the snapshot main image) is hidden until snapshot.js decodes the first still and
       adds .show — the "waiting for first snapshot…" status shows over the black wrap until then.
       (live's <video> autoplays, so it needs no such gate.) */
    #snap { display: none; }
    #snap.show { display: block; }
    /* on-video positioning is #wrap-scoped; appearance (flex/colour/background) is global. */
    #status, .overlay-status { display: flex; align-items: center; text-align: center;
      justify-content: center; color: #8aa0c6; background: rgba(11,18,32,.6); padding: 16px; }
    #wrap .overlay-status { position: absolute; inset: 0; }
    #status:empty, .overlay-status:empty { display: none; }
    /* stats HUD (top-left on the video; flows in a chrome bar) */
    .hud, .overlay-hud {
      display: flex; gap: 10px; flex-wrap: wrap; padding: 6px 10px; border-radius: 8px; background: rgba(11,18,32,.6);
      font: 600 12px ui-monospace, SFMono-Regular, Menlo, monospace; color: #cfe0ff;
      pointer-events: none; user-select: none; }
    #wrap .overlay-hud { position: absolute; top: max(10px, env(safe-area-inset-top)); left: max(10px, env(safe-area-inset-left)); }
    .hud b, .overlay-hud b { color: #38bdf8; font-weight: 700; }
    /* OSD date/time — the SINGLE timestamp (the burned-in stamp is disabled). Styled
       like the former stamp: solid black rounded box, white Helvetica,
       "YYYY-MM-DD HH:MM:SS AEST", sized to match it, bottom-right above the timeline.
       Reflects the displayed frame (live = now, offline/scrub = capture time). */
    #clock, .overlay-clock {
      padding: 5px 11px; border-radius: 6px; background: #000; color: #fff;
      font: 600 clamp(16px, 1.7vw, 22px) Helvetica, Arial, system-ui, sans-serif;
      white-space: nowrap; pointer-events: none; user-select: none; z-index: 4; }
    #wrap .overlay-clock { position: absolute; right: max(14px, env(safe-area-inset-right)); bottom: 56px; }
    #clock:empty, .overlay-clock:empty { display: none; }
    /* controls (top-right) — the legacy `.controls` bar is now the `.overlay-controls`
       overlay component (overlays/controls.js); both selectors share this styling. */
    .controls, .overlay-controls { display: flex; gap: 6px; }
    #wrap .overlay-controls { position: absolute; top: max(10px, env(safe-area-inset-top)); right: max(10px, env(safe-area-inset-right)); }
    .controls button, .overlay-controls button { width: 34px; height: 30px; padding: 0; border: 1px solid #1e2b47;
      border-radius: 7px; background: rgba(17,26,46,.85); color: #e7eefc; font-size: 15px;
      font-weight: 600; cursor: pointer; }
    .controls button:hover, .overlay-controls button:hover { background: #1e2b47; }
    .controls button.wide, .overlay-controls button.wide { width: auto; padding: 0 10px; }
    /* offline fallback — shown when the stream stops (last snapshot). The element is the
       offline OVERLAY (overlays/offline.js: #ov-offline > img.offline-img + div.offline-badge);
       both legacy id and overlay-class selectors share this styling. No z-index (as the
       legacy #offline): the positioned image paints above the (non-positioned) video, and
       overlays/offline.js re-homes its container to the FRONT of #wrap so all other chrome
       (clock/badge z:4, preview z:5, timeline/controls/hud) still paints OVER it by DOM order. */
    #offline, .offline-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
      background: #000; display: none; }
    #offline.show, .offline-img.show { display: block; }
    #offlineBadge, .offline-badge { position: absolute; bottom: 54px; left: 50%; transform: translateX(-50%);
      padding: 5px 12px; border-radius: 999px; background: rgba(176,42,42,.9); color: #fff;
      font: 600 12px system-ui; display: none; z-index: 4; }
    #offlineBadge.show, .offline-badge.show { display: block; }
    /* timeline scrubber (YouTube-style, bottom). The flex row is a row in any region (global);
       the over-the-video positioning (absolute + gradient scrim) is #wrap-scoped. */
    .timeline { display: flex; align-items: center; gap: 10px;
      padding: 10px max(14px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(14px, env(safe-area-inset-left)); }
    #wrap .overlay-timeline .timeline { position: absolute; left: 0; right: 0; bottom: 0;
      background: linear-gradient(transparent, rgba(11,18,32,.9)); }
    /* YouTube-style scrubber */
    .bar { position: relative; flex: 1; height: 16px; display: flex; align-items: center;
      cursor: pointer; outline: none; touch-action: none; }
    .bar.empty { opacity: .4; pointer-events: none; }
    .bar-track, .bar-fill { position: absolute; left: 0; top: 50%; transform: translateY(-50%);
      height: 3px; border-radius: 2px; transition: height .1s ease; }
    .bar-track { right: 0; background: rgba(255,255,255,.3); }
    .bar-fill { width: 0; background: #f00; pointer-events: none; }
    .bar:hover .bar-track, .bar:hover .bar-fill, .bar:focus-visible .bar-track, .bar:focus-visible .bar-fill { height: 5px; }
    .bar-handle { position: absolute; top: 50%; left: 0; width: 13px; height: 13px; border-radius: 50%;
      background: #f00; transform: translate(-50%, -50%) scale(0); transition: transform .1s ease; pointer-events: none; }
    .bar:hover .bar-handle, .bar:focus-visible .bar-handle { transform: translate(-50%, -50%) scale(1); }
    .seekHint { position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%); padding: 3px 10px;
      border-radius: 6px; background: rgba(0,0,0,.8); color: #fff; font: 700 13px ui-monospace, Menlo, monospace;
      display: none; pointer-events: none; white-space: nowrap; }
    .seekHint.show { display: block; }
    /* hover preview thumbnail (storyboard) */
    .preview { position: absolute; bottom: 58px; transform: translateX(-50%); display: none; z-index: 5;
      background: #000; border: 2px solid rgba(255,255,255,.7); border-radius: 8px; overflow: hidden;
      box-shadow: 0 6px 18px rgba(0,0,0,.6); pointer-events: none; }
    .preview.show { display: block; }
    .preview img { display: block; width: 160px; height: 90px; object-fit: cover; background: #111; }
    .preview .ptime { text-align: center; font: 600 11px ui-monospace, Menlo, monospace; color: #fff;
      padding: 3px 6px; background: rgba(0,0,0,.9); }
    .preview .pevent { text-align: center; font: 700 10px system-ui; padding: 2px 6px 3px;
      background: rgba(0,0,0,.92); display: none; }
    .preview .pevent.show { display: block; }
    /* astronomical-event markers on the timeline */
    .markers { position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
      height: 11px; pointer-events: none; }
    .marker { position: absolute; top: 0; width: 2px; height: 11px; transform: translateX(-50%);
      border-radius: 1px; opacity: .9; }
    #liveBtn { border: 1px solid #1e2b47; background: rgba(17,26,46,.85); color: #e7eefc;
      border-radius: 7px; padding: 4px 10px; font: 700 12px system-ui; cursor: pointer; white-space: nowrap; }
    #liveBtn.islive { color: #ff5a5a; border-color: #5a1e22; }

    /* Chrome bars (framed layout) — overlays moved off the video flow here.
       Tone matches the header (#111a2e). */
    .chrome-top { padding: 6px 12px; background: #111a2e; border-radius: 12px 12px 0 0; }
    /* top bar: FPS/Viewers left, zoom/save/etc pushed to the right */
    .chrome-top .overlay-controls { margin-left: auto; }
    /* position:relative makes the bar the positioning context for the timeline's hover
       preview (its absolute left/bottom anchor here, mirroring #wrap on-video). */
    .chrome-bottom { padding: 4px 12px; background: #111a2e; border-radius: 0 0 12px 12px; position: relative; }
    /* bottom bar: scrubber fills the left, clock sits at the right */
    .chrome-bottom .overlay-timeline { order: 0; }
    .chrome-bottom .overlay-clock { order: 1; }
    /* In the bottom bar the preview pops UP above the scrubber. .chrome-bottom is the
       positioning context (position:relative below); bottom:calc(100%+8px) places the
       preview's bottom edge 8px above the bar's top. left is clamped against this same
       offsetParent in the JS, keeping the horizontal centring consistent. (The on-video
       58px fallback is tuned for #wrap and is NOT used here.) */
    .ov-region-bottom .preview { bottom: calc(100% + 8px); }
    /* in a bar, overlays flow (belt-and-braces with the #wrap-scoping above) */
    .chrome-top .overlay, .chrome-bottom .overlay { position: static; }
    /* the scrubber fills the bottom bar */
    .chrome-bottom .overlay-timeline { flex: 1; }
    /* drop the floating look in the bar; the bar provides contrast */
    .chrome-top .overlay-hud, .chrome-top .overlay-clock, .chrome-top .overlay-status,
    .chrome-top .overlay-controls, .chrome-bottom .overlay-clock { background: none; padding: 0; }

    /* Stacked bar rows: the location label and the brand strip each take a FULL-WIDTH row
       ABOVE the functional row (label above the HUD; brand strip between the video and the
       scrubber). flex-wrap lets the rest flow onto the next line; order:-1 forces them first. */
    .chrome-top, .chrome-bottom { flex-wrap: wrap; row-gap: 6px; }
    .chrome-top .overlay-label, .chrome-bottom .overlay-brandbar { order: -1; }
    .chrome-top .overlay-label { flex: 0 0 100%; }
    /* brand strip is FULL-BLEED: cancel the bar's padding (calc(100%+24px) + negative margins)
       so it spans the entire frame width, flush under the video, with square corners — exactly
       like the reference bar. */
    .chrome-bottom .overlay-brandbar { flex: 0 0 auto; width: calc(100% + 24px); margin: -4px -12px 0; }
    /* location / camera label — flat, uppercase, letter-spaced (consumer-cam style) */
    .overlay-label { background: none; padding: 0; }
    .ov-label-text { font: 700 13px ui-monospace, SFMono-Regular, Menlo, monospace;
      letter-spacing: .14em; text-transform: uppercase; color: #cfe0ff; }
    /* brand strip — wordmark (left), promo/tagline (centre, grows), CTA pill (right) */
    .overlay-brandbar { display: flex; align-items: center; gap: 12px; }
    .bb-brand { font: 800 16px system-ui, sans-serif; letter-spacing: .04em; color: #e7eefc; white-space: nowrap; }
    .bb-promo { flex: 1; min-width: 0; text-align: center; color: #cfe0ff; font: 600 13px system-ui;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .bb-cta { margin-left: auto; text-decoration: none; padding: 5px 14px; border-radius: 999px;
      background: #38bdf8; color: #0b1220; font: 700 12px system-ui; white-space: nowrap; }
    .bb-cta:hover { background: #5cc8fa; }
    /* image mode: the brand PNG banner spans the full strip, square corners (swappable artwork) */
    .bb-img { display: block; width: 100%; height: auto; border-radius: 0; }

    /* double-tap seek (overlays/seek.js) — a full-cover layer over the video; only the cue is
       visible. pointer-events:none so it never blocks the pan/scrubber/controls (the detector
       listens on #wrap). The cue (⏪/⏩) pulses on the tapped side, accumulating a count. */
    #wrap .overlay-seek { position: absolute; inset: 0; pointer-events: none; z-index: 6; }
    .seekzone { position: absolute; top: 0; bottom: 0; width: 50%; display: flex; align-items: center; justify-content: center; }
    .seekzone-l { left: 0; }
    .seekzone-r { right: 0; }
    .seekcue { display: flex; align-items: center; gap: 8px; padding: 14px 22px; border-radius: 999px;
      background: rgba(0,0,0,.55); color: #fff; font: 800 16px system-ui, sans-serif; opacity: 0; }
    .seekcue.show { animation: seekpulse .7s ease both; }
    @keyframes seekpulse { 0% { opacity: 0; transform: scale(.7); } 18% { opacity: 1; transform: scale(1); }
      100% { opacity: 0; transform: scale(1.06); } }
    .seekcue-ico { font-size: 22px; line-height: 1; }
    .seekcue-n:empty { display: none; }

    /* weather / conditions chip (overlays/weather.js) — flows flat in a chrome bar, like the
       HUD: 🌡 temp · 💨 wind · ❄ snow depth. Hidden until data loads (the JS adds .wx-ready). */
    .overlay-weather { display: flex; align-items: center; gap: 11px;
      font: 700 13px ui-monospace, SFMono-Regular, Menlo, monospace; color: #cfe0ff; }
    .overlay-weather:not(.wx-ready) { display: none; }
    .overlay-weather > span { white-space: nowrap; }
    .overlay-weather > span:empty { display: none; }
    .wx-label { color: #8aa0c6; font-weight: 600; letter-spacing: .02em; }

    /* Narrow screens: edge-to-edge feed, tighter chrome so nothing overflows. */
    @media (max-width: 520px) {
      main { padding: 0; }
      .wrap { border-radius: 0; }
      h1 small { display: none; }
      .hud, .overlay-hud { font-size: 11px; gap: 7px; }
    }
    /* Phone in landscape: full-bleed — hide the header and fill the whole screen. */
    @media (orientation: landscape) and (max-height: 600px) {
      header { display: none; }
      main { padding: 0; }
      .wrap { max-width: none; width: 100vw; height: 100dvh; aspect-ratio: auto;
        max-height: none; border-radius: 0; }
    }
    /* While in the Fullscreen API, the wrap is the fullscreen element — fill it. */
    .wrap:fullscreen { max-width: none; width: 100vw; height: 100vh; aspect-ratio: auto;
      max-height: none; border-radius: 0; }
