/* Display Screens — local styles. */

/* The page's own scroll root — replaces .aq-content for this screen.
   In the prototype this was nested inside .aq-content; in the real app
   each page renders its own scroll container so per-page padding and
   gap don't fight the dashboard's centered max-width. */
.ds-content {
  flex: 1;
  overflow-y: auto;
  padding: 24px 32px 60px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  position: relative;
  z-index: 1;
}

/* ── Header ───────────────────────────────────────────────────── */
.ds-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
}
.ds-header h1 {
  margin: 0 0 4px;
  font-family: var(--aq-ff-display);
  font-size: 26px;
  font-weight: 500;
  letter-spacing: -0.012em;
  color: var(--aq-text);
}
.ds-header p {
  margin: 0;
  font-size: 13px;
  color: var(--aq-text-faint);
  max-width: 540px;
  line-height: 1.5;
}
.ds-header-actions { display: flex; gap: 8px; }

/* ── Buttons ──────────────────────────────────────────────────── */
.ds-btn {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 6px 12px;
  background: var(--aq-surface-2);
  border: 0;                         /* borderless per May 2026 feedback */
  border-radius: 7px;
  color: var(--aq-text);
  font: inherit; font-size: 12px;
  cursor: pointer;
  transition: background 160ms;
}
.ds-btn:hover { background: rgba(255,255,255,0.10); }
.ds-btn.ghost { background: transparent; }
.ds-btn.ghost:hover { background: rgba(255,255,255,0.06); }
.ds-btn.primary {
  background: var(--aq-accent);
  /* Use --aq-accent-ink (defined for accents that are light enough
     to need dark text — e.g. the "pearl" theme where --aq-accent
     is rgba(255,255,255,0.95)). Falls back to white when the
     theme's accent is dark. Without this, the pearl theme rendered
     "white-on-white" Register screen text. */
  color: var(--aq-accent-ink, white);
  font-weight: 500;
}
.ds-btn.primary:hover {
  background: color-mix(in srgb, var(--aq-accent) 88%, black);
}
.ds-link {
  background: transparent; border: 0; padding: 0;
  color: var(--aq-accent);
  font: inherit; font-size: 11.5px;
  cursor: pointer;
}
.ds-link:hover { text-decoration: underline; }
.ds-iconbtn {
  background: transparent; border: 0;
  padding: 4px 8px;
  border-radius: 4px;
  color: var(--aq-text-faint);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  transition: background var(--aq-d-1) var(--aq-ease), color var(--aq-d-1) var(--aq-ease);
}
.ds-iconbtn:hover { background: rgba(255,255,255,0.06); color: var(--aq-text); }

/* ── Summary strip ────────────────────────────────────────────── */
/* Apple pass (Jun 2026): the KPIs sit directly on the page canvas —
   no card bezel, no border, no inter-stat dividers. The numbers are
   the surface; chrome around them was redundant. Generous gap does
   the visual separation the 1px rules used to. */
/* Status line (2026-06-12) — replaced the four-tile KPI strip. One
   sentence under the title: live count, offline as a working link,
   prepared/empty only when non-zero. */
.ds-statusline {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  font-size: 13px;
  color: var(--aq-text-dim);
  flex-wrap: wrap;
}
.ds-status-live {
  display: inline-flex; align-items: center; gap: 7px;
  color: var(--aq-success); font-weight: 500;
}
.ds-status-live::before {
  content: ''; width: 7px; height: 7px; border-radius: 50%;
  background: var(--aq-success); box-shadow: 0 0 7px var(--aq-success);
}
.ds-status-live.is-partial { color: var(--aq-text-muted); }
.ds-status-live.is-partial::before { background: var(--aq-warn); box-shadow: 0 0 7px var(--aq-warn); }
.ds-status-sep { color: var(--aq-text-faint); }
.ds-status-offline {
  background: none; border: 0; padding: 0; font: inherit;
  color: var(--aq-danger); font-weight: 500; cursor: pointer;
}
.ds-status-offline:hover { text-decoration: underline; }
.ds-status-rest { color: var(--aq-text-dim); }

/* ── Toolbar ──────────────────────────────────────────────────── */
/* Apple pass (Jun 2026): the toolbar no longer lives in its own
   surface card — it sits on the page canvas like the summary strip
   above. Only the inner controls (search field, segmented filter,
   zone select) carry a subtle tint to read as interactive. */
