/* ════════════════════════════════════════════════════════════════════
   Family Dashboard — shared CSS

   Theme colors are exposed as CSS custom properties on :root and swapped
   at runtime by JS (`applyTheme()` writes the property block based on the
   active profile's theme). Add a new theme by adding it to state.themes
   in admin — no CSS edits required.
   ════════════════════════════════════════════════════════════════════ */

:root {
  --bg: #eff6ff;
  --panel: #ffffff;
  --text: #1e3a8a;
  --muted: #1e40af;
  --accent: #2563eb;
  /* Default warm/cool accents so gradients/glows still render before
     applyTheme() runs (or if JS fails). applyTheme overrides these
     based on the active profile's theme. */
  --accent-warm: #f59e0b;
  --accent-cool: #06b6d4;
  --border: #bfdbfe;
  --inset: #dbeafe;
  --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.04);
  --radius: 14px;
}

* { box-sizing: border-box; }
html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; }

/* ─── Layout ────────────────────────────────────────────────────── */
/* Vertical chrome (top/bottom padding) is intentionally tight so the
   .page fits inside a 1080p viewport without scrolling once Weather +
   7-day forecast + chat fill out. Bottom padding is small because the
   chat card already has its own internal padding.

   At desktop widths, .page becomes a flex column pinned to viewport
   height so the chat card can flex-grow to fill all empty space below
   the weather row — see @media (min-width: 901px) lower in the file.
   On narrow viewports the page reverts to natural block flow so the
   user gets a normal scrolling stack. */
.page {
  max-width: 1200px;
  margin: 0 auto;
  padding: 14px 28px 16px;
}

header.page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 12px;
}
.brand { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; }
.brand .greeting { color: var(--muted); font-weight: 500; margin-left: 6px; font-size: 16px; }

.profile-chip {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--panel); border: 1px solid var(--border);
  padding: 6px 12px; border-radius: 999px;
  font-size: 13px; font-weight: 600;
  box-shadow: var(--shadow);
}
.profile-chip button {
  background: transparent; border: 0; color: var(--muted);
  font-size: 20px; padding: 4px 6px; margin-left: 2px;
  line-height: 1; border-radius: 6px;
  cursor: pointer;
}
.profile-chip button:hover { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }

/* ─── Search bar ────────────────────────────────────────────────── */
.search-row {
  position: relative;
  margin-bottom: 12px;
  display: flex; align-items: stretch;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.search-row .search-prefix {
  display: flex; align-items: center;
  padding: 0 16px;
  font-size: 13px; font-weight: 700;
  /* Use accent-warm (theme's amber/champagne/gold) so the prefix
     reads as a "warning chip" — same vibe as the bright stat numbers
     in the Unraid Fallout dashboard. Block of yellow at the top of
     the page anchors the warm palette through the whole UI. */
  color: var(--accent-warm);
  background: color-mix(in srgb, var(--accent-warm) 14%, var(--panel));
  border-right: 1px solid var(--border);
  text-transform: uppercase; letter-spacing: 1px;
  white-space: nowrap;
  flex-shrink: 0;
  text-shadow:
    0 0 2px color-mix(in srgb, var(--accent-warm) 70%, transparent),
    0 0 8px color-mix(in srgb, var(--accent-warm) 40%, transparent);
}
.search-row input {
  flex: 1; min-width: 0;
  font-size: 18px;
  padding: 12px 22px;
  border: 0;
  background: transparent;
  color: var(--text);
  outline: none;
}
/* Rotating placeholder is meant to read like a dad joke caption while the
   input is empty — italic + slightly muted so it doesn't fight the user's
   own typed text once they start typing (placeholder vanishes anyway). */
.search-row input::placeholder {
  font-style: italic;
  color: var(--muted);
  opacity: 1;
}
.search-row .hint {
  align-self: center;
  padding-right: 18px;
  font-size: 11px; color: var(--muted); pointer-events: none;
  flex-shrink: 0;
}
/* Hovering the "Google search" prefix gets a help cursor so the user
   knows it's interactive (3-second hover reveals the bangs tooltip). */
.search-row .search-prefix { cursor: help; }

/* ── Search bangs tooltip ────────────────────────────────────────
   Floating panel that appears 3s after the user hovers the prefix
   label. Lists every configured search bang with a friendly label
   derived from its target host. Disappears on mouse-out. Positioned
   in JS via top/left (relative to the document) so it floats above
   the rest of the page chrome. */
.bangs-tooltip {
  position: absolute;
  z-index: 200;
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow:
    0 12px 40px rgba(0,0,0,0.25),
    0 0 30px color-mix(in srgb, var(--accent) 22%, transparent);
  padding: 14px 16px;
  min-width: 240px;
  max-width: 360px;
  font-size: 13px;
  pointer-events: none;
  animation: bangs-tooltip-in 200ms ease-out;
}
@keyframes bangs-tooltip-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0);    }
}
.bangs-tooltip-head {
  font-size: 11px; font-weight: 800;
  text-transform: uppercase; letter-spacing: 1.2px;
  color: var(--accent-warm);
  margin-bottom: 4px;
}
.bangs-tooltip-hint {
  font-size: 11px; color: var(--muted); font-style: italic;
  margin-bottom: 10px;
}
.bangs-list {
  display: flex; flex-direction: column; gap: 6px;
}
.bangs-row {
  display: flex; align-items: baseline; gap: 12px;
}
.bangs-key {
  flex: 0 0 32px;
  font-family: ui-monospace, "SF Mono", Consolas, monospace;
  font-size: 12px; font-weight: 700;
  background: var(--inset);
  border: 1px solid var(--border);
  color: var(--accent);
  padding: 2px 7px;
  border-radius: 6px;
  text-align: center;
}
.bangs-desc {
  flex: 1; min-width: 0;
  color: var(--text);
}

/* ─── Tile grid ─────────────────────────────────────────────────── */
.grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 14px;
}
.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 18px;
  box-shadow: var(--shadow);
}
.card h2 {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--muted);
  margin: 0 0 8px;
}

.col-12 { grid-column: span 12; }
.col-8  { grid-column: span 8; }
.col-6  { grid-column: span 6; }
.col-4  { grid-column: span 4; }
@media (max-width: 900px) {
  .col-8, .col-6, .col-4 { grid-column: span 12; }
}

/* ── Desktop full-height layout ────────────────────────────────────
   At >=901px the page becomes a flex column anchored to the viewport
   height, the grid takes all leftover vertical space, and the chat
   card's row uses 1fr so it absorbs whatever weather + quick-links
   left behind. Result: chat fills the bottom of the screen on every
   resolution — short on 1080p, tall on 4K. */
@media (min-width: 901px) {
  .page {
    display: flex;
    flex-direction: column;
    /* Lock the page to viewport height (not min-height) so a long chat
       history can't grow the .page past the viewport — the chat-log
       inside is overflow:auto and absorbs the overflow as a scroll
       area instead of pushing the whole dashboard down. */
    height: 100vh;
    height: 100dvh;
  }
  .grid {
    flex: 1 1 auto;
    min-height: 0;
    /* Row 1 sized by content (weather + quick-links), row 2 (chat)
       takes the rest. Subsequent rows (none today) auto-size. */
    grid-template-rows: auto 1fr;
  }
  /* Chat card needs its own flex chain so the chat-log inside can
     flex-grow without being capped by the card's content height. */
  #chatCard {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }
  #chatCard .chat {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
  }
  #chatCard .chat-log {
    flex: 1 1 auto;
    /* Override the clamp() max-height — at desktop widths chat-log
       grows to fill available space and scrolls internally when
       text overflows. */
    max-height: none;
    min-height: 0;
  }
  #chatCard .chat-form { flex: 0 0 auto; }
}

/* ─── Quick links ───────────────────────────────────────────────── */
/* 3-column grid. minmax(88px, 1fr) means three tiles fit comfortably
   inside the col-4 card's content area (~336px wide on a 1200px page),
   with 8px gaps. Up to 9 links total (max_quick_links default = 9). */
.quick-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
  gap: 8px;
}
.quick-link {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px;
  padding: 10px 6px;
  background: var(--inset);
  border: 1px solid var(--border);
  border-radius: 12px;
  font-size: 12px;
  font-weight: 600;
  text-align: center;
  transition: transform 120ms, border-color 120ms;
  word-break: break-word;
  position: relative;        /* anchors the delete badge in edit mode */
}
/* Per-tile edit-mode badges — both hidden by default, shown when the
   parent .quick-grid has the .editing class. Edit (✎) lives top-left
   in accent blue; delete (✕) lives top-right in red. Identical layout
   on either side of the tile. Both stopPropagation in JS so clicking
   them doesn't bubble up to the underlying <a>. */
