html, body {
  margin: 0; background: #000;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  overflow: hidden;
}
/* position:fixed;inset:0 instead of the more common height:100vh approach -
   in some browser/embedding contexts 100vh can resolve to 0 if it depends
   on a height cascading correctly through the whole html/body ancestor
   chain (e.g. when embedded, or with certain viewport quirks). Fixed
   positioning sizes directly against the viewport instead, sidestepping
   that whole class of issue. Confirmed working across Firefox and Vivaldi
   where the height:100vh version rendered as a 0-height (invisible) box. */
#wrap {
  position: fixed; inset: 0;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}

/* The image's box sizing (width/height/position) is controlled dynamically
   by static/js/media-fit.js to support rotation - object-fit (cover/contain)
   is set there too, never "fill", so the aspect ratio is never distorted. */
img { display: block; }
/* Hidden until live.js has a frame to show. Lives here rather than as a
   style="display:none" attribute on the tag so that /live can be served under
   a strict Content-Security-Policy - style-src without 'unsafe-inline' blocks
   inline style attributes. live.js still overrides this via imgEl.style, which
   is a CSSOM write and is not affected by CSP. */
#img { display: none; }

#placeholder {
  position: absolute; inset: 0; display: none;
  align-items: center; justify-content: center;
  background: #0a0a0a;
}
#placeholder.show { display: flex; }
#placeholderText {
  color: #f3f4f6;
  font-size: clamp(28px, 5vw, 44px);
  font-weight: 600;
  letter-spacing: 0.08em;
}
#placeholderText.hide { display: none; }

/* Top-left corner group: the connection status pill, plus (below it) up to
   4 small supporting-channel pills - see #auxChannelPills below. #status
   used to own this position:absolute anchor directly; that moved onto this
   wrapper (same restructuring #cornerBR already represents for its own two
   children) so the pill stack grows downward from #status without needing
   its own separate corner anchor. */
#cornerTL {
  position: absolute;
  top: max(14px, env(safe-area-inset-top));
  left: max(14px, env(safe-area-inset-left));
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}

/* Top-left connection status pill: small, unobtrusive, dot-based
   instead of loud colored text. */
#status {
  display: flex; align-items: center; gap: clamp(4px, 0.6vw, 7px);
  background: rgba(0,0,0,0.55); color: #ddd;
  padding: clamp(3px, 0.5vw, 5px) clamp(8px, 1.1vw, 12px);
  border-radius: 999px; font-size: clamp(9px, 1.3vw, 12px);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
#status .dot {
  width: clamp(6px, 0.8vw, 8px); height: clamp(6px, 0.8vw, 8px); border-radius: 50%;
  background: #6b7280; /* neutral gray = offline/no broadcast */
  flex-shrink: 0;
}
#status.live .dot { background: #34d399; } /* soft green = live */
#status.warn .dot { background: #d1a054; } /* muted amber = active, stale frame */

/* Supporting-channel pills, stacked below #status. At rest, each is just a
   small circle in .info-btn's exact visual style (see that rule further
   down) with the channel number in place of "i" - deliberately not a
   rounded-rect pill like #status/#viewerPill/#metaPill. Hovering expands
   it into a capsule revealing the title to the right; clicking opens that
   channel's quadrant inset (see .channel-inset below) instead of toggling
   a tooltip - a deliberate divergence from .info-btn's own click-to-toggle
   behavior elsewhere on this page, which is why this uses :hover for the
   title reveal instead of reusing static/js/info-tip.js's click-toggle
   logic (click is already spoken for here). */
