/* ============================================================
   Sentry — household security app
   Mobile-first, dark. Designed for iPhone first, scales up.
   ============================================================ */

/* The browser's own `[hidden] { display: none }` is a user-agent rule, so
   ANY author rule that sets `display` on the same element beats it and the
   element stays on screen with its `hidden` attribute set. That is not
   theoretical here: `.login-box { display: flex }` kept the sign-in form
   visible underneath the install gate. The per-selector `[hidden]` rules
   dotted further down this file are all patches for the same thing; this
   one covers every element, including ones not written yet. */
[hidden] { display: none !important; }

:root {
  color-scheme: dark;

  /* iOS dark system palette. Surfaces step up in lightness rather than
     relying on borders, chrome is a translucent material over the content
     behind it, and radii are generous. */
  --bg: #000000;
  --bg-2: #1c1c1e;
  --card: #1c1c1e;
  --card-2: #2c2c2e;
  --card-3: #3a3a3c;
  --line: rgba(84, 84, 88, 0.5);
  --line-2: rgba(84, 84, 88, 0.9);

  --text: #ffffff;
  --text-2: rgba(235, 235, 245, 0.62);
  --text-3: rgba(235, 235, 245, 0.32);

  --brand: #0a84ff;
  --brand-2: #0060df;
  --alert: #ff453a;
  --detect: #ff9f0a;
  --ok: #30d158;

  /* Tokens for the handful of places that genuinely have to flip between
     themes. Most colours in this file do not: white on a brand-coloured
     button is white in both, and the glass over a camera picture stays dark
     because the picture underneath is dark whatever the app chrome is doing.
     These five are the exceptions -- text tuned for a dark background, and
     overlays built from white at low alpha, both of which vanish on light. */
  /* Over camera imagery. The picture beneath is dark whatever the app
     chrome is doing, and these sit on a gradient scrim, so they must NOT
     follow the theme. Inheriting --text is what turned the camera titles
     and the LIVE pill black in light mode. Defined once and deliberately
     not overridden by the light palette. */
  --on-media: #ffffff;
  --on-media-2: rgba(255, 255, 255, 0.74);

  /* Floating chrome that sits on the PAGE rather than on a picture -- the
     timeline's day header and its scrub hint. These do flip. */
  --panel: rgba(17, 20, 25, 0.92);

  --danger-text: #ff6b6b;      /* destructive text on a page background */
  --accent-text: #9ccdff;      /* recognised names, filter emphasis */

  /* Timeline. The track is --bg-2, which flips, so everything drawn on it
     flips too -- unlike the chrome over the video above it. The event
     washes themselves are written as literal rgba in the rules below and
     deliberately have no token: a translucent hue over black reads dark
     and the same hue over white reads pale, so one value is right in both
     themes. These are the parts that are NOT self-correcting. */
  --seg-text: #ffdadb;         /* event label on its own coloured wash */
  --seg-ring: rgba(255, 255, 255, 0.28);  /* ring marking an unreviewed event */
  --rail: rgba(0, 0, 0, 0.26);            /* overview strip, far edge */
  --rail-2: rgba(0, 0, 0, 0.5);           /* overview strip, near edge */
  --rail-edge: rgba(255, 255, 255, 0.55); /* its viewport box */

  /* Colour-by-object. Border and icon only; see the note above. */
  --o-person: #bf5af2;
  --o-vehicle: #64d2ff;
  --o-animal: #30d158;
  --o-other: #98989d;
  --fill: rgba(255, 255, 255, 0.14);   /* subtle raised fill */
  --fill-2: rgba(255, 255, 255, 0.16); /* hairline / spinner track */
  --glow: #14263a;             /* the tint behind the sign-in screen */

  --r-sm: 9px;
  --r: 14px;
  --r-lg: 18px;
  --r-xl: 26px;

  /* Materials: chrome floats over content rather than sitting beside it. */
  /* Opaque, and exactly the black iOS uses for the status bar. Do not
     make this translucent: `apple-mobile-web-app-status-bar-style: black`
     is not overridable, so any other value shows as a seam up top. */
  --mat: #000000;
  --mat-blur: saturate(180%) blur(24px);

  /* Liquid Glass. The material only reads as glass if content actually
     passes beneath it, so the elements using these float over the scroller
     rather than sitting in flow. Three layers make it: a translucent fill, a
     bright specular rim, and an inner top highlight where the light catches
     the edge. Saturation is pushed well past 100% because that colour bleed
     from underneath is what separates glass from frosted plastic. */
  --glass: rgba(28, 28, 30, 0.60);
  --glass-edge: rgba(255, 255, 255, 0.18);
  --glass-hi: rgba(255, 255, 255, 0.12);
  --glass-blur: saturate(200%) blur(28px);
  --glass-shadow: 0 10px 34px rgba(0, 0, 0, 0.40);
  /* For rows that repeat down a list. Same look, no backdrop-filter: a blur
     layer per cell is what makes long lists stutter on iOS, so these carry
     a slightly more opaque fill and the rim instead. */
  --glass-soft: rgba(28, 28, 30, 0.78);

  --shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
  --shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.35);

  --bar-h: 54px;
  --tab-h: 58px;
  /* How much of the bottom the floating tab bar occupies: its own height
     plus the gap it sits above. Anything anchored to the bottom of a
     screen has to clear this or the bar lands on top of it -- which is
     exactly what happened to the timeline's Now button once the bar
     stopped being in flow. */
  --tabbar-clear: calc(var(--tab-h) + max(16px, var(--safe-b) - 6px));

  --safe-t: env(safe-area-inset-top, 0px);
  /* Clamped: iOS reports 34px for the home indicator, which leaves more
     clearance under the tab bar than it actually needs. min() keeps this
     at 0 on devices without an indicator. */
  --safe-b: min(env(safe-area-inset-bottom, 0px), 26px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);

  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --spring: cubic-bezier(0.34, 1.4, 0.34, 1);
}

/* ------------------------------------------------------- light theme ---
 *
 * The palette is the only thing that changes: every surface, line and text
 * colour above is a token, so this overrides those and nothing else.
 *
 * Two selectors deliberately. `[data-theme="light"]` is an explicit choice
 * and must win over everything; the media query covers "match my device"
 * and must NOT apply when dark has been chosen explicitly, hence the
 * :not() -- without it, a phone set to light would override someone who
 * asked for dark.
 */

:root[data-theme="light"] {
  color-scheme: light;

  /* iOS light system palette, mirroring the dark one: surfaces step DOWN
     in lightness away from the page rather than up. */
  --bg: #f2f2f7;
  --bg-2: #ffffff;
  --card: #ffffff;
  --card-2: #e9e9ee;
  --card-3: #dcdce2;
  --line: rgba(60, 60, 67, 0.22);
  --line-2: rgba(60, 60, 67, 0.36);

  --text: #000000;
  --text-2: rgba(60, 60, 67, 0.72);
  --text-3: rgba(60, 60, 67, 0.55);

  /* Slightly darker than the dark-theme blue: #0a84ff is tuned for contrast
     against black and is too light to read on white. */
  --brand: #0071e3;
  --brand-2: #0055b0;
  --alert: #d70015;
  --detect: #b25000;
  --ok: #177f38;

  --panel: rgba(255, 255, 255, 0.92);
  --danger-text: #d70015;
  --accent-text: #0060c0;

  /* Timeline on a white track: the pale-pink label and the white-alpha
     overlays below are invisible here, so they flip. The event washes do
     not appear in either palette -- see the note in :root. */
  --seg-text: #7a1b1e;
  --seg-ring: rgba(0, 0, 0, 0.28);
  --rail: rgba(0, 0, 0, 0.02);
  --rail-2: rgba(0, 0, 0, 0.06);
  --rail-edge: rgba(0, 0, 0, 0.45);

  --o-person: #8944ab;
  --o-vehicle: #0a7ea4;
  --o-animal: #177f38;
  --o-other: #6c6c70;
  --fill: rgba(0, 0, 0, 0.06);
  --fill-2: rgba(0, 0, 0, 0.10);
  --glow: #dce9f8;

  --mat: #ffffff;

  /* Glass over a light background: the rim goes near-white and the shadow
     drops right back, or the panel reads as grey plastic sitting in soot. */
  --glass: rgba(255, 255, 255, 0.62);
  --glass-edge: rgba(255, 255, 255, 0.85);
  --glass-hi: rgba(255, 255, 255, 0.70);
  --glass-shadow: 0 10px 30px rgba(0, 0, 0, 0.14);
  --glass-soft: rgba(255, 255, 255, 0.78);

  /* Shadows on a light background need far less opacity, or everything
     looks like it is floating in soot. */
  --shadow: 0 12px 40px rgba(0, 0, 0, 0.16);
  --shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.10);
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]):not([data-theme="light"]) {
    color-scheme: light;

    --bg: #f2f2f7;
    --bg-2: #ffffff;
    --card: #ffffff;
    --card-2: #e9e9ee;
    --card-3: #dcdce2;
    --line: rgba(60, 60, 67, 0.22);
    --line-2: rgba(60, 60, 67, 0.36);

    --text: #000000;
    --text-2: rgba(60, 60, 67, 0.72);
    --text-3: rgba(60, 60, 67, 0.55);

    --brand: #0071e3;
    --brand-2: #0055b0;
    --alert: #d70015;
    --detect: #b25000;
    --ok: #177f38;

    --panel: rgba(255, 255, 255, 0.92);
    --danger-text: #d70015;
    --accent-text: #0060c0;

    /* Timeline on a white track: the pale-pink label and the white-alpha
       overlays below are invisible here, so they flip. The event washes do
       not appear in either palette -- see the note in :root. */
    --seg-text: #7a1b1e;
    --seg-ring: rgba(0, 0, 0, 0.28);
    --rail: rgba(0, 0, 0, 0.02);
    --rail-2: rgba(0, 0, 0, 0.06);
    --rail-edge: rgba(0, 0, 0, 0.45);

    --o-person: #8944ab;
    --o-vehicle: #0a7ea4;
    --o-animal: #177f38;
    --o-other: #6c6c70;
    --fill: rgba(0, 0, 0, 0.06);
    --fill-2: rgba(0, 0, 0, 0.10);
    --glow: #dce9f8;

    --mat: #ffffff;
    --glass: rgba(255, 255, 255, 0.62);
    --glass-edge: rgba(255, 255, 255, 0.85);
    --glass-hi: rgba(255, 255, 255, 0.70);
    --glass-shadow: 0 10px 30px rgba(0, 0, 0, 0.14);
    --glass-soft: rgba(255, 255, 255, 0.78);
    --shadow: 0 12px 40px rgba(0, 0, 0, 0.16);
    --shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.10);
  }
}


* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  overscroll-behavior-y: none;
}

body {
  /* The glass needs something behind it. On a flat --bg the material has
     nothing to refract and reads as a plain tinted panel -- which is why the
     tab bar looked like glass (content scrolls under it) while Settings and
     Account looked solid. These broad, low-alpha washes are the app's own
     wallpaper, and the panels pick them up as colour bleed. No
     background-attachment needed: .app is position:fixed and scrolling
     happens inside .scroller, so the body never moves. */
  background:
    /* Fades the washes out to exactly --bg across the top edge. The status
       bar sits outside the web view (apple-mobile-web-app-status-bar-style
       is `black`) and is painted flat from theme-color, which follows --bg --
       so without this the wallpaper met that strip at full strength and drew
       a hard line across the top of the screen. */
    linear-gradient(to bottom, var(--bg) 0, transparent 160px),
    radial-gradient(58% 42% at 10% 14%, color-mix(in srgb, var(--brand) 30%, transparent), transparent 70%),
    radial-gradient(52% 38% at 94% 20%, color-mix(in srgb, var(--o-person) 26%, transparent), transparent 72%),
    radial-gradient(68% 46% at 46% 104%, color-mix(in srgb, var(--ok) 18%, transparent), transparent 74%),
    var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, "Segoe UI", Roboto, sans-serif;
  font-size: 15px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

/* Kill the 300ms tap delay and double-tap zoom on iOS. */
button, a, .tappable { touch-action: manipulation; }

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
}

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

::-webkit-scrollbar { width: 0; height: 0; }

/* --------------------------------------------------- app shell ---- */

/* `inset: 0` sizes to iOS Safari's *layout* viewport, which extends behind
   the browser toolbar -- that is what pushed the tab bar below the visible
   area. Size to the dynamic viewport instead, preferring the height JS
   measures from visualViewport where it is available. */
.app {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;    /* fallback for browsers without dvh */
  height: 100dvh;   /* the visible viewport, excluding browser chrome */
  /* Set by JS only on browsers that lack dvh; ignored otherwise. */
  height: var(--app-h, 100dvh);
  display: flex;
  flex-direction: column;
}

/* Installed there is no browser chrome, so pin the shell to both edges of
   the viewport and drop the height calculation entirely. A fixed element
   with top and bottom both 0 is exactly the screen -- no vh, no dvh, no
   measured variable, nothing that can be reported wrong mid-scroll. */
@media (display-mode: standalone), (display-mode: fullscreen) {
  .app {
    top: 0;
    bottom: 0;
    height: auto;
  }
}

/* Announced, never drawn. The app bar carried the screen name; with it gone
   the name still has to reach a screen reader on navigation. Not `display:
   none`, which would take it out of the accessibility tree entirely. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The only way out of a drill-down now. Fixed rather than in flow, so the
   top-level tabs keep a clean top edge and nothing reflows when it appears.
   Translucent over the content beneath, the way iOS floats its own controls
   over full-bleed media. */
.backchip {
  position: fixed;
  top: calc(var(--safe-t) + 8px);
  left: calc(var(--safe-l) + 10px);
  z-index: 30;
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border-radius: 50%;
  color: var(--text);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 0.5px solid var(--glass-edge);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-hi);
}
.backchip:active { background: var(--card-2); }
.backchip[hidden] { display: none; }

.status-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--text-3);
  margin-right: 4px;
  transition: background 0.3s;
}
.status-dot.ok { background: var(--ok); box-shadow: 0 0 10px rgba(47, 208, 138, 0.65); }
.status-dot.warn { background: var(--detect); }
.status-dot.bad { background: var(--alert); }

/* Took over from the dot when the app bar went. Only rendered when something
   is wrong, so it needs to read as an alert rather than as chrome. */
.status-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  margin: 10px 0 2px;
  padding: 10px 12px;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--alert) 14%, transparent);
  color: var(--alert);
  font-size: 13.5px;
  font-weight: 550;
  text-align: left;
}
.status-banner::before {
  content: "";
  width: 8px; height: 8px;
  flex: 0 0 8px;
  border-radius: 50%;
  background: currentColor;
}
.status-banner[hidden] { display: none; }
.status-banner:active { background: color-mix(in srgb, var(--alert) 22%, transparent); }