.quick-link-edit,
.quick-link-del {
  position: absolute;
  top: -8px;
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 2px solid var(--panel);
  color: white;
  font-size: 12px; font-weight: 700;
  line-height: 1;
  cursor: pointer;
  display: none;
  align-items: center; justify-content: center;
  z-index: 2;
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
  padding: 0;
  transition: transform 120ms, background 120ms;
}
.quick-link-edit { left: 3px;  background: var(--accent); }
.quick-link-del  { right: 3px; background: #ef4444; }
.quick-link-edit:hover { transform: scale(1.15); background: color-mix(in srgb, var(--accent) 80%, black); }
.quick-link-del:hover  { transform: scale(1.15); background: #dc2626; }
.quick-grid.editing .quick-link-edit,
.quick-grid.editing .quick-link-del { display: inline-flex; }
.quick-grid.editing .quick-link {
  /* Subtle "you're in edit mode" cue without going overboard. */
  border-color: color-mix(in srgb, var(--accent-warm) 50%, var(--border));
  cursor: default;
  padding-top: 26px;  /* push content below the top badges (22px h + 4px gap) */
}
.quick-grid.editing .quick-link:hover { transform: none; }
/* When edit mode is on, shrink the tile's icon + label slightly so the
   ✎ (top-left) and ✕ (top-right) badges don't overlap the artwork.
   transform: scale keeps layout dimensions identical so the grid
   doesn't reflow when the user toggles edit. */
.quick-grid.editing .quick-link .favicon,
.quick-grid.editing .quick-link .emoji {
  transform: scale(0.78);
}
.quick-grid.editing .quick-link > div:not([class]) {
  /* The label sits in a bare <div> after the icon. Shrink + tighten
     so it has room above for the badges. */
  font-size: 11px;
  transform: scale(0.92);
}
.quick-link:hover { transform: translateY(-2px); border-color: var(--accent); }
.quick-link .emoji { font-size: 22px; line-height: 1; }
.quick-link .favicon {
  width: 26px; height: 26px;
  object-fit: contain;
  border-radius: 6px;
  background: white;        /* favicon PNGs often have transparent bg
                                designed for white pages — give them a
                                white card so dark themes still see them */
  padding: 2px;
}
.quick-link.add {
  border-style: dashed;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.quick-link.add:hover { color: var(--accent); }

/* ─── Weather widget ───────────────────────────────────────────── */
.weather-head {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 14px;
}
.weather-head .now {
  display: flex; align-items: center; gap: 10px;
}
.weather-head .now .icon { font-size: 36px; line-height: 1; }
.weather-head .now .temp { font-size: 30px; font-weight: 700; line-height: 1; letter-spacing: -0.5px; }
.weather-head .now .desc { font-size: 13px; color: var(--muted); }
.weather-head .meta { font-size: 12px; color: var(--muted); text-align: right; }

/* ── Weather alert pill ───────────────────────────────────────────
   Lives in the middle of the weather-head between current temp and
   humidity/wind. Hidden when there are no active NWS alerts. Severity
   drives the background color so a glance reads urgency without
   parsing text. Pulses subtly to draw attention without becoming a
   nuisance. Click → modal listing every active alert in full. */
.weather-alert {
  display: inline-flex; align-items: center; gap: 8px;
  background: #ef4444; color: white;
  border: none;
  padding: 8px 14px;
  border-radius: 999px;
  font-size: 13px; font-weight: 700;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(239, 68, 68, 0.45);
  animation: weather-alert-pulse 2.4s ease-in-out infinite;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.weather-alert .weather-alert-icon { font-size: 15px; line-height: 1; }
.weather-alert .weather-alert-event { letter-spacing: 0.3px; }
.weather-alert .weather-alert-more {
  background: rgba(255,255,255,0.25);
  padding: 2px 7px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 800;
}
.weather-alert:hover { transform: scale(1.04); }
@keyframes weather-alert-pulse {
  0%, 100% { box-shadow: 0 4px 14px rgba(239, 68, 68, 0.40); }
  50%      { box-shadow: 0 4px 22px rgba(239, 68, 68, 0.75); }
}
/* Severity-driven palette. Severe + Extreme share red (default).
   Moderate steps down to orange; Minor to yellow. Unknown falls back
   to the default red. */
.weather-alert.sev-Moderate {
  background: #f97316;
  box-shadow: 0 4px 14px rgba(249, 115, 22, 0.45);
}
.weather-alert.sev-Moderate:hover { box-shadow: 0 4px 22px rgba(249, 115, 22, 0.75); }
.weather-alert.sev-Minor {
  background: #eab308;
  color: #1f2937;
  box-shadow: 0 4px 14px rgba(234, 179, 8, 0.45);
}
.weather-alert.sev-Minor:hover { box-shadow: 0 4px 22px rgba(234, 179, 8, 0.75); }
.weather-alert.sev-Minor .weather-alert-more { background: rgba(0,0,0,0.18); }

/* Alerts modal — wider than the default modals so the long NWS
   description doesn't read like a pillar. Each alert is its own
   colored card so multiple-alerts cases stay scannable. */
.weather-alerts-modal { max-width: none; }
.modal:has(.weather-alerts-modal) { max-width: 720px; }
.weather-alert-card {
  border: 1px solid var(--border);
  border-left-width: 6px;
  border-radius: 10px;
  padding: 14px 16px;
  margin-top: 12px;
  background: var(--inset);
  text-align: left;
}
.weather-alert-card.sev-Severe,
.weather-alert-card.sev-Extreme  { border-left-color: #ef4444; }
.weather-alert-card.sev-Moderate { border-left-color: #f97316; }
.weather-alert-card.sev-Minor    { border-left-color: #eab308; }
.weather-alert-card.sev-Unknown  { border-left-color: var(--muted); }
.weather-alert-card-head {
  display: flex; align-items: center; gap: 8px;
  font-weight: 800; font-size: 16px;
  margin-bottom: 4px;
}
.weather-alert-card-sev {
  margin-left: auto;
  font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 1px;
  color: var(--muted);
  border: 1px solid var(--border);
  padding: 2px 8px;
  border-radius: 999px;
}
.weather-alert-card-area {
  font-size: 12px; color: var(--muted); font-style: italic;
  margin-bottom: 8px;
}
.weather-alert-card-headline {
  font-size: 14px; font-weight: 600;
  margin-bottom: 8px;
}
.weather-alert-card-body {
  font-size: 13px; line-height: 1.45; color: var(--text);
  white-space: pre-wrap;
  margin-bottom: 8px;
  max-height: 240px;
  overflow-y: auto;
}
.weather-alert-card-instr {
  font-size: 13px; line-height: 1.4;
  background: color-mix(in srgb, var(--accent-warm) 12%, transparent);
  padding: 8px 10px; border-radius: 6px;
  margin-bottom: 6px;
}
.weather-alert-card-expires {
  font-size: 11px; color: var(--muted); font-style: italic;
}

.weather-strip {
  display: flex; gap: 4px;
  background: var(--inset); border: 1px solid var(--border);
  border-radius: 10px; padding: 10px 6px;
  margin-top: 10px; position: relative;
}
.weather-strip .label-strip {
  position: absolute; top: 4px; left: 10px;
  font-size: 9px; font-weight: 700;
  color: var(--muted); letter-spacing: 1px; text-transform: uppercase;
  pointer-events: none;
}
.weather-cell {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 12px 4px 6px;
}
.weather-cell .when { font-size: 11px; font-weight: 600; color: var(--muted); white-space: nowrap; }
.weather-cell.now .when { color: var(--text); }
.weather-cell .icon { font-size: 24px; line-height: 1; }
.weather-cell .t   { font-size: 15px; font-weight: 700; }
.weather-cell .lo  { color: var(--muted); font-weight: 500; margin-left: 4px; }
.weather-cell .pop { font-size: 10px; color: var(--accent); min-height: 12px; }

/* ── Compact mode for short viewports (1080p-class displays) ──────
   On 4K and 1440p the weather + quick-links cards keep their natural
   sizing. On 1080p the viewport is short enough that we deliberately
   tighten them so the chat card has more vertical room to grow into.
   Threshold of 1000px viewport height covers a 1080p monitor with a
   typical browser chrome (titlebar + tabs + URL + bookmarks ≈ 140-200px),
   while leaving 1440p (≈1300px usable) and 4K (≈2020px usable) alone. */
@media (max-height: 1000px) {
  /* Tighter card chrome — saves padding without changing the design. */
  #weatherCard.card,
  .card:has(> .quick-grid) { padding: 10px 14px; }
  #weatherCard.card h2,
  .card:has(> .quick-grid) h2 { margin: 0 0 6px; }

  /* Weather: smaller hero + tighter strip cells. */
  .weather-head .now .icon { font-size: 28px; }
  .weather-head .now .temp { font-size: 24px; }
  .weather-head .now .desc { font-size: 12px; }
  .weather-head .meta { font-size: 11px; }
  .weather-strip { padding: 8px 6px; margin-top: 6px; }
  .weather-cell { padding: 9px 4px 4px; gap: 2px; }
  .weather-cell .icon { font-size: 20px; }
  .weather-cell .t { font-size: 14px; }
  .weather-cell .pop { font-size: 9px; min-height: 10px; }

  /* Quick links: smaller tiles, tighter rows. */
  .quick-grid { gap: 8px; }
  .quick-link { padding: 10px 6px; gap: 4px; font-size: 12px; }
  .quick-link .emoji { font-size: 22px; }
  .quick-link .favicon { width: 26px; height: 26px; }
}

/* ─── Chat ──────────────────────────────────────────────────────── */
/* Card-header row — title on the left, action button tucked on the
   right. Used by chat (Clear) and quick-links (Edit). Replaces the
   bare <h2> in those cards so we have a stable spot to anchor the
   action without disturbing the rest of the grid. */
.chat-head, .card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px;
  margin: 0 0 8px;
}
.chat-head h2, .card-head h2 { margin: 0; }
.chat-clear, .head-action {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: 11px; font-weight: 700;
  letter-spacing: 0.6px; text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 999px;
  cursor: pointer;
  transition: color 120ms, border-color 120ms, background 120ms;
}
.chat-clear:hover, .head-action:hover { color: var(--accent); border-color: var(--accent); }
.chat-clear[hidden] { display: none; }
/* Active state for toggle-style head buttons (e.g. Quick Links Edit
   when the user is currently in edit mode). Filled accent so it's
   obvious you're in a special mode. */
.head-action.active {
  color: white;
  background: var(--accent);
  border-color: var(--accent);
}

/* The chat is a normal block flow inside the card — chat-log on top,
   chat-form below, both sized by their natural content. We used to use
   flex:1 on chat-log + min-height on .chat which double-counted heights
   and caused the form to overflow the card by ~9px. Plain block flow
   plus a min-height on chat-log itself behaves predictably. */
.chat {
  display: block;
}
/* Chat log scales with viewport height so a 1080p screen doesn't push
   the rest of the dashboard off-page when chat fills up, while a 4K
   screen gets a noticeably taller chat area. clamp() floors give us
   a usable size on shorter viewports too. */
.chat-log {
  min-height: clamp(140px, 18vh, 200px);
  max-height: clamp(180px, 26vh, 320px);
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 10px;
  padding: 6px 2px 8px;
}
.chat-msg {
  padding: 10px 14px; border-radius: 14px; font-size: 14px; line-height: 1.5;
  white-space: pre-wrap; word-wrap: break-word;
  max-width: 85%;
}
.chat-msg.user      { align-self: flex-end; background: var(--accent); color: white; border-bottom-right-radius: 4px; }
.chat-msg.assistant { align-self: flex-start; background: var(--inset); color: var(--text); border-bottom-left-radius: 4px; }
.chat-msg.error     { align-self: stretch; background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; font-size: 12px; }

/* Typing indicator — shown while we're waiting on the first Ollama token.
   Three pulsing dots like an iMessage typing bubble, plus a rotating
   cooking-metaphor caption ("Preheating the oven...") for ambient dad
   humor while the model thinks. Replaced in-place once content streams. */
.chat-msg.thinking {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 14px;
}
.typing-dots {
  display: inline-flex; gap: 5px; align-items: center;
  flex-shrink: 0;
}
.typing-dots span {
  width: 7px; height: 7px;
  background: var(--muted);
  border-radius: 50%;
  animation: typing-bounce 1.2s infinite ease-in-out;
}
.typing-dots span:nth-child(2) { animation-delay: 0.15s; }
.typing-dots span:nth-child(3) { animation-delay: 0.3s; }
@keyframes typing-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
  30% { transform: translateY(-5px); opacity: 1; }
}
.thinking-text {
  font-size: 13px; color: var(--muted); font-style: italic;
  transition: opacity 250ms;
}
/* Butler-style empty state: italic serif and noticeably bigger so it
   reads as polite copy rather than a placeholder. The actual phrase
   is rotated by JS (BUTLER_PHRASES) — what's hardcoded in HTML is just
   the no-JS fallback. */
.chat-empty {
  color: var(--text);
  font-family: Georgia, Cambria, "Times New Roman", serif;
  font-style: italic;
  font-size: clamp(15px, 1.8vh, 19px);
  line-height: 1.4;
  text-align: center;
  padding: 32px 24px;
  opacity: 0.85;
}
/* Grid layout for chat-form — minmax(0,1fr) is the bulletproof pattern that
   keeps the input from pushing the form wider than its parent. With flex,
   the input's intrinsic min-width can balloon the row past the chat card.
   With grid + minmax(0,...), the column is allowed to shrink to 0. */
.chat-form {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  border-top: 1px solid var(--border); padding-top: 6px;
  width: 100%;
  box-sizing: border-box;
}
.chat-form input {
  width: 100%; min-width: 0;
  padding: 10px 14px; border: 1px solid var(--border); border-radius: 10px;
  background: var(--bg); color: var(--text); font-size: 14px; outline: none;
}
.chat-form input:focus { border-color: var(--accent); }
.chat-form button {
  background: var(--accent); color: white; border: 0;
  padding: 10px 18px; border-radius: 10px; font-weight: 600; font-size: 13px;
}
.chat-form button:disabled { opacity: 0.5; cursor: not-allowed; }

/* ─── Modal (first-launch + edit dialogs) ───────────────────────── */
.modal-backdrop {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.45);
  display: flex; align-items: center; justify-content: center;
  z-index: 100;
  padding: 20px;
}
.modal {
  background: var(--panel); color: var(--text);
  border-radius: 18px; padding: 28px;
  max-width: 480px; width: 100%;
  box-shadow: 0 20px 60px rgba(0,0,0,0.25);
}
.modal h2 { margin-top: 0; font-size: 22px; }
.modal p { color: var(--muted); font-size: 14px; line-height: 1.5; }
.modal label { display: block; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-top: 14px; margin-bottom: 6px; }
.modal input[type=text], .modal input[type=url] {
  width: 100%;
  padding: 10px 14px; border: 1px solid var(--border); border-radius: 10px;
  font-size: 16px; background: var(--bg); color: var(--text); outline: none;
}
.modal input:focus { border-color: var(--accent); }

.theme-row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; }
.theme-swatch {
  width: 56px; height: 56px; border-radius: 12px;
  border: 3px solid transparent; cursor: pointer;
  position: relative;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700; text-transform: capitalize;
  transition: transform 120ms;
}
.theme-swatch:hover { transform: scale(1.05); }
.theme-swatch.selected { border-color: var(--accent); }

/* ── Link icon picker ───────────────────────────────────────────
   Single 6-column grid. The first tile is always the Upload affordance
   (dashed border, + glyph) — clicking it opens a file picker. The
   remaining tiles are shared uploaded images first, then the curated
   emoji fallbacks. Tiles are explicitly sized so emojis and thumbs are
   always legible.

   Selectors are scoped under `.modal` so they win against the generic
   `.modal button` rule that otherwise shrinks our font + border. */
.link-icon-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 8px;
  margin-top: 6px;
  max-height: 280px;
  overflow-y: auto;
  padding: 4px;
}
.modal .link-icon-pick {
  width: 100%;
  height: 60px;            /* explicit height — won't squish under scroll */
  border-radius: 10px;
  border: 2px solid transparent;
  background: var(--inset);
  font-size: 30px;         /* big enough to make the emoji unambiguous */
  font-weight: 400;
  line-height: 1;
  cursor: pointer;
  transition: transform 100ms, border-color 100ms, background 120ms;
  display: flex; align-items: center; justify-content: center;
  padding: 0;
  overflow: hidden;
  color: var(--text);
}
.modal .link-icon-pick:hover {
  transform: scale(1.08);
  background: color-mix(in srgb, var(--accent) 12%, var(--inset));
}
.modal .link-icon-pick.selected {
  border-color: var(--accent);
  box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 35%, transparent);
}
.modal .link-icon-pick img {
  width: 76%; height: 76%;
  object-fit: contain;
  background: white;
  border-radius: 6px;
  padding: 2px;
}
/* The "+ upload" tile gets a distinct dashed accent border so it
   reads as "add new" rather than "another option". Hovering deepens
   the accent so the affordance is obvious. */
.modal .link-icon-pick-upload {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 2px dashed color-mix(in srgb, var(--accent) 50%, var(--border));
  color: var(--accent);
}
.modal .link-icon-pick-upload:hover {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  border-color: var(--accent);
  transform: scale(1.08);
}
.modal .link-icon-pick-up-plus {
  font-size: 32px;
  font-weight: 700;
  line-height: 1;
}

/* Mascot picker — same vibe as theme swatches but bigger emoji and
   plain panel/border so the focus is on the mascot icon itself. */
.mascot-picker { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
.mascot-pick {
  width: 60px; height: 60px;
  border-radius: 12px;
  border: 3px solid transparent;
  background: var(--inset);
  font-size: 32px;
  line-height: 1;
  cursor: pointer;
  transition: transform 120ms, border-color 120ms;
  display: flex; align-items: center; justify-content: center;
}
.mascot-pick:hover { transform: scale(1.06); }
.mascot-pick.selected {
  border-color: var(--accent);
  box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 35%, transparent);
}
/* "No mascot" tile — same bubble dimensions as the others, but renders
   a small 🚫 above a "No mascot" caption so the option is unambiguous
   instead of a bare prohibition emoji that could read as "broken". */
.mascot-pick-none {
  flex-direction: column;
  gap: 1px;
  font-size: 14px;
}
.mascot-pick-none .mascot-pick-none-icon {
  font-size: 22px;
  line-height: 1;
}
.mascot-pick-none .mascot-pick-none-label {
  font-size: 8.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--muted);
  white-space: nowrap;
  line-height: 1.1;
}

.modal .actions {
  margin-top: 22px;
  display: flex; justify-content: flex-end; gap: 10px;
}
.modal button {
  padding: 10px 18px; border-radius: 10px; font-weight: 600; font-size: 14px;
  border: 1px solid var(--border); background: var(--panel); color: var(--text);
}
.modal button.primary { background: var(--accent); border-color: var(--accent); color: white; }
.modal button:disabled { opacity: 0.5; cursor: not-allowed; }

.profile-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.profile-list button {
  background: var(--inset); border: 1px solid var(--border); color: var(--text);
  padding: 8px 14px; border-radius: 999px; font-size: 13px; font-weight: 600;
}
.profile-list button:hover { border-color: var(--accent); color: var(--accent); }

/* ─── Browser-link / sync code UI ──────────────────────────────────
   Shown in the Settings modal (General tab) and the first-launch
   modal footer.  The .link-code-display wraps the big digit slab +
   countdown timer; .setup-link-code-footer is the subtle "already
   set up elsewhere?" row at the bottom of the Welcome screen. */