#auxChannelPills { display: flex; flex-direction: column; gap: 6px; }
.channel-pill {
  display: inline-flex; align-items: center;
  width: 18px; height: 18px; min-height: 0; padding: 0;
  border-radius: 50%; border: 1px solid rgba(255,255,255,0.25);
  background: rgba(255,255,255,0.08); color: #ddd;
  font-size: 11px; line-height: 1; cursor: pointer; flex-shrink: 0;
  overflow: hidden;
  transition: width 0.2s ease, border-radius 0.2s ease, background 0.15s ease, color 0.15s ease;
}
.channel-pill.hide { display: none; }
.channel-pill:hover, .channel-pill:focus-visible,
.channel-pill[aria-expanded="true"] { background: #2563eb; border-color: #2563eb; color: #fff; }
.channel-pill:hover, .channel-pill:focus-visible { width: auto; border-radius: 10px; }
.channel-pill-num {
  width: 18px; flex-shrink: 0; display: flex; align-items: center; justify-content: center;
}
.channel-pill-title {
  max-width: 0; opacity: 0; white-space: nowrap; padding-right: 0; font-size: 11px;
  transition: max-width 0.2s ease, opacity 0.2s ease, padding-right 0.2s ease;
}
.channel-pill:hover .channel-pill-title,
.channel-pill:focus-visible .channel-pill-title {
  max-width: 160px; opacity: 1; padding-right: 10px;
}

/* Quadrant-pinned inset panels - 1=top-left, 2=bottom-left, 3=bottom-right,
   4=top-right (counter-clockwise from top-left), a fixed mapping set on
   each element below rather than computed. Only left/right (whichever
   corner each belongs to) is set here - the corresponding top/bottom offset
   is set inline by static/js/channels-panel.js, since it has to clear
   #cornerTL/#viewerPill/#histPanel/#cornerBR, all of which vary in size
   (0-4 pills, collapsed/expanded, title/meta content) and can't be given a
   single safe fixed CSS gap. z-index 9 sits between #overlay's 5 and the
   corner pills'/panels' 10, so an open inset overlays the countdown panel
   but never covers #status/#viewerPill/#histPanel's own toggle. */
.channel-inset {
  position: absolute;
  width: clamp(220px, 32vw, 480px);
  height: clamp(180px, 26vw, 380px);
  z-index: 9;
  background: rgba(0,0,0,0.78);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 12px;
  overflow: hidden;
  display: none;
  flex-direction: column;
  box-sizing: border-box;
}
.channel-inset.show { display: flex; }
#auxInset1, #auxInset2 { left: max(14px, env(safe-area-inset-left)); }
#auxInset3, #auxInset4 { right: max(14px, env(safe-area-inset-right)); }

.channel-inset-header {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 10px;
  background: rgba(255,255,255,0.06);
  border-bottom: 1px solid rgba(255,255,255,0.1);
  flex-shrink: 0;
}
.channel-inset-title {
  flex: 1; min-width: 0;
  color: #ddd; font-size: clamp(11px, 1.4vw, 13px);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.channel-inset-open {
  color: #9ca3af; font-size: 11px; text-decoration: none; white-space: nowrap; flex-shrink: 0;
}
.channel-inset-open:hover { color: #fff; }
.channel-inset-open.hide { display: none; }
.channel-inset-close {
  background: transparent; border: none; color: #9ca3af; cursor: pointer;
  font-size: 18px; line-height: 1; padding: 0 2px; flex-shrink: 0;
}
.channel-inset-close:hover { color: #fff; }
.channel-inset-body {
  flex: 1; min-height: 0; position: relative;
  background: #000;
}
.channel-inset-body img, .channel-inset-body iframe {
  display: block; width: 100%; height: 100%; border: none; object-fit: contain;
}
.channel-inset-body iframe.hide { display: none; }
.channel-inset-fallback {
  position: absolute; inset: 0;
  display: none;
  flex-direction: column; align-items: center; justify-content: center; gap: 8px;
  color: #9ca3af; font-size: 12px; text-align: center; padding: 12px;
}
.channel-inset-fallback.show { display: flex; }
.channel-inset-fallback a { color: #93c5fd; }

/* Mobile: literal quarter-screen corners are too small to be useful, and 4
   of them at once would be unusable - switch to a near-fullscreen stacked
   overlay instead. Pure geometry override; the show/hide/close JS logic in
   channels-panel.js is unchanged, it just no longer sets a per-corner
   inline top/bottom offset that matters at this width. */
@media (max-width: 600px) {
  .channel-inset {
    position: fixed;
    inset: 12vh 12px 12px 12px !important;
    width: auto; height: auto;
  }
}

/* Top-right corner group: the viewer-count pill, plus (below it) the
   upcoming-scheduled-events widget - #viewerPill used to own this
   position:absolute anchor directly; that moved onto this wrapper (same
   restructuring #cornerTL already represents for #status) so the stack
   grows downward without needing its own separate corner anchor. */
#cornerTR {
  position: absolute;
  top: max(14px, env(safe-area-inset-top));
  right: max(14px, env(safe-area-inset-right));
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
}

/* Approximate-viewer-count pill - same look as #status, mirrored to the
   opposite corner. */
#viewerPill {
  display: flex; align-items: center; gap: clamp(4px, 0.6vw, 7px);
  background: rgba(0,0,0,0.55); color: #ddd;
  padding: clamp(3px, 0.5vw, 5px) clamp(8px, 1.1vw, 12px);
  border-radius: 999px; font-size: clamp(9px, 1.3vw, 12px);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
#viewerPill svg {
  width: clamp(12px, 1.8vw, 16px); height: auto;
  flex-shrink: 0;
}

/* Collapsed "Next event" pill morphing into a chronological list panel -
   same animated-box-property technique as #histPanel (never display:none
   for something that needs to animate open/closed), just pill-shaped
   instead of circular since the collapsed state shows real summary text
   rather than an icon. Both the collapsed and expanded sizes below are
   fixed clamp() values (not content-hugging/auto), which is what makes the
   width transition between them animate smoothly at all - same reasoning
   as #histPanel's fixed 40px circle. */
#upcomingWidget {
  display: none;
  position: relative;
  background: rgba(0,0,0,0.65);
  border: 1px solid rgba(255,255,255,0.08);
  overflow: hidden;
  box-sizing: border-box;
  width: clamp(150px, 24vw, 260px);
  height: clamp(20px, 3.4vw, 30px);
  border-radius: 999px;
  transition: width 0.28s ease, height 0.28s ease, border-radius 0.28s ease;
}
/* Hidden entirely (not just collapsed) while there are no upcoming events -
   same show/hide-if-empty convention as #metaPill above. */
#upcomingWidget.show { display: block; }
#upcomingWidget.expanded {
  width: clamp(240px, 32vw, 380px);
  height: clamp(180px, 28vw, 320px);
  border-radius: 16px;
}