/* ----------------------------------------------------- screens ---- */

.screens { flex: 1; position: relative; min-height: 0; }

.screen {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
}
.screen.active { display: flex; }

/* The app bar used to supply this inset. Without it every screen starts at
   y=0, under the notch and the clock. `.fixed` screens are exempt on purpose:
   that is the camera stage, which is meant to run full-bleed with the back
   chip floating over it. */
.screen:not(.fixed) { padding-top: calc(var(--safe-t) + 6px); }

/* The back chip floats, so a screen showing it must clear its height (36px)
   plus its offset and a gap, or it lands on the first row of content. The
   stage is exempt on purpose: there the chip is meant to overlap the video. */
/* Includes .fixed now: the live view was exempt so the video could run
   full-bleed, but that is exactly where the chip landed on the player. */
body.has-back .screen { padding-top: calc(var(--safe-t) + 66px); }

/* The live view sits tight under the chip. The chip ends at safe-t + 44
   (8px offset + 36px tall), so this leaves a 3px gap: a list can afford
   breathing room, a video just loses picture. */
body.has-back .screen.fixed { padding-top: calc(var(--safe-t) + 47px); }

/* Fullscreen hides the chip, so its clearance has to go with it or the video
   sits below a band of empty space. The second selector is not redundant:
   it matches the specificity of the .fixed rule above so that source order
   decides, otherwise the live view would keep its padding in fullscreen. */
body.stage-max .screen,
body.stage-max .screen.fixed { padding-top: 0; }

/* Screens that push in from the right, iOS-style. */
.screen.pushing { animation: pushIn 0.32s var(--ease); }
@keyframes pushIn {
  from { transform: translateX(14%); opacity: 0.4; }
  to { transform: translateX(0); opacity: 1; }
}

.scroller {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  /* Held as variables so anything inside can bleed to the screen edge and
     come back to the same gutter -- at every breakpoint, since the ones
     below re-set these rather than the padding itself. */
  --scroll-pad-l: calc(14px + var(--safe-l));
  --scroll-pad-r: calc(14px + var(--safe-r));
  padding: 0 var(--scroll-pad-r) 0 var(--scroll-pad-l);
}

/* Also clears the floating tab bar, which no longer occupies flow. Content
   scrolls under the glass; this is what stops it ending up trapped there. */
.foot-space { height: calc(34px + var(--safe-b) + var(--tab-h)); }

/* ---------------------------------------------------- tab bar ----- */

/* Lifted out of flow into a floating capsule. This is what makes the glass
   work: in flow it had content stacked above it and nothing to refract, so
   the material had nothing to do. `.app` is position:fixed, so it is already
   the containing block. `.foot-space` reserves the room it no longer takes. */
.tabbar {
  position: absolute;
  left: calc(14px + var(--safe-l));
  right: calc(14px + var(--safe-r));
  /* Sits down near the home indicator rather than clearing it entirely --
     the full safe-area inset left it floating conspicuously high. The max()
     keeps a margin on devices with no inset at all. */
  bottom: max(16px, calc(var(--safe-b) - 6px));
  height: var(--tab-h);
  display: flex;
  border-radius: calc(var(--tab-h) / 2);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 0.5px solid var(--glass-edge);
  /* The inset line is the specular catch along the top edge. */
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-hi);
  overflow: hidden;
  z-index: 20;
}

/* Without backdrop-filter the translucent fill would just look washed out,
   so fall back to the solid material. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .tabbar { background: var(--mat); }
}

.tabbar button {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  color: var(--text-3);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.01em;
  transition: color 0.15s;
  min-height: 44px;
}
.tabbar button svg { width: 23px; height: 23px; }
.tabbar button[aria-selected="true"] { color: var(--brand); }
.tabbar button[aria-selected="true"] svg { stroke-width: 2.1; }
.tabbar button:active { opacity: 0.45; }
.tabbar button[aria-selected="true"] { color: var(--brand); }

/* ------------------------------------------------- group rail ----- */

.group-rail {
  display: flex;
  gap: 8px;
  padding: 12px 0 4px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.group-rail::-webkit-scrollbar { display: none; }

.group-rail button {
  flex: 0 0 auto;
  padding: 7px 15px;
  border-radius: 999px;
  background: var(--card);
  border: 1px solid var(--line);
  color: var(--text-2);
  font-size: 13.5px;
  font-weight: 600;
  white-space: nowrap;
  min-height: 36px;
}
.group-rail button[aria-pressed="true"] {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}
.group-rail button:active { transform: scale(0.93); }
.group-rail button { transition: transform 0.3s var(--spring), background 0.15s; }

/* ------------------------------------------------- camera list ---- */

/* ---- home hero + recent activity ----------------------------------- */

.home-hero { padding: 14px 0 2px; }
.hero-greet {
  margin: 0;
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.03em;
  color: var(--text);
}
.hero-sub {
  margin: 3px 0 0;
  font-size: 13.5px;
  color: var(--text-2);
}

.rail-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin: 18px 0 9px;
  font-size: 13px;
  font-weight: 650;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-3);
}
.rail-more {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
  color: var(--brand);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
}
.rail-more svg { width: 15px; height: 15px; }

/* Horizontal strip. Bleeds to both screen edges so the card at the end is
   visibly cut off -- which is what tells you it scrolls -- while the first
   one still lines up with everything else on the page. */
.recent-rail {
  --rail-gap: 10px;
  display: flex;
  gap: var(--rail-gap);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  margin-left: calc(-1 * var(--scroll-pad-l));
  margin-right: calc(-1 * var(--scroll-pad-r));
  padding: 2px 0 4px;
  /* Keep a snapped card off the very edge as well: snapping otherwise
     lands cards against the scrollport, ignoring the spacers below. */
  scroll-padding-left: var(--scroll-pad-l);
  scroll-padding-right: var(--scroll-pad-r);
}
.recent-rail::-webkit-scrollbar { display: none; }

/* The gutter is restored with flex items, not with padding.
   Padding on a horizontally scrolling flex container is the one part of
   this that browsers disagree about -- WebKit in particular drops the
   trailing side -- and getting it wrong puts the first card hard against
   the edge of the screen. A box cannot be dropped. */
.recent-rail::before,
.recent-rail::after {
  content: "";
  flex: 0 0 auto;
  align-self: stretch;
}
/* Less the gap, which the flex container puts between the spacer and the
   first card -- otherwise the strip starts a gap further in than
   everything else on the page. */
.recent-rail::before { width: calc(var(--scroll-pad-l) - var(--rail-gap)); }
.recent-rail::after { width: calc(var(--scroll-pad-r) - var(--rail-gap)); }

.recent-card {
  flex: 0 0 158px;
  width: 158px;
  scroll-snap-align: start;
  -webkit-user-select: none;
  user-select: none;
  cursor: pointer;
  transition: transform 0.3s var(--spring);
}
.recent-card:active { transform: scale(0.965); }
.recent-card:focus-visible { outline: 3px solid var(--brand); outline-offset: 3px; border-radius: 14px; }

.recent-shot {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: 13px;
  overflow: hidden;
  background: var(--card-2);
}
.recent-shot img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.recent-scrim {
  position: absolute;
  inset: auto 0 0 0;
  height: 62%;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
  pointer-events: none;
}
/* Over a thumbnail, so it does not follow the theme -- the picture beneath
   is dark whatever the app chrome is doing. */
.recent-what {
  position: absolute;
  left: 8px; right: 8px; bottom: 7px;
  display: flex;
  align-items: center;
  gap: 5px;
  color: var(--on-media);
  font-size: 12.5px;
  font-weight: 650;
  text-transform: capitalize;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.65);
}
.recent-dot {
  flex: 0 0 6px;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--detect);
}
.recent-card.alert .recent-dot { background: var(--alert); }

.recent-meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 6px;
  padding: 6px 2px 0;
}
.recent-cam {
  font-size: 12.5px;
  font-weight: 550;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.recent-when {
  flex: 0 0 auto;
  font-size: 11.5px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

.cam-list {
  display: grid;
  gap: 14px;
  padding-top: 10px;
  grid-template-columns: minmax(0, 1fr);
}
.cam-list > * { min-width: 0; }

/* Cameras per row (Account > Live view).

   The attribute selectors carry more specificity than the plain .cam-list
   rules, including the wide-screen one further down this file, so a chosen
   count holds on every screen size instead of being overridden at the
   desktop breakpoint. */
.cam-list[data-cols="1"] { grid-template-columns: minmax(0, 1fr); }
.cam-list[data-cols="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cam-list[data-cols="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* Cards get small quickly at three across a phone, so the furniture on
   them shrinks with the column count rather than crowding the picture. */
.cam-list[data-cols="2"], .cam-list[data-cols="3"] { gap: 9px; }
.cam-list[data-cols="2"] .cam-title { font-size: 14px; }
.cam-list[data-cols="2"] .cam-last { font-size: 11px; }
.cam-list[data-cols="2"] .cam-info { left: 9px; right: 9px; bottom: 8px; }
.cam-list[data-cols="3"] .cam-title { font-size: 12px; }
.cam-list[data-cols="3"] .cam-last { font-size: 10px; }
.cam-list[data-cols="3"] .cam-info { left: 7px; right: 7px; bottom: 6px; }
.cam-list[data-cols="3"] .live-tag { top: 6px; left: 6px; padding: 3px 6px; font-size: 9px; }

/* Whole frame instead of a filled card. The black bars are the point:
   a wide lens loses its edges to the 16/9 crop. */
.cam-list[data-fit="contain"] .cam-view img,
.cam-list[data-fit="contain"] .cam-view video { object-fit: contain; }

.cam-card {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: #000;
  box-shadow: var(--shadow-sm);
  transition: transform 0.32s var(--spring);
  -webkit-user-select: none;
  user-select: none;
}
.cam-card:active { transform: scale(0.975); }
.cam-card.is-active { box-shadow: 0 0 0 2.5px var(--alert), var(--shadow-sm); }

.cam-view {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  display: block;
  overflow: hidden;
}
.cam-media { position: absolute; inset: 0; }
.cam-view img,
.cam-view video {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  background: #000;
}

/* Gradient scrim so white text stays readable over any scene. */
.cam-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,0.78) 0%, rgba(0,0,0,0.25) 28%, rgba(0,0,0,0) 55%);
  pointer-events: none;
}

.cam-info {
  position: absolute;
  left: 14px; right: 14px; bottom: 11px;
  display: flex;
  align-items: flex-end;
  gap: 10px;
  pointer-events: none;
}

.cam-title {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--on-media);
  text-shadow: 0 1px 6px rgba(0,0,0,0.8);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cam-last {
  font-size: 12.5px;
  color: rgba(255,255,255,0.82);
  text-shadow: 0 1px 6px rgba(0,0,0,0.9);
  margin-top: 1px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.live-tag {
  position: absolute;
  top: 12px; left: 12px;
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 5px 10px;
  border-radius: 999px;
  background: rgba(0,0,0,0.5);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: var(--on-media);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.live-tag i {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--alert);
  animation: breathe 2s ease-in-out infinite;
}
@keyframes breathe { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }

.cam-card.is-active { box-shadow: 0 0 0 2px var(--alert), var(--shadow-sm); }

.cam-offline {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  gap: 6px;
  color: var(--text-3);
  font-size: 13px;
  background: var(--bg-2);
  text-align: center;
  padding: 16px;
}

/* --------------------------------------------------- cam stage ---- */

/* The camera screen is an app layout, not a scrolling page: the stage is
   pinned and the timeline owns the remaining height and its own scroll. */
.screen.fixed { overflow: hidden; }

.stage-wrap {
  flex: 0 0 auto;
  padding: 10px calc(14px + var(--safe-r)) 0 calc(14px + var(--safe-l));
}

.stage {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: #000;
  box-shadow: var(--shadow);
  aspect-ratio: 16 / 9;
}
.stage-media {
  position: absolute;
  inset: 0;
  transform-origin: center center;
  will-change: transform;
  touch-action: none;      /* we own pinch and drag on the picture */
}
.stage-media.zoomed { cursor: grab; }
.stage-media.panning { cursor: grabbing; }
.stage-media img,
.stage-media video {
  width: 100%; height: 100%;
  object-fit: contain;
  background: #000;
  display: block;
}
.stage:fullscreen { aspect-ratio: auto; height: 100%; border-radius: 0; }
.stage:-webkit-full-screen { aspect-ratio: auto; height: 100%; border-radius: 0; }

/* Fallback fullscreen for iOS, which has no Element.requestFullscreen.
   Keeps our own dock rather than handing over to Apple's video player. */
body.stage-max .stage-wrap {
  position: fixed;
  inset: 0;
  z-index: 300;
  padding: 0;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
}
body.stage-max .stage {
  width: 100%;
  border-radius: 0;
  box-shadow: none;
}
body.stage-max .backchip,
body.stage-max .tabbar,
body.stage-max .tl-bar,
body.stage-max .chip-rail,
body.stage-max .vtl { display: none; }

/* ---- overlay chrome: glass, matching the app's other floating bits --- */

.stage-top {
  position: absolute;
  top: 8px; left: 8px; right: 8px;
  display: flex;
  gap: 5px;
  pointer-events: none;
  z-index: 3;
  transition: opacity 0.3s var(--ease);
}
.stage-top.hidden { opacity: 0; }

/* The live badge stays put: knowing you are watching live matters more
   than a perfectly clean frame. */
.stage-top.hidden .tag.live { opacity: 1; }

.tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 9px;
  transition: opacity 0.3s;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.42);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 0.5px solid var(--fill);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: #eef3f8;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tag[hidden] { display: none; }
.tag.muted { color: var(--on-media-2); font-weight: 650; }
.tag.live { letter-spacing: 0.06em; text-transform: uppercase; }
.tag.live i {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--alert);
  animation: breathe 2s ease-in-out infinite;
}

/* Floating control dock over the bottom of the picture. */
.stage-dock {
  position: absolute;
  left: 8px; right: 8px; bottom: 8px;
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 4px;
  border-radius: 999px;
  background: rgba(28, 28, 30, 0.6);
  backdrop-filter: var(--mat-blur);
  -webkit-backdrop-filter: var(--mat-blur);
  border: 0.5px solid rgba(255, 255, 255, 0.12);
  z-index: 4;
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}
.stage-dock.hidden {
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
}
.dock-grow { flex: 1; min-width: 0; }

.gbtn {
  flex: 0 0 auto;
  width: 32px; height: 32px;
  border-radius: 999px;
  transition: background 0.15s, transform 0.3s var(--spring);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #eaf0f6;
  transition: background 0.15s, transform 0.12s;
}
.gbtn svg { width: 16px; height: 16px; }
.gbtn:active { background: rgba(255, 255, 255, 0.22); transform: scale(0.88); }
.gbtn[hidden] { display: none; }

