:root{
  --bg:#0b0f16; --panel:#10151f; --panel2:#0c1119; --line:#1c2530;
  --ink:#e7ecf3; --muted:#8ea0b2; --muted2:#5f6f7e;
  --gold:#d5a038; --gold-ink:#10151f; --danger:#c0392b; --ok:#1d9e75;
  --safe-top:env(safe-area-inset-top,0px); --safe-bot:env(safe-area-inset-bottom,0px);
}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--bg);color:var(--ink);
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  -webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;overflow:hidden}
.screen{position:fixed;inset:0;display:flex;flex-direction:column}
[hidden]{display:none!important}
button{font:inherit;color:inherit;background:none;border:none;cursor:pointer}

/* Login */
.login{align-items:center;justify-content:center;padding:24px}
.login-card{width:100%;max-width:360px;background:var(--panel);border:.5px solid var(--line);
  border-radius:18px;padding:28px 22px}
.brand{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:600}
.brand-mark{width:30px;height:30px;border-radius:8px;background:var(--gold);color:var(--gold-ink);
  display:grid;place-items:center;font-weight:700}
.eyebrow{color:var(--muted);font-size:12px;letter-spacing:.14em;text-transform:uppercase;margin:14px 0 18px}
.login input{width:100%;height:46px;margin-bottom:12px;padding:0 14px;border-radius:12px;
  background:var(--panel2);border:.5px solid var(--line);color:var(--ink);font-size:16px}
.login button{width:100%;height:46px;border-radius:12px;background:var(--gold);color:var(--gold-ink);
  font-weight:600;font-size:16px}