.link-code-display {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  margin-top: 4px;
}
.link-code-digits {
  font-size: 30px;
  font-weight: 700;
  letter-spacing: 8px;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  background: var(--inset);
  padding: 10px 20px;
  border-radius: 10px;
  border: 2px solid var(--border);
  line-height: 1;
}
.link-code-timer {
  font-size: 12px;
  color: var(--muted);
}
.link-code-timer.expiring-soon { color: #f97316; }

/* Subtle "enter a code" footer on the Welcome / first-launch modal */
.setup-link-code-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  font-size: 13px;
  color: var(--muted);
}
/* Plain text-link style button — no box, just coloured text */
.link-btn {
  background: none !important;
  border: none !important;
  padding: 0 !important;
  color: var(--accent) !important;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.link-btn:hover { opacity: 0.8; }

/* ─── Mascot helper ─────────────────────────────────────────────────
   Friendly dog that wanders between "safe" spots in the viewport.
   Click → a speech bubble pops up with a random dad joke. Periodically
   does tricks (spin, wag, jump, shake) when stationary. Position is
   set in JS as inline top/left so transitions interpolate cleanly. */
.mascot {
  position: fixed;
  /* Wrapper is sized to hold ONLY the button. The speech bubble is
     positioned absolutely (next rule) so its width isn't bound by
     flex-sizing of the wrapper — earlier this caused the bubble to
     shrink to button-width when the mascot was in a corner spot. */
  width: 64px;
  height: 64px;
  bottom: 90px;        /* default spot — matches EDGE constant in JS */
  right: 90px;
  z-index: 9999;
  pointer-events: none;            /* let clicks pass through gaps */
  /* Slow stroll between spots — picked to feel like a walking pace
     rather than a slide. Combined with the .walking step animation
     on the button it reads as "walking" instead of "teleporting". */
  transition: top 2.4s ease-in-out,
              left 2.4s ease-in-out,
              right 2.4s ease-in-out,
              bottom 2.4s ease-in-out;
}

.mascot-button {
  pointer-events: auto;
  width: 64px; height: 64px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--panel);
  font-size: 36px;
  line-height: 1;
  cursor: pointer;
  box-shadow:
    0 4px 14px rgba(0,0,0,0.35),
    0 0 18px color-mix(in srgb, var(--accent) 35%, transparent);
  animation: mascot-bob 3.2s ease-in-out infinite;
  transition: transform 180ms;
  /* Stops user-selectable; otherwise repeated double-clicks while
     it's moving will highlight the emoji and look weird. */
  user-select: none;
}
.mascot-button:hover {
  transform: scale(1.08) rotate(-4deg);
  animation-play-state: paused;
}
.mascot-button:active { transform: scale(0.95); }
@keyframes mascot-bob {
  0%, 100% { transform: translateY(0) rotate(0); }
  25%      { transform: translateY(-3px) rotate(-2deg); }
  75%      { transform: translateY(-3px) rotate(2deg); }
}

/* ─── Tricks ──────────────────────────────────────────────────────
   Each trick is a one-shot animation we apply by adding a class to
   the mascot button. JS removes the class on `animationend` so the
   trick can be re-triggered later. The bobbing animation pauses
   automatically because the trick animation overrides `transform`. */
.mascot-button.trick-spin    { animation: mascot-spin 0.75s ease-in-out 1; }
.mascot-button.trick-wag     { animation: mascot-wag 0.32s ease-in-out 4; }
.mascot-button.trick-jump    { animation: mascot-jump 0.55s ease-out 1; }
.mascot-button.trick-shake   { animation: mascot-shake 0.18s ease-in-out 5; }
.mascot-button.trick-bow     { animation: mascot-bow 0.6s ease-in-out 1; }

@keyframes mascot-spin {
  from { transform: rotate(0deg) scale(1); }
  50%  { transform: rotate(180deg) scale(1.1); }
  to   { transform: rotate(360deg) scale(1); }
}
@keyframes mascot-wag {
  0%, 100% { transform: rotate(0deg); }
  50%      { transform: rotate(20deg); }
}
@keyframes mascot-jump {
  0%   { transform: translateY(0) scaleY(1); }
  20%  { transform: translateY(0) scaleY(0.85); }   /* anticipation crouch */
  60%  { transform: translateY(-30px) scaleY(1.05); }
  100% { transform: translateY(0) scaleY(1); }
}
@keyframes mascot-shake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-5px) rotate(-3deg); }
  75%      { transform: translateX(5px) rotate(3deg); }
}
@keyframes mascot-bow {
  0%   { transform: scaleY(1); }
  40%  { transform: scaleY(0.7) translateY(8px); }
  100% { transform: scaleY(1); }
}

/* ─── Type-specific tricks ──────────────────────────────────────────── */

/* Stomp (Godzilla) — heavy ground slam, more dramatic than the walk. */
.mascot-button.trick-stomp { animation: mascot-trick-stomp 0.7s ease-in-out 1; }
@keyframes mascot-trick-stomp {
  0%   { transform: translateY(0) scaleY(1); }
  20%  { transform: translateY(-14px) scaleY(0.9); }
  40%  { transform: translateY(0)  scaleY(1.18) scaleX(0.92); }
  55%  { transform: translateY(0)  scaleY(1) scaleX(1); }
  100% { transform: translateY(0) scaleY(1); }
}

/* Flame (Godzilla) — head tilts back as the flame emoji is spawned
   alongside it. The actual fire effect is the spawned .mascot-flame
   element; this is just the body's "breathe out" pose. */
.mascot-button.trick-flame { animation: mascot-trick-flame 1.0s ease-out 1; }
@keyframes mascot-trick-flame {
  0%   { transform: rotate(0deg) scale(1); }
  20%  { transform: rotate(-12deg) scale(1.05); }      /* wind up */
  60%  { transform: rotate(8deg) scale(1.1); }          /* breathe out */
  100% { transform: rotate(0deg) scale(1); }
}

/* Roar (Godzilla) — rapid scale up + back as if roaring. */
.mascot-button.trick-roar { animation: mascot-trick-roar 0.6s ease-in-out 1; }
@keyframes mascot-trick-roar {
  0%, 100% { transform: scale(1); }
  40%      { transform: scale(1.25); }
  60%      { transform: scale(1.18); }
}

/* Blink (Alien) — vertical squash to mimic eyes closing/opening. Runs
   twice so the user sees a clear "blink-blink". */
.mascot-button.trick-blink { animation: mascot-trick-blink 0.6s ease-in-out 2; }
@keyframes mascot-trick-blink {
  0%, 80%, 100% { transform: scaleY(1); }
  85%, 95%      { transform: scaleY(0.15); }
}

/* Pulse (Alien) — breathing scale, the "head growing/shrinking" effect. */
.mascot-button.trick-pulse { animation: mascot-trick-pulse 0.9s ease-in-out 2; }
@keyframes mascot-trick-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.3); filter: brightness(1.2) hue-rotate(20deg); }
}

/* Cheer (legacy lion mascot — kept so the keyframe is still defined
   if any cached state still references it). */
.mascot-button.trick-cheer { animation: mascot-trick-cheer 0.7s ease-in-out 1; }
@keyframes mascot-trick-cheer {
  0%, 100% { transform: rotate(0)    scale(1); }
  20%      { transform: rotate(-15deg) scale(1.1)  translateY(-5px); }
  50%      { transform: rotate(0)    scale(1.18) translateY(-8px); }
  80%      { transform: rotate(15deg)  scale(1.1)  translateY(-5px); }
}

/* Bounce (Basketball) — squash on contact, then a high lift, then
   another squash + return. Reads as a basketball dribbling. */
/* Hide — turtle pulls into his shell. Quick scale-down hold, then
   peeks back out. Kept short so it doesn't stall the trick loop. */
.mascot-button.trick-hide { animation: mascot-trick-hide 1.0s ease-in-out 1; }
@keyframes mascot-trick-hide {
  0%, 100% { transform: scale(1)    translateY(0); }
  35%, 65% { transform: scale(0.55) translateY(4px); }
}

.mascot-button.trick-bounce { animation: mascot-trick-bounce 0.7s ease-in-out 1; }
@keyframes mascot-trick-bounce {
  0%   { transform: translateY(0)   scaleY(1); }
  20%  { transform: translateY(0)   scaleY(0.78) scaleX(1.12); }   /* squash on bounce */
  55%  { transform: translateY(-26px) scaleY(1.1) scaleX(0.92); }   /* peak */
  85%  { transform: translateY(0)   scaleY(0.85) scaleX(1.08); }   /* land squash */
  100% { transform: translateY(0)   scaleY(1); }
}

/* Walking animation — fast bob + alternating tilt that reads as steps.
   Applied during transitions between spots in dog mode. */
.mascot-button.walking {
  animation: mascot-walk 0.34s linear infinite;
}
@keyframes mascot-walk {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  25%      { transform: translateY(-4px) rotate(-3deg); }
  50%      { transform: translateY(0) rotate(3deg); }
  75%      { transform: translateY(-4px) rotate(3deg); }
}

/* Jumping animation — cat-mode override that swaps the walking step for
   a stretched-out leaping pose with alternating tilt. The wrapper does
   the actual arc movement (top/left transition); this class shapes the
   *body* during the jump so it reads as "leaping" not "walking". */
.mascot-button.jumping {
  animation: mascot-leap 0.45s ease-in-out infinite alternate;
}
@keyframes mascot-leap {
  0%   { transform: rotate(-12deg) scaleY(0.92) scaleX(1.04); }
  100% { transform: rotate(12deg)  scaleY(1.08) scaleX(0.96); }
}

/* Stomping — Godzilla's ground-pounding gait. Slow, heavy compress/
   extend on each "step" with no horizontal lean. */
.mascot-button.stomping {
  animation: mascot-stomp 0.7s ease-in-out infinite;
}
@keyframes mascot-stomp {
  0%, 100% { transform: scaleY(1)    translateY(0); }
  30%      { transform: scaleY(0.85) translateY(0); }     /* compress */
  55%      { transform: scaleY(1.08) translateY(-6px); }  /* lift */
  70%      { transform: scaleY(1.05) translateY(0); }     /* slam */
}

/* Pouncing — Lion mascot's bouncy run with side-to-side sway. */
.mascot-button.pouncing {
  animation: mascot-pounce 0.5s ease-in-out infinite;
}
@keyframes mascot-pounce {
  0%, 100% { transform: translateY(0)   rotate(-6deg) scale(1); }
  50%      { transform: translateY(-5px) rotate(6deg)  scale(1.05); }
}

/* Hopping — bunny's bouncy gait. Squashes on the ground, stretches
   tall at the apex, lands with a small recovery squash. Faster and
   more vertical than the cat's leap so it reads as "hopping" not
   "leaping". */
.mascot-button.hopping {
  animation: mascot-hop 0.42s ease-out infinite;
}
@keyframes mascot-hop {
  0%   { transform: translateY(0)    scaleY(0.9) scaleX(1.08); }
  35%  { transform: translateY(-14px) scaleY(1.12) scaleX(0.94); }
  70%  { transform: translateY(0)    scaleY(0.9) scaleX(1.08); }
  100% { transform: translateY(0)    scaleY(1)   scaleX(1);    }
}

/* Burrow — class is added to the button while the wrapper-driven
   dive/pop animation runs. We don't need a per-button keyframe (the
   wrapper transform does the visible work) but the class still has to
   exist so the cleanup remove() finds something to clear. */
.mascot-button.burrow { /* no extra animation; wrapper handles dive/pop */ }

/* Burrow hole — a dark ellipse rendered on document.body at fixed
   position, lined up just under the bunny's feet. Two are spawned per
   burrow walk (one at start, one at end). Grows in on spawn and
   shrinks out via the .mascot-hole-fade modifier. */
.mascot-hole {
  position: fixed;
  width: 52px;
  height: 14px;
  border-radius: 50%;
  background: radial-gradient(ellipse at center, #1d0e04 50%, #0a0502 100%);
  box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.55) inset,
              0 2px 6px rgba(0, 0, 0, 0.35);
  pointer-events: none;
  z-index: 9998;        /* under the mascot wrapper (9999) */
  opacity: 0;
  transform-origin: center;
  animation: mascot-hole-grow 280ms ease-out forwards;
}
.mascot-hole.mascot-hole-fade {
  animation: mascot-hole-shrink 260ms ease-in forwards;
}
@keyframes mascot-hole-grow {
  from { opacity: 0; transform: scaleX(0.2) scaleY(0.4); }
  to   { opacity: 1; transform: scaleX(1)   scaleY(1);   }
}
@keyframes mascot-hole-shrink {
  from { opacity: 1; transform: scaleX(1)   scaleY(1);   }
  to   { opacity: 0; transform: scaleX(0.2) scaleY(0.4); }
}

/* Crawling — turtle's slow, low-bob plod. Tiny vertical wobble + a
   slight forward-leaning tilt that doesn't read as a strut. Combined
   with the wrapper's long top/left transition (params.walkMs ~11s) it
   reads as creeping. */
.mascot-button.crawling {
  animation: mascot-crawl 1.6s ease-in-out infinite;
}
@keyframes mascot-crawl {
  0%, 100% { transform: translateY(0)   rotate(-2deg); }
  50%      { transform: translateY(-2px) rotate(2deg); }
}

/* Rolling — turtle pulls into his shell and rolls. Continuous spin so
   the emoji visually "tumbles." Paired with a much shorter walkMs in
   rollParams so the trip is fast. */