#upcomingToggleBtn {
  position: absolute; inset: 0;
  display: flex; align-items: center; gap: 5px;
  padding: 0 clamp(8px, 1.1vw, 12px);
  background: transparent; border: none; color: #ddd; cursor: pointer;
  font-size: clamp(9px, 1.3vw, 12px); letter-spacing: 0.02em;
  text-align: left; white-space: nowrap;
  opacity: 1; pointer-events: auto;
  transition: opacity 0.18s ease;
}
/* Fades out as the panel body fades in, rather than the two-purpose single
   corner button #histToggleBtn uses - this one holds real summary text
   that has nowhere sensible to sit once the panel's showing its list. */
#upcomingWidget.expanded #upcomingToggleBtn { opacity: 0; pointer-events: none; }
#upcomingSummary { overflow: hidden; text-overflow: ellipsis; }

/* Shared with admin.css's identically-named class (see event-mode-icons.js) -
   :empty collapses the flex gap around it for schedule_mode "none" events,
   which set no icon markup at all. */
.event-mode-icon { flex: none; display: inline-flex; vertical-align: middle; }
.event-mode-icon:empty { display: none; }

#upcomingPanelBody {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  opacity: 0; pointer-events: none;
  transition: opacity 0.18s ease;
}
#upcomingWidget.expanded #upcomingPanelBody {
  opacity: 1; pointer-events: auto;
  transition-delay: 0.12s; /* content shouldn't visibly pop in before the container has grown enough to hold it */
}