.gbtn.wide {
  width: auto;
  padding: 0 13px;
  gap: 5px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 700;
}
.gbtn.text { width: auto; padding: 0 9px; font-size: 12px; font-weight: 750; }
.gbtn.primary { background: var(--brand); color: #fff; }
.gbtn.primary:active { background: var(--brand-2); }
.gbtn.primary svg { fill: #fff; stroke: none; width: 12px; height: 12px; }

.gbtn.on { color: var(--detect); background: rgba(255, 171, 46, 0.2); }
.gbtn.on svg { fill: var(--detect); stroke: none; }
.gbtn:disabled { opacity: 0.45; }

/* ---- transient stage states ---- */

.stage-state {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  gap: 8px;
  text-align: center;
  padding: 18px;
  color: var(--on-media-2);
  font-size: 13.5px;
  background: rgba(6, 8, 11, 0.9);
  z-index: 2;
}
.stage-state strong { display: block; color: var(--text-2); font-size: 14.5px; margin-bottom: 3px; }
.stage-state svg { width: 30px; height: 30px; opacity: 0.55; }

.stage-spin {
  position: absolute;
  top: 12px; right: 12px;
  width: 17px; height: 17px;
  border: 2px solid rgba(255, 255, 255, 0.25);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.75s linear infinite;
  z-index: 3;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Centre play affordance while paused. */
.stage.paused .vp-big { opacity: 1; transform: translate(-50%, -50%) scale(1); }

.stage-media video.scrub-video { object-fit: contain; }

/* The low-resolution stand-in shown while a recording downloads. Absolute
   so the full-quality <video> loads underneath at its normal size and is
   simply revealed when ready -- the stand-in is never the thing playing,
   it is only what you look at until the real one arrives. Below the
   spinner (z-index 3), which keeps saying the real one is still coming. */
.stage-media video.stage-standin {
  position: absolute;
  inset: 0;
  z-index: 2;
}
.stage { cursor: pointer; touch-action: none; }

/* ------------------------------------------------ timeline bar ---- */

.tl-bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 10px calc(14px + var(--safe-r)) 8px calc(14px + var(--safe-l));
}

.tl-count {
  font-size: 13px;
  font-weight: 700;
  color: var(--text-2);
  white-space: nowrap;
}

.tbtn {
  width: 32px; height: 32px;
  flex: 0 0 32px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-2);
  transition: background 0.15s, color 0.15s;
}
.tbtn svg { width: 15px; height: 15px; }
.tbtn:active { background: var(--card-2); color: var(--text); }

.tbtn.danger:hover, .tbtn.danger:active { color: var(--danger-text); background: rgba(245, 51, 63, 0.12); }

/* Secondary line under an empty-state heading. */
.hint {
  font-size: 12.5px;
  color: var(--text-3);
  margin-top: 4px;
}

.tl-sep {
  width: 1px;
  height: 18px;
  background: var(--line);
  margin: 0 4px;
  flex: 0 0 1px;
}

.zoom-level {
  font-size: 11.5px;
  font-weight: 700;
  color: var(--text-3);
  min-width: 34px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------- vertical timeline ---- */

.vtl {
  flex: 1;
  min-height: 0;
  position: relative;
  border-top: 1px solid var(--line);
  background: var(--bg-2);
}

.vtl-scroll {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  /* pan-y so one finger scrolls but two-finger pinch reaches our handler */
  touch-action: pan-y;
}

.vtl-track {
  position: relative;
  margin-left: calc(52px + var(--safe-l));
  margin-right: calc(12px + var(--safe-r));
}

/* Fixed playhead the timeline scrubs against. */
.vtl-playhead {
  position: absolute;
  top: 50%;
  left: 0; right: 0;
  height: 0;
  z-index: 6;
  pointer-events: none;
  border-top: 1.5px solid var(--brand);
  box-shadow: 0 0 12px rgba(47, 155, 239, 0.5);
}
.vtl-bubble {
  position: absolute;
  left: 6px;
  top: -12px;
  background: var(--brand);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  padding: 3px 8px;
  border-radius: 999px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}

.vtl-hint {
  position: absolute;
  left: 50%; bottom: 10px;
  transform: translateX(-50%);
  background: var(--panel);
  border: 1px solid var(--line);
  color: var(--text-3);
  font-size: 11.5px;
  font-weight: 600;
  padding: 5px 12px;
  border-radius: 999px;
  pointer-events: none;
  transition: opacity 0.4s;
}
.vtl-hint.gone { opacity: 0; }

/* Hour/'now' gridlines with labels in the left gutter. */
.vtl-tick {
  position: absolute;
  left: calc(-52px - var(--safe-l));
  right: 0;
  border-top: 1px solid var(--line);
  pointer-events: none;
}
.vtl-tick span {
  position: absolute;
  left: 0;
  top: -7px;
  width: 46px;
  text-align: right;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.vtl-tick.day { border-top-color: var(--line-2); }
.vtl-tick.day span { color: var(--text-2); }

/* Shading where footage actually exists on disk. */
.vtl-rec {
  position: absolute;
  left: 0; right: 0;
  background: rgba(47, 155, 239, 0.11);
  border-left: 2px solid rgba(47, 155, 239, 0.34);
  border-radius: 2px;
  pointer-events: none;
}

.vtl-motion {
  position: absolute;
  left: -8px;
  width: 4px;
  background: #34537d;
  border-radius: 2px;
  opacity: 0.7;
  pointer-events: none;
}

/* Event blocks. Tap target is padded out even when the bar is thin. */
/* ---- ribbon bands ------------------------------------------------
   One band per event (or per merged run), at its true position and true
   duration. Nothing is grown to hold a label and nothing is nudged aside --
   the icon and text moved into the popover so these can stay honest. */
.vtl-ev {
  position: absolute;
  left: 0; right: 0;
  min-height: 3px;          /* a one-second event is still findable */
  border-radius: 3px;
  background: var(--detect);
  cursor: pointer;
  transition: filter 0.12s;
}
.vtl-ev.alert { background: var(--alert); }

/* Colour by object rather than severity (Account > Timeline). Solid here
   rather than the old low-alpha wash: with no label sitting on top, the band
   no longer has to stay readable behind text. */
.vtl-ev.by-object { background: var(--o-other); }
.vtl-ev.by-object.o-person { background: var(--o-person); }
.vtl-ev.by-object.o-vehicle { background: var(--o-vehicle); }
.vtl-ev.by-object.o-animal { background: var(--o-animal); }

.vtl-ev.unreviewed { box-shadow: 0 0 0 1.5px var(--seg-ring); }
.vtl-ev.current { box-shadow: 0 0 0 2px var(--brand); }
.vtl-ev:active { filter: brightness(1.35); }

/* The band is drawn as thin as the truth requires; the hit area is as big as
   a finger requires. Without this a two-second event would be a 3px target. */
.vtl-ev::after {
  content: "";
  position: absolute;
  inset: -5px 0;
}

/* Merged run. Slightly inset and badged, so a band that stands for several
   events never reads as one long one. */
.vtl-ev.cluster {
  background: repeating-linear-gradient(
    135deg,
    var(--detect) 0 5px,
    color-mix(in srgb, var(--detect) 60%, transparent) 5px 10px
  );
}
.vtl-ev.cluster.alert {
  background: repeating-linear-gradient(
    135deg,
    var(--alert) 0 5px,
    color-mix(in srgb, var(--alert) 60%, transparent) 5px 10px
  );
}
.vtl-ev-count {
  position: absolute;
  right: 3px;
  top: 50%;
  transform: translateY(-50%);
  min-width: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--mat);
  color: var(--text);
  font-size: 10px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* ---- popover ------------------------------------------------------
   What the bands stopped carrying. Scrolls with the track it is appended
   to, so it stays beside the band it describes. */
.vtl-pop {
  position: absolute;
  left: 0; right: 0;
  z-index: 8;
  padding: 5px;
  border-radius: var(--r-sm);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 0.5px solid var(--glass-edge);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-hi);
  /* Long clusters scroll inside the card instead of running off the track. */
  max-height: 260px;
  overflow-y: auto;
}
.vtl-pop-head {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
  padding: 4px 6px 6px;
}
.vtl-pop-row {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 5px;
  border-radius: 8px;
  text-align: left;
  cursor: pointer;
}
.vtl-pop-row + .vtl-pop-row { margin-top: 2px; }
.vtl-pop-row:active { background: var(--fill); }
.vtl-pop-thumb {
  width: 52px;
  height: 34px;
  flex: 0 0 52px;
  object-fit: cover;
  border-radius: 5px;
  background: #000;
}
.vtl-pop-thumb.empty { background: var(--card-2); }
.vtl-pop-text { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.vtl-pop-title {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  font-weight: 650;
  text-transform: capitalize;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.vtl-pop-title svg { width: 14px; height: 14px; flex: 0 0 14px; }
.vtl-pop-row.alert .vtl-pop-title svg { color: var(--alert); }
.vtl-pop-meta {
  font-size: 11px;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.vtl-pop-star { margin-left: auto; color: var(--detect); }
.vtl-pop-star svg { width: 14px; height: 14px; }


.vtl-empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--text-3);
  font-size: 13.5px;
  text-align: center;
  padding: 24px;
  pointer-events: none;
}

/* ------------------------------------------------ section head ---- */

.seg {
  display: flex;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 2px;
  gap: 2px;
}
.seg button {
  padding: 6px 13px;
  border-radius: 999px;
  color: var(--text-2);
  font-size: 12.5px;
  font-weight: 650;
  min-height: 30px;
}
.seg button[aria-pressed="true"] { background: var(--card-3); color: var(--text); }
.seg.sm button { padding: 5px 11px; font-size: 12px; min-height: 28px; }

/* -------------------------------------------------- filters ------- */

.filter-rail {
  display: flex;
  gap: 8px;
  padding: 12px 0 6px;
  overflow-x: auto;
  scrollbar-width: none;
}
.filter-rail::-webkit-scrollbar { display: none; }

.pill-select {
  flex: 0 0 auto;
  border-radius: 999px !important;
  appearance: none;
  -webkit-appearance: none;
  background: var(--card) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239aa4b2' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat right 10px center;
  background-size: 15px;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--text);
  font-size: 13.5px;
  font-weight: 600;
  padding: 8px 32px 8px 15px;
  min-height: 36px;
}

/* ---------------------------------------------------- feed -------- */

/* Grid items default to min-width:auto, so a row with a fixed-width
   thumbnail and nowrap text refuses to shrink and overflows the column.
   This is what pushed clip rows off the right edge on narrow phones. */
.feed { display: grid; gap: 10px; grid-template-columns: minmax(0, 1fr); }
.feed > * { min-width: 0; }

/* One day's heading plus its tiles. Exists purely to be the sticky heading's
   containing block -- without it every heading pins to top:0 at once and they
   stack. Its children need the same min-width:0 the rule above used to give
   them directly, or a long name can force the grid wider than the screen. */
.day-section { min-width: 0; }
.day-section > * { min-width: 0; }

.day-head {
  font-size: 11px;
  font-weight: 650;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin: 18px 0 8px;
}

/* ---- list rows: used by Saved and Search (History is a grid) ------ */

.item {
  display: flex;
  gap: 12px;
  align-items: center;
  background: var(--card);
  border-radius: var(--r);
  padding: 8px;
  transition: transform 0.32s var(--spring), background 0.15s;
  -webkit-user-select: none;
  user-select: none;
}
.item:active { background: var(--card-2); transform: scale(0.985); }
.item { overflow: hidden; }

/* ---- saved clips ---------------------------------------------------- */

/* A flex spacer. Used by the bulk bar below and, since it was written,
   by the timeline bar -- which has carried the markup for one without the
   rule to make it work, leaving its controls bunched up beside the
   caption instead of ranged right. */
.grow { flex: 1; min-width: 0; }

.saved-tools { padding-top: 12px; }
.search-field.small { min-height: 40px; border-radius: 12px; }
.search-field.small input { font-size: 16px; }   /* under 16px, iOS zooms */

.saved-tools .filter-rail { padding-top: 10px; }
.saved-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 2px 2px;
}
.saved-count {
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* The note is the reason a clip was kept, so it replaces the metadata
   line rather than being squeezed in beside it. */
.item-note {
  font-size: 13px;
  color: var(--text);
  margin-top: 2px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.item-act {
  flex: 0 0 34px;
  width: 34px; height: 34px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: var(--fill);
  color: var(--text-2);
  cursor: pointer;
}
.item-act svg { width: 17px; height: 17px; }
.item-act:active { background: var(--fill-2); }

/* On a tile the note sits over the picture, so it does not follow the
   theme -- the thumbnail is dark in both. */
.shot-note {
  position: absolute;
  left: 8px; right: 8px; top: 8px;
  padding: 3px 7px;
  border-radius: 7px;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--on-media);
  font-size: 11.5px;
  font-weight: 550;
  line-height: 1.25;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  pointer-events: none;
}

/* Footage Frigate has since pruned. The row still works -- the favourite
   keeps its own copy of the metadata -- but it should not look live. */
.item-expired { opacity: 0.55; }
.item-expired .item-thumb, .shot.item-expired { background: var(--card-2); }

/* Selection mode. */
.pick {
  flex: 0 0 22px;
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 1.5px solid var(--line-2);
  position: relative;
}
.shot .pick { position: absolute; right: 8px; top: 8px; background: rgba(0,0,0,0.35); }
.is-chosen .pick { background: var(--brand); border-color: var(--brand); }
.is-chosen .pick::after {
  content: "";
  position: absolute;
  left: 6.5px; top: 3px;
  width: 5px; height: 10px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.item.is-chosen { background: var(--glow); }
.shot.is-chosen { box-shadow: 0 0 0 2.5px var(--brand); }

/* Sits above the tab bar while selecting. */
.bulk-bar {
  position: absolute;
  left: 0; right: 0;
  bottom: calc(var(--tab-h) + var(--safe-b));
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px calc(14px + var(--safe-r)) 10px calc(14px + var(--safe-l));
  background: var(--panel);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  border-top: 0.5px solid var(--line);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
}
.bulk-bar[hidden] { display: none; }
.bulk-bar .pill-btn:disabled { opacity: 0.45; }

/* The note sheet. */
.note-of {
  font-size: 12.5px;
  color: var(--text-3);
  margin-bottom: 8px;
}
.note-field {
  width: 100%;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: var(--card-2);
  color: var(--text);
  font: inherit;
  font-size: 16px;                 /* under 16px, iOS zooms on focus */
  line-height: 1.35;
  padding: 10px 12px;
  resize: vertical;
  -webkit-appearance: none;
  appearance: none;
}
.note-field:focus { outline: none; border-color: var(--brand); }
.item-thumb {
  flex: 0 0 104px;
  width: 104px;
  aspect-ratio: 16 / 10;
  border-radius: 11px;
  overflow: hidden;
  background: #000;
  position: relative;
}
.item-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.item-body { min-width: 0; flex: 1; }
.item-title {
  font-size: 15px;
  font-weight: 650;
  letter-spacing: -0.01em;
  text-transform: capitalize;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.item-sub {
  font-size: 12.5px;
  color: var(--text-2);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.item-when { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pip {
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: 1px;
}
.pip.alert { background: var(--alert); }
.pip.detection { background: var(--detect); }
.user-meta { min-width: 0; }
.user-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- account ------------------------------------------------------ */

.acct-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 26px 0 22px;
}
.acct-avatar {
  width: 76px; height: 76px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 30px;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: #fff;
  background: linear-gradient(160deg, var(--brand), #0060df);
  box-shadow: 0 8px 22px rgba(10, 132, 255, 0.28);
}
.acct-name {
  font-size: 23px;
  font-weight: 700;
  letter-spacing: -0.028em;
}
.acct-role { display: flex; align-items: center; gap: 8px; }
.role-chip {
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--card-2);
  color: var(--text-2);
  font-size: 11.5px;
  font-weight: 650;
}
.role-chip.admin { background: rgba(10, 132, 255, 0.18); color: var(--brand); }
.acct-auth { font-size: 12.5px; color: var(--text-3); }

/* ---- settings index ------------------------------------------------
   Eight groups on one page meant scrolling past most of the settings to
   reach any one of them; this is the list you land on instead. */

.set-search-wrap {
  position: relative;
  margin-bottom: 16px;
}
.set-search-wrap svg {
  position: absolute;
  left: 11px; top: 50%;
  transform: translateY(-50%);
  width: 17px; height: 17px;
  color: var(--text-3);
  pointer-events: none;
  /* The input carries a backdrop-filter, which makes it its own composited
     stacking context -- enough for WebKit to paint this positioned sibling
     underneath it and swallow the icon. An explicit layer pins it on top. */
  z-index: 2;
}
.set-search {
  width: 100%;
  height: 40px;
  padding: 0 12px 0 36px;
  border-radius: 11px;
  border: 0.5px solid var(--line);
  background: var(--card);
  color: var(--text);
  font-size: 16px;              /* 16px or iOS zooms the page on focus */
  -webkit-appearance: none;
  appearance: none;
}
.set-search::placeholder { color: var(--text-3); }
.set-search:focus { outline: none; border-color: var(--brand); }
.set-search::-webkit-search-cancel-button { -webkit-appearance: none; }

.set-groups {
  border-radius: var(--r);
  overflow: hidden;
  background: var(--card);
}
.set-link {
  display: flex;
  align-items: center;
  gap: 13px;
  width: 100%;
  padding: 13px 14px;
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
  color: var(--text);
}
.set-link + .set-link { border-top: 0.5px solid var(--line); }
.set-link:active { background: var(--fill); }
.set-icon {
  flex: 0 0 30px;
  width: 30px; height: 30px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  background: var(--fill);
  color: var(--brand);
}
.set-icon svg { width: 17px; height: 17px; }
.set-link-text { flex: 1; min-width: 0; }
.set-link-title {
  display: block;
  font-size: 15.5px;
  letter-spacing: -0.01em;
}
.set-link-hint {
  display: block;
  margin-top: 1px;
  font-size: 12.5px;
  line-height: 1.3;
  color: var(--text-2);
}
.set-chev { flex: 0 0 auto; color: var(--text-3); display: grid; place-items: center; }
.set-chev svg { width: 17px; height: 17px; }

/* Back out of a group, for anyone not reaching for the appbar arrow. */
.set-back {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-bottom: 14px;
  padding: 6px 12px 6px 8px;
  border: 0;
  border-radius: 999px;
  background: var(--fill);
  color: var(--brand);
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
}
.set-back svg { width: 16px; height: 16px; }
.set-back:active { background: var(--fill-2); }

/* Search results: the real rows, under the name of the group they live in. */
.set-result-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  margin: 16px 0 6px;
  padding: 0 15px;
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
}
.set-result-head:first-child { margin-top: 4px; }
.set-result-head i {
  font-style: normal;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--brand);
}
.set-result { margin-bottom: 2px; }

.acct-card { margin-bottom: 26px; }
/* Group caption, inset to line up with the row text below it. */
.acct-card-head {
  font-size: 13px;
  font-weight: 650;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-3);
  margin: 0 0 8px 15px;
}
/* Grouped-list look: one rounded container, hairlines between rows. */
.acct-rows {
  background: var(--card);
  border-radius: var(--r);
  overflow: hidden;
}
.acct-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 15px;
  min-height: 56px;
}
/* Hairlines start where the text does rather than cutting the full width,
   so the rows read as one list instead of a stack of boxes. */
.acct-row + .acct-row::after {
  content: "";
  position: absolute;
  left: 15px; right: 0; top: 0;
  border-top: 0.5px solid var(--line);
  /* Painted after the row's children, so without this the hairline would
     sit over the transparent <select> and swallow taps along its line. */
  pointer-events: none;
}
/* Rows that open something on tap say so by responding to the tap. */
.acct-row.is-tappable { cursor: pointer; }
.acct-row.is-tappable:active { background: var(--fill); }
/* A row whose control is too wide to sit beside its label -- the camera
   checkboxes -- drops under it instead of squeezing both. */
.acct-row.stacked {
  display: block;
  align-items: stretch;
  padding-bottom: 8px;
}

/* Multi-select lists: an inset group with a tick per row, rather than a
   loose column of browser checkboxes. */
.push-cams {
  margin-top: 12px;
  border-radius: 10px;
  overflow: hidden;
  background: var(--fill);
}
.push-cams > * { min-width: 0; }
.push-cam {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px;
  font-size: 14.5px;
  color: var(--text);
  cursor: pointer;
}
.push-cam + .push-cam { border-top: 0.5px solid var(--line); }
.push-cam:active { background: var(--fill-2); }
/* The tick sits at the end of the row, where the eye can run down the
   column of them; the label takes the space that leaves. */
.push-cam input {
  order: 2;
  flex: 0 0 22px;
  width: 22px;
  height: 22px;
  margin: 0;
  position: relative;
  border-radius: 50%;
  border: 1.5px solid var(--line-2);
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  transition: background 0.15s var(--ease), border-color 0.15s var(--ease);
}
.push-cam input:checked { background: var(--brand); border-color: var(--brand); }
/* White on --brand, which is blue in both themes -- the same reasoning as
   the label on a brand button. */
.push-cam input:checked::after {
  content: "";
  position: absolute;
  left: 6.5px; top: 3px;
  width: 5px; height: 10px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.push-cam input:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.push-cam span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
/* Two-line checkbox: used where the option needs a word of explanation
   (the timeline layers). Camera lists pass no second line and keep the
   single-line form. */
.push-cam:has(.opt-two) { align-items: flex-start; }
.push-cam:has(.opt-two) input { margin-top: 1px; }
.push-cam .opt-two { display: block; }
.push-cam .opt-two b { font-weight: 500; color: var(--text); }
.push-cam .opt-sub {
  display: block;
  margin-top: 1px;
  font-size: 12px;
  font-style: normal;
  line-height: 1.3;
  color: var(--text-3);
}
.acct-row-text { flex: 1; min-width: 0; }
.acct-row-label {
  font-size: 15.5px;
  letter-spacing: -0.01em;
  color: var(--text);
}
/* --text-2, not --text-3: these carry the actual explanation of what a
   setting does, and at 2.6:1 in the dark theme the fainter one was barely
   legible on the screen it matters most on. */
.acct-row-hint {
  font-size: 12.5px;
  color: var(--text-2);
  margin-top: 2px;
  line-height: 1.32;
}
.acct-row > .switch { flex: 0 0 auto; }

/* The chosen value, beside a chevron. The <select> itself is transparent
   and covers the whole row: iOS has no way to open a picker other than
   through a real <select>, and stretching it makes the row the tap target
   the way it reads. The old boxed control also capped itself at 48% of the
   row and clipped its own longer options. */
.acct-value {
  display: flex;
  align-items: center;
  gap: 1px;
  margin-left: auto;
  min-width: 0;
  max-width: 62%;
}
.acct-value-text {
  font-size: 15px;
  color: var(--text-2);
  text-align: right;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.acct-value > svg {
  flex: 0 0 16px;
  width: 16px; height: 16px;
  color: var(--text-3);
  margin-right: -4px;
}
/* Keyboard focus lands on an invisible control, so the visible half has to
   show it. */
.acct-value:focus-within .acct-value-text { color: var(--brand); }

.acct-select {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  border: 0;
  padding: 0;
  margin: 0;
  background: none;
  color: inherit;
  font-size: 16px;          /* never rendered, but keeps iOS from zooming */
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
}

/* Read-only values match the editable ones: a row should not change size
   or weight depending on whether it happens to be settable. */
.acct-note { font-size: 15px; color: var(--text-2); flex: 0 0 auto; }
.acct-note.mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
}
.pill-btn.danger-text { color: var(--alert); text-decoration: none; }

/* ---- pull to refresh ---------------------------------------------- */

/* Sits in the scroller's flow at the top and is pulled down from under
   the app bar, so it scrolls away naturally when the list moves. */
/* Only while springing back or holding at the trigger. Absent during the drag
   itself, so the content tracks the finger 1:1 instead of lagging behind it. */
.scroller.ptr-settling { transition: transform 0.28s var(--spring); }

/* A zero-height flex item directly above the scroller, so it takes no space
   in the normal layout and needs no magic offset to line up with the top of
   the content -- flex has already put it exactly there. The spinner hangs
   below its own box into the gap the pull opens. `.screen` does not clip, so
   unlike inside the scroller there is nothing here to cut it off. */
.ptr {
  flex: 0 0 auto;
  height: 0;
  position: relative;
  display: flex;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  z-index: 3;
}
/* ---- the face -----------------------------------------------------
   Two eyes and a mouth rather than a bare ring. Relative, not absolute: it
   stays a flex item so `justify-content: center` still centres it, and it
   overflows its zero-height parent downward into the gap the pull opens. */
.ptr > .ptr-face {
  position: relative;
  top: 10px;
  width: 34px;
  height: 26px;
}

.ptr-eye {
  position: absolute;
  top: 2px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--text-3);
  /* So the catchlight below is cut off as the lid closes, rather than
     floating over a shut eye. */
  overflow: hidden;
  /* Squashes from the top, the way an eyelid closes -- scaling about the
     centre would make it shrink to a dot instead of shutting. */
  transform-origin: 50% 0;
  transition: background 0.2s var(--ease), height 0.2s var(--spring);
}
.ptr-eye.left { left: 7px; }
.ptr-eye.right { right: 7px; }

/* A smile: the bottom half of a ring, so it curves the right way. Drawn with
   borders rather than an SVG arc to keep it a single element that the spin
   below can rotate as one piece. */
.ptr-mouth {
  position: absolute;
  left: 50%;
  bottom: 3px;
  width: 20px;
  height: 10px;
  margin-left: -10px;
  border: 2px solid var(--text-3);
  border-top: 0;
  border-radius: 0 0 20px 20px;
  transition: border-color 0.2s var(--ease), width 0.2s var(--spring);
}

/* Armed: the face lights up and grins wider. This is the "let go" state. */
.ptr.ready .ptr-eye { background: var(--brand); }
.ptr.ready .ptr-mouth {
  border-color: var(--brand);
  width: 24px;
  margin-left: -12px;
}

/* Working: the mouth becomes the spinner, orbiting the face. Eyes hold open
   so it still reads as a face rather than as a loose arc. */
.ptr.spin .ptr-eye { background: var(--brand); }
.ptr.spin .ptr-mouth {
  border-color: var(--brand);
  /* About the face's centre, not the mouth's own, so it sweeps a circle. */
  transform-origin: 50% -8px;
  animation: ptr-turn 0.7s linear infinite;
}

/* ---- the wink -----------------------------------------------------
   One eye only. Both would be a blink; a wink is half the point.
   Fired once halfway down and once the moment it arms, so the pull
   acknowledges you and then says "let go" without a word of text. */
@keyframes ptr-wink {
  0%, 100% { height: 5px; }
  40%, 55% { height: 1px; }
}
.ptr-face.wink .ptr-eye.right { animation: ptr-wink 260ms var(--ease); }

/* The commit wink: shuts fast, then HOLDS shut before opening. The drag's
   260ms blink is over before you have registered it, and this is the beat
   that actually means the refresh fired. */
@keyframes ptr-wink-long {
  0%        { height: 5px; }
  16%, 68%  { height: 1px; }
  100%      { height: 5px; }
}
.ptr-face.wink-long .ptr-eye.right { animation: ptr-wink-long 520ms var(--ease); }

/* ---- twinkle ------------------------------------------------------
   A catchlight in each eye that breathes rather than blinks. Deliberately
   low contrast and slow: it should register as the face being alive, not as
   a second thing demanding attention next to the wink. */
.ptr-eye::after {
  content: "";
  position: absolute;
  top: 0.5px;
  left: 0.5px;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: #fff;
  opacity: 0;
  transition: opacity 0.25s var(--ease);
}
/* Only once it is armed or committing -- twinkling through the whole drag
   would make the arming itself less noticeable, which is the one moment
   that has to land. */
.ptr.ready .ptr-eye::after,
.ptr.spin .ptr-eye::after,
.ptr-face.wink-long .ptr-eye::after {
  animation: ptr-twinkle 1.5s ease-in-out infinite;
}
@keyframes ptr-twinkle {
  0%, 100% { opacity: 0.2; transform: scale(0.75); }
  50%      { opacity: 0.8; transform: scale(1.15); }
}

/* Once the mouth is actually turning, a wink reads as a stutter. */
.ptr.spin .ptr-face.wink .ptr-eye.right { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .ptr-face.wink .ptr-eye.right,
  .ptr-face.wink-long .ptr-eye.right { animation: none; }
  /* The catchlight stays, it just stops breathing. */
  .ptr.ready .ptr-eye::after,
  .ptr.spin .ptr-eye::after,
  .ptr-face.wink-long .ptr-eye::after { animation: none; opacity: 0.6; }
}
@keyframes ptr-turn { to { transform: rotate(360deg); } }

/* The indicator is absolutely positioned, so its scroller must contain it. */
.screen > .scroller { position: relative; }

/* ---- history: a wall of moments ---------------------------------- */

/* Day heading: a real title with the count beside it, iOS-style. */
.day-head {
  position: sticky;
  top: 0;
  z-index: 4;
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.026em;
  color: var(--text);
  margin: 0 0 12px;
  padding: 20px 0 10px;
  /* No fill at all. Being sticky, this used to mask the rows sliding under
     it -- first with an opaque --bg band, then with glass -- but any fill
     reads as a panel pasted onto the wallpaper, which is exactly what looked
     wrong. The halo below does the legibility work instead, so the gradient
     runs unbroken behind the text. */
  background: transparent;
  /* Tinted with --bg rather than black, so it stays a soft halo in light
     theme instead of a drop shadow. */
  text-shadow: 0 1px 3px var(--bg), 0 0 12px var(--bg), 0 0 22px var(--bg);
}
.day-count {
  font-size: 15px;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

/* Two up on a phone. Three small tiles could not show a face; two can.
   16:9 matches the cameras, so nothing is cropped away. */
.shot-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin-bottom: 6px;
}
@media (min-width: 520px) { .shot-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 760px) { .shot-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* Tiles per row (Account > History).

   Written as a descendant of the feed's attribute so it outranks the two
   breakpoints above; a chosen count is a choice on every screen size, not
   a phone-only one. */
.feed[data-cols="1"] .shot-grid { grid-template-columns: minmax(0, 1fr); }
.feed[data-cols="2"] .shot-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.feed[data-cols="3"] .shot-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* One tile per row is nearly full width, so the time and the camera name
   can afford to sit where they are legible rather than shrink. */
.feed[data-cols="1"] .shot-time { font-size: 15px; }
.feed[data-cols="3"] .shot-time { font-size: 12px; }

/* Results per row on the search screen. The container is the grid itself
   there (one flat list, no day headings), so the attribute sits on it
   rather than on a parent -- either way it outranks the breakpoints. */
.shot-grid[data-cols="1"] { grid-template-columns: minmax(0, 1fr); }
.shot-grid[data-cols="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.shot-grid[data-cols="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* The detailed list. Rows are the same .item used by Saved and search, so
   this only has to stack them. */
.shot-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 8px;
  margin-bottom: 6px;
}
.shot-list > * { min-width: 0; }
@media (min-width: 760px) {
  .shot-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.shot {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--card);
  border-radius: var(--r);
  overflow: hidden;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
  transition: transform 0.34s var(--spring);
}
.shot img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.shot.no-img img { opacity: 0; }
.shot.no-img { background: var(--card-2); }
.shot:active { transform: scale(0.955); }
.shot:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }

/* One gradient, bottom only -- the timestamp is the sole thing that has
   to stay legible over the picture. */
.shot-scrim {
  position: absolute;
  inset: auto 0 0 0;
  height: 52%;
  background: linear-gradient(to top, rgba(0,0,0,0.72), transparent);
  pointer-events: none;
}

.shot-time {
  position: absolute;
  left: 10px; bottom: 8px;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: #fff;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
  pointer-events: none;
}
.shot-time i {
  font-style: normal;
  font-size: 10px;
  font-weight: 500;
  opacity: 0.72;
  margin-left: -2px;
}

/* Severity as a small dot beside the time, not a bar across the frame. */
.shot-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--detect);
  flex: 0 0 6px;
}
.shot.alert .shot-dot { background: var(--alert); }

