Files
pondering-orb/web/styles.css
T
Diablo_Rain a1e3e6852f feat: Phase 1.5 — health prober, Oracle's Memory, off-thread EPG, PWA
Resilience + scale pass on top of the v1 core.

Stream health prober:
- New SSRF-validated /probe endpoint (HEAD -> ranged-GET fallback, 6s timeout,
  5-min TTL cache, concurrency-capped, no-store) + orb_probe_total metrics
- Front-end health.js: lazy bounded-concurrency probing (current + sampled
  channels, not the whole catalog), LIVE/DEGRADED/DEAD scoring, guide skull
  glyph, instant no-signal on zap to a known-dead channel

Oracle's Memory (memory.js):
- Session resurrection (last channel, volume, mute, theater/dim/ambient)
- Exponential-decay per-channel uptime history; pinning; dead-channel auto-skip

Off-thread XMLTV:
- lib/xmltv-parse.js (pure, hand-rolled scanner, no DOMParser/regex backtracking)
  run in epg-worker.js module worker; wired into the iptv-org provider
- Unit-tested in node: entity decode, TZ offsets, 20k programmes off-thread

Service worker / PWA:
- manifest.webmanifest + orb icons; sw.js (cache-first shell + hls.js,
  stale-while-revalidate for JSON-over-relay, never caches streams)
- CSP gains worker-src + manifest-src; relay sends no-store on /relay + /probe
  so the SW is the sole metadata freshness authority; webmanifest MIME fix

Docs: README de-emojified (title only) and reframed as a peer parallel build.

Verified: SSRF probes blocked, legit probe + cache, new headers present,
PWA assets served, parser tests 8/8, JS syntax clean, zero Rust warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:25:55 -04:00

213 lines
12 KiB
CSS