#upcomingList {
  list-style: none; margin: 0; padding: 10px 14px; flex: 1; min-height: 0;
  overflow-y: auto; display: flex; flex-direction: column; gap: 10px;
  font-size: clamp(11px, 1.5vw, 13px); color: #ddd;
}
#upcomingList li { line-height: 1.4; }
#upcomingList .event-mode-icon { margin-right: 4px; }
.upcoming-event-time { display: block; color: #9ca3af; font-size: 0.9em; }
.upcoming-event-cancelled { opacity: 0.7; }
.upcoming-event-cancelled .upcoming-event-title { text-decoration: line-through; }
.upcoming-event-reason { display: block; color: #f3a5a5; font-size: 0.85em; margin-top: 2px; }

/* Calendar-feed link (GET /live/events.ics) - always visible, unlike
   .subscribe-all-btn below (no feature detection needed, it's a plain
   link). Same pill styling as that button for a consistent header row.
   .calendar-feed-copy (static/js/calendar-feed-link.js's "Copy link"
   button) starts hidden via .hide - revealed only once a secure context
   confirms navigator.clipboard is actually available, same feature-
   detection spirit as .subscribe-all-btn's own .hide below. */
.calendar-feed-link, .calendar-feed-copy {
  background: transparent; border: 1px solid rgba(255,255,255,0.25);
  color: #ddd; border-radius: 999px; cursor: pointer;
  font-size: 11px; padding: 3px 10px; flex-shrink: 0;
  text-decoration: none;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.calendar-feed-link:hover, .calendar-feed-copy:hover { border-color: rgba(255,255,255,0.5); color: #fff; }
.calendar-feed-copy.hide { display: none; }

/* Web Push opt-in buttons (static/js/push-subscribe-panel.js) - hidden by
   default via .hide, only revealed once the browser is confirmed to
   support the Push API (see that module's init()). Deliberately plain
   text-button styling, same "no loud chrome" restraint as the rest of this
   page's overlays. */
.subscribe-all-btn {
  background: transparent; border: 1px solid rgba(255,255,255,0.25);
  color: #ddd; border-radius: 999px; cursor: pointer;
  font-size: 11px; padding: 3px 10px; flex-shrink: 0;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.subscribe-all-btn:hover { border-color: rgba(255,255,255,0.5); }
.subscribe-all-btn.active { background: #2563eb; border-color: #2563eb; color: #fff; }
.subscribe-all-btn.hide { display: none; }

.upcoming-event-subscribe {
  display: block; margin-top: 4px;
  background: transparent; border: 1px solid rgba(255,255,255,0.2);
  color: #9ca3af; border-radius: 999px; cursor: pointer;
  font-size: 0.85em; padding: 2px 8px;
}
.upcoming-event-subscribe:hover:not(:disabled) { border-color: rgba(255,255,255,0.45); color: #ddd; }
.upcoming-event-subscribe.active { background: #2563eb; border-color: #2563eb; color: #fff; }
.upcoming-event-subscribe:disabled { opacity: 0.6; cursor: default; }

/* In-flight subscribe/unsubscribe request (see push-subscribe-panel.js's
   withPending()) - a distinct pulsing look from the plain static dimming
   above (used for e.g. "covered by Subscribe all"), so a slow push-service
   round-trip reads as "working", not "stuck" or "permanently disabled". */
.subscribe-all-btn.pending,
.upcoming-event-subscribe.pending { cursor: wait; animation: push-btn-pending 1.1s ease-in-out infinite; }
@keyframes push-btn-pending { 0%, 100% { opacity: 0.45; } 50% { opacity: 0.85; } }

/* Bottom-left collapsible viewer-side histogram + stretch-curve override
   panel (static/js/live-stretch-panel.js). A direct sibling of #overlay
   under #wrap, same as #status/#viewerPill above - not nested inside
   #overlay, which is pointer-events:none with no existing opt-in-child
   precedent anywhere in this file. Never display:none: the histogram
   widget's own ResizeObserver-driven redraw depends on its canvases'
   laid-out box size actually changing, and display:none can't be
   transitioned anyway - collapse/expand instead animates width/height/
   border-radius between a small circle and a clamp()-sized panel. */
#histPanel {
  position: absolute;
  bottom: max(14px, env(safe-area-inset-bottom));
  left: max(14px, env(safe-area-inset-left));
  z-index: 10;
  background: rgba(0,0,0,0.65);
  border: 1px solid rgba(255,255,255,0.08);
  overflow: hidden;
  box-sizing: border-box;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  transition: width 0.28s ease, height 0.28s ease, border-radius 0.28s ease;
}
/* "Roughly a fourth to a third of the viewport in each direction": the
   30vw/30vh middle term satisfies that on typical viewports; the floor
   keeps the curve genuinely draggable on a small phone given the widget's
   own ~18px per-handle hit-radius, the ceiling stops it dominating a large
   desktop monitor where 30vw/30vh alone would be excessive. */
#histPanel.expanded {
  width: clamp(240px, 30vw, 460px);
  height: clamp(200px, 30vh, 380px);
  border-radius: 16px;
}

/* Fixed top-left position inside #histPanel - collapsing the panel to
   exactly 40x40 naturally leaves only this button visible (everything else
   below is overflow:hidden-clipped), no separate collapsed-state element
   needed. */
#histToggleBtn {
  position: absolute; top: 0; left: 0;
  width: 40px; height: 40px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: none; color: #ddd; cursor: pointer;
  z-index: 1;
}
#histToggleBtn svg { width: 16px; height: auto; }
#histToggleBtn:hover:not(:disabled) { filter: brightness(1.3); }
/* Forced-collapsed and disabled while the stream is inactive - see
   setStreamActive() in live-stretch-panel.js. There's nothing to stretch
   without a live frame, and the override has no effect on the background/
   placeholder image anyway. */
#histToggleBtn:disabled { opacity: 0.4; cursor: not-allowed; }

#histPanelBody {
  position: absolute; inset: 0;
  padding: 48px 14px 14px;
  display: flex; flex-direction: column; gap: 8px;
  box-sizing: border-box;
  /* opacity/pointer-events, not visibility/display - box size must keep
     tracking #histPanel's own animated size so the histogram widget's
     ResizeObserver stays correctly driven throughout the transition. */
  opacity: 0; pointer-events: none;
  transition: opacity 0.18s ease;
}
#histPanel.expanded #histPanelBody {
  opacity: 1; pointer-events: auto;
  transition-delay: 0.12s; /* content shouldn't visibly pop in before the container has grown enough to hold it */
}

#histPanelHeader { display: flex; align-items: center; gap: 6px; }
#histPanelTitle {
  flex: 1;
  font-size: clamp(12px, 1.8vw, 14px);
  font-weight: 600;
  color: #f3f4f6;
}
#histInfoBtn {
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; padding: 0;
  border-radius: 50%; border: 1px solid rgba(255,255,255,0.3);
  background: rgba(255,255,255,0.12); color: #ddd;
  font-size: 10px; font-style: italic; font-family: Georgia, 'Times New Roman', serif;
  line-height: 1; cursor: pointer;
}
#histInfoBtn:hover, #histInfoBtn[aria-expanded="true"] { background: rgba(255,255,255,0.28); color: #fff; }
#histInfoText {
  display: none;
  font-size: 10.5px; line-height: 1.4; color: #cbd5e1;
}
#histInfoText.show { display: block; }