.mascot-button.rolling {
  animation: mascot-roll 0.45s linear infinite;
}
@keyframes mascot-roll {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* Teleport — alien's body shimmers while position fades. The wrapper
   does the opacity fade (set inline in JS); this class adds a subtle
   shimmer pulse on the emoji itself for extra alien vibe. */
.mascot-button.teleport {
  animation: mascot-shimmer 0.4s ease-in-out infinite alternate;
}
@keyframes mascot-shimmer {
  0%   { filter: brightness(1)   hue-rotate(0deg); }
  100% { filter: brightness(1.4) hue-rotate(40deg); }
}

/* Godzilla flame breath — a 🔥 emoji that floats out from the mascot,
   shrinking and fading. The 🦖 glyph faces LEFT on every common emoji
   font (Segoe UI Emoji on Windows, Apple Color Emoji, Google Noto, etc.)
   so flames always exit the LEFT side of the button (the head) and
   travel further left, regardless of where the mascot is on screen.
   We previously flipped this when the mascot was anchor-left, but that
   flipped flames to come out the butt for half the page. */
.mascot-flame {
  position: absolute;
  bottom: 12px;
  right: 56px;       /* anchored just outside the button on the LEFT
                         (right:56px from a 64px wrapper = element's
                         right edge sits ~8px from wrapper's left edge,
                         placing the flame to the left of the head). */
  font-size: 22px;
  pointer-events: none;
  animation: mascot-flame-puff 1.3s ease-out forwards;
  z-index: 1;
}
@keyframes mascot-flame-puff {
  0%   { transform: translate(0, 0)         scale(0.6); opacity: 1; }
  60%  { transform: translate(-50px, -10px) scale(1.4); opacity: 0.8; }
  100% { transform: translate(-100px, -20px) scale(1.7); opacity: 0; }
}
/* Bubble is positioned absolutely relative to the wrapper so its size
   doesn't depend on flexbox sizing of the wrapper. Default position:
   above the button (bottom of bubble = top of button + 10px gap),
   right-aligned with the button (so the tail points at the button's
   top-right corner). Anchor classes flip it for left/top variants. */
.mascot-bubble {
  position: absolute;
  bottom: calc(100% + 10px);
  right: 0;
  pointer-events: auto;
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 12px 16px;
  font-size: 13px;
  line-height: 1.45;
  width: 260px;            /* explicit width so flex sizing of wrapper
                              can't squish it on corner spots */
  box-sizing: border-box;
  box-shadow:
    0 6px 22px rgba(0,0,0,0.35),
    0 0 14px color-mix(in srgb, var(--accent) 18%, transparent);
  animation: mascot-pop 220ms ease-out;
}
/* Speech-bubble tail pointing down at the mascot. Two stacked triangles —
   the outer matches --border, the inner matches --panel. Positioned
   relative to the bubble, near the button (anchor-aware). */
.mascot-bubble::after,
.mascot-bubble::before {
  content: '';
  position: absolute;
  bottom: -9px;
  right: 22px;
  width: 0; height: 0;
  border: 9px solid transparent;
  border-bottom: 0;
}
.mascot-bubble::before {
  border-top-color: var(--border);
}
.mascot-bubble::after {
  bottom: -8px;
  border-top-color: var(--panel);
}

/* Left-anchored: bubble extends RIGHT from the button. Tail points
   down at the button's left edge. */
.mascot.anchor-left .mascot-bubble {
  right: auto;
  left: 0;
}
.mascot.anchor-left .mascot-bubble::before,
.mascot.anchor-left .mascot-bubble::after {
  right: auto;
  left: 22px;
}

/* Top-anchored: button is at top of wrapper, bubble appears BELOW.
   Tail points UP at the button's bottom edge. */
.mascot.anchor-top .mascot-bubble {
  bottom: auto;
  top: calc(100% + 10px);
}
.mascot.anchor-top .mascot-bubble::before,
.mascot.anchor-top .mascot-bubble::after {
  bottom: auto;
  top: -9px;
  border-bottom: 9px solid;
  border-top: 0;
}
.mascot.anchor-top .mascot-bubble::before { border-bottom-color: var(--border); }
.mascot.anchor-top .mascot-bubble::after  { top: -8px; border-bottom-color: var(--panel); }
@keyframes mascot-pop {
  from { transform: translateY(6px) scale(0.85); opacity: 0; }
  to   { transform: translateY(0) scale(1); opacity: 1; }
}

/* ── Mascot drag + release animations ────────────────────────────
   While the user is dragging we suspend the slow stroll transition
   on top/left so the mascot tracks the cursor 1:1 instead of trailing
   behind. The dragging cursor flips to "grabbing" for visual feedback. */
.mascot.dragging {
  transition: none;
  cursor: grabbing;
}
.mascot.dragging .mascot-button {
  cursor: grabbing;
  animation-play-state: paused;
  transform: scale(1.06);
}
.mascot-button { cursor: grab; }

/* Per-type release animations — fire once when the user drops the
   mascot. Each type gets a distinct vibe so drag-and-drop is a
   discoverable little reward instead of a silent no-op. */
.mascot-button.release-dog,
.mascot-button.release-cat {
  /* Petting reaction: gentle head wobble + happy bob, ~6 cycles. */
  animation: mascot-release-pet 0.45s ease-in-out 6;
}
@keyframes mascot-release-pet {
  0%, 100% { transform: translateY(0)    rotate(0); }
  25%      { transform: translateY(-3px) rotate(-7deg); }
  50%      { transform: translateY(-1px) rotate(0); }
  75%      { transform: translateY(-3px) rotate(7deg); }
}

.mascot-button.release-basketball {
  /* Dribbled across a small footprint — each cycle the ball bounces
     up, then lands a little to one side, then up again, then lands on
     the other side. Reads like a sport handle (crossover dribble)
     rather than just bouncing in place. Six cycles of the four-step
     left/center/right zig-zag = ~24 bounces total. */
  animation: mascot-release-dribble 0.9s cubic-bezier(.5,0,.5,1) 6;
}
@keyframes mascot-release-dribble {
  /* Cycle starts at center, goes left, back to center, right, center.
     Each "land" frame squashes scale-y; each "peak" stretches it. */
  0%   { transform: translate(0,    0)   scale(1, 1); }
  10%  { transform: translate(-8px, -22px) scale(0.96, 1.08); }   /* peak left */
  25%  { transform: translate(-16px, 0)   scale(1.1, 0.9);   }   /* land left */
  40%  { transform: translate(0,    -22px) scale(0.96, 1.08); }   /* peak center */
  55%  { transform: translate(0,    0)   scale(1.1, 0.9);   }   /* land center */
  70%  { transform: translate(8px,  -22px) scale(0.96, 1.08); }   /* peak right */
  85%  { transform: translate(16px, 0)   scale(1.1, 0.9);   }   /* land right */
  100% { transform: translate(0,    0)   scale(1, 1); }
}

.mascot-button.release-godzilla {
  /* Angry rumble + big roar shake while he belches flames (spawned
     in JS via spawnFlame x6). */
  animation: mascot-release-rage 0.16s linear 14;
}
@keyframes mascot-release-rage {
  0%, 100% { transform: translate(0, 0)    rotate(0); }
  20%      { transform: translate(-3px, 1px) rotate(-3deg); }
  40%      { transform: translate(3px, -1px) rotate(3deg);  }
  60%      { transform: translate(-2px, 2px) rotate(-2deg); }
  80%      { transform: translate(2px, -2px) rotate(2deg);  }
}

.mascot-button.release-alien {
  /* Quick teleport flickers — opacity flashes with tiny offsets so it
     looks like he's blinking out and back in. Resolves in place. */
  animation: mascot-release-teleport 0.32s ease-out 5;
}
@keyframes mascot-release-teleport {
  0%   { opacity: 1; transform: translate(0, 0)     scale(1); }
  35%  { opacity: 0; transform: translate(-8px, -4px) scale(0.85); }
  70%  { opacity: 0; transform: translate(6px, 4px)   scale(0.9);  }
  100% { opacity: 1; transform: translate(0, 0)       scale(1);    }
}

.mascot-button.release-turtle {
  /* Sleepy turtle — slow rhythmic "breathing" rock while 💤 emojis
     drift up out of his head (spawned in JS as .mascot-release-prop
     .snooze elements). Long cycles + small motion so it reads as a
     contented nap rather than animation-twitch. */
  animation: mascot-release-snooze 1.6s ease-in-out 3;
}
@keyframes mascot-release-snooze {
  0%, 100% { transform: scale(1)    rotate(0); }
  50%      { transform: scale(1.05) rotate(-3deg); }
}

.mascot-button.release-bunny {
  /* Excited hops in place — tighter than the wandering hop so the
     beat reads as energetic rather than ambling. */
  animation: mascot-release-hop 0.32s cubic-bezier(.4,0,.6,1) 7;
}
@keyframes mascot-release-hop {
  0%, 100% { transform: translateY(0)     scale(1, 1); }
  40%      { transform: translateY(-26px) scale(0.92, 1.1); }
  80%      { transform: translateY(0)     scale(1.1, 0.9); }
}

/* Floating "prop" element used by some release animations. Layout
   defaults work for the bunny's carrot (sits next to the head, brief
   pop-in pop-out). Modifier classes (.snooze, etc.) override layout
   and animation for props that need to behave differently. */
.mascot-release-prop {
  position: absolute;
  top: 0; left: 100%;
  margin-left: 4px;
  font-size: 28px;
  pointer-events: none;
  animation: mascot-release-prop-flash 1.8s ease-out forwards;
}
@keyframes mascot-release-prop-flash {
  0%   { opacity: 0; transform: translateY(8px) rotate(-12deg) scale(0.6); }
  20%  { opacity: 1; transform: translateY(0)   rotate(-4deg)  scale(1);   }
  60%  { opacity: 1; transform: translateY(-4px) rotate(8deg)  scale(1.1); }
  100% { opacity: 0; transform: translateY(-12px) rotate(20deg) scale(0.9); }
}
/* Sleeping 💤 — floats up and slightly to the right above the turtle's
   head, fading out as it drifts. JS spawns 4 of these on a stagger so
   the z's come in sequence like a comic-strip nap. */
.mascot-release-prop.snooze {
  top: -6px;
  left: 50%;
  margin-left: 4px;
  font-size: 22px;
  animation: mascot-release-snooze-float 2.4s ease-out forwards;
}
@keyframes mascot-release-snooze-float {
  0%   { opacity: 0; transform: translate(0, 0)        rotate(-12deg) scale(0.5); }
  20%  { opacity: 1; transform: translate(4px, -16px)  rotate(-6deg)  scale(1);   }
  100% { opacity: 0; transform: translate(28px, -64px) rotate(18deg)  scale(1.3); }
}


/* ── Per-type drag FX ────────────────────────────────────────────────
   Floating text particles spawned while the user drags certain mascot
   types (cat → purring, dog → barking). Elements are appended as
   children of the .mascot-button so they naturally sit above it. */

/* Cat vibrates gently when being dragged.
   The .mascot.dragging .mascot-button rule pauses animations with a
   lower-specificity selector (3 classes); this 4-class rule wins and
   keeps the purr rumble running. */
.mascot.dragging .mascot-button.dragging-cat {
  animation: cat-drag-purr 0.09s ease-in-out infinite alternate;
  animation-play-state: running;
  transform: scale(1.06);   /* keep the drag scale from the base rule */
}
@keyframes cat-drag-purr {
  from { transform: scale(1.06) rotate(-1.5deg) translateY(0);    }
  to   { transform: scale(1.06) rotate(1.5deg)  translateY(-1px); }
}

/* Shared layout for all drag-FX floating words. */
.mascot-drag-fx {
  position: absolute;
  bottom: 100%;
  pointer-events: none;
  white-space: nowrap;
  font-weight: 700;
  line-height: 1;
  z-index: 2;
  transform-origin: center bottom;
}

/* ~purr~ words — italic, drifts up slowly. */
.mascot-drag-fx.purr {
  font-size: 11px;
  color: var(--accent-cool, var(--accent));
  font-style: italic;
  animation: drag-fx-purr 1.35s ease-out forwards;
}
@keyframes drag-fx-purr {
  0%   { opacity: 0; transform: translateY(6px)  scale(0.75); }
  15%  { opacity: 1; transform: translateY(0)     scale(1);    }
  80%  { opacity: 1; transform: translateY(-22px) scale(1);    }
  100% { opacity: 0; transform: translateY(-34px) scale(0.9);  }
}

/* "Woof!" words — bold pop, then drifts up. */
.mascot-drag-fx.bark {
  font-size: 13px;
  color: var(--accent);
  animation: drag-fx-bark 0.88s ease-out forwards;
}
@keyframes drag-fx-bark {
  0%   { opacity: 0; transform: translateY(0)    scale(0.5);  }
  18%  { opacity: 1; transform: translateY(-8px) scale(1.2);  }
  60%  { opacity: 1; transform: translateY(-18px) scale(1);   }
  100% { opacity: 0; transform: translateY(-26px) scale(0.8); }
}

/* ════════════════════════════════════════════════════════════════════
   SIDE WIDGETS — opt-in panels that flank the main .page on wide
   viewports.

   Layout: each side is a flex row of 1 or 2 widget-columns. The inner
   column always sits adjacent to the .page (16px gap); the outer
   column extends further out and only renders when there's enough
   horizontal room. Anchored to the page edges via calc(50% + 600px)
   so the widgets follow the centered content instead of glueing to
   the viewport edge — important on ultrawide where there's a lot of
   space between page and screen edge.

   Visibility:
     vw <  1900px → side-widgets hidden entirely (just .page)
     vw >= 1900px → inner column visible (4 slots)
     vw >= 2400px → inner + outer visible (8 slots)
   ════════════════════════════════════════════════════════════════════ */
.side-widgets {
  position: fixed;
  top: 0;
  height: 100vh;
  padding: 24px 0;
  display: flex;
  flex-direction: row;
  gap: 16px;
  overflow-y: auto;
  z-index: 5;          /* above page but below modals (z=100) and dirty edge (z=9998) */
}
/* Anchor to the page edges. .page is centered with max-width 1200px,
   so its left edge is at calc(50% - 600px) and right edge at
   calc(50% + 600px). 16px gap keeps the widget from touching the
   page's outer halo. */
.side-widgets-left  { right: calc(50% + 600px + 16px); }
.side-widgets-right { left:  calc(50% + 600px + 16px); }

.widget-column {
  width: 320px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  flex: 0 0 auto;
}
/* Hide everything below 1900px — no room beside a 1200px page for a
   320px column with margin. */
@media (max-width: 1899px) {
  .side-widgets { display: none; }
}
/* Hide outer columns between 1900–2399px (inner-only mode). Inner
   columns still visible. */
@media (max-width: 2399px) {
  .side-widgets .widget-column.outer { display: none; }
}

.widget-slot {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 16px;
  box-shadow:
    var(--shadow),
    inset 0 0 50px color-mix(in srgb, var(--accent) 6%, transparent),
    0 0 14px color-mix(in srgb, var(--accent) 14%, transparent);
  flex: 0 0 auto;
  /* Every widget claims exactly half the column height regardless of
     content, so all four slots line up cleanly. Math: column space =
     100vh - 48px (top+bottom padding) - 18px (gap between two slots),
     halved = 50vh - 33px. Keep the height fixed even when only one of
     the two slots is populated — the user expects "half the screen"
     either way. Long widget content scrolls inside .widget-body. */
  height: calc(50vh - 33px);
  display: flex;
  flex-direction: column;
}
.widget-slot:empty { display: none; }
/* Header (h3) stays pinned at the top of the slot; the body region
   added by JS fills the rest. We deliberately CLIP overflow rather
   than auto-scroll: a widget that doesn't fit is a bug to be fixed
   in the widget (show fewer items, smaller font), not papered over
   with a scrollbar — the user wants a glanceable surface, not a
   list to scroll through. */
.widget-slot > h3 { flex: 0 0 auto; }
.widget-slot > .widget-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  /* Each widget fills the body with its own flex column wrapper, so
     the body itself is just a bounded box. */
  display: flex;
  flex-direction: column;
  margin-top: 8px;
}

/* Same L-shaped corner brackets as .card — uses --accent-cool so it
   matches the rest of the dashboard's framing language. */