.ds-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0;
  background: transparent;
  border: 0;
  flex-wrap: wrap;
}
.ds-search {
  /* Don't grow to fill the toolbar — a 1000px-wide search field looked
     absurd and shoved the All/Online/Offline filters to the far right.
     Capped width keeps the filters sitting right beside it; the
     toolbar-spacer below still pushes the result count to the edge. */
  flex: 0 1 320px;
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--aq-surface-2);
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 6px 10px;
  min-width: 220px;
  max-width: 360px;
  transition: border-color var(--aq-d-1) var(--aq-ease);
}
.ds-search:focus-within { border-color: var(--aq-line-strong); }
.ds-search > svg { color: var(--aq-text-faint); }
.ds-search input {
  flex: 1; min-width: 0;
  background: transparent; border: 0;
  color: var(--aq-text); font: inherit; font-size: 12.5px;
  outline: none;
}
.ds-search input::placeholder { color: var(--aq-text-faint); }
.ds-segmented {
  display: inline-flex;
  background: var(--aq-surface-2);
  border: 0;
  border-radius: 7px;
  padding: 2px;
}
.ds-segmented button {
  background: transparent; border: 0;
  padding: 5px 12px;
  border-radius: 5px;
  font: inherit; font-size: 12px;
  color: var(--aq-text-dim);
  cursor: pointer;
}
.ds-segmented button:hover { color: var(--aq-text); }
.ds-segmented button.is-active {
  background: rgba(255,255,255,0.08);
  color: var(--aq-text);
}
.ds-select {
  background: var(--aq-surface-2);
  border: 0;
  border-radius: 7px;
  padding: 6px 9px;
  color: var(--aq-text); font: inherit; font-size: 12px;
  outline: none; cursor: pointer;
}

/* ── Layout ───────────────────────────────────────────────────── */
.ds-layout {
  display: grid;
  grid-template-columns: 1fr 360px;
  gap: 16px;
  align-items: flex-start;
  min-width: 0;
}
/* Below ~1100px the fixed 360px panel ate a third of the page. Single
   column; the detail panel flows after the grid at full width. */
@media (max-width: 1100px) {
  .ds-layout { grid-template-columns: 1fr; }
  .ds-detail { position: static; padding: 16px 0 0; border-top: 1px solid var(--aq-line-soft); }
}

/* ── Site groups (2026-06-12) ─────────────────────────────────── */
.ds-site-group { display: flex; flex-direction: column; gap: 36px; }
.ds-site-group + .ds-site-group { margin-top: 38px; }
.ds-site-head {
  display: flex; align-items: center; gap: 10px;
  margin-bottom: 18px;
}
.ds-site-name {
  font-size: 11px; font-weight: 600;
  letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--aq-text-faint);
}
.ds-site-count { font-size: 11px; color: var(--aq-text-faint); font-variant-numeric: tabular-nums; }
.ds-site-rule { flex: 1; height: 1px; background: var(--aq-line-soft); }

/* Collapsed empty-zone rows */
.ds-empty-zone {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 2px;
  border-bottom: 1px solid var(--aq-line-soft);
  font-size: 12.5px; color: var(--aq-text-faint);
}
.ds-empty-zone:last-child { border-bottom: 0; }
.ds-empty-zone b { color: var(--aq-text-dim); font-weight: 500; }
.ds-empty-zone-note { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-empty-zone-act { margin-left: auto; opacity: 0; transition: opacity var(--aq-d-1) var(--aq-ease); }
.ds-empty-zone:hover .ds-empty-zone-act,
.ds-empty-zone:focus-within .ds-empty-zone-act { opacity: 1; }
.ds-empty-zone-act button {
  background: none; border: 0; padding: 0; font: inherit;
  font-size: 11.5px; color: var(--aq-text-dim); cursor: pointer;
}
.ds-empty-zone-act button:hover { color: var(--aq-danger); }

/* ── Zone block ───────────────────────────────────────────────── */
/* Borderless treatment per May 2026 feedback: the per-zone box outline
   and the head/body underline read as redundant chrome — the zone is
   already visually defined by the spacing between sections and the
   bold header. Group sections now stack with breathing-room gap only. */
/* Bumped from 28→36px when the zone headers got a divider underline
   (May 2026 feedback: headers blended in). The extra room + the rule
   together give each zone a real "section" feel rather than a hairline
   distinction from the next group of tiles. */
.ds-grid-wrap { display: flex; flex-direction: column; gap: 36px; min-width: 0; }
.ds-zone-block {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
}
.ds-zone-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  /* Underline + extra bottom padding turns the header into a real
     section divider — fixes the "Pelagic / Seahorse Kingdom headers
     blend into the page background" feedback. The line uses the same
     --aq-line token used everywhere else in the surface chrome so it
     stays consistent with modals/cards/etc. */
  padding-bottom: 10px;
  border-bottom: 1px solid var(--aq-line);
  margin-bottom: 16px;
}

/* Subtle icon-only zone-header affordances (rename + delete). Both
   hidden by default, fade in when the curator hovers anywhere in
   the zone header. Editing a zone is rare; the icons shouldn't
   compete with the primary "Pelagic" line. */