.histRow { display: flex; gap: 6px; }
/* live.css has no other buttons and thus no existing hover/press/disabled
   rules to inherit (unlike admin.css's global button:hover rule) - this
   panel needs its own affordance from scratch. */
.histActionBtn {
  flex: 1;
  background: rgba(255,255,255,0.12);
  color: #f3f4f6;
  border: 1px solid rgba(255,255,255,0.18);
  border-radius: 8px;
  padding: 6px 10px;
  font-size: clamp(10px, 1.6vw, 12px);
  cursor: pointer;
}
.histActionBtn:hover:not(:disabled) { filter: brightness(1.15); }
.histActionBtn:active:not(:disabled) { filter: brightness(0.9); }
.histActionBtn:disabled { opacity: 0.4; cursor: not-allowed; }

#histogramWrap {
  position: relative; flex: 1; min-height: 0;
  background: #111; border-radius: 8px; overflow: hidden;
}
#liveHistBarsCanvas, #liveHistCurveCanvas { position: absolute; inset: 0; width: 100%; height: 100%; }
#liveHistCurveCanvas { cursor: grab; touch-action: none; }
#liveHistCurveCanvas:active { cursor: grabbing; }

/* Central overlay: shown over the live image OR the background image,
   centered, with a soft scrim panel behind the text for contrast on
   any underlying image. */