.widget-slot::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  background:
    linear-gradient(to right,  var(--accent-cool), var(--accent-cool)) top    left  / 14px 2px no-repeat,
    linear-gradient(to bottom, var(--accent-cool), var(--accent-cool)) top    left  / 2px 14px no-repeat,
    linear-gradient(to right,  var(--accent-cool), var(--accent-cool)) top    right / 14px 2px no-repeat,
    linear-gradient(to bottom, var(--accent-cool), var(--accent-cool)) top    right / 2px 14px no-repeat,
    linear-gradient(to right,  var(--accent-cool), var(--accent-cool)) bottom left  / 14px 2px no-repeat,
    linear-gradient(to bottom, var(--accent-cool), var(--accent-cool)) bottom left  / 2px 14px no-repeat,
    linear-gradient(to right,  var(--accent-cool), var(--accent-cool)) bottom right / 14px 2px no-repeat,
    linear-gradient(to bottom, var(--accent-cool), var(--accent-cool)) bottom right / 2px 14px no-repeat;
  filter: drop-shadow(0 0 3px color-mix(in srgb, var(--accent-cool) 50%, transparent));
}

.widget-slot h3 {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--accent-warm);
  margin: 0 0 10px;
  text-shadow:
    0 0 2px color-mix(in srgb, var(--accent-warm) 60%, transparent),
    0 0 8px color-mix(in srgb, var(--accent-warm) 35%, transparent);
}

/* On viewports too narrow to leave room beside the 1200px .page, hide
   the side widgets entirely so they don't overlap the main content.
   Threshold = 1200 (page) + 2*(320 + 16) margins = ~1672px. */
@media (max-width: 1700px) {
  .side-widgets { display: none; }
}

/* ─── Calendar widget ──────────────────────────────────────────────
   Hero card at the top showcases the very next event with its full
   weekday/month name and "in Nd" hint; the rest of the upcoming list
   stacks below as compact date-stamped rows. The wrap fills the slot
   so the hero card sits flush at top and the list flows under it. */
.cal-wrap { display: flex; flex-direction: column; height: 100%; gap: 8px; }
.cal-hero {
  background: color-mix(in srgb, var(--accent-warm) 16%, var(--inset));
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 12px;
  text-align: center;
}
.cal-hero-head {
  font-size: 9.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.6px; color: var(--accent-warm);
}
.cal-hero-name {
  font-size: 16px; font-weight: 800; color: var(--text);
  margin-top: 2px; line-height: 1.15;
}
.cal-hero-date {
  font-size: 11px; color: var(--muted); margin-top: 2px;
}
.cal-list { display: flex; flex-direction: column; gap: 0; flex: 1; min-height: 0; }
.cal-row {
  display: flex; align-items: center; gap: 10px;
  padding: 4px 0;
  border-bottom: 1px dashed color-mix(in srgb, var(--border) 70%, transparent);
}
.cal-row:last-child { border-bottom: 0; }
.cal-date {
  flex: 0 0 44px;
  text-align: center;
  background: color-mix(in srgb, var(--accent-warm) 14%, var(--inset));
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 3px 0;
}
.cal-wd  { font-size: 9px; font-weight: 700; text-transform: uppercase; color: var(--accent-warm); letter-spacing: 0.5px; }
.cal-md  { font-size: 12px; font-weight: 700; color: var(--text); }
.cal-event { flex: 1; min-width: 0; }
.cal-name  { font-size: 12.5px; font-weight: 600; color: var(--text); }
.cal-away  { font-size: 10.5px; color: var(--muted); margin-top: 1px; }
.cal-empty { font-size: 11px; color: var(--muted); padding: 4px 0; }

/* Expand button — top-right of the cal-wrap, visible on hover. */
.cal-wrap { position: relative; }
.cal-expand-btn {
  position: absolute; top: 0; right: 0;
  width: 24px; height: 24px;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; line-height: 1;
  background: color-mix(in srgb, var(--accent) 18%, var(--panel));
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--accent);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s;
  z-index: 2;
}
.widget-body:hover .cal-expand-btn { opacity: 1; }

/* ─── Space Invaders easter egg ─────────────────────────────────────
   Floating ship drifts along the bottom of the viewport. Gentle pulse
   draws the eye without being intrusive. Game overlay is a full-screen
   black canvas; ✕ button closes it. */
#siShip {
  position: fixed; bottom: 14px; left: 100px;
  z-index: 100; cursor: pointer; line-height: 0;
  animation: siPulse 2.8s ease-in-out infinite;
  transition: transform 0.12s, filter 0.12s;
}
#siShip:hover { transform: scale(1.35); filter: brightness(1.6); animation-play-state: paused; }
#siShip canvas { display: block; image-rendering: pixelated; }
@keyframes siPulse {
  0%,100% { opacity: 0.45; filter: brightness(0.9); }
  50%     { opacity: 0.95; filter: brightness(1.45) drop-shadow(0 0 7px currentColor); }
}
#siOverlay { position: fixed; inset: 0; z-index: 9990; background: #000; display: none; }
#siCanvas  { display: block; width: 100%; height: 100%; }
#siClose {
  position: fixed; top: 14px; right: 18px; z-index: 9991;
  width: 36px; height: 36px; border-radius: 50%;
  background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.22);
  color: #fff; font-size: 18px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.15s;
}
#siClose:hover { background: rgba(255,255,255,0.26); }

/* ─── Asteroids easter egg ─────────────────────────────────────────
   Trigger: a small rock that slowly falls & rotates down the screen.
   Clicking it launches the full-screen Asteroids game.            */
#astRock {
  position: fixed;
  z-index: 100;
  cursor: pointer;
  line-height: 0;
  animation: astRockPulse 3.4s ease-in-out infinite;
  transition: filter 0.12s, transform 0.12s;
  transform-origin: center;
}
#astRock:hover {
  transform: scale(1.38);
  filter: brightness(1.9) drop-shadow(0 0 8px #ddd);
  animation-play-state: paused;
}
#astRock canvas { display: block; image-rendering: pixelated; }
@keyframes astRockPulse {
  0%,100% { opacity: 0.38; }
  50%     { opacity: 0.85; filter: drop-shadow(0 0 5px #aaa); }
}
#astOverlay { position: fixed; inset: 0; z-index: 9990; background: #000; display: none; }
#astCanvas  { display: block; width: 100%; height: 100%; }
#astClose {
  position: fixed; top: 14px; right: 18px; z-index: 9991;
  width: 36px; height: 36px; border-radius: 50%;
  background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.22);
  color: #fff; font-size: 18px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.15s;
}
#astClose:hover { background: rgba(255,255,255,0.26); }

/* ── Dino Runner trigger ── */
#dinoRunner {
  position: fixed; z-index: 100; cursor: pointer; line-height: 0;
  transition: filter 0.12s;
}
#dinoRunner:hover { filter: brightness(1.9) drop-shadow(0 0 10px #7ecfff); }
#dinoRunner canvas { display: block; image-rendering: pixelated; }
#dinoOverlay { position: fixed; inset: 0; z-index: 9990; background: #0d1117; display: none; }
#dinoCanvas  { display: block; width: 100%; height: 100%; }
#dinoClose {
  position: fixed; top: 14px; right: 18px; z-index: 9991;
  width: 36px; height: 36px; border-radius: 50%;
  background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.22);
  color: #fff; font-size: 18px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.15s;
}
#dinoClose:hover { background: rgba(255,255,255,0.26); }

/* ─── Password gate overlay ────────────────────────────────────────
   Full-screen lock screen shown to unrecognized browsers when the
   admin has enabled the password gate. Uses hardcoded dark palette so
   it's presentable before any theme loads. */
.pw-gate {
  position: fixed; inset: 0; z-index: 9999;
  background: #0f172a;
  display: flex; align-items: center; justify-content: center;
}
.pw-card {
  background: #1e293b;
  border: 1px solid #334155;
  border-radius: 16px;
  padding: 40px 36px 36px;
  width: min(400px, 92vw);
  display: flex; flex-direction: column; align-items: center;
  gap: 10px;
  box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}
.pw-lock { font-size: 42px; line-height: 1; margin-bottom: 4px; }
.pw-title {
  font-size: 22px; font-weight: 800; color: #f1f5f9;
  letter-spacing: -0.3px;
}
.pw-subtitle {
  font-size: 13px; color: #94a3b8; text-align: center;
  line-height: 1.5; margin-bottom: 6px;
}
.pw-error {
  font-size: 12px; color: #f87171; text-align: center;
  background: rgba(248,113,113,0.12); border: 1px solid rgba(248,113,113,0.3);
  border-radius: 6px; padding: 6px 12px; width: 100%;
}
.pw-form {
  display: flex; flex-direction: column; gap: 10px; width: 100%;
  margin-top: 4px;
}
.pw-input {
  width: 100%; padding: 10px 14px;
  background: #0f172a; border: 1px solid #334155;
  border-radius: 8px; color: #f1f5f9; font-size: 15px;
  outline: none;
}
.pw-input:focus { border-color: #38bdf8; box-shadow: 0 0 0 2px rgba(56,189,248,0.2); }
.pw-btn {
  width: 100%; padding: 10px;
  background: #38bdf8; color: #0f172a;
  border: 0; border-radius: 8px;
  font-size: 14px; font-weight: 700; cursor: pointer;
}
.pw-btn:hover { background: #7dd3fc; }
.pw-btn:disabled { opacity: 0.5; cursor: default; }
.pw-attempts { font-size: 11px; color: #64748b; }

/* ─── Calendar popup modal ─────────────────────────────────────────
   Full month grid shown when the user clicks the expand button. Reuses
   the existing .modal-backdrop / #modal infrastructure with a wider
   .modal--cal override. */
.modal--cal {
  width: min(900px, 96vw);
  max-height: 90vh;
  overflow-y: auto;
  padding: 0;
}
.cpop-header {
  display: flex; align-items: center; gap: 8px;
  padding: 16px 20px 12px;
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; background: var(--panel); z-index: 1;
}
.cpop-month {
  flex: 1; text-align: center;
  font-size: 17px; font-weight: 700; color: var(--text);
}
.cpop-nav {
  width: 32px; height: 32px;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px; line-height: 1;
  background: var(--inset); border: 1px solid var(--border);
  border-radius: 6px; color: var(--accent); cursor: pointer;
}
.cpop-nav:hover { background: color-mix(in srgb, var(--accent) 16%, var(--inset)); }
.cpop-close {
  margin-left: auto;
  width: 28px; height: 28px;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px;
  background: transparent; border: 1px solid var(--border);
  border-radius: 6px; color: var(--muted); cursor: pointer;
}
.cpop-close:hover { color: var(--text); background: var(--inset); }
.cpop-grid {
  display: grid; grid-template-columns: repeat(7, 1fr);
  gap: 1px;
  background: var(--border);
  padding: 0 16px 16px;
  background: var(--panel);
}
.cpop-dh {
  padding: 8px 4px 4px;
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.6px; color: var(--muted); text-align: center;
}
.cpop-cell {
  min-height: 70px;
  padding: 6px 6px 4px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 13px; font-weight: 600; color: var(--text);
  background: var(--inset);
  display: flex; flex-direction: column; gap: 3px;
}
.cpop-cell--empty {
  background: transparent; border-color: transparent;
}
.cpop-cell--today {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--inset));
}
.cpop-cell--today > :first-child { color: var(--accent); }
.cpop-chip {
  font-size: 9.5px; font-weight: 600; line-height: 1.3;
  background: color-mix(in srgb, var(--accent-warm) 22%, var(--panel));
  color: var(--text);
  border-radius: 4px;
  padding: 2px 4px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ─── Countdown widget ─────────────────────────────────────────────
   Fills the half-height slot: emoji+label header pinned to the top, a
   stage in the middle that vertically centers the BIG day count
   (clamped to ~12vh so it dominates without overflowing), and the
   target date pinned at the bottom. */
.cd-wrap {
  display: flex; flex-direction: column; height: 100%;
  align-items: stretch; gap: 8px;
  position: relative;             /* anchors .cd-bg theme layer */
  border-radius: 8px;
  overflow: hidden;               /* clips themed bg + decorations to the slot */
}
/* Direct children of cd-wrap sit ABOVE the themed bg layer. */
.cd-wrap > .cd-head,
.cd-wrap > .cd-stage,
.cd-wrap > .cd-date,
.cd-wrap > .cd-breakdown { position: relative; z-index: 1; }

/* ── Themed background layer for preset countdowns ──────────────
   Pulled in by JS (renderCountdownBg) when the user picks a preset
   label like "Birthday" / "Vacation" / "Christmas". Free-form labels
   skip this entirely so a custom event keeps the clean panel look. */
.cd-bg {
  position: absolute; inset: 0;
  z-index: 0;
  pointer-events: none;
  border-radius: inherit;
}
/* Decorative emoji "stickers" scattered across the themed bg at
   stable-but-jittered positions (deterministic per label so they
   don't bounce around on silent refreshes). */
.cd-bg .cd-deco {
  position: absolute;
  font-size: clamp(22px, 4.2vh, 38px);
  line-height: 1;
  opacity: 0.32;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.18));
  user-select: none;
}

/* Per-theme gradient palettes. Each blends with the active --panel so
   themes still feel right on dark vs light themes. */