.err{color:#f0997b;font-size:13px;min-height:18px;margin-top:8px}

/* Topbar */
.app{background:var(--bg)}
.topbar{display:flex;align-items:center;gap:8px;padding:calc(10px + var(--safe-top)) 12px 10px;
  background:var(--panel);border-bottom:.5px solid var(--line)}
.topbar .title{flex:1;font-size:15px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  display:flex;align-items:baseline;gap:10px;min-width:0}
/* Which address served this page, and which build is running. Quiet — it is a fact, not a feature —
   but never hidden: Guardian answers to several names and they are not the same application. */
.topbar .title .host{font-size:11px;font-weight:400;color:var(--muted2);letter-spacing:.02em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.statusbar .origin{color:var(--muted2)}
.statusbar .build{color:var(--muted2)}
.top-actions{display:flex;gap:4px}
.icon-btn{position:relative;width:38px;height:38px;border-radius:10px;display:grid;place-items:center;color:var(--ink)}
.icon-btn:active{background:var(--panel2)}
.badge{position:absolute;top:3px;right:3px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;
  background:var(--danger);color:#fff;font-size:10px;display:grid;place-items:center}

/* Grid */
/* align-content:start is the whole fix for the squashed wall.
   The grid is a flex child with a definite height, and a grid's DEFAULT align-content is stretch —
   so eight rows of cameras were being crushed into whatever height was left over: 32 tiles ÷ 460px
   = 57px each, and every camera became a letterbox slit. The aspect-ratio on .tile was correct and
   simply being overruled. Told to start at the top, each row takes the height its aspect-ratio
   asks for, and the wall scrolls — which is what a wall of 32 cameras on a phone must do. */
/* min-height:0 is not cosmetic. A grid that is a flex child defaults to min-height:auto, which
   means it refuses to shrink below its content — so in 'fit' mode the wall would silently grow
   PAST the bottom of the screen and take the toolbar with it, and no amount of correct row maths
   would save it. This one line is what makes "no scrolling, ever" true. */
.grid{flex:1;min-height:0;display:grid;align-content:start;gap:3px;padding:3px;background:#05070a;overflow:auto}
.grid.split-1{grid-template-columns:1fr}
.grid.split-4{grid-template-columns:1fr 1fr}
.grid.split-9{grid-template-columns:1fr 1fr 1fr}

/* ---------- THE WALL (fit) ----------
   Every camera on the glass at once. Columns and rows are solved in JS against the real shape of
   the screen; here we only say: fill the space, do not scroll, and let the tile take the height
   its row gives it instead of the one its aspect-ratio wants. */
.grid.fit{overflow:hidden;align-content:stretch}
.grid.fit .tile{aspect-ratio:auto;height:100%;min-height:0}
/* Read from three metres, not thirty centimetres. The label scales with the glass. */
.grid.fit .tile .lbl{font-size:clamp(10px,.95vw,17px);padding:2px 8px;background:rgba(0,0,0,.62)}
.grid.fit .tile .ts{font-size:clamp(9px,.7vw,13px)}
.grid.fit .tile .norec{font-size:clamp(9px,.72vw,13px)}

.tile{position:relative;aspect-ratio:16/10;background:#131b24;overflow:hidden;display:grid;place-items:center}
.tile img{width:100%;height:100%;object-fit:cover;display:block}
.tile .ph{color:#34424f;font-size:12px;text-align:center;padding:8px}
.tile .lbl{position:absolute;top:5px;left:6px;font-size:10px;color:#cdd6e0;
  background:rgba(0,0,0,.5);padding:1px 6px;border-radius:4px;max-width:80%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* Recording is the NORMAL state, so it gets a dot, not a shout. Thirty-two red REC badges spend
   the operator's attention on the thirty-two things that are fine, and leave nothing left over
   for the one that is not. */
.tile .rec{position:absolute;top:7px;right:7px;width:8px;height:8px;border-radius:50%;
  background:var(--danger);box-shadow:0 0 0 2px rgba(0,0,0,.45)}

/* NOT recording is the state that ruins an investigation, and it used to look identical to every
   other tile. Now it is the only thing on the wall wearing a word. */
.tile .norec{position:absolute;top:5px;right:6px;font-size:9px;font-weight:600;letter-spacing:.04em;
  color:#fff;background:var(--danger);padding:1px 6px;border-radius:4px}
.tile .ts{position:absolute;bottom:5px;left:6px;font-size:9px;color:#9fb0c0}
.tile.off{background:#0f151d}

/* Statusbar + quality */
.statusbar{display:flex;align-items:center;justify-content:space-between;padding:7px 14px;
  background:var(--panel2);font-size:11px;color:var(--muted)}
.quality{display:flex;gap:6px}
.q{font-size:11px;color:var(--muted);border:.5px solid var(--line);padding:3px 10px;border-radius:11px}
.q.on{background:var(--gold);color:var(--gold-ink);border-color:var(--gold)}

/* Toolbar */
.toolbar{display:flex;align-items:center;justify-content:space-around;
  padding:8px 6px calc(8px + var(--safe-bot));background:var(--panel2);border-top:.5px solid var(--line)}
.tool{display:flex;flex-direction:column;align-items:center;gap:3px;color:var(--ink);padding:4px 8px;min-width:56px}
.tool span{font-size:9px;color:var(--muted)}
.tool:active{opacity:.6}

/* Live video + fullscreen */
.fs-video{max-width:100%;max-height:100%;width:100%;height:100%;object-fit:contain;background:#000}
.live-tag{font-size:10px;color:#9fb0c0;border:.5px solid #2b3641;padding:1px 6px;border-radius:8px;margin-left:6px}
.live-tag.on{color:#10151f;background:var(--danger);border-color:var(--danger)}
.live-tag.on::before{content:'● '}

/* PTZ overlay */
.ptz-pad{position:absolute;right:16px;bottom:96px;width:132px;height:132px;z-index:3;
  display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);gap:6px}
.ptz-b{background:rgba(16,21,31,.72);border:.5px solid #2b3641;border-radius:10px;color:#e7ecf3;display:grid;place-items:center}
.ptz-b:active{background:var(--gold);color:var(--gold-ink)}
.ptz-b.up{grid-column:2;grid-row:1}.ptz-b.left{grid-column:1;grid-row:2}.ptz-b.right{grid-column:3;grid-row:2}
.ptz-b.down{grid-column:2;grid-row:3}.ptz-b.zin{grid-column:1;grid-row:1}.ptz-b.zout{grid-column:3;grid-row:1}

/* Recordings */
.rec-group{padding:10px 16px 4px;font-size:11px;color:var(--gold);text-transform:uppercase;letter-spacing:.08em;background:var(--panel2);position:sticky;top:0}
.rec-list{flex:1;overflow:auto;background:var(--bg)}
.rec-item{display:flex;align-items:center;gap:10px;width:100%;text-align:left;padding:12px 16px;
  border-bottom:.5px solid var(--line);color:var(--ink)}
.rec-item:active{background:var(--panel2)}
.rec-cam{flex:1;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rec-when{font-size:11px;color:var(--muted)}
.rec-size{font-size:11px;color:var(--muted2);min-width:52px;text-align:right}
.rec-player{position:fixed;inset:0;background:#000;z-index:7;display:flex;align-items:center;justify-content:center}
.rec-video{max-width:100%;max-height:100%}

/* Fullscreen */
.fs{background:#000}
.fs-back{position:absolute;top:calc(10px + var(--safe-top));left:10px;z-index:2;width:40px;height:40px;
  border-radius:20px;background:rgba(0,0,0,.5);color:#fff;display:grid;place-items:center}
.fs-name{position:absolute;top:calc(16px + var(--safe-top));left:0;right:0;text-align:center;font-size:14px;color:#e7ecf3;pointer-events:none}
.fs-stage{flex:1;display:grid;place-items:center;background:#000}
.fs-stage img{max-width:100%;max-height:100%;object-fit:contain}
.fs-tools{display:flex;justify-content:center;gap:28px;padding:12px 0 calc(14px + var(--safe-bot));background:#0c1119}

/* Events sheet */
.sheet{position:fixed;left:0;right:0;bottom:0;max-height:70%;background:var(--panel);
  border-top:.5px solid var(--line);border-radius:16px 16px 0 0;display:flex;flex-direction:column;z-index:5}
.sheet-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:.5px solid var(--line);font-weight:600}
.ev-list{overflow:auto;padding:6px 0 calc(6px + var(--safe-bot))}
.ev{display:flex;gap:10px;padding:10px 16px;border-bottom:.5px solid var(--line)}
.ev .dot{width:8px;height:8px;border-radius:4px;margin-top:6px;flex:none}
.ev.info .dot{background:var(--muted)} .ev.warning .dot{background:var(--gold)} .ev.critical .dot{background:var(--danger)}
.ev .txt{font-size:13px} .ev .meta{font-size:11px;color:var(--muted);margin-top:2px}

/* Devices drawer (hamburger) */
.drawer-back{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:6;display:flex}
.drawer-panel{width:84%;max-width:340px;height:100%;background:var(--panel);border-right:.5px solid var(--line);
  display:flex;flex-direction:column;padding-top:var(--safe-top)}
.drawer-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;
  border-bottom:.5px solid var(--line);font-weight:600}
.drawer-sub{padding:10px 16px 4px;color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.1em}
.dev-list{flex:1;overflow:auto;padding:4px 0}
.dev{display:flex;align-items:center;gap:10px;width:100%;text-align:left;padding:12px 16px;border-bottom:.5px solid var(--line);color:var(--ink)}
.dev:active{background:var(--panel2)}
.dev-dot{width:8px;height:8px;border-radius:4px;background:var(--muted2);flex:none}
.dev-dot.rec{background:var(--danger)}
.dev-name{flex:1;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dev-tag{font-size:9px;color:#fff;background:var(--danger);padding:1px 6px;border-radius:4px}
.drawer-foot{display:flex;justify-content:space-around;padding:12px 8px calc(12px + var(--safe-bot));border-top:.5px solid var(--line)}
.dev-act{display:flex;flex-direction:column;align-items:center;gap:3px;color:var(--ink);padding:4px 10px}
.dev-act span{font-size:9px;color:var(--muted)}

/* Update banner. Gold, not red: a new version is good news, and red is spoken for — it means a
   camera is not recording. If everything can be red, nothing is. */
.update-bar{position:fixed;left:50%;top:calc(12px + var(--safe-top));transform:translateX(-50%);
  display:flex;align-items:center;gap:12px;z-index:10;
  background:var(--gold);color:var(--gold-ink);font-size:13px;font-weight:600;
  padding:8px 10px 8px 16px;border-radius:12px;box-shadow:0 6px 24px rgba(0,0,0,.45)}
.update-bar button{background:var(--gold-ink);color:var(--gold);font-weight:600;font-size:12px;
  padding:6px 12px;border-radius:8px}

/* Toast */
.toast{position:fixed;left:50%;bottom:calc(80px + var(--safe-bot));transform:translateX(-50%);
  background:var(--panel);border:.5px solid var(--line);color:var(--ink);font-size:13px;
  padding:10px 16px;border-radius:12px;z-index:9;max-width:82%;text-align:center}

svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}

/* ---------- live tiles ----------
   The player is a custom element; it creates its own <video> inside. Both the element and the
   video must fill the tile, or the picture sits in a letterboxed island in the corner. */
.tile .feed{position:absolute;inset:0;display:block;width:100%;height:100%}
.tile .feed video{width:100%;height:100%;object-fit:cover;background:#000;display:block}
.tile img.poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.tile.is-live .lbl::before{content:'● ';color:var(--danger)}

/* A camera that stopped sending frames must LOOK wrong. The most dangerous state in a
   surveillance system is a stale picture that still looks like a camera. */
.tile.dead{outline:1px solid var(--danger);outline-offset:-1px}
.tile.dead .feed video,
.tile.dead img.poster{filter:grayscale(1) brightness(.45)}
.tile.dead .ts{color:#f0997b}

/* On the wall, read across a room, a one-pixel outline is invisible — which means the frozen
   camera reads as a working one, which is the exact failure this is for. Make it impossible to
   miss from the doorway, and make it MOVE: peripheral vision is blind to colour and sharp to
   motion, and nobody is looking straight at the TV when it matters. */
.grid.fit .tile.dead{outline:3px solid var(--danger);outline-offset:-3px;animation:deadPulse 2s ease-in-out infinite}
@keyframes deadPulse{
  0%,100%{outline-color:var(--danger)}
  50%{outline-color:rgba(192,57,43,.25)}
}
/* Respect the operator who cannot take a blinking screen all day. The outline stays; only the
   blinking goes. */
@media (prefers-reduced-motion: reduce){
  .grid.fit .tile.dead{animation:none}
}

/* The wall is the video. The status bar is not the video. */
.grid.fit ~ .statusbar{display:none}

/* ---------- responsive ----------
   The viewer was written for a phone and it shows: on a tablet or a reception desktop it was a
   stretched phone app with two enormous cameras. The split control stays in the operator's
   hands — this only stops the layout from wasting the glass he actually has. */
@media (min-width: 700px) {
  .grid{gap:2px;padding:2px}
  .grid.split-1{grid-template-columns:1fr}
  .grid.split-4{grid-template-columns:repeat(3,1fr)}
  .grid.split-9{grid-template-columns:repeat(4,1fr)}
  .tile{aspect-ratio:16/9}
  .toolbar{justify-content:center;gap:24px}
  .topbar .title{font-size:17px}
}
@media (min-width: 1200px) {
  .grid.split-4{grid-template-columns:repeat(4,1fr)}
  .grid.split-9{grid-template-columns:repeat(6,1fr)}
  .login-card{max-width:400px}
}
/* Landscape phone: the toolbar and statusbar eat the video. Give the picture the screen. */
@media (max-height: 480px) and (orientation: landscape) {
  .statusbar{display:none}
  .toolbar{padding:4px 6px}
  .tool span{display:none}
  .topbar{padding:4px 10px}
}