#overlay {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
  pointer-events: none; z-index: 5; padding: 24px; box-sizing: border-box;
}
#overlayPanel {
  display: none;
  flex-direction: column; align-items: center; gap: 10px;
  padding: 36px 56px;
  background: rgba(0, 0, 0, 0.55);
  border-radius: 20px;
  border: 1px solid rgba(255, 255, 255, 0.08);
}
#overlayPanel.show { display: flex; }

#overlayState {
  display: none;
  color: #f3f4f6;
  font-size: clamp(28px, 5vw, 44px);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-shadow: 0 2px 16px rgba(0,0,0,0.5);
}
#overlayState.show { display: block; }

#overlayTitle {
  display: none;
  color: #f3f4f6;
  font-size: clamp(22px, 4vw, 34px);
  font-weight: 500;
  text-shadow: 0 2px 16px rgba(0,0,0,0.5);
}
#overlayTitle.show { display: block; }

/* Bottom-right corner stack (#overlayTitleCorner + #metaPill) - see the
   HTML comment in live.html for why this is a sibling of #overlay rather
   than nested inside it, and why a flex column rather than two
   independently absolutely-positioned elements (avoids guessing a fixed
   pixel offset that would break the moment the title wraps to 2 lines).
   Shrink-to-fit height means the last DOM child sits at the true
   bottom-right anchor point - see live.html for the ordering. */
#cornerBR {
  position: absolute;
  bottom: max(16px, env(safe-area-inset-bottom));
  right: max(16px, env(safe-area-inset-right));
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
}

/* Live-only title treatment: small caption tucked in the corner instead of
   the big centered panel, so it doesn't dominate the image while the
   stream is actually playing. Independent of #overlayPanel entirely. */
#overlayTitleCorner {
  display: none;
  max-width: min(60vw, 420px);
  color: #f3f4f6;
  font-size: clamp(11px, 1.8vw, 16px);
  font-weight: 500;
  text-align: right;
  background: rgba(0, 0, 0, 0.5);
  padding: 6px 14px;
  border-radius: 10px;
  text-shadow: 0 2px 8px rgba(0,0,0,0.5);
  pointer-events: none;
}
#overlayTitleCorner.show { display: block; }

/* Session/frame info pill - shown only once live.js has received a
   metadata sidecar with at least one displayable field (see
   POST /upload/meta in app.py, build_single_metadata()/
   build_stack_metadata() in client/push_frame.py/stack_and_push.py).
   Same look as #status/#viewerPill; position:relative anchors #metaInfoTip. */
#metaPill {
  display: none;
  position: relative;
  align-items: center; gap: 6px;
  background: rgba(0,0,0,0.55); color: #ddd;
  padding: clamp(3px, 0.5vw, 5px) clamp(8px, 1.1vw, 12px);
  border-radius: 999px; font-size: clamp(9px, 1.3vw, 12px);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
#metaPill.show { display: flex; }

/* Adapted from static/css/admin.css's .info-btn/.info-tip (not shared -
   /live and /admin are separate pages with separate stylesheets, see
   static/js/info-tip.js's own docstring). Two real differences from the
   admin version: fixed dark colors instead of admin's light/dark theme
   variables (this page has no theme toggle, always dark), and the tip
   opens *upward* (bottom: calc(100% + gap)) rather than admin's downward
   (top: calc(100% + gap)) - this pill sits at the bottom of the viewport,
   so an admin-style downward tip would run off the bottom edge. */
.info-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; min-height: 0; margin: 0; padding: 0;
  border-radius: 50%; border: 1px solid rgba(255,255,255,0.25);
  background: rgba(255,255,255,0.08); color: #ddd;
  font-size: 11px; font-style: italic; font-family: Georgia, 'Times New Roman', serif;
  line-height: 1; cursor: pointer; flex-shrink: 0;
}
.info-btn:hover, .info-btn[aria-expanded="true"] { color: #fff; background: #2563eb; border-color: #2563eb; }

.info-tip {
  display: none;
  position: absolute; z-index: 30; bottom: calc(100% + 8px); right: 0;
  width: max-content; max-width: min(260px, calc(100vw - 48px));
  background: rgba(20, 20, 20, 0.92); color: #f3f4f6;
  border: 1px solid rgba(255,255,255,0.12); border-radius: 8px;
  padding: 10px 12px; font-size: 12px; line-height: 1.5;
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
  text-align: left;
}
.info-tip.show { display: block; }

#overlayCountdownWrap { display: none; flex-direction: column; align-items: center; gap: 4px; }
#overlayCountdownWrap.show { display: flex; }
#overlayCountdownLabel {
  color: #cbd5e1;
  font-size: clamp(12px, 1.6vw, 15px);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-shadow: 0 1px 8px rgba(0,0,0,0.5);
}
#overlayCountdown {
  display: none;
  color: #f3f4f6;
  font-size: clamp(40px, 8vw, 72px);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  text-shadow: 0 2px 16px rgba(0,0,0,0.5);
}
#overlayCountdown.show { display: block; }