.cd-bg.cd-theme-birthday {
  background: linear-gradient(135deg,
    color-mix(in srgb, #f472b6 38%, var(--panel)) 0%,
    color-mix(in srgb, #fde68a 32%, var(--panel)) 100%);
}
.cd-bg.cd-theme-school-out {
  background: linear-gradient(160deg,
    color-mix(in srgb, #fbbf24 38%, var(--panel)) 0%,
    color-mix(in srgb, #38bdf8 28%, var(--panel)) 100%);
}
.cd-bg.cd-theme-school-in {
  background: linear-gradient(160deg,
    color-mix(in srgb, #1e40af 32%, var(--panel)) 0%,
    color-mix(in srgb, #ef4444 28%, var(--panel)) 60%,
    color-mix(in srgb, #fbbf24 26%, var(--panel)) 100%);
}
.cd-bg.cd-theme-vacation {
  background: linear-gradient(180deg,
    color-mix(in srgb, #38bdf8 36%, var(--panel)) 0%,
    color-mix(in srgb, #fb7185 30%, var(--panel)) 70%,
    color-mix(in srgb, #fbbf24 28%, var(--panel)) 100%);
}
.cd-bg.cd-theme-christmas {
  background: linear-gradient(135deg,
    color-mix(in srgb, #166534 50%, var(--panel)) 0%,
    color-mix(in srgb, #991b1b 42%, var(--panel)) 100%);
}
.cd-bg.cd-theme-halloween {
  background: linear-gradient(160deg,
    color-mix(in srgb, #f97316 50%, var(--panel)) 0%,
    color-mix(in srgb, #1e1b4b 50%, var(--panel)) 100%);
}
.cd-bg.cd-theme-field-trip {
  background: linear-gradient(160deg,
    color-mix(in srgb, #fbbf24 38%, var(--panel)) 0%,
    color-mix(in srgb, #16a34a 32%, var(--panel)) 100%);
}
.cd-bg.cd-theme-game-day {
  background: linear-gradient(180deg,
    color-mix(in srgb, #16a34a 38%, var(--panel)) 0%,
    color-mix(in srgb, #16a34a 30%, var(--panel)) 50%,
    color-mix(in srgb, #1e40af 30%, var(--panel)) 100%);
}
.cd-bg.cd-theme-music-concert {
  background: linear-gradient(160deg,
    color-mix(in srgb, #7c3aed 48%, var(--panel)) 0%,
    color-mix(in srgb, #db2777 36%, var(--panel)) 60%,
    color-mix(in srgb, #f59e0b 28%, var(--panel)) 100%);
}
/* Christmas + halloween have darker palettes — darken the decorations
   too so they don't blow out against the deeper background. */
.cd-bg.cd-theme-christmas .cd-deco,
.cd-bg.cd-theme-halloween .cd-deco { opacity: 0.42; }
.cd-head {
  display: flex; align-items: center; gap: 8px;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  padding-bottom: 8px;
}
.cd-emoji { font-size: 22px; line-height: 1; }
.cd-label {
  font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.6px; color: var(--accent-warm);
  flex: 1; min-width: 0;
  text-shadow: 0 1px 3px rgba(0,0,0,0.5), 0 0 8px rgba(0,0,0,0.2);
}
.cd-stage {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 4px;
  /* position:relative anchors the day-of fireworks + banner overlay
     to the stage box rather than the page. */
  position: relative;
  overflow: hidden;
}
.cd-big {
  font-size: clamp(56px, 12vh, 96px);
  font-weight: 800; line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  transform-origin: center;
  text-shadow: 0 2px 6px rgba(0,0,0,0.5), 0 0 16px rgba(0,0,0,0.2);
}
.cd-sub {
  font-size: 12px; color: var(--muted);
  letter-spacing: 0.4px; text-transform: lowercase;
  text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.cd-date {
  text-align: center;
  font-size: 12px; color: var(--text);
  border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  padding-top: 8px; font-style: italic;
  text-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.cd-empty { font-size: 11px; color: var(--muted); padding: 4px 0; }

/* ── Time-perspective breakdown (weeks / months / years) ─────────
   Vertical stack under the big day count — three rows showing the
   same span of time in different units so a glance gives multiple
   perspectives. Days stays the visual hero (cd-big at clamp 56-96px);
   these breakdown rows are noticeably smaller but still readable. */
.cd-breakdown {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  margin-top: 8px;
  color: var(--muted);
  font-size: clamp(13px, 1.7vh, 16px);
  text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.cd-bd-part {
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
}
.cd-bd-num {
  font-weight: 800;
  color: var(--accent-warm);
  margin-right: 4px;
  font-size: 1.15em;
  text-shadow: 0 1px 4px rgba(0,0,0,0.5);
}

/* ── Escalation states (3, 2, 1, 0 days) ─────────────────────────
   Color cues persist regardless of the animation gate so the urgent
   state is always visible. The .cd-animate suffix fires the actual
   one-shot motion — set on first render of a page load only.

   3 days: warm pulse (gentle "hey, this is coming up")
   2 days: orange shake (slightly more urgent)
   1 day:  red heartbeat (much more urgent)
   0 days: handled separately — fireworks + banner overlay */
.cd-stage.urgent-3 .cd-big {
  color: #f59e0b;
  text-shadow: 0 0 14px color-mix(in srgb, #f59e0b 55%, transparent);
}
.cd-stage.urgent-3.cd-animate .cd-big {
  animation: cd-pulse 0.55s ease-in-out 8;
}
@keyframes cd-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.18); }
}

.cd-stage.urgent-2 .cd-big {
  color: #f97316;
  text-shadow:
    0 0 14px color-mix(in srgb, #f97316 60%, transparent),
    0 0 28px color-mix(in srgb, #f97316 30%, transparent);
}
.cd-stage.urgent-2.cd-animate .cd-big {
  animation: cd-shake 0.5s ease-in-out 6;
}
@keyframes cd-shake {
  0%, 100% { transform: translateX(0)   scale(1);    }
  15%      { transform: translateX(-5px) scale(1.06); }
  30%      { transform: translateX(5px)  scale(1.10); }
  45%      { transform: translateX(-4px) scale(1.06); }
  60%      { transform: translateX(4px)  scale(1.10); }
  75%      { transform: translateX(-2px) scale(1.04); }
}

.cd-stage.urgent-1 .cd-big {
  color: #ef4444;
  text-shadow:
    0 0 16px color-mix(in srgb, #ef4444 70%, transparent),
    0 0 32px color-mix(in srgb, #ef4444 35%, transparent);
}
.cd-stage.urgent-1.cd-animate .cd-big {
  animation: cd-heartbeat 0.85s ease-in-out 8;
}
@keyframes cd-heartbeat {
  0%, 100% { transform: scale(1);    }
  20%      { transform: scale(1.32); }
  40%      { transform: scale(1.05); }
  60%      { transform: scale(1.26); }
  80%      { transform: scale(1);    }
}

/* ── Day-of: fireworks + banner ──────────────────────────────────
   The .cd-fireworks container fades out after ~7s so the celebration
   eventually settles. The banner pops in once and stays — the user
   shouldn't have to wait for it to render. */
.cd-fireworks {
  position: absolute; inset: 0;
  pointer-events: none;
  overflow: hidden;
  animation: cd-fireworks-out 1.2s ease-out 6.5s forwards;
}
@keyframes cd-fireworks-out {
  to { opacity: 0; visibility: hidden; }
}
.cd-spark {
  position: absolute;
  font-size: clamp(22px, 3.6vh, 38px);
  line-height: 1;
  filter: drop-shadow(0 0 10px color-mix(in srgb, var(--accent-warm) 60%, transparent));
  animation: cd-burst 1.4s ease-out infinite;
  /* Per-spark --tx / --ty / animation-delay set inline by JS so each
     spark flies a different direction with a staggered start. */
}
@keyframes cd-burst {
  0%   { transform: translate(0, 0) scale(0.3); opacity: 0; }
  18%  { opacity: 1; }
  100% { transform: translate(var(--tx), var(--ty)) scale(1.5); opacity: 0; }
}

.cd-banner {
  position: relative;
  z-index: 2;
  background: linear-gradient(135deg, var(--accent-warm), var(--accent));
  color: white;
  font-weight: 900;
  font-size: clamp(13px, 1.7vh, 16px);
  text-transform: uppercase;
  letter-spacing: 1.2px;
  padding: 10px 18px;
  border-radius: 999px;
  box-shadow:
    0 4px 22px rgba(0,0,0,0.35),
    0 0 36px color-mix(in srgb, var(--accent-warm) 50%, transparent);
  white-space: nowrap;
  text-align: center;
  animation: cd-banner-pop 0.6s cubic-bezier(.34,1.56,.64,1) 0.3s both;
}
@keyframes cd-banner-pop {
  0%   { transform: scale(0);    opacity: 0; }
  60%  { transform: scale(1.12); opacity: 1; }
  100% { transform: scale(1);                 }
}
.cd-sub-today {
  position: relative;
  z-index: 2;
  margin-top: 10px;
  font-size: 13px;
  color: var(--accent-warm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 1px;
  text-align: center;
}

/* ── Birthday celebration overlay ─────────────────────────────────
   Page-wide fireworks + banner that pops in on the user's birthday.
   Sits above the mascot (z=9999) and dirty-edge layer (z=9998) so
   nothing draws over it. pointer-events:none so the user can still
   click underneath if they're impatient. Auto-dismisses via JS after
   ~6s by toggling .fade-out, which kicks a 1.2s opacity transition. */
.bday-overlay {
  position: fixed; inset: 0;
  pointer-events: none;
  z-index: 99999;
  overflow: hidden;
  transition: opacity 1.2s ease-out;
}
.bday-overlay.fade-out { opacity: 0; }
.bday-spark {
  position: absolute;
  font-size: clamp(32px, 5.5vh, 72px);
  line-height: 1;
  filter: drop-shadow(0 0 20px gold)
          drop-shadow(0 0 36px color-mix(in srgb, var(--accent-warm) 70%, transparent));
  animation: bday-burst 1.8s ease-out infinite;
  transform-origin: center;
}
@keyframes bday-burst {
  0%   { transform: translate(0, 0) scale(calc(var(--size) * 0.3)); opacity: 0; }
  20%  { opacity: 1; }
  100% { transform: translate(var(--tx), var(--ty)) scale(calc(var(--size) * 1.6)); opacity: 0; }
}
.bday-banner {
  position: fixed;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  background: linear-gradient(135deg, #f59e0b 0%, #ec4899 50%, #6366f1 100%);
  color: white;
  font-weight: 900;
  font-size: clamp(24px, 4.5vw, 60px);
  text-transform: uppercase;
  letter-spacing: 2px;
  padding: 20px 40px;
  border-radius: 999px;
  box-shadow:
    0 12px 60px rgba(0,0,0,0.4),
    0 0 80px color-mix(in srgb, #f59e0b 60%, transparent);
  white-space: nowrap;
  text-align: center;
  text-shadow: 0 2px 8px rgba(0,0,0,0.3);
  animation: bday-banner-pop 0.8s cubic-bezier(.34,1.56,.64,1) 0.2s both;
}
@keyframes bday-banner-pop {
  0%   { transform: translate(-50%, -50%) scale(0)    rotate(-12deg); opacity: 0; }
  60%  { transform: translate(-50%, -50%) scale(1.12) rotate(2deg);   opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1)    rotate(0);                  }
}

/* ─── Sun & Moon widget ────────────────────────────────────────────
   Four vertical zones fill the slot: a small solstice-countdown bar
   at the top, the moon emoji as the visual hero (large glyph, phase
   name, illumination %), a sunrise/sunset card-row with warm/cool
   gradient backgrounds, and a centered hint footer with the next
   event countdown plus today's daylight duration. */
.sm-wrap { display: flex; flex-direction: column; height: 100%; gap: 10px; }

/* Solstice countdown — kid-friendly framing of the daylight cycle.
   Two visual modes via .longest (warm sun gradient) and .shortest
   (cool winter gradient); JS picks the right one based on direction. */
.sm-solstice {
  display: flex; align-items: center; justify-content: center;
  gap: 8px;
  font-size: 11px; font-weight: 600;
  letter-spacing: 0.3px;
  text-align: center;
  border-radius: 999px;
  padding: 5px 12px;
  border: 1px solid var(--border);
}
/* Per-season gradients. Summer/winter inherit the original solstice
   palettes (warm sun / cool winter) so the transition from the old
   pill is visually seamless. Spring picks up a soft floral pink+green
   and fall a deep amber+rust. .longest / .shortest kept as aliases
   for any older state that hasn't been re-rendered yet. */
.sm-solstice.summer,
.sm-solstice.longest {
  background: linear-gradient(135deg,
    color-mix(in srgb, #fbbf24 26%, var(--inset)),
    color-mix(in srgb, #f97316 18%, var(--inset)));
  color: var(--text);
}
.sm-solstice.winter,
.sm-solstice.shortest {
  background: linear-gradient(135deg,
    color-mix(in srgb, #93c5fd 26%, var(--inset)),
    color-mix(in srgb, #6366f1 18%, var(--inset)));
  color: var(--text);
}
.sm-solstice.spring {
  background: linear-gradient(135deg,
    color-mix(in srgb, #f9a8d4 24%, var(--inset)),
    color-mix(in srgb, #4ade80 20%, var(--inset)));
  color: var(--text);
}
.sm-solstice.fall {
  background: linear-gradient(135deg,
    color-mix(in srgb, #fb923c 26%, var(--inset)),
    color-mix(in srgb, #b45309 20%, var(--inset)));
  color: var(--text);
}
.sm-season-note {
  font-size: 10px;
  opacity: 0.75;
  font-weight: 500;
  margin-left: 2px;
}
.sm-solstice-icon { font-size: 14px; line-height: 1; }
.sm-solstice-days {
  font-size: 14px; font-weight: 800; color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.sm-moon {
  flex: 1;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
  background:
    radial-gradient(ellipse at 50% 38%,
      color-mix(in srgb, var(--accent-cool) 28%, transparent) 0%,
      transparent 65%),
    color-mix(in srgb, var(--accent-cool) 10%, var(--inset));
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px;
}
.sm-moon-glyph {
  font-size: clamp(48px, 9vh, 76px);
  line-height: 1;
  /* Soft halo so the emoji feels like it's emitting light rather than
     just sitting on a flat panel. */
  filter: drop-shadow(0 0 14px color-mix(in srgb, var(--accent-cool) 60%, transparent))
          drop-shadow(0 0 28px color-mix(in srgb, var(--accent-cool) 30%, transparent));
}
.sm-moon-name {
  font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.6px; color: var(--accent-warm); margin-top: 6px;
}
.sm-moon-illum { font-size: 11px; color: var(--muted); margin-top: 2px; }
.sm-row {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
}
.sm-cell {
  text-align: center;
  background: var(--inset);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 6px;
  min-width: 0;
  position: relative;
  overflow: hidden;
}
/* Sunrise gets a warm dawn gradient (yellow → amber → orange); sunset
   a cool dusk gradient (lavender → indigo). The color-mix pulls in
   the active theme's --inset so the cells still feel theme-aware
   instead of always being saturated. */
.sm-cell.sunrise {
  background: linear-gradient(160deg,
    color-mix(in srgb, #fde68a 38%, var(--inset)) 0%,
    color-mix(in srgb, #fbbf24 28%, var(--inset)) 55%,
    color-mix(in srgb, #f97316 22%, var(--inset)) 100%);
}
.sm-cell.sunset {
  background: linear-gradient(160deg,
    color-mix(in srgb, #c4b5fd 32%, var(--inset)) 0%,
    color-mix(in srgb, #a78bfa 26%, var(--inset)) 55%,
    color-mix(in srgb, #6366f1 22%, var(--inset)) 100%);
}
.sm-icon  {
  font-size: 28px; line-height: 1;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.25));
}
.sm-label {
  font-size: 9.5px; font-weight: 700; text-transform: uppercase;
  color: var(--text); margin-top: 4px; letter-spacing: 0.5px;
  opacity: 0.78;
}
.sm-val {
  font-size: 14px; font-weight: 800; color: var(--text);
  margin-top: 2px; font-variant-numeric: tabular-nums;
}
.sm-hint {
  text-align: center;
  font-size: 11.5px; color: var(--accent-warm); font-weight: 600;
  border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  padding-top: 8px;
  display: flex; flex-direction: column; gap: 2px;
}
.sm-daylight { color: var(--muted); font-weight: 500; font-style: italic; }
.sm-empty { font-size: 11px; color: var(--muted); padding: 4px 0; }

/* ─── On This Day widget ───────────────────────────────────────────
   Short head label, then a vertically-centered stage with a HUGE year
   number anchoring the fact below it. Entries that don't start with
   "In YYYY," skip the year cell and render the full text centered.
   The body uses a serif italic ("Georgia/Cambria" — system fonts, no
   blackletter) with an accent-warm tint and a left edge bar so it
   reads like a callout quote rather than plain prose. */
.otd-wrap { display: flex; flex-direction: column; height: 100%; gap: 10px; }
.otd-head {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.6px; color: var(--accent-warm);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  padding-bottom: 6px;
}
.otd-stage {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center; gap: 6px;
}
.otd-year {
  font-size: clamp(44px, 10vh, 88px);
  font-weight: 800; line-height: 1;
  /* Gradient text from accent-cool → accent-warm gives the year a
     painterly glow rather than a flat color. background-clip:text is
     widely supported; the plain `color: var(--accent)` is the fallback
     if the browser ignores the clip. */
  color: var(--accent);
  background: linear-gradient(135deg,
    var(--accent-cool, var(--accent)) 0%,
    var(--accent) 50%,
    var(--accent-warm) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  font-variant-numeric: tabular-nums;
  filter: drop-shadow(0 2px 10px color-mix(in srgb, var(--accent) 35%, transparent));
}
.otd-date {
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 1.4px;
  color: var(--accent-warm);
  margin-top: -2px;
}
.otd-body {
  /* Serif italic for a "quote" feel — Georgia and Cambria are present
     on essentially every Win/Mac/Linux+browser combo we'll see. The
     fallback chain ends at the generic `serif` so something readable
     always renders. Explicitly NOT blackletter / "Old English". */
  font-family: Georgia, Cambria, "Times New Roman", serif;
  font-style: italic;
  font-size: clamp(14px, 2vh, 18px);
  line-height: 1.45;
  color: var(--text);
  max-width: 32ch;
  text-align: left;
  padding: 10px 14px;
  margin-top: 4px;
  border-left: 3px solid var(--accent-warm);
  border-radius: 0 8px 8px 0;
  background: color-mix(in srgb, var(--accent-warm) 10%, var(--inset));
  box-shadow: inset 0 0 30px color-mix(in srgb, var(--accent) 4%, transparent);
}
.otd-empty { font-size: 11px; color: var(--muted); }
.otd-wiki {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 1px;
  color: var(--accent);
  pointer-events: auto;       /* widget bodies use overflow:hidden which
                                  doesn't block events, but make intent
                                  explicit since this is a click target */
  padding: 4px 10px;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border));
  border-radius: 999px;
  margin-top: 6px;
  transition: background 120ms, color 120ms, border-color 120ms;
}
.otd-wiki:hover {
  background: var(--accent);
  color: white;
  border-color: var(--accent);
}

/* ─── Word of the Day widget ───────────────────────────────────────
   Mirrors the On This Day layout: small head label → centered stage
   with a BIG gradient hero (the word) → caps letterspaced part-of-
   speech stamp → italic-serif example in a tinted left-bar panel.
   The plain-prose definition sits between the stamp and the panel. */
.wod-wrap { display: flex; flex-direction: column; height: 100%; gap: 10px; }
.wod-head {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.6px; color: var(--accent-warm);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  padding-bottom: 6px;
}
.wod-stage {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center; gap: 6px;
}
.wod-word {
  font-size: clamp(18px, 3.5vh, 30px);
  font-weight: 800; line-height: 1;
  /* Same gradient treatment as the OTD year — cool → blue → warm so
     the hero feels "painterly" rather than flat. */
  color: var(--accent);
  background: linear-gradient(135deg,
    var(--accent-cool, var(--accent)) 0%,
    var(--accent) 50%,
    var(--accent-warm) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 2px 10px color-mix(in srgb, var(--accent) 35%, transparent));
}
.wod-pos {
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 1.4px;
  color: var(--accent-warm);
  margin-top: -2px;
}
.wod-def {
  font-size: clamp(13px, 1.7vh, 16px);
  line-height: 1.4;
  color: var(--text);
  max-width: 32ch;
  margin-top: 2px;
}
.wod-example {
  font-family: Georgia, Cambria, "Times New Roman", serif;
  font-style: italic;
  font-size: clamp(13px, 1.8vh, 17px);
  line-height: 1.45;
  color: var(--text);
  max-width: 32ch;
  text-align: left;
  padding: 10px 14px;
  margin-top: 4px;
  border-left: 3px solid var(--accent-warm);
  border-radius: 0 8px 8px 0;
  background: color-mix(in srgb, var(--accent-warm) 10%, var(--inset));
  box-shadow: inset 0 0 30px color-mix(in srgb, var(--accent) 4%, transparent);
}
/* Long-word size reduction so the hero word never overflows the slot. */
.wod-word[data-len="long"]  { font-size: clamp(14px, 2.8vh, 22px); }
.wod-word[data-len="xlong"] { font-size: clamp(11px, 2.2vh, 16px); }

/* ─── To-Do List widget ────────────────────────────────────────────
   Full-height flex column: scrollable list on top, sticky add-row
   pinned to the bottom. Checkboxes toggle a strikethrough + muted
   color on the text. */
.todo-wrap {
  display: flex; flex-direction: column; height: 100%; gap: 0;
  overflow: hidden;
}
.todo-list {
  flex: 1; overflow-y: auto; list-style: none;
  margin: 0; padding: 0 0 4px;
  display: flex; flex-direction: column; gap: 2px;
}
.todo-item {
  display: flex; align-items: center; gap: 6px;
  padding: 5px 4px; border-radius: 6px;
  transition: background 0.15s;
}
.todo-item:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.todo-label {
  flex: 1; display: flex; align-items: center; gap: 8px;
  cursor: pointer; min-width: 0;
}
.todo-label input[type="checkbox"] {
  flex-shrink: 0; width: 15px; height: 15px;
  accent-color: var(--accent); cursor: pointer;
}
.todo-text {
  font-size: 13px; line-height: 1.35; color: var(--text);
  word-break: break-word;
}
.todo-done .todo-text {
  text-decoration: line-through;
  color: var(--muted);
}
.todo-del {
  flex-shrink: 0;
  background: none; border: none; cursor: pointer;
  color: var(--muted); font-size: 11px; padding: 2px 4px;
  border-radius: 4px; opacity: 0; transition: opacity 0.15s, color 0.15s;
}
.todo-item:hover .todo-del { opacity: 1; }
.todo-del:hover { color: #dc2626; }
.todo-empty {
  font-size: 12px; color: var(--muted); font-style: italic;
  padding: 8px 4px;
}
.todo-add-row {
  display: flex; gap: 6px; padding: 8px 0 0;
  border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
.todo-input {
  flex: 1; min-width: 0;
  background: var(--inset); border: 1px solid var(--border);
  color: var(--text); border-radius: 6px;
  font-size: 12px; padding: 5px 8px;
  outline: none;
}
.todo-input:focus { border-color: var(--accent); }
.todo-add-btn {
  flex-shrink: 0;
  background: var(--accent); color: #fff; border: none;
  border-radius: 6px; font-size: 12px; font-weight: 600;
  padding: 5px 10px; cursor: pointer;
  transition: opacity 0.15s;
}
.todo-add-btn:hover { opacity: 0.85; }

/* ─── Notes widget ─────────────────────────────────────────────────
   A simple textarea that fills the slot. Auto-saves with a tiny
   "Saved" flash in the bottom-right corner. */
.notes-wrap {
  display: flex; flex-direction: column; height: 100%; gap: 0;
}
.notes-area {
  flex: 1; resize: none; min-height: 0;
  background: var(--inset); border: 1px solid var(--border);
  color: var(--text); border-radius: 8px;
  font-size: 13px; line-height: 1.5;
  padding: 10px 12px; outline: none;
  font-family: inherit;
}
.notes-area:focus { border-color: var(--accent); }
.notes-area::placeholder { color: var(--muted); }
.notes-status {
  font-size: 11px; color: var(--muted);
  text-align: right; padding-top: 4px; min-height: 18px;
}

/* ─── Poem of the Day widget ───────────────────────────────────────
   Same OTD bones — gradient title hero → "by Author" caps stamp →
   italic-serif body in a tinted left-bar panel. Lines preserve their
   verse breaks via <br> joined in JS. */
.poem-wrap { display: flex; flex-direction: column; height: 100%; gap: 10px; }
.poem-head {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.6px; color: var(--accent-warm);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  padding-bottom: 6px;
}
.poem-stage {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center; gap: 6px;
}
.poem-title {
  font-family: Georgia, Cambria, "Times New Roman", serif;
  font-size: clamp(22px, 4vh, 36px);
  font-weight: 800; line-height: 1.1;
  color: var(--accent);
  background: linear-gradient(135deg,
    var(--accent-cool, var(--accent)) 0%,
    var(--accent) 50%,
    var(--accent-warm) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 2px 10px color-mix(in srgb, var(--accent) 35%, transparent));
}
.poem-byline {
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 1.4px;
  color: var(--accent-warm);
  margin-top: -2px;
}
.poem-body {
  font-family: Georgia, Cambria, "Times New Roman", serif;
  font-style: italic;
  font-size: clamp(13px, 1.8vh, 17px);
  line-height: 1.5;
  color: var(--text);
  text-align: left;
  max-width: 32ch;
  padding: 10px 14px;
  margin-top: 4px;
  border-left: 3px solid var(--accent-warm);
  border-radius: 0 8px 8px 0;
  background: color-mix(in srgb, var(--accent-warm) 10%, var(--inset));
  box-shadow: inset 0 0 30px color-mix(in srgb, var(--accent) 4%, transparent);
}

/* ─── Inspirational Phrase widget ──────────────────────────────────
   Big stylized opening quote-mark serves as the gradient hero (same
   visual rhythm as the OTD year + WOD word + POD title), then the
   quote text in a tinted italic panel, with the attribution as the
   caps letterspaced "stamp" below. */
.insp-wrap { display: flex; flex-direction: column; height: 100%; gap: 10px; }
.insp-head {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.6px; color: var(--accent-warm);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  padding-bottom: 6px;
}
.insp-stage {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center; gap: 6px;
}
.insp-mark {
  font-family: Georgia, Cambria, "Times New Roman", serif;
  font-size: clamp(56px, 12vh, 110px);
  font-weight: 800; line-height: 0.7;
  color: var(--accent);
  background: linear-gradient(135deg,
    var(--accent-cool, var(--accent)) 0%,
    var(--accent) 50%,
    var(--accent-warm) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 2px 12px color-mix(in srgb, var(--accent) 35%, transparent));
  margin-bottom: -8px;     /* tighten gap to the quote so they read as one unit */
}
.insp-quote {
  font-family: Georgia, Cambria, "Times New Roman", serif;
  font-style: italic;
  font-size: clamp(14px, 2.1vh, 19px);
  line-height: 1.45;
  color: var(--text);
  text-align: center;
  max-width: 32ch;
  padding: 10px 14px;
  border-left: 3px solid var(--accent-warm);
  border-radius: 0 8px 8px 0;
  background: color-mix(in srgb, var(--accent-warm) 10%, var(--inset));
  box-shadow: inset 0 0 30px color-mix(in srgb, var(--accent) 4%, transparent);
}
.insp-author {
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 1.4px;
  color: var(--accent-warm);
  margin-top: 2px;
}

/* Sports widget — 2-column grid (one game per column) per league.
   Single column when only 1 game is available for that league. */
.sports-league      { margin-bottom: 14px; }
.sports-league:last-child { margin-bottom: 0; }
.sports-league-head {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--text);
  margin-bottom: 6px;
}
.sports-league-head .live-pill {
  font-size: 9px; font-weight: 800;
  background: color-mix(in srgb, var(--accent) 80%, transparent);
  color: var(--bg);
  padding: 2px 6px; border-radius: 4px;
  letter-spacing: 0.4px;
}
.sports-grid {
  display: grid;
  gap: 6px;
}
.sports-grid.two-col { grid-template-columns: 1fr 1fr; }
.sports-grid.one-col { grid-template-columns: 1fr; }
.sports-game {
  display: flex; flex-direction: column; gap: 2px;
  background: var(--inset); border: 1px solid var(--border);
  border-radius: 8px; padding: 7px 8px;
  font-size: 11.5px;
  min-width: 0;          /* let the cell shrink within the grid track */
}
.sports-game-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 6px;
}
.sports-game-row .team {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: 5px;
  white-space: nowrap; overflow: hidden;
}
.sports-game-row .team .logo {
  width: 14px; height: 14px; flex-shrink: 0; object-fit: contain;
}
.sports-game-row .team .abbr {
  overflow: hidden; text-overflow: ellipsis;
}
.sports-game-row .score {
  font-weight: 700; font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.sports-game.live .score { color: var(--accent-warm); }
.sports-game .winner { font-weight: 700; }
.sports-game-meta {
  font-size: 9.5px; color: var(--muted); margin-top: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sports-empty { font-size: 11px; color: var(--muted); padding: 4px 0; }

/* ════════════════════════════════════════════════════════════════════
   VISUAL FLOURISHES (ported from the Fallout/Pip-Boy styling on the
   Unraid dashboard). Theme-tinted via var(--accent) so each effect
   shifts color when the user picks a different theme.

     1. Dirty-screen vignette: corner grime patches + center vignette
        layered as a fixed-position fullscreen overlay above content
        (pointer-events:none so it never blocks clicks).
     2. Corner brackets on every panel: L-shaped frame ticks drawn via
        eight background-image gradients on a ::before pseudo-element,
        with a drop-shadow halo so the brackets glow.
     3. Phosphor inner+outer halo on cards: inset accent wash + outer
        accent halo via box-shadow (color-mix lets us reuse --accent
        at the right opacity without per-theme color lists).
     4. Text glow on titles + key numbers: two-layer text-shadow (tight
        inner glow + broader outer halo) so headings read as "lit".
     5. Status-dot glow + focus halo on the search bar.
   ════════════════════════════════════════════════════════════════════ */

/* (1) Dirty screen overlay. Sits above the page but pointer-events:none
   keeps it from intercepting any clicks. Sepia corner patches simulate
   dust buildup on a CRT; the center vignette darkens edges. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9998;
  background:
    radial-gradient(circle at 0% 0%,    rgba(120,80,40,0.10) 0%, transparent 28%),
    radial-gradient(circle at 100% 0%,  rgba(120,80,40,0.08) 0%, transparent 28%),
    radial-gradient(circle at 0% 100%,  rgba(120,80,40,0.12) 0%, transparent 28%),
    radial-gradient(circle at 100% 100%,rgba(120,80,40,0.10) 0%, transparent 28%),
    radial-gradient(ellipse 130% 110% at center,
      transparent 35%,
      rgba(0,0,0,0.12) 75%,
      rgba(0,0,0,0.30) 100%);
}

/* (2) Corner brackets — L-shaped 16px×2px ticks drawn on a ::before
   pseudo-element. Eight stacked linear-gradient backgrounds: two per
   corner (one horizontal arm, one vertical). filter:drop-shadow halos
   only the painted ticks (not the rectangular bounds), giving the
   brackets a phosphor glow. Applied to every panel-style container. */
.card,
.modal,
.admin-section {
  position: relative;
}
/* Corner brackets use --accent-cool (the theme's "structural" green/
   sage/cyan accent) so they read as a separate visual layer from the
   --accent-driven interactive elements (buttons, focus rings, search
   chip). On the blue theme this gives Fallout-style phosphor green
   ticks against the cyan-blue interactive accents. */
.card::before,
.modal::before,
.admin-section::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  background:
    linear-gradient(to right,  var(--accent-cool), var(--accent-cool)) top    left  / 16px 2px no-repeat,
    linear-gradient(to bottom, var(--accent-cool), var(--accent-cool)) top    left  / 2px 16px no-repeat,
    linear-gradient(to right,  var(--accent-cool), var(--accent-cool)) top    right / 16px 2px no-repeat,
    linear-gradient(to bottom, var(--accent-cool), var(--accent-cool)) top    right / 2px 16px no-repeat,
    linear-gradient(to right,  var(--accent-cool), var(--accent-cool)) bottom left  / 16px 2px no-repeat,
    linear-gradient(to bottom, var(--accent-cool), var(--accent-cool)) bottom left  / 2px 16px no-repeat,
    linear-gradient(to right,  var(--accent-cool), var(--accent-cool)) bottom right / 16px 2px no-repeat,
    linear-gradient(to bottom, var(--accent-cool), var(--accent-cool)) bottom right / 2px 16px no-repeat;
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--accent-cool) 55%, transparent));
}

/* (3) Phosphor halo on cards — inner accent wash + outer accent glow.
   color-mix lets us hit a fixed alpha against any theme's --accent
   without per-theme overrides. Layered on top of --shadow so the soft
   drop shadow underneath the panel still anchors it to the bg. */
.card,
.admin-section {
  box-shadow:
    var(--shadow),
    inset 0 0 50px color-mix(in srgb, var(--accent) 6%, transparent),
    0 0 14px color-mix(in srgb, var(--accent) 14%, transparent);
}

/* (4) Phosphor text glow + targeted color pops:
     - Card section titles (h2) use --accent-warm so they punch in amber
       (blue theme) / champagne (pink) / gold (red) instead of muted gray.
     - Brand title and weather temp keep currentColor for the halo so
       the temperature color band (cyan→amber→red) shows through.
     - Two-layer text-shadow gives a tight inner glow + broader halo. */
.card h2 {
  color: var(--accent-warm);
  text-shadow:
    0 0 2px color-mix(in srgb, var(--accent-warm) 65%, transparent),
    0 0 10px color-mix(in srgb, var(--accent-warm) 35%, transparent);
}
/* Brand title gets a warm phosphor halo (amber/champagne/gold) so the
   page header pulls in the warm palette color even though the title
   text itself stays white for readability. */
.brand {
  text-shadow:
    0 0 2px color-mix(in srgb, var(--accent-warm) 65%, transparent),
    0 0 10px color-mix(in srgb, var(--accent-warm) 40%, transparent);
}
.weather-head .now .temp,
.weather-cell .t {
  text-shadow:
    0 0 2px color-mix(in srgb, currentColor 65%, transparent),
    0 0 8px color-mix(in srgb, currentColor 35%, transparent);
}
/* Pastel themes don't carry the dark-CRT phosphor aesthetic — the
   currentColor halo just smudges the temperature numbers against the
   light surfaces. Drop the text-shadow entirely on every pastel theme
   (pink, red, green) so the digits read crisply; dark themes keep the
   glow since it suits the Fallout/Pip-Boy CRT look. */
:root[data-theme="pink"] .weather-head .now .temp,
:root[data-theme="pink"] .weather-cell .t,
:root[data-theme="red"] .weather-head .now .temp,
:root[data-theme="red"] .weather-cell .t,
:root[data-theme="green"] .weather-head .now .temp,
:root[data-theme="green"] .weather-cell .t {
  text-shadow: none;
}

/* Profile chip: green "active" status dot before the name + cool halo
   on the chip frame itself. Two small dots of green at the top-right
   of the page balance the amber prefix at the top-left, and the chip
   ring picks up the cool accent so the whole header reads three-color. */
.profile-chip { gap: 10px; }
.profile-chip::before {
  content: '';
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--accent-cool);
  box-shadow: 0 0 6px var(--accent-cool);
  flex-shrink: 0;
}

/* (5) Search-bar focus halo + send-button glow + profile chip halo —
   little accents elsewhere so the lit-up feel isn't only on cards. */
/* Focus halo lives on the wrapper now (the wrapper holds the border).
   :focus-within fires when the inner <input> has focus. */
.search-row:focus-within {
  border-color: var(--accent);
  box-shadow:
    var(--shadow),
    0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent),
    0 0 16px color-mix(in srgb, var(--accent) 22%, transparent);
}
.profile-chip {
  box-shadow:
    var(--shadow),
    0 0 12px color-mix(in srgb, var(--accent) 18%, transparent);
}
.chat-form button {
  box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 35%, transparent);
}
.chat-form button:hover:not(:disabled) {
  box-shadow: 0 0 16px color-mix(in srgb, var(--accent) 55%, transparent);
}
.quick-link:hover {
  box-shadow: 0 0 14px color-mix(in srgb, var(--accent) 28%, transparent);
}

/* Defensive: flex children inside cards default to min-width:auto, which
   lets a long input or label push siblings past the parent's right edge.
   Forcing min-width:0 + box-sizing:border-box on form controls inside
   cards keeps the input + send button row from escaping the chat card. */
.card input,
.card textarea,
.card select,
.card form {
  min-width: 0;
  max-width: 100%;
  box-sizing: border-box;
}
.chat,
.chat-form {
  width: 100%;
  min-width: 0;
}
.chat-form input { min-width: 0; }

/* ─── Admin page ────────────────────────────────────────────────── */
.admin-page { max-width: none; }  /* fill the full .page width */
.admin-section {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 22px;
  margin-bottom: 18px; box-shadow: var(--shadow);
}
.admin-section h2 {
  margin: 0 0 14px; font-size: 16px; font-weight: 700;
}
.admin-section p.muted { color: var(--muted); font-size: 13px; margin-top: -8px; }

.form-row { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 12px; align-items: flex-end; }
.form-field { flex: 1; min-width: 180px; }
.form-field label {
  display: block; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;
  color: var(--muted); margin-bottom: 4px;
}
.form-field input[type=text], .form-field input[type=url], .form-field input[type=number],
.form-field select, .form-field textarea {
  width: 100%; padding: 9px 12px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--bg); color: var(--text); font-size: 14px; font-family: inherit;
  outline: none;
}
.form-field input:focus, .form-field select:focus, .form-field textarea:focus { border-color: var(--accent); }
.form-field textarea { min-height: 80px; resize: vertical; }
.form-field input[type=color] { width: 44px; height: 32px; border: 1px solid var(--border); border-radius: 6px; padding: 2px; background: var(--bg); }

.toggle-row { display: flex; align-items: center; gap: 10px; font-size: 14px; }

.admin-actions {
  position: sticky; bottom: 0;
  background: var(--bg); padding: 14px 0 8px; margin-top: 18px;
  display: flex; justify-content: flex-end; gap: 10px;
  border-top: 1px solid var(--border);
}
.admin-actions button {
  padding: 10px 22px; border-radius: 10px; font-weight: 600; font-size: 14px;
  border: 1px solid var(--border); background: var(--panel); color: var(--text);
}
.admin-actions button.primary { background: var(--accent); border-color: var(--accent); color: white; }
.admin-actions .saved-indicator { color: var(--muted); font-size: 12px; align-self: center; margin-right: auto; }

.bang-table { width: 100%; border-collapse: collapse; }
.bang-table td { padding: 4px 6px 4px 0; }
.bang-table input { width: 100%; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); font-size: 13px; outline: none; }
.bang-table .key-col { width: 80px; }
.bang-table .actions-col { width: 36px; text-align: right; }
.icon-btn {
  background: transparent; border: 0; color: var(--muted); font-size: 16px;
  padding: 4px 8px; border-radius: 6px;
}
.icon-btn:hover { background: var(--inset); color: var(--accent); }

.theme-editor { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.theme-card { background: var(--inset); border: 1px solid var(--border); border-radius: 10px; padding: 12px; }
.theme-card h3 { margin: 0 0 8px; font-size: 13px; font-weight: 700; text-transform: capitalize; }
.theme-card .swatch-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.theme-card label { font-size: 11px; color: var(--muted); display: flex; align-items: center; gap: 6px; }

.profile-rows { display: flex; flex-direction: column; gap: 10px; }
.profile-row { background: var(--inset); border: 1px solid var(--border); border-radius: 10px; padding: 12px; }
.profile-row .row-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.profile-row h3 { margin: 0; font-size: 14px; }
.profile-row .device-count { font-size: 11px; color: var(--muted); }

/* ── Uploaded icons (admin) ─────────────────────────────────────
   Thumbnail grid of every image in the shared icon library, with a
   delete badge on each so the admin can prune the library when an
   image is no longer needed. Mirrors the quick-link tile layout but
   smaller and without text labels. */
.uploaded-icons {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: 10px;
  margin-top: 8px;
}
.uploaded-icon {
  position: relative;
  aspect-ratio: 1 / 1;
  background: var(--inset);
  border: 1px solid var(--border);
  border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  /* No overflow:hidden here — the delete badge sits at top:-6px / right:-6px
     and needs to escape the box to render its full circle. The img inside
     is sized to 70%/70% and centered, so it can't visually exceed the
     rounded corners on its own. */
}
.uploaded-icon img {
  width: 70%; height: 70%;
  object-fit: contain;
  background: white;
  border-radius: 6px;
  padding: 2px;
}
.uploaded-icon-del {
  position: absolute;
  top: -6px; right: -6px;
  width: 20px; height: 20px;
  border-radius: 50%;
  border: 2px solid var(--panel);
  background: #ef4444;
  color: white;
  font-size: 11px; font-weight: 700;
  line-height: 1;
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  z-index: 2;
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
  padding: 0;
  transition: transform 120ms, background 120ms;
}
.uploaded-icon-del:hover { transform: scale(1.15); background: #dc2626; }

/* ── Merge profiles row ───────────────────────────────────────
   Two dropdowns + a Merge button on a single row, wrapping on
   narrower screens. The button aligns with the bottom of the
   dropdowns since the labels above sit on the same baseline. */
.merge-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px;
  margin-top: 8px;
}
.merge-row .form-field {
  flex: 1 1 220px;
  min-width: 200px;
}
.merge-row select {
  width: 100%;
}
.merge-row #mergeBtn {
  flex: 0 0 auto;
  padding: 8px 18px;
}

/* ─── Admin tabs ─────────────────────────────────────────────── */
.admin-tab-bar {
  display: flex; gap: 2px; flex-wrap: wrap;
  border-bottom: 2px solid var(--border); margin-bottom: 18px;
}
.admin-tab-btn {
  padding: 10px 20px;
  border: 1px solid transparent; border-bottom: none;
  background: transparent; color: var(--muted);
  font-size: 13px; font-weight: 600; cursor: pointer;
  border-radius: 8px 8px 0 0; margin-bottom: -2px;
  transition: color 100ms, background 100ms;
}
.admin-tab-btn:hover { color: var(--text); background: var(--inset); }
.admin-tab-btn.active {
  color: var(--text); background: var(--panel);
  border-color: var(--border); border-bottom-color: var(--panel);
}
.admin-tab-panel { display: none; }
.admin-tab-panel.active { display: block; }

/* Profile picker in the Profiles tab */
.profile-picker-wrap {
  display: flex; align-items: center; gap: 12px; margin-bottom: 14px;
}
.profile-picker-wrap select {
  padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--bg); color: var(--text); font-size: 14px;
  min-width: 200px; outline: none;
}
.profile-picker-wrap select:focus { border-color: var(--accent); }

/* Device list inside profile row */
.device-list { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; }
.device-item {
  display: flex; align-items: center; gap: 8px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 6px; padding: 4px 8px;
}
.device-item code {
  flex: 1; font-size: 11px; color: var(--muted);
  font-family: monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.device-item .dev-remove {
  color: #dc2626; font-size: 11px; padding: 2px 6px; flex-shrink: 0;
}

/* ─── Modal tabs ─────────────────────────────────────────────── */
.modal-tab-bar {
  display: flex; gap: 0; flex-wrap: wrap;
  border-bottom: 2px solid var(--border); margin-bottom: 14px;
}
.modal-tab-btn {
  padding: 8px 14px; border: none; background: transparent;
  color: var(--muted); font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.6px; cursor: pointer;
  border-radius: 6px 6px 0 0;
  border-bottom: 2px solid transparent; margin-bottom: -2px;
  transition: color 100ms;
}
.modal-tab-btn:hover { color: var(--text); }
.modal-tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
.modal-tab-panel { display: none; }
.modal-tab-panel.active { display: block; }