.ds-zone-edit,
.ds-zone-delete {
  appearance: none;
  background: transparent;
  border: 0;
  padding: 4px;
  border-radius: 5px;
  color: var(--aq-text-faint);
  cursor: pointer;
  opacity: 0;
  transition: opacity 140ms ease, background-color 140ms ease,
              color 140ms ease;
  display: inline-grid;
  place-items: center;
}
.ds-zone-edit { margin-left: auto; }
.ds-zone-delete { margin-left: 0; }
.ds-zone-head:hover .ds-zone-edit,
.ds-zone-head:hover .ds-zone-delete {
  opacity: 0.55;
}
.ds-zone-edit:hover {
  opacity: 1 !important;
  background: color-mix(in srgb, var(--aq-accent) 14%, transparent);
  color: var(--aq-accent);
}
.ds-zone-delete:hover {
  opacity: 1 !important;
  background: color-mix(in srgb, var(--aq-danger) 14%, transparent);
  color: var(--aq-danger);
}
.ds-zone-head h3 {
  margin: 0;
  font-family: var(--aq-ff-display);
  /* 14px/500 read as body text against the dark canvas — operator
     couldn't tell the header apart from the content. Bumped to 16/600
     to give it actual heading hierarchy. */
  font-size: 16px;
  font-weight: 600;
  color: var(--aq-text);
  letter-spacing: -0.005em;
}
.ds-zone-head span {
  font-family: var(--aq-ff-mono);
  font-size: 10px;
  color: var(--aq-text-faint);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.ds-grid {
  display: grid;
  /* Fluid (2026-06-12): repeat(4,1fr) squeezed tiles to ~160px once the
     360px detail panel opened at 1280w. Tiles keep a substantial
     minimum and the column count adapts instead — fewer, bigger
     bezels beats more, smaller ones (these are TVs, not thumbnails). */
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 12px;
}

/* ── Screen tile ──────────────────────────────────────────────── */
.ds-tile {
  background: transparent;
  border: 0;
  padding: 0;
  text-align: left;
  cursor: pointer;
  font: inherit;
  /* The default button focus outline wrapped the whole tile INCLUDING
     the caption below the frame — read as an ugly white box on click
     (2026-06-12 feedback). Focus lives on the frame instead. */
  outline: none;
  /* position: relative so the bulk-select checkbox can anchor
     to the tile's top-left corner. Wave B feature. */
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* Tile frame — styled like a wall-mounted display with a black
   bezel around the kiosk screen. Mirrors the campaign-editor's
   ce-bezel-phone treatment (layered box-shadows for chrome) but
   tuned for a TV rather than a phone: thicker bottom bezel, less
   rounded corners. Aspect ratio matches what the kiosk renders
   natively (16:9 landscape, 9:16 portrait) so snapshots aren't
   stretched. */
/* All tile frames are the SAME size in the grid (16:9, full cell
   width). For portrait orientation, the INNER screen area shows as
   a vertical 9:16 rectangle nested inside the same landscape
   bezel — so the curator sees the orientation difference without
   the tile shrinking or dominating the row. The bezel reads as a
   uniform TV chassis across the fleet; only the lit-up screen
   area inside changes shape. */
/* Fixed-size tile frames so a landscape and portrait tile are
   guaranteed to look like the SAME physical screen, just rotated
   90°. Independent of how many columns the grid happens to render
   at any given viewport. The dimensions are transposed:
     • Landscape: 320 × 180 (16:9)
     • Portrait:  180 × 320 (9:16)
   Same diagonal, same area, perfectly transposed. */
.ds-tile-frame {
  /* Matte-black display bezel — same material as the marketing page's
     .kdevice frame (2026-06-12): gradient catches light at the top
     edge and lifts again at the bottom lip, instead of flat #0a0b0d. */
  background: linear-gradient(180deg, #26282d 0%, #15161a 6%, #0f1013 50%, #090a0c 93%, #1a1c1f 100%);
  border-radius: 9px;
  /* Slimmed 8/8/14 → 5/5/7 (2026-06-12 feedback): the thick chin read
     as bulky at tile size — the marketing frame's proportions only
     work at hero scale. */
  padding: 5px 5px 7px;
  aspect-ratio: 16 / 9;
  width: 100%;
  max-width: 320px;
  margin: 0 auto;
  overflow: hidden;
  position: relative;
  box-shadow:
    inset 0 1.5px 0 rgba(255, 255, 255, 0.09),
    inset 0 -1px 2px rgba(0, 0, 0, 0.55),
    0 0 0 1px rgba(0, 0, 0, 0.65),
    0 10px 22px -8px rgba(0, 0, 0, 0.60),
    0 24px 44px -18px rgba(0, 0, 0, 0.72);
  transition: transform 160ms, box-shadow 160ms;
}
/* Recessed glass — thin black gap + inner shadow where the screen
   meets the bezel, exactly like .kdevice::before on the landing page. */
.ds-tile-frame::before {
  content: '';
  position: absolute;
  inset: 5px 5px 7px;
  border-radius: 4px;
  z-index: 3;
  pointer-events: none;
  box-shadow: 0 0 0 1px #000, inset 0 2px 6px 1px rgba(0, 0, 0, 0.5);
}
.ds-tile-frame.is-portrait {
  aspect-ratio: 9 / 16;
  padding: 10px 8px 16px;
  max-width: 180px;                /* = 320 × 9/16 → portrait width */
}

/* ── Tile states: empty / prepared / live ──────────────────────────
   Empty    = dashed placeholder + a plus to set up (no device, no content).
   Prepared = the slot's own species art, DIMMED (standby) — no bezel,
              since no device is bound.
   Live (the default .ds-tile-body) keeps the physical-screen bezel +
   bright snapshot. */
.ds-tile-frame.is-empty {
  background: transparent;
  box-shadow: none;
  border: 1.5px dashed var(--aq-line-strong, rgba(255, 255, 255, 0.18));
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px;
}
.ds-tile-ghost {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  height: 100%;
  text-align: center;
  color: var(--aq-text-faint);
}
.ds-tile-plus { color: var(--aq-text-faint); opacity: 0.7; }
.ds-tile-ghost-note { font-size: 11px; color: var(--aq-text-faint); }

/* "Ready / waiting for device" = a configured screen that's off. Solid
   dark fill, no bezel, a single faint monitor glyph centred. No photo,
   no text — the caption carries the name + status. */
.ds-tile-frame.is-prepared {
  background: #0d1620;
  box-shadow: none;
  border: 0;
  overflow: hidden;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ds-tile-prepared {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}
.ds-tile-prepared-icon { color: var(--aq-text-faint); opacity: 0.5; }

/* ── Set-up-screen modal ───────────────────────────────────────────
   Guided empty-slot setup, stripped to essentials: no card-in-card, no
   header underline, no footer overline/tint, no per-row dividers — pure
   spacing carries the rhythm. Inputs are fill-based (no border); the
   only line left is the modal's own outer edge. */
.ds-setup-card { min-width: 420px; }
.ds-setup-card .aq-popup-header { border-bottom: 0; padding-bottom: 4px; }
.ds-setup-card .aq-popup-footer { border-top: 0; background: transparent; }
/* Close = just the glyph, no bordered circle. A faint fill only on hover. */
.ds-setup-card .aq-popup-close { border: 0; background: transparent; }
.ds-setup-card .aq-popup-close:hover { background: rgba(255, 255, 255, 0.08); }
.ds-setup-body {
  padding: 4px 24px 18px;
  gap: 0;
}
.ds-setup-body .x-form-field {
  display: block;          /* stack: label above control */
  padding: 13px 0;
  border-top: 0;
}
.ds-setup-body .x-form-label { margin-bottom: 8px; }
.ds-setup-body .x-form-control { width: 100%; }
.ds-setup-body .x-input {
  width: 100%;
  box-sizing: border-box;
  border: 0;                       /* fill-based, no outline */
  background: var(--aq-surface-2);
}
.ds-setup-body .x-input:focus {
  outline: 0;
  box-shadow: 0 0 0 2px var(--aq-accent-line, rgba(255, 255, 255, 0.22));
}

/* Skeleton tile — shown while /api/screens resolves on cold-load
   so the layout doesn't pop. Pulses subtly via the keyframe below.
   Per May 2026 follow-up: replaced the previous "Loading fleet…"
   text. */
.ds-tile-skeleton {
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.04),
    inset 0 0 0 2px #000;
}
.ds-tile-skeleton .ds-tile-body {
  background: linear-gradient(90deg,
    var(--aq-surface-2) 0%,
    color-mix(in srgb, var(--aq-surface-2) 80%, var(--aq-text) 10%) 50%,
    var(--aq-surface-2) 100%);
  background-size: 200% 100%;
  animation: ds-skeleton-shimmer 1.4s ease-in-out infinite;
  border-radius: 3px;
}
.ds-skeleton-line {
  display: inline-block;
  height: 12px;
  background: linear-gradient(90deg,
    var(--aq-surface-2) 0%,
    color-mix(in srgb, var(--aq-surface-2) 80%, var(--aq-text) 12%) 50%,
    var(--aq-surface-2) 100%);
  background-size: 200% 100%;
  animation: ds-skeleton-shimmer 1.4s ease-in-out infinite;
  border-radius: 4px;
  color: transparent;
}
@keyframes ds-skeleton-shimmer {
  0%   { background-position:  150% 0; }
  100% { background-position: -150% 0; }
}
.ds-tile:hover .ds-tile-frame {
  transform: translateY(-2px);
  box-shadow:
    inset 0 1.5px 0 rgba(255, 255, 255, 0.12),
    inset 0 -1px 2px rgba(0, 0, 0, 0.55),
    0 0 0 1px rgba(0, 0, 0, 0.65),
    0 14px 30px -10px rgba(0, 0, 0, 0.65),
    0 30px 52px -20px rgba(0, 0, 0, 0.75);
}
/* Selected state — crisp offset ring (2026-06-12). The previous wide
   accent halo (4px ring + 24px glow) rendered as a fuzzy white blob
   with the pearl accent. Now a macOS-style focus ring: a hairline at
   a 2px gap from the bezel, plus lift + deeper shadow. Reads as
   deliberate hardware selection rather than a smudge, and works with
   every accent because the ring is thin and offset, not a glow. */
.ds-tile.is-selected .ds-tile-frame {
  transform: translateY(-3px) scale(1.02);
  box-shadow:
    inset 0 1.5px 0 rgba(255, 255, 255, 0.12),
    inset 0 -1px 2px rgba(0, 0, 0, 0.55),
    0 0 0 1px rgba(0, 0, 0, 0.65),
    0 0 0 3px var(--aq-page),
    0 0 0 4px color-mix(in srgb, var(--aq-accent) 38%, var(--aq-line-strong)),
    0 16px 36px -10px rgba(0, 0, 0, 0.65),
    0 32px 56px -22px rgba(0, 0, 0, 0.75);
}
.ds-tile.is-selected:hover .ds-tile-frame {
  /* Slightly punchier on hover-of-selected so the cursor still
     reads as "you can click again to deselect". */
  transform: translateY(-4px) scale(1.03);
}

/* Bulk-checked state — for multi-select bulk actions (assign species,
   move to zone, etc.). Same vocabulary as is-selected but a subtler
   lift + dimmer halo so it doesn't compete with the primary
   selection of the detail panel. The corner checkbox is the
   primary signal; this is just reinforcement. */
.ds-tile.is-bulk-checked .ds-tile-frame {
  transform: translateY(-1px) scale(1.01);
  box-shadow:
    inset 0 1.5px 0 rgba(255, 255, 255, 0.10),
    inset 0 -1px 2px rgba(0, 0, 0, 0.55),
    0 0 0 1px rgba(0, 0, 0, 0.65),
    0 0 0 3px var(--aq-page),
    0 0 0 4px color-mix(in srgb, var(--aq-accent) 24%, var(--aq-line-strong)),
    0 10px 24px -10px rgba(0, 0, 0, 0.55);
}
/* When both selected AND bulk-checked, selected's stronger
   treatment wins — the cascade already handles this because
   is-selected is declared after is-bulk-checked above. */
/* Power-LED dot removed entirely (2026-06-12 feedback) — the caption
   dot already carries the state; the on-bezel LED doubled the signal. */
.ds-tile:focus-visible .ds-tile-frame {
  box-shadow:
    inset 0 1.5px 0 rgba(255, 255, 255, 0.12),
    inset 0 -1px 2px rgba(0, 0, 0, 0.55),
    0 0 0 1px rgba(0, 0, 0, 0.65),
    0 0 0 3px var(--aq-page),
    0 0 0 4px var(--aq-accent-line),
    0 10px 22px -8px rgba(0, 0, 0, 0.60);
}

.ds-tile-body {
  height: 100%;
  background: linear-gradient(135deg, oklch(0.30 0.06 220) 0%, oklch(0.18 0.04 240) 100%);
  /* Slight inner radius so the screen visually nests inside the
     bezel without sharp 90° corners. */
  border-radius: 3px;
  padding: 8px 10px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  position: relative;
  overflow: hidden;
}
.ds-tile-body.offline {
  background: linear-gradient(135deg, #15161a, #0a0b0d);
}
.ds-tile-body.idle {
  background: linear-gradient(135deg, #1b1c1f, #131316);
}
.ds-tile-pattern {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
}
.ds-tile-content {
  position: relative;
  font-family: var(--aq-ff-display);
  font-size: 11px;
  color: rgba(255,255,255,0.92);
  letter-spacing: -0.005em;
  line-height: 1.3;
  z-index: 1;
}
.ds-tile-content.ds-tile-offline,
.ds-tile-content.ds-tile-idle {
  font-family: var(--aq-ff-mono);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--aq-text-faint);
}
.ds-tile-meta {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

/* ── Status pill ──────────────────────────────────────────────── */
.ds-statuspill {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 6px;
  border-radius: 99px;
  font-family: var(--aq-ff-mono);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.ds-statusdot {
  width: 4px; height: 4px;
  border-radius: 50%;
}

.ds-tile-info {
  /* Caption block: a dot·name·status row, plus an optional exception
     reason line below. Constrained to the tile's own width (max 320,
     centred) so it sits UNDER the screen, not splaying to the cell edges. */
  width: 100%;
  max-width: 320px;
  margin: 0 auto;
  padding: 7px 2px 0;
}
.ds-tile-info-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 7px;
}
.ds-tile-reason {
  margin-top: 3px;
  font-size: 11px;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Exception flags — small icon cluster, top-right of the live tile. Only
   rendered when there's a problem (weak wifi / control down / syncing /
   stale snapshot), so a healthy tile shows nothing. */
.ds-tile-flags {
  position: absolute;
  top: 7px; right: 7px;
  display: flex;
  gap: 5px;
  z-index: 3;
}
.ds-tile-flag {
  width: 20px; height: 20px;
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
.ds-tile-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  flex-shrink: 0;
}
.ds-tile-state {
  margin-left: auto;
  flex-shrink: 0;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.01em;
  white-space: nowrap;
  /* Quiet by design (2026-06-12): the dot carries the state colour. */
  color: var(--aq-text-faint);
  font-variant-numeric: tabular-nums;
}
.ds-tile-id {
  font-family: var(--aq-ff-mono);
  font-size: 12px;
  font-weight: 500;
  color: var(--aq-text);
  letter-spacing: 0.04em;
  /* Left-justified, takes the remaining width and truncates so the
     status on the right always has room. */
  text-align: left;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* .ds-tile-zone + .ds-tile-spec rules retained for backwards-compat
   in case any other component renders them — the tile component itself
   no longer emits these elements. */
.ds-tile-zone {
  font-size: 11px;
  color: var(--aq-text-dim);
}
.ds-tile-spec {
  font-family: var(--aq-ff-mono);
  font-size: 9.5px;
  color: var(--aq-text-faint);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Bulk-select checkbox — top-right corner of the screen body. Hidden
   by default so it doesn't clutter the tile; revealed on tile hover
   or when it's already checked (so the curator can see + uncheck it
   without hovering). Z-index sits above the body content but below
   the status pill so the ONLINE/IDLE label still wins.
   Click handler in the JSX stops propagation so toggling the
   checkbox doesn't also trigger the tile's onClick (which would
   open the detail panel). */
.ds-tile-bulkbox {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  width: 20px;
  height: 20px;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.40);
  border: 1px solid rgba(255, 255, 255, 0.35);
  color: rgba(255, 255, 255, 0.95);
  display: grid;
  place-items: center;
  font-size: 11px;
  line-height: 1;
  cursor: pointer;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  opacity: 0;
  transition: opacity 120ms ease, border-color 120ms ease,
              background-color 120ms ease;
}
.ds-tile:hover .ds-tile-bulkbox,
.ds-tile-bulkbox.is-checked {
  opacity: 1;
}
/* Checked state — keep the box transparent so it doesn't read as a
   solid white square; just brighten the border and the tick mark
   so the selection is visible against the screen body behind it.
   Per May 2026 feedback: "make it a subtle tick, transparent box". */
.ds-tile-bulkbox.is-checked {
  background: rgba(0, 0, 0, 0.30);
  border-color: rgba(255, 255, 255, 0.80);
}

.ds-empty {
  padding: 60px;
  text-align: center;
  color: var(--aq-text-faint);
  font-size: 13px;
  background: var(--aq-surface);
  border: 1px solid var(--aq-line);
  border-radius: 10px;
}

/* ── Detail panel ─────────────────────────────────────────────── */
/* Match the .aq-card surface used by the dashboard so the right pane
   reads as the same material family as the rest of the CMS — gradient
   surface, soft inner edge highlight, drop shadow. Per May 2026
   feedback. */
.ds-detail {
  position: relative;
  /* Apple pass (Jun 2026): the panel sits flush on the page canvas —
     no card surface colour, no border, no shadow. Just the content on
     the same background as the rest of the page. */
  background: transparent;
  border: 0;
  border-radius: var(--aq-r-lg);
  padding: 0 0 0 4px;
  box-shadow: none;
  display: flex;
  flex-direction: column;
  gap: 16px;
  align-self: flex-start;
  position: sticky;
  top: 16px;
}
.ds-detail-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}
.ds-detail-head h3 {
  margin: 4px 0 1px;
  font-family: var(--aq-ff-display);
  font-size: 16px;
  font-weight: 500;
  color: var(--aq-text);
  letter-spacing: -0.005em;
}
.ds-detail-id {
  font-family: var(--aq-ff-mono);
  font-size: 11px;
  font-weight: 500;
  color: var(--aq-text-faint);
  letter-spacing: 0.06em;
}
.ds-detail-site {
  font-size: 11.5px;
  color: var(--aq-text-faint);
}

.ds-preview {
  /* Apple pass (Jun 2026): no bordered frame around the preview — it
     was a box nested inside the detail card (boxes-on-boxes). The
     preview body fills the space flush with just its own rounding. */
  background: transparent;
  border: 0;
  border-radius: 10px;
  padding: 0;
  aspect-ratio: 16 / 10;
}

/* Hover-zoom popup entrance.
   Comparison palette — swap the cubic-bezier + duration to taste:
     A) Linear / Vercel  220ms cubic-bezier(0.16, 1, 0.3, 1)
        "ease-out-expo". Quick start, long gentle settle. Confident.
     B) Apple HIG        200ms cubic-bezier(0.4, 0, 0.2, 1)
        Standard ease. Smooth, symmetric, no character. Safe default.
     C) Material 3       260ms cubic-bezier(0.05, 0.7, 0.1, 1)
        Emphasized decelerate. Slower start, very pronounced settle.
     D) macOS Quick Look 260ms cubic-bezier(0.34, 1.56, 0.64, 1)  ← active
        Spring with overshoot. Curve y > 1 mid-flight = scale briefly
        passes 1.0 before settling. Reads as magnetic / tactile.
   Scale delta 0.94→1.0 — large enough to read as a growth gesture but
   small enough to feel like a settled UI rather than a cartoon pop.
   transform-origin set on the element inline so the popup grows FROM
   the source preview (left edge if it sits to the right of source,
   right edge if it sits to the left).
   prefers-reduced-motion users get instant appearance — opacity only,
   no transform, no duration. */
/* Hover-zoom popup — spring entrance is driven imperatively by
   window.AquaOSSpring (see /shared/spring.js) in displays.jsx's
   useLayoutEffect, so there's NO CSS animation on this class.
   The class exists only to hold the will-change hint and the
   reduced-motion fallback. */
.ds-hover-popup {
  will-change: transform, opacity;
}
@media (prefers-reduced-motion: reduce) {
  .ds-hover-popup { opacity: 1 !important; transform: none !important; }
}
/* Hero variant — gets a slightly thicker padding + a deeper border
   radius so it carries the panel like a featured image rather than
   a reference thumbnail. Per May 2026 follow-up. */
.ds-preview.is-hero {
  aspect-ratio: 16 / 9;
  padding: 0;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
.ds-preview.is-portrait { aspect-ratio: 9 / 14; max-height: 380px; margin: 0 auto; width: 70%; }
.ds-preview-body {
  height: 100%;
  background: linear-gradient(135deg, oklch(0.30 0.06 220) 0%, oklch(0.18 0.04 240) 100%);
  border-radius: 4px;
  padding: 16px 18px;
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: flex-end;
}
.ds-preview-body.offline {
  background: linear-gradient(135deg, #15161a, #0a0b0d);
  align-items: center;
  justify-content: center;
}
.ds-preview-body.idle {
  background: linear-gradient(135deg, #1b1c1f, #131316);
  align-items: center;
  justify-content: center;
}
.ds-preview-pattern {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
}
.ds-preview-content {
  position: relative;
  z-index: 1;
}
.ds-preview-eyebrow {
  font-family: var(--aq-ff-mono);
  font-size: 9.5px;
  color: rgba(255,255,255,0.6);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin-bottom: 4px;
}
.ds-preview-title {
  font-family: var(--aq-ff-display);
  font-size: 18px;
  font-weight: 500;
  color: white;
  letter-spacing: -0.01em;
  line-height: 1.2;
}
.ds-preview-fallback {
  font-family: var(--aq-ff-mono);
  font-size: 10px;
  color: var(--aq-text-faint);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* Status + last-seen overlay metadata — sit on top of the preview
   as small frosted-glass pills rather than as a separate row below. */
.ds-preview-overlay {
  position: absolute;
  z-index: 2;
  pointer-events: none;
  display: inline-flex;
  align-items: center;
  font-family: var(--aq-ff-mono);
  font-size: 9px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.88);
  background: rgba(0, 0, 0, 0.40);
  border: 1px solid rgba(255, 255, 255, 0.08);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  padding: 3px 7px;
  border-radius: 999px;
}
.ds-preview-overlay--status {
  top: 10px;
  left: 10px;
  /* Status pill brings its own coloured background — let it render
     unaltered. Wrapper just provides positioning. */
  background: transparent;
  border: 0;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  padding: 0;
}
.ds-preview-overlay--lastseen {
  bottom: 10px;
  right: 10px;
}

/* Compact action surface. Daily-driver actions (Manage species,
   Launch kiosk) sit side-by-side. A subtle gear icon to the right
   opens a popover for the long tail of operational actions
   (Edit screen, Restart, Device lifecycle, Swap). */
.ds-detail-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.ds-action-primary-row {
  position: relative;       /* anchor for the absolute popover */
  display: flex;
  align-items: center;
  gap: 6px;
}
.ds-action-primary-row .ds-btn {
  flex: 1 1 0;
  justify-content: center;
  white-space: nowrap;
  min-width: 0;
}

/* Subtle gear icon — sized to match button height so the row reads
   as one strip. Faded by default; hover/active picks up the same
   surface treatment as the secondary buttons. */
.ds-iconbtn-settings {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: 7px;
  /* Borderless — matches the Manage species / Launch kiosk buttons
     which carry tint alone, no outline. */
  background: var(--aq-surface-2);
  border: 0;
  color: var(--aq-text-dim);
  display: inline-grid;
  place-items: center;
  cursor: pointer;
  transition: background-color 140ms ease, color 140ms ease;
}
.ds-iconbtn-settings:hover {
  color: var(--aq-text);
  background: rgba(255,255,255,0.10);
}
.ds-iconbtn-settings[aria-expanded="true"] {
  color: var(--aq-text);
  background: var(--aq-surface-2);
}

/* Local .ds-settings-menu* styles removed — the gear icon now opens
   the shared .aq-popup-modal/.aq-popup-card (defined in styles.css)
   so popup windows across the app share one visual language. */

.ds-section h4 {
  margin: 0 0 10px;
  font-family: var(--aq-ff-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--aq-text-faint);
}

.ds-kv-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px 12px;
}
.ds-kv-grid > div {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 12px;
}
.ds-kv-grid > div > span {
  color: var(--aq-text-faint);
  font-size: 10.5px;
  font-family: var(--aq-ff-mono);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.ds-kv-grid > div > b {
  font-weight: 500;
  color: var(--aq-text);
  font-size: 12px;
}
.ds-mono { font-family: var(--aq-ff-mono); font-size: 11.5px !important; letter-spacing: 0.04em; }

.ds-events { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.ds-events li {
  display: flex;
  gap: 10px;
  font-size: 11.5px;
  color: var(--aq-text-dim);
  line-height: 1.5;
}
.ds-events em { color: var(--aq-text); font-style: normal; }
.ds-event-time {
  width: 60px;
  flex-shrink: 0;
  font-family: var(--aq-ff-mono);
  font-size: 10.5px;
  color: var(--aq-text-faint);
}
.ds-status-idle {
  background: none; border: 0; padding: 0; font: inherit;
  color: var(--aq-warn); font-weight: 500; cursor: pointer;
}
.ds-status-idle:hover { text-decoration: underline; }

/* ── Schedules manager (Model 2, 2026-06-12) ──────────────────── */
.ds-sched-row {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 4px;
  border-bottom: 1px solid var(--aq-line-soft);
  cursor: pointer; color: var(--aq-text-dim);
  transition: background var(--aq-d-1) var(--aq-ease);
}
.ds-sched-row:hover { background: var(--aq-surface-2); }
.ds-sched-label {
  display: flex; align-items: center;
  font-size: 10.5px; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--aq-text-faint); margin-bottom: 8px;
}
.ds-sched-block {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 0; flex-wrap: wrap;
}
.ds-sched-days { display: flex; gap: 3px; }
.ds-sched-day {
  width: 28px; height: 26px; border-radius: 6px;
  border: 1px solid var(--aq-line); background: transparent;
  color: var(--aq-text-faint); font: 500 10.5px Inter, var(--aq-ff-sans);
  cursor: pointer;
  transition: background var(--aq-d-1) var(--aq-ease), color var(--aq-d-1) var(--aq-ease);
}
.ds-sched-day:hover { color: var(--aq-text); }
.ds-sched-day.is-on {
  background: var(--aq-accent-soft);
  border-color: var(--aq-accent-line);
  color: var(--aq-text);
}
.ds-sched-x {
  width: 24px; height: 24px; border-radius: 6px; border: 0;
  background: transparent; color: var(--aq-text-faint);
  font-size: 14px; cursor: pointer; line-height: 1;
}
.ds-sched-x:hover { color: var(--aq-danger); }
.ds-sched-screen {
  display: flex; align-items: center; gap: 9px;
  padding: 6px 2px; font-size: 12.5px; cursor: pointer;
  border-bottom: 1px solid var(--aq-line-soft);
}
.ds-sched-screen:last-child { border-bottom: 0; }
.ds-sched-screen input { accent-color: var(--aq-accent); }