/* Final-60-seconds ring - see live.js's tickCountdown() for the swap
   logic and the dashoffset math. Square container so the SVG (viewBox
   0 0 120 120) stays a perfect circle at any size. */
#overlayCountdownRing {
  display: none;
  position: relative;
  width: clamp(96px, 18vw, 160px);
  height: clamp(96px, 18vw, 160px);
}
#overlayCountdownRing.show { display: block; }
#overlayCountdownRing svg { width: 100%; height: 100%; }
.ring-track {
  fill: none;
  stroke: rgba(255,255,255,0.18);
  stroke-width: 8;
}
.ring-progress {
  fill: none;
  stroke: #f3f4f6;
  stroke-width: 8;
  stroke-linecap: round;
  /* 2*pi*52 (the circle's r, both here and in live.html's SVG markup,
     and RING_RADIUS in live.js - all three must agree) - the whole
     stroke is one dash exactly as long as the circle's own
     circumference, so dashoffset=0 draws a complete circle and
     dashoffset=circumference draws nothing. rotate(-90deg): SVG circles
     start drawing at the 3 o'clock point by default: rotating the
     progress circle (only - .ring-track stays unrotated, a plain full
     circle has no visible "start" anyway) moves that start to 12
     o'clock, which reads as a more natural clock-style countdown. */
  stroke-dasharray: 326.73;
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
  transition: stroke-dashoffset 0.9s linear;
}
#overlayCountdownSeconds {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #f3f4f6;
  font-size: clamp(28px, 5vw, 44px);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 2px 12px rgba(0,0,0,0.5);
}

/* Mobile: safe-area padding for notches, tighter panel to leave more
   room for the image on small/portrait screens. */
@media (max-width: 600px) {
  #overlayPanel {
    padding: clamp(20px, 6vw, 36px) clamp(20px, 8vw, 56px);
    border-radius: 14px;
    gap: 8px;
  }
  #overlay { padding: max(16px, env(safe-area-inset-top)) 16px 16px; }
  #histPanel.expanded {
    width: clamp(200px, 60vw, 320px);
    height: clamp(180px, 45vh, 300px);
  }
  #histPanelBody { padding: 42px 10px 10px; }
  #upcomingWidget.expanded {
    width: clamp(200px, 60vw, 320px);
    height: clamp(160px, 40vh, 280px);
  }
}
@media (max-width: 380px) {
  #overlayState { font-size: clamp(20px, 6vw, 28px); }
  #overlayTitle { font-size: clamp(16px, 5vw, 22px); }
  #overlayCountdown { font-size: clamp(32px, 12vw, 48px); }
  #overlayTitleCorner { font-size: clamp(10px, 3vw, 13px); padding: 5px 10px; }
  .histActionBtn { font-size: clamp(9px, 3vw, 11px); padding: 5px 6px; }
}

/* Impressum/Datenschutz links - bottom-center, since every corner is
   already taken by #cornerTL/#cornerTR/#cornerBR/#histPanel. Deliberately
   as unobtrusive as this page's other corner chrome (translucent, small
   text) while staying always visible - unlike everything else on this
   page, this one isn't allowed to be hidden behind a toggle/collapse. */
#legalLinks {
  position: absolute;
  bottom: max(6px, env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  font-size: 10px;
  color: rgba(255,255,255,0.35);
  white-space: nowrap;
}
#legalLinks a { color: inherit; text-decoration: none; }
#legalLinks a:hover { color: rgba(255,255,255,0.8); }