/* A recognised face is the single most useful thing we can surface. */
.shot-face {
  position: absolute;
  left: 8px; top: 8px;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--on-media);
  max-width: calc(100% - 16px);
  padding: 3px 9px 3px 3px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.42);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 0.5px solid var(--fill-2);
  color: #fff;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: -0.01em;
  pointer-events: none;
}
.shot-face img {
  width: 18px; height: 18px;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 18px;
}
.shot-face span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.shot-fav {
  position: absolute;
  right: 9px; top: 9px;
  width: 15px; height: 15px;
  color: var(--detect);
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6));
  pointer-events: none;
}
.shot-fav svg { width: 15px; height: 15px; fill: var(--detect); stroke: none; }

/* Camera name, bottom right, quiet. Only worth it across cameras. */
.shot-cam {
  position: absolute;
  right: 10px; bottom: 9px;
  font-size: 11px;
  font-weight: 500;
  color: rgba(255, 255, 255, 0.72);
  text-transform: capitalize;
  letter-spacing: -0.005em;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
  max-width: 50%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  pointer-events: none;
}

.empty {
  padding: 44px 20px;
  text-align: center;
  color: var(--text-3);
  font-size: 14px;
}
.empty svg { width: 34px; height: 34px; margin-bottom: 10px; opacity: 0.5; }