/* The Pondering Orb — red/black phosphor CRT theme.
All CRT effects are CSS (no WebGL). prefers-reduced-motion kills every animation. */
:root{
--red: #ff2b2b;
--red-dim: #b21d1d;
--red-deep: #6e0f0f;
--red-glow: rgba(255,43,43,.55);
--amber: #ff6a3d;
--bg: #050102;
--panel: #120406;
--panel-2: #1a0608;
--line: #3a0d0f;
--ink: #ffd9d4; /* warm off-white text on red/black */
--ink-dim: #c98b86;
--seg-on: #ff2b2b;
--seg-off: #2a0809;
--focus: #ff2b2b;
--osd-bg: rgba(10,1,2,.82);
--ease: cubic-bezier(.2,.7,.2,1);
}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--bg);color:var(--ink);
font-family:"Consolas","DejaVu Sans Mono","Courier New",monospace;overflow:hidden}
body{line-height:1.4}
button{font-family:inherit;color:inherit}
img{display:block}
/* ===================== layout root ===================== */
.orb-root{position:fixed;inset:0;display:flex;flex-direction:column;background:radial-gradient(120% 90% at 50% 0%, #160406 0%, var(--bg) 70%)}
/* ===================== the screen ===================== */
.screen{position:relative;flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content:center;
padding:clamp(8px,2vw,28px);transition:padding .25s var(--ease)}
.bezel{position:relative;width:100%;height:100%;max-width:100%;max-height:100%;aspect-ratio:16/9;
margin:auto;background:#000;border-radius:18px/26px;overflow:hidden;
box-shadow:0 0 0 2px #240608, 0 0 0 10px #0c0203, 0 0 60px rgba(0,0,0,.9),
inset 0 0 120px rgba(0,0,0,.9);}
.video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000}
/* ambient backlight canvas sits BEHIND the bezel */
.ambient{position:absolute;inset:0;width:100%;height:100%;
filter:blur(60px) saturate(1.7);transform:scale(1.18);opacity:0;
transition:opacity .5s var(--ease);pointer-events:none;z-index:0}
.orb-root[data-mode-ambient="true"] .ambient{opacity:.85}
.screen{z-index:1}
/* ---- CRT overlays ---- */
.crt-scan{position:absolute;inset:0;pointer-events:none;z-index:6;
background:repeating-linear-gradient(to bottom, rgba(0,0,0,0) 0, rgba(0,0,0,0) 2px, rgba(0,0,0,.16) 3px)}
.crt-vignette{position:absolute;inset:0;pointer-events:none;z-index:6;
background:radial-gradient(120% 120% at 50% 50%, rgba(0,0,0,0) 55%, rgba(0,0,0,.55) 100%);
animation:breathe 4s ease-in-out infinite}
@keyframes breathe{0%,100%{transform:scale(1);opacity:.85}50%{transform:scale(1.012);opacity:1}}
/* ===================== OSD (shared) ===================== */
.osd{position:absolute;z-index:7;background:var(--osd-bg);border:1px solid var(--line);
color:var(--ink);backdrop-filter:blur(2px);box-shadow:0 0 18px rgba(0,0,0,.7)}
.osd[hidden]{display:none}
/* channel banner */
.osd-channel{left:18px;bottom:18px;display:flex;gap:14px;align-items:center;padding:12px 16px;border-radius:8px;
min-width:300px;max-width:70%;opacity:0;transform:translateY(8px) scale(.98);
transition:opacity .3s var(--ease),transform .3s var(--ease)}
.osd-channel.show{opacity:1;transform:none}
.osd-logo{width:96px;height:54px;object-fit:contain;background:#000;border:1px solid var(--line);border-radius:4px}
.osd-meta{min-width:0;flex:1}
.osd-line1{display:flex;align-items:baseline;gap:10px}
.osd-num{font-weight:bold;color:var(--red);font-size:26px;text-shadow:0 0 10px var(--red-glow)}
.osd-name{font-size:18px;font-weight:bold;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.osd-now{color:var(--ink-dim);font-size:13px;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.osd-prog{height:4px;background:var(--seg-off);border-radius:2px;margin-top:7px;overflow:hidden}
.osd-prog-fill{display:block;height:100%;width:0;background:var(--red);box-shadow:0 0 8px var(--red-glow)}
.osd-live{display:flex;align-items:center;gap:6px;color:var(--red);font-weight:bold;font-size:13px;align-self:flex-start}
/* number entry (7-seg look) */
.osd-number{right:22px;top:22px;padding:8px 14px;border-radius:6px}
.seg{font-weight:bold;font-size:40px;letter-spacing:6px;color:var(--seg-on);
text-shadow:0 0 14px var(--red-glow);font-variant-numeric:tabular-nums}
.live-dot{width:9px;height:9px;border-radius:50%;background:var(--red);box-shadow:0 0 8px var(--red);
display:inline-block;animation:pulse 2s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
/* ===================== scrying static ===================== */
.static{position:absolute;inset:0;width:100%;height:100%;z-index:5;opacity:0;pointer-events:none;
transition:opacity .12s linear}
.static.show{opacity:.9}
/* ===================== the orb clouds over ===================== */
.nosignal{position:absolute;inset:0;z-index:5;display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:10px;background:#050102;text-align:center;padding:20px}
.nosignal[hidden]{display:none}
.nosignal-bars{width:min(70%,520px);height:46%;border:1px solid var(--line);
background:linear-gradient(90deg,#6e0f0f 0 14%,#b21d1d 14% 28%,#ff6a3d 28% 42%,#ffd9d4 42% 56%,
#b21d1d 56% 70%,#6e0f0f 70% 84%,#1a0608 84% 100%);opacity:.5;filter:saturate(.8)}
.nosignal-text{color:var(--red);font-weight:bold;font-size:clamp(18px,3vw,30px);letter-spacing:4px;
text-shadow:0 0 14px var(--red-glow)}
.nosignal-sub{color:var(--ink-dim);font-size:13px;letter-spacing:1px}
/* ===================== ghost controls ===================== */
.controls{position:absolute;left:0;right:0;bottom:0;z-index:8;display:flex;align-items:center;gap:8px;
padding:12px 14px;background:linear-gradient(to top, rgba(5,1,2,.92), rgba(5,1,2,0));
opacity:0;transform:translateY(6px);transition:opacity .25s var(--ease),transform .25s var(--ease);
pointer-events:none;flex-wrap:wrap}
.screen.controls-visible .controls,.controls:focus-within{opacity:1;transform:none;pointer-events:auto}
.ctl{background:var(--panel);border:1px solid var(--line);color:var(--ink);border-radius:6px;
padding:6px 10px;font-size:13px;cursor:pointer;min-height:34px;transition:border-color .15s,box-shadow .15s,color .15s}
.ctl:hover{border-color:var(--red-dim);color:#fff}
.ctl-spacer{flex:1 1 auto}
.ctl-clock{color:var(--ink-dim);font-size:13px;font-variant-numeric:tabular-nums;padding:0 4px}
.ctl-live{display:flex;align-items:center;gap:6px;color:var(--red);font-weight:bold;font-size:12px}
.vol{width:90px;accent-color:var(--red)}
.qual-menu{position:absolute;right:14px;bottom:58px;z-index:9;background:var(--panel-2);border:1px solid var(--line);
border-radius:6px;padding:4px;min-width:120px;box-shadow:0 0 18px rgba(0,0,0,.8)}
.qual-menu[hidden]{display:none}
.qual-menu button{display:block;width:100%;text-align:left;background:none;border:0;color:var(--ink);
padding:7px 10px;border-radius:4px;cursor:pointer;font-size:13px}
.qual-menu button:hover,.qual-menu button[aria-checked="true"]{background:var(--red-deep);color:#fff}
/* ===================== dim mode ===================== */
.dim-scrim{position:absolute;inset:0;z-index:4;background:rgba(0,0,0,.72);opacity:0;pointer-events:none;
transition:opacity .3s var(--ease)}
.orb-root[data-mode-dim="true"] .dim-scrim{opacity:1}
.orb-root[data-mode-dim="true"] .bezel{z-index:5;position:relative}
.orb-root[data-mode-dim="true"].activity .dim-scrim{opacity:.4}
/* ===================== theater mode ===================== */
.orb-root[data-mode-theater="true"] .screen{padding:0}
.orb-root[data-mode-theater="true"] .bezel{border-radius:0;box-shadow:none}
.orb-root[data-mode-theater="true"] .brand-bug{opacity:0}
/* ===================== the almanac (guide) ===================== */
.guide{position:absolute;left:0;right:0;bottom:0;height:46%;z-index:20;background:var(--panel);
border-top:2px solid var(--red-deep);box-shadow:0 -12px 40px rgba(0,0,0,.8);
transform:translateY(100%);transition:transform .28s var(--ease);display:flex;flex-direction:column}
.guide:not([hidden]){display:flex}
.guide.show{transform:translateY(0)}
.guide-head{display:flex;align-items:center;gap:14px;padding:8px 14px;border-bottom:1px solid var(--line);
background:var(--panel-2)}
.guide-title{margin:0;font-size:15px;letter-spacing:3px;color:var(--red);text-shadow:0 0 10px var(--red-glow)}
.guide-clock{color:var(--ink-dim);font-variant-numeric:tabular-nums;margin-left:auto;font-size:13px}
.guide-scroll{position:relative;flex:1;overflow:auto;outline:none}
.guide-timehdr{position:sticky;top:0;z-index:3;display:flex;height:26px;background:var(--panel-2);
border-bottom:1px solid var(--line);margin-left:var(--ch-col,150px)}
.guide-timehdr .tick{flex:0 0 var(--hour-w,180px);font-size:11px;color:var(--ink-dim);
padding:5px 8px;border-left:1px solid var(--line)}
.guide-rows{position:relative}
.guide-row{display:flex;height:var(--row-h,46px);border-bottom:1px solid #220708}
.guide-chcell{position:sticky;left:0;z-index:2;flex:0 0 var(--ch-col,150px);display:flex;align-items:center;gap:8px;
padding:0 8px;background:var(--panel-2);border-right:1px solid var(--line);cursor:pointer}
.guide-chcell:hover{background:var(--red-deep)}
.guide-chcell .num{color:var(--red);font-weight:bold;font-variant-numeric:tabular-nums;min-width:24px}
.guide-chcell .nm{font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.guide-chcell .skull{margin-left:auto;opacity:.55;font-size:12px}
.guide-chcell .pin{background:none;border:0;cursor:pointer;font-size:14px;line-height:1;padding:0 2px;
color:var(--ink-dim);margin-left:6px;flex:0 0 auto}
.guide-chcell .skull + .pin{margin-left:6px}
.guide-chcell .pin:not(.skull + .pin){margin-left:auto}
.guide-chcell .pin.on{color:var(--red);text-shadow:0 0 8px var(--red-glow)}
.guide-chcell .pin:hover{color:#fff}
.guide-progs{position:relative;flex:1}
.guide-prog{position:absolute;top:3px;bottom:3px;background:var(--panel-2);border:1px solid var(--line);
border-radius:4px;padding:5px 8px;font-size:12px;overflow:hidden;cursor:pointer;white-space:nowrap;text-overflow:ellipsis}
.guide-prog:hover,.guide-prog:focus{border-color:var(--red-dim);outline:none}
.guide-prog.airing{background:rgba(255,43,43,.14);border-color:var(--red-dim)}
.guide-prog .t{color:var(--ink-dim);font-size:10px}
.guide-playhead{position:absolute;top:0;bottom:0;width:2px;background:var(--red);box-shadow:0 0 8px var(--red);
z-index:4;pointer-events:none;left:var(--ch-col,150px)}
/* ===================== help / dialog ===================== */
.help{position:absolute;inset:0;z-index:40;display:flex;align-items:center;justify-content:center;
background:rgba(5,1,2,.7);backdrop-filter:blur(2px)}
.help[hidden]{display:none}
.help-card{background:var(--panel-2);border:1px solid var(--red-deep);border-radius:10px;padding:22px 26px;
max-width:560px;width:90%;box-shadow:0 0 50px rgba(0,0,0,.85)}
.help-card h2{margin:0 0 14px;color:var(--red);letter-spacing:3px;text-shadow:0 0 10px var(--red-glow)}
.help-card dl{display:grid;grid-template-columns:auto 1fr;gap:6px 16px;margin:0 0 16px}
.help-card dt{color:var(--red);font-weight:bold;white-space:nowrap}
.help-card dd{margin:0;color:var(--ink-dim)}
.mundane-toggle{display:flex;align-items:center;gap:8px;margin-top:12px;color:var(--ink-dim);font-size:13px}
kbd{background:#000;border:1px solid var(--line);border-bottom-width:2px;border-radius:4px;padding:1px 6px;
font-family:inherit;color:var(--ink)}
/* ===================== brand bug ===================== */
.brand-bug{position:absolute;right:14px;top:10px;z-index:7;font-size:11px;letter-spacing:3px;
color:var(--red);opacity:.6;text-shadow:0 0 8px var(--red-glow);pointer-events:none;transition:opacity .3s}
/* ===================== focus rings (a11y) ===================== */
:focus-visible{outline:2px solid var(--focus);outline-offset:2px;box-shadow:0 0 8px var(--red-glow)}
/* ===================== responsive ===================== */
@media (max-width:768px){
.guide{height:70%}
.osd-channel{min-width:0;max-width:88%}
.vol{display:none}
.brand-bug{display:none}
}
/* ===================== reduced motion kill-switch ===================== */
@media (prefers-reduced-motion: reduce){
.crt-vignette{animation:none}
.live-dot{animation:none}
.ambient{display:none}
.static{display:none}
*{transition-duration:.001ms !important;animation-duration:.001ms !important;animation-iteration-count:1 !important}
}