.skeleton {
  background: linear-gradient(100deg, var(--card) 30%, var(--card-2) 50%, var(--card) 70%);
  background-size: 220% 100%;
  animation: shimmer 1.3s infinite linear;
  border-radius: var(--r);
}
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -60% 0; } }

/* ---------------------------------------------------- sheet ------- */

.sheet-scrim {
  position: fixed;
  inset: 0;
  /* Lighter than it was: the sheet above is translucent now, and a heavy
     scrim behind it turned the glass into murky grey rather than something
     you can see through. The blur does the separating instead. */
  background: rgba(0, 0, 0, 0.32);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  z-index: 100;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s var(--ease);
}
.sheet-scrim.open { opacity: 1; pointer-events: auto; }

.sheet {
  width: 100%;
  max-width: 620px;
  max-height: 92vh;      /* fallback for iOS < 15.4 */
  max-height: 92dvh;
  /* Same material as the tab bar and back chip, so the floating layer reads
     as one surface. The scrim behind it is what it refracts. */
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  border: 0.5px solid var(--glass-edge);
  border-bottom: none;
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-hi);
  display: flex;
  flex-direction: column;
  transform: translateY(100%);
  transition: transform 0.3s var(--ease);
  padding-bottom: var(--safe-b);
}
.sheet-scrim.open .sheet { transform: translateY(0); }
.sheet.compact { padding-bottom: calc(12px + var(--safe-b)); }

.sheet-grab {
  width: 38px; height: 4px;
  border-radius: 2px;
  background: var(--card-3);
  margin: 9px auto 2px;
  flex: 0 0 auto;
}

/* ----------------------------------------------------- menu ------- */

.menu-user {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 18px 16px;
}
.avatar {
  width: 42px; height: 42px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--brand), #7b5cff);
  display: grid;
  place-items: center;
  font-weight: 700;
  font-size: 17px;
  color: #fff;
  flex: 0 0 42px;
}
.menu-name { font-weight: 700; font-size: 16px; }
.menu-sub { font-size: 12.5px; color: var(--text-3); }

.menu-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 15px 18px;
  color: var(--text);
  text-decoration: none;
  border-top: 1px solid var(--line);
  font-size: 15.5px;
  font-weight: 600;
  min-height: 52px;
}
.menu-item:active { background: var(--card); }
.menu-item.danger { color: var(--danger-text); }

/* ---------------------------------------------------- system ------ */

.stat-grid {
  display: grid;
  /* auto-fill needs a real minimum: with minmax(0, 1fr) the browser
     cannot work out how many tracks fit and the tiles collapse. Overflow
     is handled by min-width on the children instead. */
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 10px;
  padding-top: 14px;
}
.stat-grid > * { min-width: 0; }
.stat {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 13px 15px;
}
.stat-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-3);
  font-weight: 650;
}
.stat-value {
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -0.03em;
  margin-top: 3px;
  font-variant-numeric: tabular-nums;
}
.stat-sub { font-size: 12px; color: var(--text-2); }

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
  margin-top: 14px;
}
.card-head {
  padding: 13px 15px;
  font-weight: 700;
  font-size: 15px;
  letter-spacing: -0.01em;
  border-bottom: 1px solid var(--line);
}
.card-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
th, td { padding: 11px 15px; text-align: left; white-space: nowrap; }
th {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-3);
  font-weight: 700;
  border-bottom: 1px solid var(--line);
}
td { border-bottom: 1px solid var(--line); font-variant-numeric: tabular-nums; }
tr:last-child td { border-bottom: none; }
.td-name { font-weight: 650; text-transform: capitalize; }

.bar {
  height: 5px;
  background: var(--card-3);
  border-radius: 3px;
  overflow: hidden;
  margin-top: 5px;
  min-width: 80px;
}
.bar > i { display: block; height: 100%; background: var(--brand); border-radius: 3px; }

/* ---------------------------------------------------- login ------- */

.login-body {
  display: grid;
  place-items: center;
  min-height: 100vh;     /* fallback for iOS < 15.4 */
  min-height: 100dvh;
  padding: calc(20px + var(--safe-t)) 20px calc(20px + var(--safe-b));
  background:
    radial-gradient(900px 460px at 50% -5%, var(--glow) 0%, transparent 62%),
    var(--bg);
}

.login-box {
  width: min(370px, 100%);
  display: flex;
  flex-direction: column;
  gap: 11px;
}

.login-brand {
  display: grid;
  place-items: center;
  gap: 12px;
  margin-bottom: 14px;
}
.login-badge {
  width: 62px; height: 62px;
  border-radius: 19px;
  background: linear-gradient(150deg, var(--brand), var(--brand-2));
  display: grid;
  place-items: center;
  box-shadow: 0 10px 30px rgba(47, 155, 239, 0.32);
}
.login-badge svg { width: 32px; height: 32px; color: #fff; stroke-width: 2.1; }

.login-box h1 {
  font-size: 25px;
  font-weight: 700;
  letter-spacing: -0.03em;
  margin: 0;
  text-align: center;
}
.login-sub {
  text-align: center;
  color: var(--text-3);
  font-size: 14px;
  margin: 2px 0 6px;
}

.login-box input {
  width: 100%;
  padding: 15px 16px;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--text);
  font-size: 16px;           /* 16px stops iOS zooming the page on focus */
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.login-box input:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 4px rgba(47, 155, 239, 0.14);
}
.login-box input::placeholder { color: var(--text-3); }

.login-box button {
  margin-top: 4px;
  padding: 15px;
  border-radius: 14px;
  background: var(--brand);
  color: #fff;
  font-size: 16px;
  font-weight: 700;
  min-height: 52px;
}
.login-box button:active { background: var(--brand-2); transform: scale(0.99); }

.error {
  color: var(--danger-text);
  background: rgba(255, 77, 79, 0.1);
  border: 1px solid rgba(255, 77, 79, 0.25);
  border-radius: 12px;
  padding: 11px 13px;
  font-size: 13.5px;
  margin: 0;
  text-align: center;
}

/* ============================================================
   Custom video player
   ============================================================ */

/* ============================================================
   Timeline extras: day rules and the overview strip
   ============================================================ */

.vtl-daymark {
  position: absolute;
  left: calc(-52px - var(--safe-l));
  right: 0;
  border-top: 1.5px dashed var(--line-2);
  pointer-events: none;
}
.vtl-daymark span {
  position: absolute;
  right: 4px; top: -9px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
  background: var(--bg-2);
  padding: 0 6px;
  border-radius: 4px;
}

/* ============================================================
   Calendar
   ============================================================ */

.cal { padding: 4px 4px 12px; }
.cal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.cal-title { font-size: 15.5px; font-weight: 700; }
.cal-nav { display: flex; gap: 6px; }
.cal-nav button {
  width: 34px; height: 34px;
  border-radius: 50%;
  background: var(--card);
  border: 1px solid var(--line);
  color: var(--text-2);
  display: grid; place-items: center;
}
.cal-nav button:active { background: var(--card-2); }
.cal-nav svg { width: 16px; height: 16px; }

.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
}
.cal-dow {
  text-align: center;
  font-size: 10.5px;
  font-weight: 700;
  color: var(--text-3);
  text-transform: uppercase;
  padding-bottom: 4px;
}
.cal-day {
  aspect-ratio: 1;
  border-radius: 11px;
  background: var(--card);
  border: 1px solid transparent;
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-height: 40px;
  position: relative;
}
.cal-day.blank { background: none; pointer-events: none; }
.cal-day.muted { color: var(--text-3); }
.cal-day:active { background: var(--card-3); }
.cal-day.today { border-color: var(--line-2); }
.cal-day.selected { background: var(--brand); color: #fff; }
.cal-day .dots { display: flex; gap: 2px; height: 4px; }
.cal-day .dots i { width: 4px; height: 4px; border-radius: 50%; display: block; }
.cal-day .dots i.a { background: var(--alert); }
.cal-day .dots i.d { background: var(--detect); }
.cal-day.selected .dots i { background: rgba(255,255,255,0.9); }

.cal-actions { display: flex; gap: 8px; margin-top: 12px; }
.cal-actions button { flex: 1; }

/* ============================================================
   Admin
   ============================================================ */

.admin-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 18px 0 10px;
}
.admin-head h2 { font-size: 17px; font-weight: 700; letter-spacing: -0.02em; margin: 0; }

.user-list { display: grid; gap: 9px; grid-template-columns: minmax(0, 1fr); }
.user-list > * { min-width: 0; }

.user-row {
  display: flex;
  align-items: center;
  gap: 11px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 11px 13px;
}
.user-av {
  width: 36px; height: 36px;
  border-radius: 50%;
  display: grid; place-items: center;
  font-weight: 700;
  font-size: 15px;
  color: #fff;
  flex: 0 0 36px;
  background: linear-gradient(135deg, #3c7fd0, #6b5ce0);
}
.user-av.admin { background: linear-gradient(135deg, #e0803c, #d05c5c); }
.user-meta { flex: 1; min-width: 0; }
.user-name { font-weight: 650; font-size: 15px; }
.user-sub { font-size: 12px; color: var(--text-3); margin-top: 1px; }
.role-tag {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--card-3);
  color: var(--text-2);
}
.role-tag.admin { background: rgba(224, 128, 60, 0.18); color: #e8a366; }
.user-actions { display: flex; gap: 6px; }

.settings-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 15px;
  display: grid;
  gap: 13px;
}

.field { display: grid; gap: 6px; }
.field > span {
  font-size: 12.5px;
  font-weight: 650;
  color: var(--text-2);
}
.field > span em {
  display: block;
  font-style: normal;
  font-weight: 500;
  font-size: 11.5px;
  color: var(--text-3);
  margin-top: 1px;
}
.field input, .field select {
  width: 100%;
  padding: 12px 13px;
  border-radius: 11px;
  border: 1px solid var(--line);
  background: var(--bg-2);
  color: var(--text);
  font-size: 16px;      /* 16px stops iOS zooming on focus */
  outline: none;
}
.field input:focus, .field select:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px rgba(47,155,239,0.15);
}

.btn-wide {
  width: 100%;
  padding: 13px;
  border-radius: 12px;
  background: var(--brand);
  color: #fff;
  font-size: 15px;
  font-weight: 700;
  min-height: 48px;
}
.btn-wide:active { background: var(--brand-2); }
.btn-wide.danger { background: #c0392f; }
.btn-wide.ghost {
  background: var(--card-2);
  color: var(--text);
  border: 1px solid var(--line);
}

.form-msg { margin: 0; font-size: 13px; text-align: center; min-height: 18px; }
.form-msg.ok { color: var(--ok); }
.form-msg.bad { color: var(--danger-text); }

/* The enrolment bundle is fetched by link, so its download control is an
   <a>. Everything above assumes a <button>, which is a block-level box
   with centred text -- an anchor is neither. */
a.btn-wide {
  display: grid;
  place-items: center;
  box-sizing: border-box;
  text-decoration: none;
}

/* ------------------------------------------- device enrolment ------- */

.admin-note {
  margin: -4px 0 10px;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--text-3);
}

.user-av.device { background: linear-gradient(135deg, #3f9d78, #2f7f92); }
.user-av.device svg { width: 18px; height: 18px; }
.user-av.off { background: var(--card-3); color: var(--text-3); }

.wiz-lead {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-2);
}

.wiz-qr {
  justify-self: center;
  width: min(232px, 62vw);
  padding: 12px;
  border-radius: var(--r);
  /* The quiet zone has to stay light whatever the surrounding theme is:
     a dark margin round the symbol is what stops a scanner finding it. */
  background: #fff;
}
.wiz-qr svg { display: block; width: 100%; height: auto; }
/* The global `svg` rule above is for this app's line icons: it sets
   stroke: currentColor and stroke-width: 1.9, both of which inherit. A QR
   module is one viewBox unit, so that stroke is nearly two modules wide and
   outlines every module in the text colour -- the symbol disappears into a
   blank square. Undo it for the code's own shapes. */
.wiz-qr svg rect, .wiz-qr svg path { stroke: none; }

.wiz-url {
  margin: 0;
  font-size: 11.5px;
  text-align: center;
  color: var(--text-3);
  /* A URL has no spaces to break at and would otherwise push the sheet
     wider than the screen. */
  overflow-wrap: anywhere;
}

.wiz-secret {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 13px;
  border-radius: var(--r);
  background: var(--bg-2);
  border: 1px solid var(--line);
}
.wiz-secret-label {
  font-size: 12px;
  font-weight: 650;
  color: var(--text-3);
  flex: 0 0 auto;
}
.wiz-secret-value {
  flex: 1;
  min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 14px;
  color: var(--text);
  user-select: all;
  overflow-wrap: anywhere;
}
.tbtn.ok { color: var(--ok); }

.wiz-warn {
  margin: 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-3);
  text-align: center;
}

.wiz-help { font-size: 12.5px; color: var(--text-2); }
.wiz-help summary { cursor: pointer; font-weight: 650; color: var(--text-2); }
.wiz-help ul { margin: 9px 0 0; padding-left: 18px; display: grid; gap: 7px; }
.wiz-help li { line-height: 1.45; color: var(--text-3); }
.wiz-help b { color: var(--text-2); font-weight: 650; }

.modal-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 16px 12px;
}
.modal-title { font-size: 17px; font-weight: 700; letter-spacing: -0.02em; flex: 1; }
.sheet-close {
  width: 32px; height: 32px;
  border-radius: 50%;
  background: var(--card-2);
  color: var(--text-2);
  display: grid; place-items: center;
  flex: 0 0 32px;
}
.sheet-close:active { background: var(--card-3); }
.sheet-close svg { width: 17px; height: 17px; }
.modal-body { overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 0 16px 16px; }

.pill-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: var(--card);
  border: 0.5px solid var(--line);
  border-radius: 999px;
  color: var(--text);
  font-size: 13.5px;
  font-weight: 600;
  padding: 8px 15px;
  min-height: 36px;
}
.pill-btn svg { width: 15px; height: 15px; color: var(--text-2); }
.pill-btn:active { background: var(--card-2); transform: scale(0.94); }
.pill-btn { transition: background 0.15s, transform 0.3s var(--spring); }
.pill-btn.primary {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
  font-weight: 600;
}
.pill-btn.primary svg { color: #fff; }
.pill-btn.primary:active { background: var(--brand-2); }

/* Structural only: heights are set inline by the timeline renderer, so
   these deliberately carry no styling of their own. */
.vtl-content, .vtl-pad { /* intentionally empty */ }

.item-fav {
  margin-left: auto;
  width: 34px; height: 34px;
  flex: 0 0 34px;
  display: grid; place-items: center;
  border-radius: 50%;
  color: var(--text-3);
}
.item-fav svg { width: 18px; height: 18px; }
.item-fav.on { color: var(--detect); }
.item-fav.on svg { fill: var(--detect); stroke: none; }
.item-fav:active { background: var(--card-3); }

.item-expired { opacity: 0.55; }

/* ============================================================
   Timeline: filters, overview map, day header, now button
   ============================================================ */

.chip-rail {
  flex: 0 0 auto;
  display: flex;
  gap: 7px;
  padding: 0 calc(14px + var(--safe-r)) 9px calc(14px + var(--safe-l));
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.chip-rail::-webkit-scrollbar { display: none; }

.chip {
  flex: 0 0 auto;
  padding: 6px 13px;
  border-radius: 999px;
  background: var(--card);
  border: 1px solid var(--line);
  color: var(--text-2);
  font-size: 12.5px;
  font-weight: 650;
  white-space: nowrap;
  min-height: 32px;
}
.chip[aria-pressed="true"] {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}
.chip:active { transform: scale(0.96); }
.chip b {
  margin-left: 6px;
  font-weight: 700;
  font-size: 11px;
  opacity: 0.62;
  font-variant-numeric: tabular-nums;
}
.chip[aria-pressed="true"] b { opacity: 0.85; }
.chip-rail[hidden] { display: none; }

/* Sticky label naming the day you are currently scrolled through. */
.vtl-day {
  position: absolute;
  top: 0; left: 0;
  z-index: 7;
  background: var(--panel);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom-right-radius: 10px;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding: 4px 11px 5px;
  font-size: 11px;
  font-weight: 750;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-2);
  pointer-events: none;
}

/* Whole-range overview strip on the right edge.

   Doubles as a scrollbar and a map: time labels give it an anchor, so a
   drag lands somewhere deliberate instead of somewhere approximate. */
.vtl-map {
  position: absolute;
  top: 0; bottom: 0;
  right: 0;
  width: 44px;
  background: linear-gradient(to left, var(--rail-2), var(--rail));
  backdrop-filter: blur(14px) saturate(150%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  border-left: 0.5px solid var(--line);
  z-index: 6;
  touch-action: none;
  cursor: grab;
}
.vtl-map:active { cursor: grabbing; }

/* Time scale. `i` is the hairline, `b` the label beside it. */
.vtl-map-scale { position: absolute; inset: 0; pointer-events: none; }
.vtl-map-scale i {
  position: absolute;
  left: 0; right: 0;
  height: 0;
  border-top: 0.5px solid var(--line);
}
.vtl-map-scale b {
  position: absolute;
  left: 0; right: 0;
  margin-top: 2px;
  text-align: center;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.vtl-map-ticks { position: absolute; inset: 0; }
.vtl-map-ticks i {
  position: absolute;
  left: 15px; right: 6px;
  min-height: 3px;
  border-radius: 2px;
  background: var(--detect);
}
/* Alerts reach further left so they read as a distinct column. */
.vtl-map-ticks i.alert {
  background: var(--alert);
  left: 6px;
  min-height: 4px;
}

/* The viewport. Deliberately flat -- a glow here bled into the ticks. */
.vtl-map-view {
  position: absolute;
  left: 2px; right: 2px;
  min-height: 18px;
  border-radius: 6px;
  background: var(--fill);
  border: 1.5px solid var(--rail-edge);
  pointer-events: none;
  transition: background 0.15s var(--ease);
}
.vtl-map:active .vtl-map-view {
  background: var(--fill-2);
  border-color: var(--text);
}

.vtl-now {
  position: absolute;
  right: 56px;
  bottom: calc(14px + var(--tabbar-clear));
  z-index: 8;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 8px 15px;
  border-radius: 999px;
  background: var(--brand);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
  min-height: 38px;
}
.vtl-now[hidden] { display: none; }
.vtl-now svg { width: 15px; height: 15px; }
.vtl-now:active { background: var(--brand-2); transform: scale(0.96); }

.vtl-scroll { padding-right: 44px; }
.vtl-hint { bottom: 14px; }

/* Overview strip turned off in Account > Timeline. The strip is absolutely
   positioned, so hiding it alone would leave the track laid out around a
   44px column that is no longer there. */
.vtl.no-map .vtl-scroll { padding-right: 0; }
.vtl.no-map .vtl-now { right: 12px; }

/* Switch + separator for the settings form */
.field.row {
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 12px;
}
.field-sep { height: 1px; background: var(--line); margin: 4px 0; }

.switch {
  appearance: none;
  -webkit-appearance: none;
  width: 50px; height: 30px;
  border-radius: 999px;
  background: var(--card-3);
  position: relative;
  transition: background 0.2s;
  flex: 0 0 50px;
  padding: 0;
  border: none;
}
.switch::after {
  content: "";
  position: absolute;
  top: 3px; left: 3px;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: #fff;
  transition: transform 0.2s var(--ease);
}
.switch:checked { background: var(--brand); }
.switch:checked::after { transform: translateX(20px); }

/* Sign in with Home Assistant */
.or {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text-3);
  font-size: 12px;
  margin: 4px 0;
}
.or::before, .or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

.ha-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 14px;
  border-radius: 14px;
  background: var(--card);
  border: 1px solid var(--line-2);
  color: var(--text);
  font-size: 15px;
  font-weight: 650;
  text-decoration: none;
  min-height: 52px;
}
.ha-btn svg { width: 20px; height: 20px; color: #41bdf5; }
.ha-btn:active { background: var(--card-2); }

.user-tag-ha {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(65, 189, 245, 0.16);
  color: #6fcdf7;
}

/* ============================================================
   Search
   ============================================================ */

.search-bar {
  flex: 0 0 auto;
  padding: 12px calc(14px + var(--safe-r)) 6px calc(14px + var(--safe-l));
}

.search-field {
  display: flex;
  align-items: center;
  gap: 9px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 0 12px;
  min-height: 46px;
}
.search-field:focus-within {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px rgba(47, 155, 239, 0.14);
}
.search-field > svg { width: 18px; height: 18px; color: var(--text-3); flex: 0 0 18px; }

.search-field input {
  flex: 1;
  min-width: 0;
  background: none;
  border: none;
  outline: none;
  color: var(--text);
  font-size: 16px;            /* 16px stops iOS zooming on focus */
  padding: 12px 0;
  -webkit-appearance: none;
}
.search-field input::placeholder { color: var(--text-3); }
.search-field input::-webkit-search-cancel-button { display: none; }

.search-clear {
  width: 26px; height: 26px;
  border-radius: 50%;
  background: var(--card-3);
  color: var(--text-2);
  display: grid; place-items: center;
  flex: 0 0 26px;
}
.search-clear svg { width: 13px; height: 13px; }
.search-clear[hidden] { display: none; }

.suggest {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  padding: 4px 0 12px;
}
.suggest button {
  padding: 7px 13px;
  border-radius: 999px;
  background: var(--card);
  border: 1px solid var(--line);
  color: var(--text-2);
  font-size: 13px;
  font-weight: 600;
  min-height: 34px;
}
.suggest button:active { background: var(--card-2); }

.relevance {
  font-size: 10.5px;
  font-weight: 700;
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(47, 155, 239, 0.16);
  color: #7cc0f7;
  white-space: nowrap;
  flex: 0 0 auto;
}

.search-note {
  font-size: 12.5px;
  color: var(--text-3);
  padding: 2px 0 10px;
}

/* ============================================================
   Live feed loading state
   ============================================================ */

/* No `position` here on purpose: .cam-media and .stage-media are already
   `position: absolute; inset: 0`, and this rule comes later with equal
   specificity -- setting position would override theirs, collapse them to
   zero height and blank the video entirely. */
.feed-host { isolation: isolate; }

/* The media is never hidden. An earlier version faded it in only once a
   "playing" signal arrived, which meant a working stream stayed invisible
   whenever that signal did not fire. The spinner simply sits on top and
   gets out of the way. */
.feed-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.feed-spin {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgba(11, 14, 18, 0.85);
  z-index: 3;
  pointer-events: none;
  transition: opacity 0.3s var(--ease);
}
.feed-spin i {
  width: 26px;
  height: 26px;
  border: 2.5px solid var(--fill-2);
  border-top-color: rgba(255, 255, 255, 0.8);
  border-radius: 50%;
  display: block;
  animation: spin 0.8s linear infinite;
}
.feed-host.is-live .feed-spin { opacity: 0; }

/* ============================================================
   Install prompt
   ============================================================ */

.install-bar {
  position: fixed;
  left: 10px;
  right: 10px;
  bottom: calc(var(--tab-h) + var(--safe-b) + 10px);
  z-index: 90;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 12px;
  border-radius: var(--r);
  background: rgba(22, 26, 32, 0.94);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--line-2);
  box-shadow: var(--shadow);
  animation: installIn 0.4s var(--ease);
  max-width: 560px;
  margin: 0 auto;
}
.install-bar[hidden] { display: none; }

@keyframes installIn {
  from { transform: translateY(120%); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}

.install-icon {
  width: 40px; height: 40px;
  border-radius: 10px;
  flex: 0 0 40px;
  display: block;
}

.install-copy { flex: 1; min-width: 0; }
.install-title {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.install-sub {
  font-size: 12.5px;
  color: var(--text-2);
  margin-top: 1px;
}
.install-sub b { color: var(--text); font-weight: 650; }

/* The share glyph, so the iOS instructions point at the real button. */
.install-sub svg {
  width: 13px; height: 13px;
  vertical-align: -2px;
  margin: 0 1px;
  color: var(--brand);
}

.install-go {
  flex: 0 0 auto;
  padding: 9px 15px;
  border-radius: 999px;
  background: var(--brand);
  color: #fff;
  font-size: 13.5px;
  font-weight: 700;
  min-height: 38px;
}
.install-go:active { background: var(--brand-2); transform: scale(0.96); }
.install-go[hidden] { display: none; }

.install-x {
  flex: 0 0 30px;
  width: 30px; height: 30px;
  border-radius: 50%;
  color: var(--text-3);
  display: grid;
  place-items: center;
}
.install-x svg { width: 15px; height: 15px; }
.install-x:active { background: var(--card-3); }

/* Installed: the app owns the whole screen, so give the tab bar the home
   indicator room iOS reserves. */
@media (display-mode: standalone) {
  .install-bar { display: none !important; }
}

/* ============================================================
   Install gate
   ============================================================ */

.gate {
  position: fixed;
  inset: 0;
  z-index: 500;
  display: grid;
  place-items: center;
  padding: calc(24px + var(--safe-t)) 24px calc(24px + var(--safe-b));
  background:
    radial-gradient(900px 460px at 50% -5%, var(--glow) 0%, transparent 62%),
    var(--bg);
}
.gate[hidden] { display: none; }

.gate-inner {
  width: min(360px, 100%);
  text-align: center;
  display: grid;
  gap: 12px;
  justify-items: center;
}
.gate-icon {
  width: 74px; height: 74px;
  border-radius: 19px;
  box-shadow: 0 10px 30px rgba(47, 155, 239, 0.28);
}
.gate h1 {
  font-size: 23px;
  font-weight: 700;
  letter-spacing: -0.03em;
  margin: 6px 0 0;
}
.gate-sub {
  color: var(--text-2);
  font-size: 14.5px;
  line-height: 1.5;
  margin: 0 0 6px;
}
.gate-sub b { color: var(--text); font-weight: 650; }
.gate-sub svg {
  width: 15px; height: 15px;
  vertical-align: -3px;
  margin: 0 2px;
  color: var(--brand);
}
.gate .btn-wide { width: 100%; }
.gate-skip {
  color: var(--text-3);
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  padding: 8px;
  background: none;
  border: none;
}
.gate-skip:active { color: var(--text-2); }

/* ============================================================
   People + deep search
   ============================================================ */

.people-strip {
  display: flex;
  gap: 14px;
  padding: 14px 0 6px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.people-strip::-webkit-scrollbar { display: none; }
.people-strip[hidden] { display: none; }

.person {
  flex: 0 0 auto;
  display: grid;
  justify-items: center;
  gap: 6px;
  width: 64px;
  transition: transform 0.3s var(--spring);
}
.person:active { transform: scale(0.92); }

.person-face {
  width: 58px; height: 58px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--card-2);
  box-shadow: 0 0 0 2px transparent;
  transition: box-shadow 0.18s;
}
.person[aria-pressed="true"] .person-face {
  box-shadow: 0 0 0 2.5px var(--brand);
}
.person-none {
  width: 58px; height: 58px;
  border-radius: 50%;
  background: var(--card-2);
  display: grid;
  place-items: center;
  color: var(--text-3);
}
.person-none svg { width: 26px; height: 26px; }

.person-name {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--text-2);
  max-width: 64px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.person[aria-pressed="true"] .person-name { color: var(--text); font-weight: 600; }
.person-count { font-size: 11px; color: var(--text-3); }

.active-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 4px 0 10px;
}
.active-filters[hidden] { display: none; }

.fchip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 6px 5px 11px;
  border-radius: 999px;
  background: rgba(10, 132, 255, 0.16);
  color: #6cb6ff;
  font-size: 12.5px;
  font-weight: 550;
  letter-spacing: -0.01em;
}
.fchip b { font-weight: 650; color: var(--accent-text); text-transform: capitalize; }
.fchip button {
  width: 17px; height: 17px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: inherit;
  background: var(--fill);
}
.fchip button svg { width: 10px; height: 10px; stroke-width: 2.6; }
.fchip button:active { background: rgba(255, 255, 255, 0.28); }

.fchip.clear {
  background: var(--card-2);
  color: var(--text-2);
  padding: 5px 12px;
  font-weight: 550;
}

.search-note {
  font-size: 13px;
  color: var(--text-3);
  padding: 2px 0 12px;
  letter-spacing: -0.01em;
}

/* How closely a result matched the words typed. Only ever drawn beside a
   title in the list view, where there is room for it. */
.item-score {
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--fill);
  color: var(--text-2);
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}

/* ============================================================
   Desktop
   Everything below is gated on a breakpoint. Nothing here applies at
   phone widths, so the mobile/PWA layout is unchanged by its presence.
   The two structural pieces -- .sidebar and .tl-panel -- are inert on
   mobile: the sidebar is display:none, and .tl-panel is display:contents
   so its children lay out exactly as if it were not there.
   ============================================================ */

.sidebar { display: none; }
.tl-panel { display: contents; }

@media (min-width: 900px) {

  /* ---- shell: fixed sidebar, everything else to its right ---- */
  .app {
    --side-w: 228px;
    --tl-w: 340px;
    display: grid;
    grid-template-columns: var(--side-w) minmax(0, 1fr);
    /* One row now: the app bar that occupied "bar" is gone, and an empty
       auto row would still reserve its border and background. */
    grid-template-rows: minmax(0, 1fr);
    grid-template-areas: "side screens";
  }

  .sidebar {
    grid-area: side;
    display: flex;
    flex-direction: column;
    min-height: 0;
    background: var(--bg-2);
    border-right: 1px solid var(--line);
    padding: 0 0 10px;
  }
  .screens { grid-area: screens; }
  .tabbar { display: none; }

  .side-brand {
    display: flex;
    align-items: center;
    gap: 9px;
    height: calc(var(--bar-h) + var(--safe-t));
    padding: var(--safe-t) 16px 0;
    font-size: 14.5px;
    font-weight: 650;
    letter-spacing: -0.01em;
    border-bottom: 1px solid var(--line);
    flex: 0 0 auto;
  }
  .side-brand svg { width: 19px; height: 19px; color: var(--brand); flex: 0 0 19px; }
  .side-brand span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  .side-nav { padding: 10px 8px; display: grid; gap: 1px; flex: 0 0 auto; }
  .side-nav button {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 10px;
    height: 36px;
    border-radius: var(--r-sm);
    color: var(--text-2);
    font-size: 13.5px;
    font-weight: 550;
    text-align: left;
    transition: background 0.12s, color 0.12s;
  }
  .side-nav button svg { width: 17px; height: 17px; flex: 0 0 17px; }
  .side-nav button:hover { background: var(--card); color: var(--text); }
  .side-nav button[aria-selected="true"] {
    background: var(--brand);
    color: #fff;
  }

  /* Jump straight to any camera without going via the wall. */
  .side-cams {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 6px 8px;
    border-top: 1px solid var(--line);
    display: grid;
    gap: 1px;
    align-content: start;
  }
  .side-cams-head {
    font-size: 10px;
    font-weight: 650;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--text-3);
    padding: 8px 10px 4px;
  }
  .side-cam {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 0 10px;
    height: 32px;
    border-radius: var(--r-sm);
    color: var(--text-2);
    font-size: 13px;
    font-weight: 500;
    text-align: left;
    transition: background 0.12s, color 0.12s;
  }
  .side-cam:hover { background: var(--card); color: var(--text); }
  .side-cam[aria-selected="true"] { background: var(--card-2); color: var(--text); }
  .side-cam i {
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--ok);
    flex: 0 0 6px;
  }
  .side-cam i.off { background: var(--text-3); }
  .side-cam span {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  .side-user {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 9px;
    margin: 0 8px;
    padding: 0 8px;
    height: 44px;
    border-radius: var(--r-sm);
    border-top: none;
    color: var(--text);
    text-align: left;
  }
  .side-user:hover { background: var(--card); }
  .side-avatar {
    width: 26px; height: 26px;
    border-radius: 50%;
    background: var(--brand);
    color: #fff;
    display: grid;
    place-items: center;
    font-size: 12px;
    font-weight: 700;
    flex: 0 0 26px;
  }
  .side-who { min-width: 0; display: grid; }
  .side-name {
    font-size: 13px; font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .side-role { font-size: 11px; color: var(--text-3); }

  /* ---- content ---- */
  .screens { max-width: none; margin: 0; }
  .scroller { --scroll-pad-l: 20px; --scroll-pad-r: 20px; }

  .cam-list {
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
    gap: 12px;
    padding-top: 16px;
  }
  .cam-card { cursor: pointer; }
  .cam-card:hover { border-color: var(--line-2); }

  .shot-grid { grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; }
  .shot:hover { transform: translateY(-2px); }
  .shot:hover img { transform: scale(1.04); }
  .shot img { transition: transform 0.4s var(--ease); }
  .day-head { top: -1px; }
  .feed { gap: 6px; }
  .ev-grid, .stat-grid { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }

  /* ---- camera detail: video beside the timeline ---- */
  #screen-camera { flex-direction: row; overflow: hidden; }

  .stage-wrap {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;      /* let aspect-ratio set the width */
    padding: 16px 0 16px 20px;
  }
  /* Size from WIDTH, never `width: auto`. Everything inside the stage is
     absolutely positioned, so it has no intrinsic width -- `auto` collapses
     it to nothing and the aspect ratio then gives it no height either.
     Instead take the widest 16:9 box that still fits the available height,
     capped at the column width. No letterboxing, no JS. */
  .stage {
    width: min(100%, calc((100dvh - var(--bar-h) - 56px) * 16 / 9));
    max-width: 100%;
  }
  .stage-media img, .stage-media video { object-fit: contain; }

  .tl-panel {
    display: flex;
    flex-direction: column;
    flex: 0 0 var(--tl-w);
    min-height: 0;
    border-left: 1px solid var(--line);
    margin-left: 20px;
  }
  .tl-bar, .chip-rail { padding-left: 14px; padding-right: 14px; }
  .vtl { flex: 1; min-height: 0; }
  .vtl-scroll { padding-right: 44px; }

  /* Hover reveals the dock on desktop; there is no tap to rely on. */
  .stage:hover .stage-dock { opacity: 1; transform: none; pointer-events: auto; }

  /* ---- forms and sheets sit centred rather than bottom-anchored ---- */
  .sheet-scrim { align-items: center; }
  .sheet {
    border-radius: var(--r-lg);
    border: 1px solid var(--line);
    max-width: 480px;
    transform: scale(0.97);
    max-height: 86vh;
  }
  .sheet-scrim.open .sheet { transform: scale(1); }
  .sheet-grab { display: none; }
  .settings-card { max-width: 560px; }
  .user-list { max-width: 720px; }

  .install-bar { display: none; }
}

/* Laptops: the picture matters more than the chrome, so give the sidebar
   and timeline less and the video the difference. */
@media (min-width: 900px) and (max-width: 1279px) {
  .app { --side-w: 192px; --tl-w: 272px; }
  .side-brand span, .side-nav button span, .side-cam span { font-size: 12.5px; }
  .stage-wrap { padding-left: 14px; }
  .tl-panel { margin-left: 14px; }
  .scroller { --scroll-pad-l: 14px; --scroll-pad-r: 14px; }
}

@media (min-width: 1400px) {
  .app { --tl-w: 380px; }
  .cam-list { grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); }
}

/* ------------------------------------------------- responsive ----- */

/* Phones stay single-column; tablets and desktops get a grid and a
   roomier stage, but the navigation model stays identical. */
/* Tablets in portrait: still the mobile shell, just wider. */
@media (min-width: 700px) and (max-width: 899px) {
  .cam-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .scroller { --scroll-pad-l: calc(22px + var(--safe-l)); --scroll-pad-r: calc(22px + var(--safe-r)); }
}

/* Superseded by the desktop layout above; the tab bar is gone at these
   widths and .screens is no longer a centred column. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ------------------------------- install-before-sign-in gate ------- */

.install-gate { text-align: center; }
.gate-steps {
  margin: 4px 0 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
  display: grid;
  gap: 12px;
  text-align: left;
}
.gate-steps li {
  counter-increment: step;
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  align-items: center;
  gap: 11px;
  font-size: 14.5px;
  line-height: 1.4;
  color: var(--text-2);
}
.gate-steps li::before {
  content: counter(step);
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--brand);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
}
.gate-note {
  margin: 2px 0 0;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--text-3);
}

/* ------------------------------------------- event overview -------- */

.ev-wrap { display: grid; gap: 14px; grid-template-columns: minmax(0, 1fr); }
.ev-wrap > * { min-width: 0; }

.ev-shot {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--card);
  aspect-ratio: 16 / 9;
}
.ev-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ev-badge {
  position: absolute;
  left: 10px;
  top: 10px;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  background: var(--mat);
  backdrop-filter: var(--mat-blur, blur(12px));
  color: var(--text);
}
.ev-badge.alert { background: rgba(255, 69, 58, 0.9); color: #fff; }

.ev-title {
  margin: 0;
  font-size: 21px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.2;
}
.ev-when { margin-top: -8px; font-size: 13px; color: var(--text-3); }

.ev-chips { display: flex; flex-wrap: wrap; gap: 7px; }
.ev-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 11px;
  border-radius: 999px;
  background: var(--card-2);
  color: var(--text-2);
  font-size: 12.5px;
}
.ev-chip svg { width: 14px; height: 14px; }
.ev-chip.face { background: rgba(10, 132, 255, 0.18); color: #7ab8ff; font-weight: 650; }

.ev-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; }
.ev-actions:has(> :only-child) { grid-template-columns: minmax(0, 1fr); }
.ev-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 46px;
  padding: 12px;
  border-radius: 12px;
  background: var(--card-2);
  border: 1px solid var(--line);
  color: var(--text);
  font-size: 14.5px;
  font-weight: 650;
}
.ev-btn svg { width: 17px; height: 17px; }
.ev-btn.primary { background: var(--brand); border-color: transparent; color: #fff; }
.ev-btn.primary:active { background: var(--brand-2); }

.ev-facts {
  margin: 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
}
.ev-facts dt {
  padding: 12px 14px;
  font-size: 13.5px;
  color: var(--text-3);
  white-space: nowrap;
}
.ev-facts dd {
  margin: 0;
  padding: 12px 14px;
  font-size: 13.5px;
  color: var(--text);
  text-align: right;
  overflow-wrap: anywhere;
}
.ev-facts dt:not(:first-of-type), .ev-facts dd:not(:first-of-type) {
  border-top: 0.5px solid var(--line);
}

/* --------------------------------------------- recovery codes ------ */

.code-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.code-grid > * { min-width: 0; }
.code-chip {
  padding: 9px 10px;
  border-radius: var(--r-sm);
  background: var(--bg-2);
  border: 1px solid var(--line);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13.5px;
  letter-spacing: 0.04em;
  text-align: center;
  color: var(--text);
  user-select: all;
}
.acct-note.ok { color: var(--ok); }

/* Second step of sign-in: the code field is the only thing on screen, so
   it is sized like a keypad entry rather than a normal text input. */
#twoFactorForm input[name="code"] {
  text-align: center;
  font-size: 22px;
  letter-spacing: 0.28em;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.login-hint {
  margin: 2px 0 0;
  font-size: 12.5px;
  color: var(--text-3);
  text-align: center;
}
.login-hint.link { color: var(--brand); text-decoration: none; }

/* =====================================================================
   Liquid Glass — surface sweep
   =====================================================================
   Applied in one place rather than edited into each rule above. Every
   selector here already sets a flat --card / --card-2 / --bg-2 fill; this
   replaces that fill with the glass material wholesale. Specificity is
   unchanged, so it wins purely on source order and any rule that genuinely
   needs a solid fill can still override it by being more specific.

   `* { box-sizing: border-box }` is set near the top, so adding a rim to
   things that had no border does not change their outer size. */

/* Panels, controls and chrome: few enough on screen at once to afford a
   real backdrop-filter each. */
.settings-card, .acct-rows, .set-groups, .set-search, .card, .stat,
.search-field, .suggest button, .sheet-close, .pill-btn, .chip, .fchip.clear,
.ev-facts, .ev-chip, .ev-btn, .cal-nav button, .btn-wide.ghost, .role-chip,
.code-chip, .wiz-secret, .note-field, .ha-btn, .group-rail button,
.field input, .field select, .login-box input {
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 0.5px solid var(--glass-edge);
  box-shadow: inset 0 1px 0 var(--glass-hi);
}

/* Rows and tiles that repeat down a list. Deliberately no backdrop-filter:
   one blur layer per cell is exactly what makes a long list stutter on iOS,
   and at this size the rim carries the look on its own. */
.seg, .item, .shot, .recent-shot, .ev-shot, .cal-day, .user-row,
.person-face, .person-none, .cam-offline, .vtl-daymark span {
  background: var(--glass-soft);
  border: 0.5px solid var(--glass-edge);
  box-shadow: inset 0 1px 0 var(--glass-hi);
}

/* The timeline is one large surface, so it carries a single blur layer --
   affordable, unlike the event blocks inside it, which repeat and stay on
   the no-filter treatment above. */
.vtl {
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 0.5px solid var(--glass-edge);
  box-shadow: inset 0 1px 0 var(--glass-hi);
}

/* Left transparent on purpose: as opaque strips above the timeline they read
   as separate panels, and the point is one continuous pane of glass. */
.tl-bar, .chip-rail { background: transparent; }

/* Both of these predate the tokens and hand-rolled their own translucency.
   Moving them onto the shared material keeps the floating layer consistent
   rather than three slightly different glasses. */
.stage-dock {
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 0.5px solid var(--glass-edge);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-hi);
}
.vtl-map {
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-left: 0.5px solid var(--glass-edge);
}

/* Desktop sidebar: one big panel, so it gets the full material. */
@media (min-width: 900px) {
  .sidebar {
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
  }
}

/* Interaction states stay opaque tints: a pressed control that is still
   see-through does not read as pressed. */
.menu-item:active, .set-link:active, .acct-row:active { background: var(--card-2); }

/* Without backdrop-filter support the translucent fills would just look
   washed out, so everything falls back to the solid surfaces. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .settings-card, .acct-rows, .set-groups, .set-search, .card, .stat,
  .search-field, .suggest button, .sheet-close, .pill-btn, .chip, .fchip.clear,
  .ev-facts, .ev-chip, .ev-btn, .cal-nav button, .btn-wide.ghost, .role-chip,
  .code-chip, .wiz-secret, .note-field, .ha-btn, .group-rail button,
  .field input, .field select, .login-box input,
  .seg, .item, .shot, .recent-shot, .ev-shot, .cal-day, .user-row,
  .person-face, .person-none, .cam-offline, .vtl-daymark span, .vtl,
  .sheet { background: var(--card); }
}

/* =====================================================================
   Home Assistant device chips (home screen)
   =====================================================================
   A rail directly above recent activity, scrolling the same way so the two
   read as one stack rather than two separate ideas. Compact rows: the name
   on the left, the state button on the right.

   Same glass as the rest of the floating layer, so they belong to the tab
   bar and sheets rather than looking like a widget bolted on. */
.dev-rail {
  --rail-gap: 10px;
  display: flex;
  gap: var(--rail-gap);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  /* Bleeds to the screen edge and back, matching the recent rail exactly --
     two rails that stop at different margins look like a mistake. */
  margin-left: calc(-1 * var(--scroll-pad-l));
  margin-right: calc(-1 * var(--scroll-pad-r));
  /* Vertical room for the chips' own shadow. The rail clips on the y axis,
     so a shadow larger than this padding gets sliced mid-blur and reads as a
     hard band under the row rather than as depth. */
  padding: 6px var(--scroll-pad-r) 10px var(--scroll-pad-l);
  scroll-padding-left: var(--scroll-pad-l);
  scroll-padding-right: var(--scroll-pad-r);
}
.dev-rail::-webkit-scrollbar { display: none; }
.dev-rail[hidden] { display: none; }

.dev-chip {
  flex: 0 0 auto;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 172px;
  max-width: 240px;
  padding: 9px 9px 9px 14px;
  border-radius: 16px;
  text-align: left;
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 0.5px solid var(--glass-edge);
  /* --shadow-sm, deliberately not --glass-shadow. The latter is tuned for the
     tab bar and sheets, which float above the whole page; on small chips
     inside a scroller its 34px blur reads as smudge, and the rail clips it
     anyway. --shadow-sm is already the short, tight one, and already carries
     its own value per theme. */
  box-shadow: var(--shadow-sm), inset 0 1px 0 var(--glass-hi);
  cursor: pointer;
  transition: transform 0.16s var(--spring), border-color 0.2s var(--ease),
              background 0.2s var(--ease);
}
/* Not a button when there is nothing to press: a contact sensor has nothing
   to actuate, and a row that looks tappable but is not is worse than one
   that plainly is not. */
div.dev-chip { cursor: default; }

.dev-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  flex: 1;
}
.dev-name {
  font-size: 13.5px;
  font-weight: 650;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dev-state {
  font-size: 11.5px;
  color: var(--text-2);
  white-space: nowrap;
}

/* The button. Carries the open/closed glyph and the colour together, so the
   state survives being read without colour vision. */
.dev-btn {
  position: relative;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 13px;
  color: var(--ok);
  background: color-mix(in srgb, var(--ok) 15%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ok) 30%, transparent);
  transition: color 0.2s var(--ease), background 0.2s var(--ease),
              box-shadow 0.2s var(--ease), transform 0.2s var(--spring);
}
/* Sized explicitly so the ring, which is also an svg child here, is not
   caught by the glyph's sizing. */
.dev-btn > svg:not(.dev-ring) { width: 21px; height: 21px; }

/* ---- open / unlocked ---------------------------------------------- */
.dev-chip.is-open {
  border-color: color-mix(in srgb, var(--alert) 40%, transparent);
}
.dev-chip.is-open .dev-state { color: var(--alert); font-weight: 650; }
.dev-chip.is-open .dev-btn {
  color: var(--alert);
  background: color-mix(in srgb, var(--alert) 17%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--alert) 38%, transparent),
              0 0 14px color-mix(in srgb, var(--alert) 30%, transparent);
}

/* ---- unavailable ---------------------------------------------------
   Dimmed rather than hidden: an entity that vanished from HA should be
   visibly wrong here, not silently absent. */
.dev-chip.is-gone { opacity: 0.45; cursor: default; }
.dev-chip.is-gone .dev-state { color: var(--text-3); }
.dev-chip.is-gone .dev-btn {
  color: var(--text-3);
  background: var(--fill);
  box-shadow: none;
}

/* ---- in flight -----------------------------------------------------
   Held until the poll confirms. A garage takes seconds to move, and a chip
   that flipped the instant the request returned would be guessing. */
.dev-chip.is-busy { pointer-events: none; }
.dev-chip.is-busy .dev-btn { animation: dev-pulse 1.1s ease-in-out infinite; }
@keyframes dev-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

/* ---- hold to open --------------------------------------------------
   Doors and locks are held rather than confirmed in a sheet. The outline
   drawing itself around the action button is the whole feedback: it says how
   far in you are, so letting go early is an obvious cancel.

   The stroke traces the button's own squircle. Perimeter of a 37.5 square
   with an 11.75 corner radius:
     2*(37.5 - 2*11.75) * 2  +  2*pi*11.75  =  56 + 73.83  =  129.83
   which is both the dash length and the starting offset, so the stroke
   begins fully retracted and lands exactly closed at zero. It follows from
   the rect geometry in HomeDevices.ring(); change one and change both. */
.dev-ring {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.dev-ring rect {
  fill: none;
  stroke-width: 2.5;
}
.dev-ring-track {
  stroke: color-mix(in srgb, currentColor 18%, transparent);
  opacity: 0;
  transition: opacity 0.14s var(--ease);
}
.dev-ring-fill {
  stroke: currentColor;
  stroke-linecap: round;
  stroke-dasharray: 129.83;
  stroke-dashoffset: 129.83;
  opacity: 0;
  /* The release: a quick unwind, so an abandoned hold is plainly abandoned. */
  transition: stroke-dashoffset 0.22s var(--ease), opacity 0.22s var(--ease);
}

/* Duration must match HomeDevices.HOLD_MS. Linear: a progress indicator that
   eases is lying about how much time is left. */
.dev-chip.is-holding .dev-ring-fill {
  opacity: 1;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 1400ms linear, opacity 0.12s var(--ease);
}
.dev-chip.is-holding .dev-ring-track { opacity: 1; }
.dev-chip.is-holding .dev-btn { transform: scale(1.07); }

/* Closed, and now working. The outline stays whole while the request is in
   flight and the poll catches up, rather than vanishing at the one moment
   the user is looking for confirmation that the hold took. */
.dev-chip.is-busy .dev-ring-fill,
.dev-chip.is-busy .dev-ring-track {
  opacity: 1;
  stroke-dashoffset: 0;
  transition: none;
}

/* The whole chip is one hold target, so a long press must not select the
   label or raise the iOS callout menu mid-hold. */
.dev-chip.is-held {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

.dev-chip:active { transform: scale(0.97); }
button.dev-chip:hover {
  border-color: color-mix(in srgb, var(--brand) 40%, var(--glass-edge));
}

@media (prefers-reduced-motion: reduce) {
  .dev-chip { transition: none; }
  .dev-chip:active { transform: none; }
  .dev-chip.is-busy .dev-btn { animation: none; opacity: 0.6; }
  .dev-chip.is-holding .dev-btn { transform: none; }
  /* The outline's sweep is kept: it is the timer, not decoration. Only the
     scale and the unwind are dropped. */
  .dev-ring-fill { transition: none; }
  .dev-chip.is-holding .dev-ring-fill { transition: stroke-dashoffset 1400ms linear; }
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .dev-chip { background: var(--card); }
}

/* Used by the admin list for an entity Home Assistant no longer knows. */
.acct-note.bad { color: var(--alert); }

/* Sort control and result count above the entity picker. */
.dev-pick-tools {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 10px;
}
.dev-pick-tools .admin-note {
  margin: 0;
  flex: 1;
  min-width: 0;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Icon chooser in the device editor. Glass like the rest of the floating
   layer, with the selected one lit rather than merely outlined -- at this
   size a border alone is easy to miss. */
.icon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
  gap: 8px;
  margin: 2px 0 16px;
}
.icon-pick {
  display: grid;
  place-items: center;
  height: 50px;
  border-radius: 13px;
  color: var(--text-2);
  background: var(--glass-soft);
  border: 0.5px solid var(--glass-edge);
  box-shadow: inset 0 1px 0 var(--glass-hi);
  cursor: pointer;
  transition: color 0.15s var(--ease), background 0.15s var(--ease),
              border-color 0.15s var(--ease), transform 0.15s var(--spring);
}
.icon-pick svg { width: 22px; height: 22px; }
.icon-pick:active { transform: scale(0.94); }
.icon-pick.is-on {
  color: var(--brand);
  background: color-mix(in srgb, var(--brand) 18%, transparent);
  border-color: color-mix(in srgb, var(--brand) 55%, transparent);
}

/* Reorder controls on the device rows. Stacked so the pair occupies one
   row's height rather than widening it, and sized to stay tappable -- the
   arrows are small, but the buttons around them are not. */
.row-move {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 0 0 auto;
  margin-right: 2px;
}
.move-btn {
  display: grid;
  place-items: center;
  width: 30px;
  height: 22px;
  border-radius: 7px;
  color: var(--text-2);
  background: var(--fill);
  cursor: pointer;
  transition: background 0.15s var(--ease), color 0.15s var(--ease);
}
.move-btn svg { width: 15px; height: 15px; }
.move-btn:active { background: var(--fill-2); color: var(--text); }
/* Dimmed rather than hidden at the ends: a control that vanishes makes the
   row jump and the pair stop lining up down the list. */
.move-btn[disabled] { opacity: 0.28; cursor: default; }

/* A section switched off in the home-screen editor. Dimmed rather than
   moved out of sight, so it is obvious it still exists and can come back. */
.acct-row.is-off .acct-row-label,
.acct-row.is-off .acct-row-hint { opacity: 0.5; }

/* ---- device states at the time of an event -------------------------
   Answers "what was the garage doing when this happened?", which the live
   tiles cannot: by the time you are reviewing, they show now. Hidden until
   the lookup returns, so an event with no Home Assistant behind it shows
   nothing rather than an empty heading. */
.ev-devs { margin-top: 18px; }
.ev-devs[hidden] { display: none; }
.ev-devs-head {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
  margin-bottom: 9px;
}
.ev-devs-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.ev-dev {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 12px 7px 10px;
  border-radius: 999px;
  font-size: 12.5px;
  background: var(--glass-soft);
  border: 0.5px solid var(--glass-edge);
  box-shadow: inset 0 1px 0 var(--glass-hi);
  color: var(--text-2);
}
.ev-dev svg { width: 16px; height: 16px; }
.ev-dev-name { font-weight: 650; color: var(--text); }
.ev-dev-state { font-variant-numeric: tabular-nums; }

/* Open at the time is the case worth noticing, so it is coloured like the
   home-screen chips rather than left to be read. */
.ev-dev.is-open {
  color: var(--alert);
  border-color: color-mix(in srgb, var(--alert) 40%, transparent);
  background: color-mix(in srgb, var(--alert) 12%, transparent);
}
.ev-dev.is-open .ev-dev-name { color: var(--alert); }

/* Fullscreen hides the tab bar, so the clearance goes with it -- otherwise
   the Now button floats in the middle of nowhere. */
body.stage-max .vtl-now { bottom: 14px; }

/* Device states while replaying. Sits between the timeline bar and the
   filter chips, so it reads as context for what is playing rather than as
   another control. */
.stage-devs {
  margin: 0 calc(14px + var(--safe-r)) 8px calc(14px + var(--safe-l));
}
.stage-devs .ev-devs-head { margin-bottom: 6px; }

/* The active state for a toggling timeline button. The others in that bar
   are momentary actions, so this needs to look held rather than pressed. */
.tbtn.is-on {
  color: var(--brand);
  background: color-mix(in srgb, var(--brand) 18%, transparent);
}
.tbtn[hidden] { display: none; }
