  :root {
    color-scheme: light;
    --paper:    #fcfcfb;   /* near-pure white, faintest warm undertone */
    --paper-2:  #f3f2ee;   /* recessed track */
    --paper-3:  #e8e6df;   /* placeholder fill */
    --ink:      #1a1612;
    --ink-2:    #4a3f31;
    --ink-soft: #908576;
    --accent:   #4a3f31;   /* warm dark grey - matches ink-2, no longer red */
    --accent-2: #1a1612;   /* near-black ink - same hue as the title text */
    --hairline: rgba(26,22,18,0.14);  /* faint divider lines + chart gridlines */
    --pill:     #fcfcfb;              /* active-toggle sliding pill */
    /* Depth shadow recipes - replace flat 1px borders */
    --edge:    inset 0 0 0 1px rgba(255,255,255,0.55), 0 0 0 1px rgba(26,22,18,0.04);
    --edge-lg: inset 0 0 0 1px rgba(255,255,255,0.6), 0 0 0 1px rgba(26,22,18,0.06), 0 8px 28px rgba(26,22,18,0.06);
    --recess:  inset 0 1px 2px rgba(26,22,18,0.06), inset 0 0 0 1px rgba(26,22,18,0.04);
    --raised:  inset 0 0 0 1px rgba(255,255,255,0.8), 0 0 0 1px rgba(26,22,18,0.05), 0 1px 2px rgba(26,22,18,0.07);
  }
  /* ---- Charcoal dark theme. Set via data-theme="dark" on <html> (an
     inline script in index.html applies the saved choice before paint;
     the switcher lives in Settings). The whole UI is variable-driven, so
     flipping ink<->paper inverts cleanly: text reads light, the stats
     squares become light marks on dark, the collage cutouts (transparent
     PNGs) sit on the charcoal directly. Shadow recipes are re-cut for a
     dark surface (faint light edges, deeper drop shadows). ---- */
  :root[data-theme="dark"] {
    color-scheme: dark;
    --paper:    #17181c;   /* charcoal page bg */
    --paper-2:  #212329;   /* recessed track */
    --paper-3:  #2c2f36;   /* placeholder fill */
    --ink:      #ece8e1;   /* warm off-white text / marks */
    --ink-2:    #b7afa2;   /* secondary text */
    --ink-soft: #837c70;   /* muted text */
    --accent:   #d8d2c6;   /* light warm - pills read light-on-dark */
    --accent-2: #ece8e1;
    --hairline: rgba(236,232,225,0.20);  /* faint light divider on charcoal */
    --pill:     #3a3e48;                 /* raised, clearly lighter than the track */
    --edge:    inset 0 0 0 1px rgba(255,255,255,0.05), 0 0 0 1px rgba(0,0,0,0.4);
    --edge-lg: inset 0 0 0 1px rgba(255,255,255,0.06), 0 0 0 1px rgba(0,0,0,0.5), 0 10px 30px rgba(0,0,0,0.5);
    --recess:  inset 0 1px 2px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,255,255,0.04);
    --raised:  inset 0 0 0 1px rgba(255,255,255,0.07), 0 0 0 1px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.5);
  }
  * { box-sizing: border-box; }
  html, body { margin: 0; padding: 0; height: 100%; background: var(--paper); color: var(--ink); }
  body {
    font-family: ui-serif, "Iowan Old Style", "Apple Garamond", Georgia, serif;
    -webkit-font-smoothing: antialiased;
    overflow: hidden;
  }
  .mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: 0.02em; }

  /* ============ Stage / views ============
     Stage is a flex column: a static title block sits on top, the views
     slider fills the remaining height. The title NEVER moves when the
     view changes - only its text fades when the new view's title differs
     from the current one. */
  .stage {
    position: fixed; inset: 0; overflow: hidden;
    display: flex; flex-direction: column;
  }
  .static-head {
    flex: 0 0 auto;
    padding: 84px 32px 22px;
    text-align: center; position: relative; z-index: 5;
  }
  .static-head .pre {
    display: inline-block;
    background: none; border: 0; padding: 0;
    font: italic 400 clamp(13px, 1.4vw, 18px)/1 ui-serif, "Iowan Old Style", Georgia, serif;
    color: var(--ink-2); letter-spacing: 0.06em; margin-bottom: 6px;
    cursor: pointer; transition: color 140ms ease;
  }
  .static-head .pre:hover {
    color: var(--ink);
    text-decoration: underline; text-underline-offset: 3px;
    text-decoration-thickness: 1px;
  }
  .static-head h1 {
    margin: 0; padding: 0;
    font: 700 clamp(24px, 3.2vw, 40px)/1 ui-serif, "Iowan Old Style", "Bookman Old Style", Georgia, serif;
    letter-spacing: 0.06em; color: var(--ink); text-transform: uppercase;
    font-style: normal;
    transition: opacity 240ms cubic-bezier(.7,.05,.2,1);
  }
  .static-head.swap-out h1 { opacity: 0; }
  @media (max-width: 700px) {
    .static-head { padding: 96px 18px 22px; }
  }

  .views {
    flex: 1 1 auto; min-height: 0;
    position: relative; display: flex;
    transition: transform 480ms cubic-bezier(.7,.05,.2,1);
  }
  .view  { flex: 0 0 100%; height: 100%; overflow: auto; padding: 8px 56px 132px; position: relative; }
  /* Collage and stats views both fit the viewport (no scroll). Atlas
     keeps normal scroll. */
  .view#v0, .view#v1 { overflow: hidden; padding: 8px 32px 88px; }
  .view#v0 { display: flex; }
  .view#v1 { display: flex; flex-direction: column; }
  @media (max-width: 700px) { .view { padding: 8px 18px 132px; } .view#v0, .view#v1 { padding: 8px 16px 88px; } }

  /* ============ Top bar - window picker (left) + menu button (right). ============
     Visible on every view; the picker is the universal time-window control. */
  .top {
    position: fixed; top: 22px; left: 0; right: 0;
    display: flex; justify-content: space-between; align-items: center;
    padding: 0 28px; z-index: 30; pointer-events: none;
  }
  .top > * { pointer-events: auto; }
  .top .window-pick {
    display: inline-flex; gap: 0; padding: 4px;
    background: var(--paper-2); border-radius: 999px;
    box-shadow: var(--recess);
    position: relative;
  }
  .top .window-pick button {
    background: transparent; border: 0; color: var(--ink-soft);
    font: 10px/1 ui-monospace, Menlo, monospace; letter-spacing: 0.18em;
    padding: 9px 16px; border-radius: 999px; cursor: pointer; text-transform: uppercase;
    transition: color 200ms ease;
    position: relative; z-index: 1;
  }
  .top .window-pick button:hover { color: var(--ink); }
  .top .window-pick button[aria-current="true"] { color: var(--ink); }
  .menu-btn {
    background: var(--paper); border: 0; color: var(--ink);
    font: 11px/1 ui-monospace, Menlo, monospace; padding: 9px 14px 8px;
    border-radius: 999px; cursor: pointer; letter-spacing: 0.18em; text-transform: lowercase;
    box-shadow: var(--raised);
    transition: transform 160ms ease;
  }
  .menu-btn:hover { transform: translateY(-1px); }
  .menu-btn:active { transform: translateY(0); }
  /* Compact top bar on mobile - the time-window pill goes from
     spacious to thumb-tight without losing legibility. */
  @media (max-width: 700px) {
    .top { top: 12px; padding: 0 12px; }
    .top .window-pick { padding: 3px; }
    .top .window-pick button {
      font-size: 8.5px; padding: 6px 10px; letter-spacing: 0.12em;
    }
    .menu-btn { font-size: 9px; padding: 6px 11px 5px; letter-spacing: 0.14em; }
    .return-to-atlas { top: 10px; left: 10px; padding: 5px 10px; font-size: 8.5px; }
  }
  @media (max-width: 420px) {
    .top .window-pick button { font-size: 8px; padding: 5px 7px; letter-spacing: 0.08em; }
  }

  /* ============ Bottom slider - recessed track, raised active pill ============ */
  .slider {
    position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%);
    display: flex; gap: 0; padding: 4px; z-index: 30;
    background: var(--paper-2); border-radius: 999px;
    box-shadow: var(--recess);
    position: fixed; /* re-emit for the pill positioning */
  }
  .slider button {
    background: transparent; border: 0; color: var(--ink-soft);
    font: 10px/1 ui-monospace, Menlo, monospace; letter-spacing: 0.2em;
    padding: 11px 24px; border-radius: 999px; cursor: pointer; text-transform: uppercase;
    transition: color 200ms ease;
    position: relative; z-index: 1;
  }
  .slider button:hover { color: var(--ink); }
  .slider button[aria-current="true"] { color: var(--ink); }

  /* Shared sliding pill - JS animates transform and width to the
     active button's position. The pill is the only element with the
     raised-paper look; buttons just toggle their text colour. */
  .seg-pill {
    position: absolute; top: 4px; bottom: 4px; left: 0;
    background: var(--pill); border-radius: 999px;
    box-shadow: var(--raised);
    transition: transform 320ms cubic-bezier(.7,.05,.2,1), width 320ms cubic-bezier(.7,.05,.2,1);
    will-change: transform, width;
    pointer-events: none; z-index: 0;
  }

  /* ============ Menu dropdown ============ */
  #menu-dd {
    position: fixed; top: 58px; right: 28px; z-index: 25;
    width: 360px; max-width: calc(100vw - 24px);
    max-height: calc(100vh - 80px); overflow: auto;
    background: var(--paper); border-radius: 12px;
    box-shadow: var(--edge-lg);
    padding: 14px; opacity: 0; transform: translateY(-6px);
    pointer-events: none; transition: opacity 200ms ease, transform 200ms ease;
  }
  #menu-dd.open { opacity: 1; transform: translateY(0); pointer-events: auto; }

  .lock-row {
    display: flex; gap: 6px; align-items: stretch;
    background: var(--paper-2); border-radius: 8px;
    box-shadow: var(--recess);
    padding: 4px;
  }
  .lock-row input {
    flex: 1; background: transparent; border: 0; outline: 0;
    color: var(--ink); padding: 8px 10px;
    font: 13px ui-monospace, Menlo, monospace;
  }
  .lock-row input::placeholder { color: var(--ink-soft); }
  .lock-row button {
    background: var(--paper); border: 0; color: var(--ink);
    width: 32px; padding: 0; border-radius: 6px; cursor: pointer;
    font: 14px/1 ui-monospace, Menlo, monospace;
    box-shadow: var(--raised);
    display: flex; align-items: center; justify-content: center;
  }
  .lock-row button:hover { color: var(--accent); }
  .lock-hint {
    margin: 8px 4px 0; font: 10px ui-monospace, Menlo, monospace;
    color: var(--ink-soft); letter-spacing: 0.04em;
  }
  .lock-err { color: var(--accent); }
  /* Tiny "built by teddy" attribution - appears under the lock-screen
     password field AND at the foot of the about-card. Same monospace
     micro-type used by .lock-hint so the two contexts read consistent. */
  .built-by {
    margin: 14px 4px 0;
    font: 10px ui-monospace, Menlo, monospace;
    color: var(--ink-soft);
    letter-spacing: 0.04em;
  }
  .built-by a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
  }
  .built-by a:hover { color: var(--ink); }
  .about-card .built-by { margin-top: 18px; }

  .menu-items { padding: 4px 4px; display: none; }
  .menu-items.show { display: block; }
  .menu-items h3 {
    font: 9px/1 ui-monospace, Menlo, monospace; letter-spacing: 0.24em;
    color: var(--ink-soft); text-transform: uppercase; margin: 12px 4px 4px;
  }
  .menu-items a {
    display: block; padding: 9px 8px; color: var(--ink); text-decoration: none;
    font: 14px ui-serif, Georgia, serif; border-radius: 6px;
    transition: background 120ms ease;
  }
  .menu-items a:hover { background: var(--paper-2); color: var(--accent); }

  /* ============ Settings cards inside the drawer ============
     Form controls match the time-picker's design language: recessed
     track + raised paper element. Toggles, sliders, and segmented
     pickers share the same depth-shadow recipe. */
  .menu-section {
    margin-top: 14px; padding-top: 12px;
    border-top: 1px solid rgba(26,22,18,0.07);
  }
  .menu-section:first-of-type { margin-top: 6px; border-top: 0; padding-top: 0; }
  .menu-section h3 {
    font: 700 9px ui-monospace, Menlo, monospace; letter-spacing: 0.20em;
    color: var(--ink); text-transform: uppercase;
    margin: 0 0 8px 4px;
  }
  /* Collapsible section - header is a button row with caret on the right.
     Default state is collapsed so accidental clicks don't touch settings. */
  .menu-section.collapsible > .section-header {
    display: flex; align-items: center; justify-content: space-between;
    cursor: pointer; user-select: none;
    padding: 4px 6px 6px 0;
    margin: 0 0 4px 0;
  }
  .menu-section.collapsible > .section-header h3 { margin: 0; }
  .menu-section.collapsible > .section-header .caret {
    width: 10px; height: 10px; transition: transform 200ms cubic-bezier(.7,.05,.2,1);
    color: var(--ink-soft);
  }
  .menu-section.collapsible[data-open="true"] > .section-header .caret { transform: rotate(90deg); }
  .menu-section.collapsible > .section-body {
    max-height: 0; overflow: hidden;
    transition: max-height 280ms cubic-bezier(.7,.05,.2,1);
  }
  .menu-section.collapsible[data-open="true"] > .section-body {
    max-height: 800px;
  }
  .menu-row {
    display: grid; grid-template-columns: 1fr auto; gap: 12px;
    align-items: center;
    padding: 8px 6px 8px 10px;
    border-radius: 6px;
  }
  .menu-row + .menu-row { box-shadow: inset 0 1px 0 rgba(26,22,18,0.06); }
  .menu-row .label {
    font: 12.5px ui-serif, Georgia, serif; color: var(--ink);
  }
  .hint {
    display: block;
    font: 9px ui-monospace, Menlo, monospace;
    color: var(--ink-soft); letter-spacing: 0.04em; margin-top: 1px;
  }

  /* Toggle switch (raised pill that slides) */
  .switch {
    position: relative; width: 38px; height: 20px;
    background: var(--paper-2); border-radius: 999px;
    box-shadow: var(--recess);
    cursor: pointer;
    border: 0; padding: 0;
  }
  .switch::after {
    content: ''; position: absolute; top: 2px; left: 2px;
    width: 16px; height: 16px;
    background: var(--pill); border-radius: 999px;
    box-shadow: var(--raised);
    transition: transform 220ms cubic-bezier(.7,.05,.2,1), background 200ms ease;
  }
  .switch[aria-checked="true"] {
    background: var(--ink-2);
  }
  .switch[aria-checked="true"]::after {
    transform: translateX(18px);
    background: var(--pill);
  }

  /* Slider - title row (label + value badge with breathing room) and
     a track row underneath. Value sits at the far right with a wide
     gap so it never crowds the label text. */
  .slider-row {
    display: block;
    padding: 8px 6px 10px 10px;
  }
  .slider-row .head {
    display: flex; align-items: baseline; gap: 24px;
    margin-bottom: 6px;
  }
  .slider-row .head .label-block { flex: 1; min-width: 0; }
  .slider-row .label {
    font: 12.5px ui-serif, Georgia, serif; color: var(--ink);
  }
  .slider-row .value {
    flex: 0 0 auto;
    font: 11px ui-monospace, Menlo, monospace;
    color: var(--ink); font-variant-numeric: tabular-nums;
    background: var(--paper-2); padding: 3px 10px; border-radius: 4px;
    box-shadow: var(--recess);
    min-width: 48px; text-align: center;
  }
  .slider-row .slider-track {
    margin-top: 2px;
  }
  .slider-track input[type="range"] {
    -webkit-appearance: none; appearance: none;
    width: 100%; height: 18px; background: transparent; cursor: pointer;
  }
  .slider-track input[type="range"]::-webkit-slider-runnable-track {
    height: 4px; background: var(--paper-2); border-radius: 999px;
    box-shadow: var(--recess);
  }
  .slider-track input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none;
    width: 16px; height: 16px; margin-top: -6px;
    background: var(--paper); border-radius: 999px;
    box-shadow: var(--raised);
    cursor: pointer;
  }
  .slider-track input[type="range"]::-moz-range-track {
    height: 4px; background: var(--paper-2); border-radius: 999px;
    box-shadow: var(--recess);
  }
  .slider-track input[type="range"]::-moz-range-thumb {
    width: 16px; height: 16px;
    background: var(--paper); border-radius: 999px;
    box-shadow: var(--raised); border: 0;
    cursor: pointer;
  }

  /* Segmented picker (same pill-on-recess pattern as the time picker) */
  .seg {
    display: inline-flex; padding: 4px;
    background: var(--paper-2); border-radius: 999px;
    box-shadow: var(--recess);
    position: relative;
  }
  .seg button {
    background: transparent; border: 0; color: var(--ink-soft);
    font: 9.5px ui-monospace, Menlo, monospace;
    letter-spacing: 0.16em; text-transform: uppercase;
    padding: 6px 12px; border-radius: 999px; cursor: pointer;
    position: relative; z-index: 1;
    transition: color 200ms ease;
  }
  .seg button[aria-current="true"] {
    color: var(--ink);
    background: var(--pill);
    box-shadow: var(--raised);
  }

  /* Inline live-audio player - sits at the top of the unlocked drawer.
     A single play/stop button with a quiet running-indicator dot. */
  .live-audio {
    display: flex; align-items: center; gap: 12px;
    padding: 10px 12px; margin: 0 0 6px;
    background: var(--paper-2); border-radius: 8px;
    box-shadow: var(--recess);
  }
  .live-audio .pulse {
    width: 8px; height: 8px; border-radius: 50%;
    background: var(--ink-soft); flex-shrink: 0;
    transition: background 200ms ease;
  }
  .live-audio[data-on="true"] .pulse {
    background: #c44a3b;
    animation: pulsate 1.6s ease-in-out infinite;
  }
  @keyframes pulsate {
    0%, 100% { opacity: 0.4; transform: scale(1); }
    50%      { opacity: 1;   transform: scale(1.25); }
  }
  .live-audio .label {
    flex: 1; font: 12.5px ui-serif, Georgia, serif; color: var(--ink);
  }
  .live-audio .label .hint {
    font: 9px ui-monospace, Menlo, monospace; color: var(--ink-soft);
    letter-spacing: 0.04em;
  }
  .live-audio button {
    background: var(--paper); border: 0; color: var(--ink);
    padding: 6px 12px; border-radius: 4px;
    font: 9.5px ui-monospace, Menlo, monospace;
    letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer;
    box-shadow: var(--raised);
    display: inline-flex; align-items: center; gap: 6px;
  }
  .live-audio button svg { width: 9px; height: 9px; }
  .live-audio button + button { margin-left: 4px; }
  /* Live spectrogram - hidden until the user clicks LISTEN. We
     collapse via max-height (not display:none) so the canvas inside
     keeps its layout dimensions and paints correctly on expand. */
  .live-spectro {
    width: 100%; height: 0;
    background: var(--paper-2); border-radius: 6px;
    margin: 0; overflow: hidden;
    box-shadow: var(--recess);
    transition: height 240ms cubic-bezier(.7,.05,.2,1), margin 240ms ease;
  }
  .live-audio[data-on="true"] + .live-spectro {
    height: 70px; margin: 0 0 8px;
  }
  .live-status {
    font: 9.5px ui-monospace, Menlo, monospace; color: var(--ink-soft);
    letter-spacing: 0.04em; text-transform: lowercase;
    margin: -4px 0 6px 0; padding: 0 4px;
    min-height: 13px;
  }
  .live-status.err { color: #a83a2d; }
  .menu-links a .l-hint {
    display: block; font: 8.5px ui-serif, Georgia, serif; text-transform: none;
    letter-spacing: 0; color: var(--ink-soft); margin-top: 2px;
  }
  .menu-links a.full { grid-column: 1 / -1; }

  /* External BirdNET pages - small grid of cards */
  .menu-links {
    display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
    margin: 4px 0 2px;
  }
  .menu-links a {
    display: flex; align-items: center; justify-content: center;
    padding: 9px 10px;
    background: var(--paper-2);
    color: var(--ink); text-decoration: none;
    font: 10.5px ui-monospace, Menlo, monospace;
    letter-spacing: 0.10em; text-transform: lowercase;
    border-radius: 6px;
    box-shadow: var(--raised);
    transition: transform 160ms ease, color 160ms ease;
  }
  .menu-links a:hover { transform: translateY(-1px); color: var(--ink); background: var(--paper); }
  .menu-save-row {
    display: flex; gap: 8px; align-items: center; justify-content: flex-end;
    margin-top: 10px; padding: 0 4px;
  }
  .menu-save-row .save-state {
    flex: 1; font: 9px ui-monospace, Menlo, monospace;
    color: var(--ink-soft); letter-spacing: 0.04em;
  }
  .menu-save-row .save-state.ok    { color: #2d5d3a; }
  .menu-save-row .save-state.err   { color: var(--accent); }
  .menu-save-row button {
    background: var(--ink); color: var(--paper); border: 0;
    padding: 7px 14px; border-radius: 6px;
    font: 10px ui-monospace, Menlo, monospace; letter-spacing: 0.14em;
    text-transform: uppercase; cursor: pointer;
    box-shadow: var(--raised);
  }
  .menu-save-row button:disabled { opacity: 0.4; cursor: default; }

  /* ============ View 1 - Collage (Galliformes-style poster) ============
     Viewport-fit, no scroll. The shared static-head provides the title;
     this view's content is just the cluster of bird cutouts which fills
     the remaining viewport height. */
  .gcollage {
    flex: 1 1 auto; width: 100%; max-width: 1300px; margin: 0 auto;
    position: relative; min-height: 0;
  }
  /* Centre-out bloom: each tile fades + scales in; JS staggers the
     animation-delay by distance from the cluster centre. backwards
     fill-mode holds the hidden start state through the delay, and we keep
     no forwards transform so the hover scale still works afterward. */
  @keyframes gtile-in {
    from { opacity: 0; transform: scale(0.82); }
    to   { opacity: 1; transform: scale(1); }
  }
  .gtile.entering {
    animation: gtile-in 420ms cubic-bezier(.2,.7,.3,1) backwards;
  }
  @media (prefers-reduced-motion: reduce) {
    .gtile.entering { animation: none; }
  }
  /* Atlas: cards rise + fade in, row by row (JS staggers by row). */
  @keyframes bird-card-in {
    from { opacity: 0; transform: translateY(16px) scale(0.96); }
    to   { opacity: 1; transform: none; }
  }
  .bird-card.entering { animation: bird-card-in 480ms cubic-bezier(.2,.7,.3,1) backwards; }
  /* Stats: the WHOLE graph populates left-to-right - columns, gridlines and
     x-axis ticks stagger by their x; the y-axis + side panel fade in just
     behind. Opacity only (these elements use transform for layout, so
     animating transform here would fight their positioning). */
  @keyframes stats-fade-in { from { opacity: 0; } to { opacity: 1; } }
  .stats-tl-col.entering, .stats-tl-gridline.entering,
  .stats-tl-xtick.entering, .stats-tl-yaxis.entering,
  .stats-side h3.entering, .stats-side small.entering, .stats-side li.entering {
    animation: stats-fade-in 340ms ease backwards;
  }
  @media (prefers-reduced-motion: reduce) {
    .bird-card.entering, .stats-tl-col.entering, .stats-tl-gridline.entering,
    .stats-tl-xtick.entering, .stats-tl-yaxis.entering,
    .stats-side h3.entering, .stats-side small.entering, .stats-side li.entering { animation: none; }
  }
  .gtile {
    position: absolute;
    background: transparent; border: 0; padding: 0; margin: 0;
    overflow: visible; /* let drop-shadow extend past the bbox */
    transition: transform 130ms ease;
    /* Events are handled at the .collage level via alpha-mask
       hit-testing - the tile rectangles must not intercept them. */
    pointer-events: none;
  }
  /* Hover state is driven by JS alpha-mask hit-testing (.is-hover),
     NOT the CSS :hover pseudo - the rectangular tile bounding boxes
     overlap, so :hover would light up the wrong bird. .is-hover only
     ever sits on the one tile whose silhouette is actually under the
     cursor. */
  .gtile.is-hover { transform: scale(1.04); z-index: 3; }
  .gtile img {
    width: 100%; height: 100%; object-fit: contain; display: block;
    /* Cutouts arrive as alpha-bbox-cropped transparent-bg PNGs, so
       object-fit: contain leaves no whitespace inside the tile - the
       bird touches all four sides. Soft drop shadow keeps it readable
       on paper. */
    filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10));
    transition: filter 130ms ease;
  }
  .gtile.is-hover img {
    filter: drop-shadow(0 3px 10px rgba(26,22,18,0.26)) saturate(1.1);
  }
  /* Hover pill - surfaces the windowed count for the bird under the
     cursor. Confirms (Anna's-hummingbird-is-#1 paranoia) that the
     collage scaling reflects the selected window, not all-time. */
  .collage-tip {
    position: absolute; left: 50%; bottom: 14px;
    transform: translateX(-50%);
    padding: 6px 14px;
    background: var(--paper);
    border-radius: 999px;
    box-shadow: 0 6px 18px rgba(26,22,18,0.12),
                inset 0 0 0 1px rgba(255,255,255,0.6);
    font: italic 13px/1.3 ui-serif, "Iowan Old Style", Georgia, serif;
    color: var(--ink);
    letter-spacing: 0.02em;
    opacity: 0; pointer-events: none;
    transition: opacity 150ms ease;
    white-space: nowrap;
    z-index: 4;
  }
  .collage-tip[aria-hidden="false"] { opacity: 1; }
  .collage-tip .ct-name { font-style: normal; font-weight: 600; }
  .collage-tip .ct-n { font-style: normal; font-weight: 700; }
  .collage-tip .ct-w { color: var(--ink-soft); }

  .empty {
    color: var(--ink-soft); font: 10px ui-monospace, Menlo, monospace;
    padding: 120px 0; text-align: center; letter-spacing: 0.18em;
    text-transform: uppercase;
  }

  /* Page header is now shared at .stage > .static-head - see top of CSS. */
  /* Stats grid: charts stack on the LEFT, text sections on the RIGHT.
     Each chart is a full-bleed card; each text section is a list with
     subtle separators. Sized to fit the viewport without scrolling. */
  .stats-grid {
    flex: 1 1 auto; min-height: 0;
    display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 80px;
    max-width: 1100px; margin: 0 auto; width: 100%;
    align-items: stretch;
  }
  @media (max-width: 900px) {
    .stats-grid { grid-template-columns: 1fr; gap: 40px; }
    /* Single-column: the timeline has no tall grid sibling to stretch
       against, so pin it to a usable height instead of letting it
       collapse to its (zero) intrinsic height. clamp() keeps it sane
       on both short landscape phones and tall portrait ones. */
    .stats-timeline { height: clamp(260px, 44vh, 420px); }
  }
  /* Phones: the timeline keeps its editorial form but uses fixed wider
     columns (set in drawHistograms) and scrolls horizontally for the rest,
     with larger labels/ticks so it's legible + tappable. */
  @media (max-width: 700px) {
    .stats-timeline { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; }
    .stats-tl-label { font-size: 11px; }
    .stats-tl-label .sci { font-size: 9px; }
    .stats-tl-xtick { font-size: 8px; }
  }
  /* Desktop two-column: size the grid to the side panel's content height
     (the timeline stretches to match it) and centre it vertically, so the
     chart sits beside the text at the same height instead of stretching to
     the full viewport. */
  @media (min-width: 901px) {
    .stats-grid { flex: 0 1 auto; margin: auto; }
  }

  /* Editorial detection timeline. One evenly-spaced column per species,
     ordered by last-detection time (x = time). Black square fills its
     column so neighbours touch at the gridline; its height up the column
     encodes count. Small rotated label + per-column timestamp at the
     bottom. Fits the viewport - never scrolls. */
  .stats-timeline {
    position: relative;
    flex: 1 1 auto; min-height: 0;
    padding: 0 0 28px 32px;   /* left = y-axis (quantity), bottom = timestamps */
  }
  .stats-tl-yaxis {
    position: absolute; left: 0; top: 0; bottom: 28px; width: 28px;
    border-right: 1px solid var(--hairline);
  }
  .stats-tl-ytick {
    position: absolute; right: 5px;
    font: 8px/1 "SF Mono", Menlo, monospace; color: var(--ink-soft);
    transform: translateY(50%);
  }
  .stats-tl-ytick::after {
    content: ''; position: absolute; right: -5px; top: 50%;
    width: 4px; height: 1px; background: var(--hairline);
  }
  .stats-tl-plot {
    position: relative; height: 100%;
  }
  /* Evenly-spaced cell, one per species. JS sets left (column centre)
     and width (the column slot); the square and label centre within it. */
  .stats-tl-col {
    position: absolute; top: 0; bottom: 0;
    transform: translateX(-50%);
    cursor: pointer;
  }
  /* Faint vertical hairlines at every x-axis tick - the "no detections
     in this slice" period reads as the empty space between marks. */
  .stats-tl-gridline {
    position: absolute; top: 0; bottom: 0;
    width: 1px;
    background: var(--hairline);
    pointer-events: none;
  }
  .stats-tl-square {
    position: absolute; left: 50%;
    transform: translateX(-50%);
    background: var(--ink);
    /* Paper-coloured side borders. box-sizing: border-box keeps the box
       column-filling, so the fill just insets 1px each side. Where two
       squares touch, the abutting borders read as a crisp white line
       directly between them; on the cluster's outer edges they vanish
       into the matching paper background. */
    border-left: 1px solid var(--paper);
    border-right: 1px solid var(--paper);
    transition: background 140ms ease, transform 140ms ease;
  }
  .stats-tl-col { cursor: pointer; }
  /* Cross-highlight: hovering a square or a table row lights up the
     matching species on the other side - the square scales up and its
     label scales + shifts to the accent ink, so the whole column reads
     as selected. */
  .stats-tl-col.sync-hi .stats-tl-square {
    background: var(--accent);
    transform: translateX(-50%) scale(1.18);
  }
  .stats-tl-col.sync-hi .stats-tl-label {
    transform: rotate(-90deg) translateY(55%) scale(1.1);
  }
  .stats-tl-col.sync-hi .stats-tl-label .com,
  .stats-tl-col.sync-hi .stats-tl-label .sci { color: var(--accent); }
  /* Two-line species label, rotated to read bottom-to-top like the
     reference exhibition checklist. Sits just above the square -
     rotate(-90deg) then translateY(50%) re-centres the stack on the
     column regardless of label height. Common name and scientific
     name sit as two adjacent columns of vertical text. The sync-hi
     scale uses translateY(55%) to stay centred while enlarged. */
  .stats-tl-label {
    position: absolute; left: 50%;
    transform-origin: 0% 100%;
    /* translateY(50%) becomes a horizontal shift after the -90deg turn
       that re-centres the rotated label on the column (= on its square),
       so the name reads up the middle of each block. */
    transform: rotate(-90deg) translateY(50%);
    white-space: nowrap;
    font: 10px/1.15 ui-serif, "Iowan Old Style", Georgia, serif;
    color: var(--ink);
    transition: transform 130ms ease;
  }
  .stats-tl-label .com {
    display: block; font-weight: 600;
    transition: color 130ms ease;
  }
  .stats-tl-label .sci {
    display: block;
    font-style: italic; color: var(--ink-soft); font-size: 8.5px;
    transition: color 130ms ease;
  }
  /* Ticks live inside .stats-tl-plot so their left:% aligns with the
     gridlines (both share the plot's content box). bottom is negative
     to push the label into the x-axis gutter below the plot. */
  .stats-tl-xtick {
    position: absolute; bottom: -19px;
    transform: translateX(-50%);
    font: 7px/1 "SF Mono", Menlo, monospace; color: var(--ink-soft);
    white-space: nowrap;
  }
  .stats-tl-empty {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    font: 11px ui-monospace, Menlo, monospace; color: var(--ink-soft);
    text-transform: lowercase; letter-spacing: 0.18em;
  }
  /* Trim note - shown when the plot can't fit every species column and
     falls back to the most-heard subset. A paper background keeps it
     legible even if a tall label reaches the corner behind it. */
  .stats-tl-cap {
    position: absolute; top: 0; right: 0; z-index: 2;
    background: var(--paper); padding: 1px 0 3px 8px;
    font: 8.5px/1 "SF Mono", Menlo, monospace; color: var(--ink-soft);
    letter-spacing: 0.04em;
  }

  /* (Old chart-card / .stats-charts CSS removed - replaced by the
     editorial .stats-timeline above.) */

  .stats-side {
    display: flex; flex-direction: column; justify-content: center;
    align-self: stretch; max-height: 100%;
    padding: 6px 0;
  }
  .stats-side h3 {
    font: 700 10px ui-monospace, Menlo, monospace; color: var(--ink);
    letter-spacing: 0.18em; text-transform: uppercase; margin: 0 0 1px;
    border-left: 2px solid var(--accent); padding-left: 10px;
  }
  /* Tight, content-sized groups - they sit just below their headers
     rather than spreading to fill the column. */
  .stats-side .grp { flex: 0 0 auto; }
  .stats-side .grp + .grp { margin-top: 22px; }
  .stats-side .grp small {
    display: block; font: 9px ui-monospace, Menlo, monospace; color: var(--ink-soft);
    letter-spacing: 0.06em; margin: 0 0 3px 12px;
  }
  .stats-side ol { list-style: none; padding: 0; margin: 0; }
  .stats-side li {
    display: grid; grid-template-columns: 44px 1fr auto; gap: 10px;
    padding: 2px 0;
    box-shadow: inset 0 -1px 0 rgba(26,22,18,0.05);
    font: 12.5px/1.25 ui-serif, Georgia, serif; color: var(--ink);
    align-items: baseline;
    transition: background 140ms ease, color 140ms ease;
  }
  .stats-side li[data-sci] { cursor: pointer; }
  .stats-side li[data-sci]:hover { background: var(--paper-2); }
  /* Cross-highlight from a hovered timeline square. */
  .stats-side li.sync-hi { background: var(--paper-2); }
  .stats-side li.sync-hi span:not(.yr):not(.ct) { color: var(--accent); }
  .stats-side li .yr { font: 9.5px ui-monospace, Menlo, monospace; color: var(--accent-2); letter-spacing: 0.04em; }
  .stats-side li .ct { font: 11px ui-monospace, Menlo, monospace; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

  /* (Histogram strip removed - charts moved into the stats-grid LEFT
     column as full-bleed chart cards. See .stats-charts above.) */

  /* ============ View 3 - Atlas ============
     Field-guide grid: one card per species, each card holds the cutout,
     names, window/all-time detection counts, and small action chips for
     audio playback, Wikipedia, and eBird. */
  .atlas-controls {
    max-width: 1280px; margin: 0 auto 18px;
    display: flex; justify-content: flex-end; align-items: center;
  }
  .atlas-sort {
    display: inline-flex; padding: 3px; position: relative;
    background: var(--paper-2); border-radius: 999px;
    box-shadow: var(--recess);
  }
  .atlas-sort button {
    background: transparent; border: 0; color: var(--ink-soft);
    width: 30px; height: 26px; padding: 0; border-radius: 999px;
    cursor: pointer; position: relative; z-index: 1;
    display: inline-flex; align-items: center; justify-content: center;
    transition: color 200ms ease;
  }
  .atlas-sort button svg { width: 13px; height: 13px; display: block; }
  .atlas-sort button:hover { color: var(--ink); }
  .atlas-sort button[aria-current="true"] { color: var(--ink); }
  .atlas-sort button .tip {
    position: absolute; top: calc(100% + 6px); left: 50%;
    transform: translateX(-50%); white-space: nowrap;
    font: 9px ui-monospace, Menlo, monospace; letter-spacing: 0.14em;
    text-transform: lowercase; color: var(--ink-soft);
    background: var(--paper); padding: 3px 8px; border-radius: 4px;
    box-shadow: var(--raised);
    opacity: 0; pointer-events: none; transition: opacity 160ms ease;
  }
  .atlas-sort button:hover .tip { opacity: 1; }
  .atlas-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 22px;
    max-width: 1280px; margin: 0 auto;
  }
  /* Mobile atlas: a compact 2-up grid instead of one giant card per row,
     so you can scan many birds and tap easily. Inline actions are hidden -
     tapping the card opens the detail modal, which has play/wiki/ebird +
     recordings - making the whole card one big touch target. */
  @media (max-width: 700px) {
    /* minmax(0,1fr) lets the columns shrink below their content's intrinsic
       width (otherwise the chips force overflow). Selectors are scoped under
       .atlas-grid so they out-specify the base .bird-card rules that appear
       later in the file. */
    .atlas-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
    .atlas-grid .bird-card { min-height: 0; padding: 12px 12px 14px; border-radius: 9px; }
    .atlas-grid .bird-card .stat { top: 10px; left: 11px; font-size: 8px; }
    .atlas-grid .bird-card .stat .n { font-size: 11px; }
    .atlas-grid .bird-card .img-wrap { margin: 30px 0 8px; }
    .atlas-grid .bird-card .img-wrap img { max-height: 96px; }
    .atlas-grid .bird-card h3 { font-size: 12.5px; }
    .atlas-grid .bird-card .sci { font-size: 9.5px; }
    .atlas-grid .bird-card .actions { display: none; }
    .atlas-grid .lifer-badge { top: 9px; right: 9px; font-size: 7px; padding: 3px 5px; letter-spacing: 0.1em; }
  }
  .bird-card {
    position: relative;
    background: var(--paper);
    border-radius: 10px;
    padding: 16px 16px 12px;
    box-shadow: var(--edge-lg);
    display: flex; flex-direction: column;
    min-height: 320px;
    cursor: pointer;
    transition: transform 200ms ease, box-shadow 200ms ease;
  }
  .bird-card:hover { transform: translateY(-1px); box-shadow: var(--edge-xl, 0 6px 16px rgba(26,22,18,0.12)); }
  .bird-card .actions, .bird-card .spectro-wrap { cursor: auto; }
  .bird-card .actions button, .bird-card .actions a { cursor: pointer; }
  .bird-card .stat {
    position: absolute; top: 14px; left: 16px;
    font: 9px/1.5 ui-monospace, Menlo, monospace;
    color: var(--ink-soft); letter-spacing: 0.04em;
  }
  /* "lifer" badge - shown top-right when a species' first-ever detection
     falls inside the selected window (set in renderAtlas). pointer-events
     off so it never blocks the card's click-to-open. */
  .lifer-badge {
    position: absolute; top: 12px; right: 12px; z-index: 2;
    font: 700 8px/1 ui-monospace, Menlo, monospace;
    letter-spacing: 0.14em; text-transform: uppercase;
    color: var(--paper); background: var(--accent);
    padding: 4px 7px; border-radius: 999px;
    box-shadow: 0 2px 6px rgba(26,22,18,0.18);
    pointer-events: none;
  }
  .bird-card .stat .n {
    color: var(--ink); font: 700 13px/1 ui-serif, Georgia, serif;
    font-variant-numeric: tabular-nums; margin-right: 4px;
    letter-spacing: 0;
  }
  .bird-card .stat .lbl { display: block; }
  .bird-card .img-wrap {
    flex: 1 1 auto; min-height: 0;
    display: flex; align-items: center; justify-content: center;
    margin: 38px 0 10px;
    padding: 4px;
  }
  .bird-card .img-wrap img {
    max-width: 100%; max-height: 160px; display: block;
    filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10));
  }
  .bird-card h3 {
    margin: 0;
    font: 700 14.5px/1.15 ui-serif, "Iowan Old Style", Georgia, serif;
    color: var(--ink); letter-spacing: 0.01em;
  }
  .bird-card .sci {
    font: italic 11px/1.2 ui-serif, Georgia, serif;
    color: var(--ink-2); margin: 2px 0 0;
  }
  .bird-card .actions {
    display: flex; gap: 6px; align-items: stretch;
    margin-top: 12px; padding-top: 10px;
    box-shadow: inset 0 1px 0 rgba(26,22,18,0.07);
  }
  .bird-card .chip {
    flex: 0 0 auto;
    background: var(--paper-2);
    border: 0; color: var(--ink);
    padding: 5px 9px;
    border-radius: 4px;
    font: 9.5px/1 ui-monospace, Menlo, monospace;
    text-transform: lowercase; letter-spacing: 0.10em;
    text-decoration: none;
    cursor: pointer;
    display: inline-flex; align-items: center; gap: 5px;
    box-shadow: var(--raised);
    transition: transform 160ms ease, color 160ms ease;
    white-space: nowrap;
  }
  .bird-card .chip:hover {
    color: var(--ink); transform: translateY(-1px);
  }
  .bird-card .chip[data-active="true"] {
    color: var(--paper);
    background: var(--ink);
  }
  .bird-card .chip svg { width: 10px; height: 10px; display: block; }
  .bird-card .chip.play { padding-left: 7px; }
  .bird-card .chip.ext::after { content: '↗'; opacity: 0.6; font-size: 11px; }

  /* Selected card highlight - matches the page-slider and time-window
     pill aesthetic: the active element is a slightly *brighter* paper
     surface (raised) on top of a slightly *recessed* background.
     No accent colour, no glowing ring - just the same depth shadow
     recipe the rest of the chrome uses. */
  .bird-card { transition: transform 280ms cubic-bezier(.7,.05,.2,1), box-shadow 280ms ease, background 200ms ease; }
  .bird-card[data-active="true"] {
    background: var(--paper);
    box-shadow:
      inset 0 0 0 1px rgba(255,255,255,0.85),
      0 0 0 1px rgba(26,22,18,0.06),
      0 10px 26px rgba(26,22,18,0.10);
    transform: translateY(-1px);
  }
  .bird-card[data-pulse="true"] {
    animation: bird-card-pulse 520ms cubic-bezier(.45,.05,.2,1);
  }
  /* Soft fade-in of the raised state, no ring pulse */
  @keyframes bird-card-pulse {
    0%   { transform: translateY(2px); }
    100% { transform: translateY(-1px); }
  }

  .atlas-empty {
    grid-column: 1 / -1;
    padding: 80px 16px; text-align: center;
    font: 12px/1.6 ui-serif, Georgia, serif; color: var(--ink-2);
  }
  .atlas-empty p { margin: 0 0 4px; }
  .atlas-empty p.hint {
    font: 10px ui-monospace, Menlo, monospace; color: var(--ink-soft);
    letter-spacing: 0.06em;
  }

  /* ============ Admin screen (system / logs / tools) ============
     Overlays the slider when activated via #admin=... hash. Lives
     inside the same shell as the rest of the app so the header,
     menu button, and time-window pill stay put - feels native. */
  .admin-screen {
    position: fixed; inset: 60px 0 60px 0;
    background: var(--bg);
    overflow-y: auto;
    z-index: 5;
    opacity: 0; pointer-events: none;
    transition: opacity 200ms ease;
  }
  .admin-screen[aria-hidden="false"] { opacity: 1; pointer-events: auto; }
  .admin-frame {
    max-width: 1100px; margin: 0 auto; padding: 0 28px 40px;
  }
  .admin-title {
    text-align: center; margin: 0 auto 28px;
  }
  .admin-title h1 {
    margin: 0;
    font: 32px/1.1 ui-serif, "Iowan Old Style", Georgia, serif;
    color: var(--ink); letter-spacing: 0.005em;
  }
  /* Return-to-atlas pill in the same slot the time-window pill
     normally lives - matches its visual language. */
  .return-to-atlas {
    position: fixed; top: 18px; left: 20px; z-index: 6;
    display: none; align-items: center; gap: 6px;
    padding: 7px 14px; border-radius: 999px;
    background: var(--paper-2);
    color: var(--ink-soft);
    font: 10px ui-monospace, Menlo, monospace; letter-spacing: 0.18em;
    text-transform: lowercase; cursor: pointer;
    text-decoration: none;
    box-shadow: var(--recess);
    transition: color 200ms ease;
  }
  body.admin-on .return-to-atlas { display: inline-flex; }
  body.admin-on #slider { visibility: hidden; }
  body.admin-on .static-head { opacity: 0; pointer-events: none; }
  /* Hide the time-window pill on admin pages (it doesn't apply
     there) - the return-to-atlas link takes its slot. */
  body.admin-on #winPick { visibility: hidden; }
  .return-to-atlas:hover { color: var(--ink); }
  .return-to-atlas svg { width: 10px; height: 10px; }
  .admin-body { padding-top: 4px; }
  .admin-grid {
    display: grid; gap: 14px;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    margin-bottom: 22px;
  }
  .admin-card {
    background: var(--paper); border-radius: 8px;
    padding: 14px 16px; box-shadow: var(--edge);
  }
  .admin-card h3 {
    margin: 0 0 6px; font: 10px ui-monospace, Menlo, monospace;
    letter-spacing: 0.18em; text-transform: uppercase;
    color: var(--ink-soft); font-weight: 500;
  }
  .admin-card .v { font: 20px ui-serif, Georgia, serif; color: var(--ink); }
  .admin-card .sub { font: 11px ui-monospace, Menlo, monospace; color: var(--ink-soft); margin-top: 4px; }
  .admin-card.alert { box-shadow: 0 0 0 1px var(--accent), var(--edge); }
  .admin-card.alert h3 { color: var(--accent); }
  .admin-card.warn { box-shadow: 0 0 0 1px #c47a1f, var(--edge); }
  .admin-card.warn h3 { color: #c47a1f; }
  .admin-section-head {
    font: 700 11px ui-monospace, Menlo, monospace;
    letter-spacing: 0.18em; text-transform: uppercase;
    color: var(--ink); margin: 24px 0 10px;
    border-left: 2px solid var(--accent-2); padding-left: 10px;
  }
  table.admin-tbl {
    width: 100%; border-collapse: collapse;
    font: 12px ui-monospace, Menlo, monospace;
    background: var(--paper); border-radius: 8px; overflow: hidden;
    box-shadow: var(--edge);
  }
  table.admin-tbl th, table.admin-tbl td {
    border-bottom: 1px solid var(--rule, #c8bfae);
    padding: 9px 12px; text-align: left;
  }
  table.admin-tbl tr:last-child th, table.admin-tbl tr:last-child td { border-bottom: 0; }
  table.admin-tbl th {
    color: var(--ink-soft); text-transform: uppercase;
    font-size: 9.5px; letter-spacing: 0.14em; font-weight: 500;
  }
  table.admin-tbl .pill {
    display: inline-block; padding: 1px 8px; border-radius: 999px;
    font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase;
  }
  table.admin-tbl .pill.active { background: rgba(90,122,58,.18); color: #4d6730; }
  table.admin-tbl .pill.inactive { background: rgba(168,52,29,.18); color: var(--accent); }
  table.admin-tbl .pill.failed { background: rgba(168,52,29,.18); color: var(--accent); }
  table.admin-tbl button.restart {
    font: 9.5px ui-monospace, Menlo, monospace;
    letter-spacing: 0.04em; text-transform: lowercase;
    padding: 4px 10px; border: 0; background: var(--paper-2);
    border-radius: 4px; cursor: pointer; color: var(--ink-soft);
    box-shadow: var(--raised);
  }
  table.admin-tbl button.restart:hover { color: var(--ink); }
  table.admin-tbl button.restart[disabled] { opacity: .5; cursor: wait; }
  .admin-logs-toolbar {
    display: flex; gap: 8px; align-items: center; margin-bottom: 14px;
  }
  .admin-logs-toolbar select, .admin-logs-toolbar input[type="number"] {
    font: 12px ui-monospace, Menlo, monospace;
    padding: 6px 10px; border: 1px solid var(--rule, #c8bfae);
    border-radius: 4px; background: var(--paper); color: var(--ink);
  }
  .admin-logs-toolbar label {
    font: 9.5px ui-monospace, Menlo, monospace; letter-spacing: 0.14em;
    text-transform: uppercase; color: var(--ink-soft);
  }
  .admin-logs-pane {
    background: #1c1a14; color: #d7cba8;
    font: 12px/1.5 ui-monospace, Menlo, monospace;
    padding: 14px 16px; border-radius: 8px;
    max-height: 65vh; overflow: auto; white-space: pre-wrap;
  }
  .admin-actions-grid {
    display: grid; gap: 12px;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  }
  .admin-action {
    background: var(--paper); border-radius: 8px;
    padding: 14px 16px; box-shadow: var(--edge);
  }
  .admin-action h4 { margin: 0 0 6px; font: 15px ui-serif, Georgia, serif; color: var(--ink); }
  .admin-action p { margin: 0 0 10px; font: 12.5px ui-serif, Georgia, serif; color: var(--ink-soft); }
  .admin-action button.run {
    font: 9.5px ui-monospace, Menlo, monospace; letter-spacing: 0.14em;
    text-transform: lowercase; padding: 6px 12px; border: 0;
    background: var(--ink); color: var(--paper); border-radius: 999px;
    cursor: pointer;
  }
  .admin-action button.run[disabled] { opacity: .5; cursor: wait; }
  .admin-action .out {
    margin-top: 10px; font: 10.5px ui-monospace, Menlo, monospace;
    color: var(--ink-soft); white-space: pre-wrap;
  }
  .admin-action.deploy pre {
    background: #1c1a14; color: #d7cba8;
    font: 11px ui-monospace, Menlo, monospace;
    padding: 12px 14px; border-radius: 6px;
    overflow: auto; white-space: pre-wrap; word-break: break-all;
    margin: 0;
  }
  .admin-action.deploy button.copy {
    font: 9.5px ui-monospace, Menlo, monospace; letter-spacing: 0.06em;
    text-transform: lowercase; padding: 5px 10px;
    background: var(--paper-2); color: var(--ink-soft);
    border: 0; border-radius: 4px; cursor: pointer; box-shadow: var(--raised);
    margin-top: 8px;
  }
  .admin-action.deploy button.copy:hover { color: var(--ink); }
  .admin-unreachable {
    background: var(--paper); border-radius: 8px;
    padding: 14px 16px; color: var(--accent);
    font: 12px ui-monospace, Menlo, monospace;
    margin-bottom: 18px; box-shadow: 0 0 0 1px var(--accent), var(--edge);
  }
  /* When admin screen is shown, the slider underneath is hidden so it
     doesn't compete for scrolling. The static title gets swapped out
     too - admin is its own section with its own visual identity.
     Menu button + time-window pill stay visible (still useful). */
  body.admin-on #views { display: none; }
  body.admin-on .nav-band,
  body.admin-on #slider { visibility: hidden; }
  body.admin-on .static-head { opacity: 0; pointer-events: none; transition: opacity 200ms ease; }

  /* Mobile compact: drawer takes near-full width, admin pads tighter,
     section heads + cards downsize. */
  @media (max-width: 700px) {
    #menu-dd { top: 50px; right: 8px; left: 8px; width: auto; padding: 10px; }
    .admin-screen { inset: 52px 0 60px 0; }
    .admin-frame { padding: 0 14px 32px; }
    .admin-title { margin-bottom: 18px; }
    .admin-title h1 { font-size: 22px; }
    .admin-grid { gap: 10px; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
    .admin-card { padding: 12px 14px; }
    .admin-card .v { font-size: 18px; }
    .admin-section-head { margin: 18px 0 8px; font-size: 10px; letter-spacing: 0.12em; }
    table.admin-tbl th, table.admin-tbl td { padding: 7px 8px; font-size: 11px; }
    .admin-actions-grid { gap: 10px; grid-template-columns: 1fr; }
    .admin-action { padding: 12px 14px; }
    /* Slider track bottom nav: tuck closer to bottom edge */
    .slider { bottom: 14px; padding: 3px; }
    .slider button { font-size: 9.5px; padding: 6px 11px; }
  }
  @media (max-width: 700px) {
    /* Detail-modal: full-screen on small screens. */
    #detail-modal { padding: 12px; }
    .modal-card { padding: 18px; border-radius: 10px; }
    .modal-info h2 { font-size: 22px; }
    .modal-img { aspect-ratio: 1 / 1; padding: 10px; }
    .modal-grid { gap: 18px; }
    .rec-row .when { font-size: 10.5px; }
    .rec-row .when small { font-size: 8.5px; }
  }

  /* ============ Detail modal (atlas card expansion) ============
     Click a bird card -> this fades in over a soft ink backdrop with
     the full sketch, taxonomic info, Wikipedia summary, and a
     scrollable list of every past audio capture for the species. */
  /* /about - a brief explainer popup floated over the index page.
     Reuses .modal-backdrop / .modal-close; opened by the #about hash. */
  #about-modal {
    position: fixed; inset: 0; z-index: 60;
    display: flex; align-items: center; justify-content: center;
    padding: 24px;
    pointer-events: none; opacity: 0;
    transition: opacity 200ms ease;
  }
  #about-modal[aria-hidden="false"] { pointer-events: auto; opacity: 1; }
  .about-card {
    position: relative;
    max-width: 432px; width: 100%;
    background: var(--paper);
    border-radius: 14px;
    box-shadow:
      inset 0 0 0 1px rgba(255,255,255,0.6),
      0 30px 80px rgba(26,22,18,0.24);
    padding: 34px 34px 28px;
    transform: translateY(10px) scale(0.985);
    transition: transform 260ms cubic-bezier(.32,.72,.32,1);
  }
  #about-modal[aria-hidden="false"] .about-card { transform: none; }
  .about-card .about-eyebrow {
    margin: 0 0 7px;
    font: italic 400 13px/1 ui-serif, "Iowan Old Style", Georgia, serif;
    color: var(--ink-2); letter-spacing: 0.06em;
  }
  .about-card h2 {
    margin: 0 0 13px;
    font: 700 22px/1.25 ui-serif, "Iowan Old Style", Georgia, serif;
    letter-spacing: 0.01em; color: var(--ink);
  }
  .about-card .about-body {
    margin: 0;
    font: 400 14.5px/1.62 ui-serif, "Iowan Old Style", Georgia, serif;
    color: var(--ink-2);
  }
  /* Plain inline link - same ink as the prose, a simple underline, no
     hover treatment. */
  .about-card .about-body a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
  }
  .about-card .about-explore {
    margin-top: 20px;
    background: none; border: 0; padding: 0;
    font: 700 11px/1 ui-monospace, Menlo, monospace;
    letter-spacing: 0.08em; text-transform: lowercase;
    color: var(--ink-soft); cursor: pointer;
    transition: color 140ms ease;
  }
  .about-card .about-explore:hover { color: var(--ink); }
  #detail-modal {
    position: fixed; inset: 0; z-index: 50;
    display: flex; align-items: center; justify-content: center;
    padding: 24px;
    pointer-events: none; opacity: 0;
    /* One opacity fade for backdrop + card together; the card also
       morphs via transform (see morphModalOpen). Driven by .is-open so
       the fade can run *during* the close morph, not after it. */
    transition: opacity 260ms ease;
  }
  #detail-modal.is-open { pointer-events: auto; opacity: 1; }
  .modal-backdrop {
    position: absolute; inset: 0;
    background: rgba(26,22,18,0.32);
    backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  }
  .modal-card {
    position: relative;
    max-width: 920px; max-height: calc(100vh - 48px);
    width: 100%;
    background: var(--paper);
    border-radius: 14px;
    box-shadow:
      inset 0 0 0 1px rgba(255,255,255,0.6),
      0 30px 80px rgba(26,22,18,0.22);
    padding: 30px 34px 26px;
    overflow: auto;
    /* The expand-from-card animation is driven by inline transform
       and opacity set on open/close - see openDetailModal. CSS
       transition handles the in-between glide. */
    transform-origin: 50% 50%;
    will-change: transform, opacity;
  }
  .modal-card.is-morphing {
    transition: transform 300ms cubic-bezier(.22,.61,.36,1);
  }
  .modal-close {
    position: absolute; top: 16px; right: 16px;
    background: var(--paper-2); border: 0;
    width: 30px; height: 30px; border-radius: 999px;
    font: 16px/1 ui-monospace, Menlo, monospace; color: var(--ink);
    box-shadow: var(--raised);
    cursor: pointer; z-index: 1;
  }
  .modal-close:hover { color: var(--accent-2); }
  .modal-grid {
    display: grid; grid-template-columns: 1fr 1.15fr; gap: 32px;
    align-items: start;
  }
  @media (max-width: 700px) {
    .modal-grid { grid-template-columns: 1fr; gap: 22px; }
    .modal-card { padding: 22px; }
  }
  .modal-img {
    position: relative;
    background: var(--paper-2); border-radius: 10px;
    display: flex; align-items: center; justify-content: center;
    padding: 18px;
    aspect-ratio: 4 / 3;
    box-shadow: var(--recess);
  }
  .modal-img img {
    max-width: 100%; max-height: 100%;
    filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10));
    transition: opacity 220ms ease;
  }
  .modal-img img.swapping { opacity: 0; }
  /* Pose toggle - bottom-left corner of the modal sketch, icon strip.
     Same recipe as the time-window slider: recessed paper-2 well, a
     white sliding pill behind the active button (via .seg-pill), and
     a color shift on the icon itself. */
  .pose-toggle {
    position: absolute; left: 10px; bottom: 10px;
    display: inline-flex; padding: 3px;
    background: var(--paper-2); border-radius: 999px;
    box-shadow: var(--recess);
    z-index: 2;
  }
  .pose-toggle button {
    background: transparent; border: 0; color: var(--ink-soft);
    width: 30px; height: 26px; padding: 0; border-radius: 999px;
    cursor: pointer; position: relative; z-index: 1;
    display: inline-flex; align-items: center; justify-content: center;
    transition: color 200ms ease;
  }
  .pose-toggle button svg { width: 13px; height: 13px; display: block; }
  .pose-toggle button:hover { color: var(--ink); }
  .pose-toggle button[aria-current="true"] { color: var(--ink); }
  .pose-toggle button .tip {
    position: absolute; bottom: calc(100% + 6px); left: 50%;
    transform: translateX(-50%); white-space: nowrap;
    font: 9px ui-monospace, Menlo, monospace; letter-spacing: 0.14em;
    text-transform: lowercase; color: var(--ink-soft);
    background: var(--paper); padding: 3px 8px; border-radius: 4px;
    box-shadow: var(--raised);
    opacity: 0; pointer-events: none; transition: opacity 160ms ease;
  }
  .pose-toggle button:hover .tip { opacity: 1; }
  .pose-toggle[data-unavailable="true"] { display: none; }
  .pose-toggle button[data-unavailable="true"] { display: none; }
  .modal-info h2 {
    margin: 4px 36px 0 0;
    font: 700 26px/1.1 ui-serif, "Iowan Old Style", "Bookman Old Style", Georgia, serif;
    color: var(--ink); letter-spacing: 0.01em;
  }
  .modal-info .sci {
    font: italic 13.5px/1.3 ui-serif, Georgia, serif;
    color: var(--ink-2); margin: 5px 0 18px;
  }
  .modal-stats {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
    margin-bottom: 18px;
  }
  .modal-stats > div {
    background: var(--paper-2);
    border-radius: 6px; padding: 10px 12px;
    box-shadow: var(--recess);
  }
  .modal-stats .n {
    display: block;
    font: 800 17px/1 ui-serif, Georgia, serif;
    color: var(--ink); font-variant-numeric: tabular-nums;
  }
  .modal-stats .lbl {
    display: block; margin-top: 4px;
    font: 8.5px ui-monospace, Menlo, monospace;
    color: var(--ink-soft); letter-spacing: 0.12em;
    text-transform: uppercase;
  }
  .modal-info .desc {
    font: 13px/1.55 ui-serif, Georgia, serif;
    color: var(--ink); margin: 0 0 14px;
  }
  .modal-info .desc.placeholder { color: var(--ink-soft); font-style: italic; }
  .modal-meta {
    display: flex; flex-wrap: wrap; gap: 16px;
    font: 9.5px ui-monospace, Menlo, monospace; color: var(--ink-soft);
  }
  .modal-meta .k { letter-spacing: 0.12em; text-transform: uppercase; }
  .modal-meta .v { color: var(--ink); margin-left: 6px; }
  .modal-meta .v.rare { color: var(--accent-2); }
  .modal-recordings {
    margin-top: 24px; padding-top: 16px;
    box-shadow: inset 0 1px 0 rgba(26,22,18,0.07);
  }
  .modal-recordings .rec-head {
    display: flex; align-items: baseline; justify-content: space-between;
    margin-bottom: 8px;
  }
  .modal-recordings h3 {
    font: 700 10px ui-monospace, Menlo, monospace;
    letter-spacing: 0.18em; text-transform: uppercase;
    color: var(--ink); margin: 0;
    border-left: 2px solid var(--accent-2); padding-left: 10px;
  }
  .modal-recordings .rec-count {
    font: 9px ui-monospace, Menlo, monospace; color: var(--ink-soft);
    letter-spacing: 0.06em;
  }
  .modal-recordings ol {
    list-style: none; padding: 0; margin: 0;
    max-height: 220px; overflow-y: auto;
    display: flex; flex-direction: column; gap: 4px;
    padding-right: 4px;
  }
  .rec-row {
    display: grid; grid-template-columns: 30px 1fr auto; gap: 10px;
    align-items: center;
    padding: 6px 10px; border-radius: 6px;
    background: var(--paper);
    box-shadow: var(--edge);
    transition: background 160ms ease;
  }
  .rec-row:hover { background: var(--paper-2); }
  .rec-row.expanded { grid-template-columns: 30px 1fr auto; }
  /* Spectrogram strip - inline below the row header. Click on the row
     toggles open/closed independent of playback so you can scan
     spectrograms visually without listening. */
  .rec-spectro {
    grid-column: 1 / -1;
    margin-top: 8px;
    height: 0; overflow: hidden;
    border-radius: 4px;
    /* Paper background - the spectrogram canvas paints ink ON it,
       matching the sketch palette. */
    background: var(--paper-2);
    box-shadow: var(--recess);
    position: relative;
    transition: height 240ms cubic-bezier(.7,.05,.2,1);
  }
  .rec-row.expanded .rec-spectro { height: 88px; }
  /* The spectrogram is rendered client-side onto a canvas - we fetch
     the mp3, decode via Web Audio, run an STFT, and paint columns
     with our ink palette. No BirdNET-Pi PNG involved. */
  .rec-spectro canvas {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    display: block;
    opacity: 0; transition: opacity 240ms ease;
  }
  .rec-spectro canvas.ready { opacity: 1; }
  .rec-spectro .rec-spectro-loading {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    color: var(--ink-soft);
    font: 9px ui-monospace, Menlo, monospace; letter-spacing: 0.14em;
    text-transform: lowercase;
    pointer-events: none;
  }
  /* Played portion - soft ink wash over the paper ground. */
  .rec-spectro .rec-spectro-played {
    position: absolute; left: 0; top: 0; bottom: 0; width: 0%;
    background: linear-gradient(90deg, rgba(26,22,18,0.16), rgba(26,22,18,0.06));
    pointer-events: none;
  }
  /* Cursor - sliding paper pill with a raised shadow, same recipe as
     the time-window slider's seg-pill. Pops against both the bright
     paper regions of the spectrogram and the inked vocalization
     bands. */
  .rec-spectro .rec-spectro-cursor {
    position: absolute; top: 50%; left: 0;
    width: 14px; height: 20px;
    transform: translate(-50%, -50%);
    background: var(--paper);
    border-radius: 999px;
    box-shadow: 0 1px 3px rgba(26,22,18,0.30), inset 0 0 0 1px rgba(26,22,18,0.18);
    pointer-events: none;
    opacity: 0;
  }
  .rec-spectro .rec-spectro-cursor::after {
    content: '';
    position: absolute; left: 50%; top: -100%; bottom: -100%;
    width: 1.5px; transform: translateX(-50%);
    background: rgba(251,247,238,0.85);
    box-shadow: 0 0 0 0.5px rgba(26,22,18,0.20);
  }
  .rec-spectro.armed .rec-spectro-cursor { opacity: 1; }
  /* Dark mode: the spectrogram ground is charcoal (paintSpectrogram paints
     with --paper), so the dark played-veil and the --paper cursor pill would
     both vanish into it. Flip the veil light, and make the cursor a light
     pill so it still reads against the dark ground. */
  :root[data-theme="dark"] .rec-spectro .rec-spectro-played {
    background: linear-gradient(90deg, rgba(236,232,225,0.18), rgba(236,232,225,0.07));
  }
  :root[data-theme="dark"] .rec-spectro .rec-spectro-cursor {
    background: var(--ink);
  }
  .rec-spectro .rec-spectro-scrub {
    position: absolute; inset: 0;
    cursor: ew-resize;
  }
  .rec-row .play {
    background: var(--paper-2); border: 0; color: var(--ink);
    width: 24px; height: 24px; border-radius: 999px;
    display: inline-flex; align-items: center; justify-content: center;
    box-shadow: var(--raised); cursor: pointer;
    padding: 0;
  }
  .rec-row .play svg { width: 8px; height: 8px; }
  .rec-row .play[data-active="true"] {
    background: var(--ink); color: var(--paper);
  }
  .rec-row .when {
    font: 11px/1.2 ui-monospace, Menlo, monospace; color: var(--ink);
  }
  .rec-row .when small {
    display: block; font-size: 9px; color: var(--ink-soft);
    letter-spacing: 0.06em; margin-top: 1px;
  }
  .rec-row .conf {
    font: 10px ui-monospace, Menlo, monospace;
    color: var(--ink-soft); letter-spacing: 0.06em;
    font-variant-numeric: tabular-nums;
  }
  .rec-empty {
    padding: 14px;
    font: 11px/1.5 ui-serif, Georgia, serif; color: var(--ink-soft);
    text-align: center;
  }
  .modal-actions {
    display: flex; gap: 8px; margin-top: 18px;
  }
  .modal-actions .chip {
    flex: 0 0 auto;
    background: var(--paper-2);
    border: 0; color: var(--ink);
    padding: 7px 12px;
    border-radius: 4px;
    font: 10px/1 ui-monospace, Menlo, monospace;
    text-transform: lowercase; letter-spacing: 0.12em;
    text-decoration: none;
    display: inline-flex; align-items: center; gap: 5px;
    box-shadow: var(--raised);
    transition: transform 160ms ease;
  }
  .modal-actions .chip:hover { transform: translateY(-1px); }
  .modal-actions .chip.ext::after { content: '↗'; opacity: 0.6; }

  /* Spectrogram appears on first audio play. Recoloured via CSS filter
     so BirdNET-Pi's default colour spectrogram comes out as a clean
     monochrome ink strip, matching the rest of the chart. Click to
     replay; a thin progress bar tracks playback in real time. */
  .bird-card .spectro-wrap {
    position: relative;
    min-height: 0; margin: 6px 0 2px;
    cursor: pointer;
    overflow: hidden;
    border-radius: 3px;
    box-shadow: var(--recess);
  }
  .bird-card .spectro-wrap:empty { display: none; }
  /* Canvas is painted client-side in the active theme's paper/ink palette
     (paintSpectrogram reads data-theme), so it follows light/dark mode with
     no CSS filter - same renderer as the modal recording spectrograms. */
  .bird-card .spectro-wrap canvas {
    width: 100%; height: 44px; display: block;
  }
  /* Progress wash: a translucent veil that grows as the clip plays. Ink in
     light mode; in dark mode a light veil so it stays visible over the
     charcoal spectrogram ground. */
  .bird-card .spectro-wrap::after {
    content: '';
    position: absolute; top: 0; bottom: 0; left: 0;
    width: var(--prog, 0%);
    background: linear-gradient(to right, rgba(26,22,18,0.30), rgba(26,22,18,0.05));
    pointer-events: none;
    transition: width 60ms linear;
  }
  :root[data-theme="dark"] .bird-card .spectro-wrap::after {
    background: linear-gradient(to right, rgba(236,232,225,0.26), rgba(236,232,225,0.05));
  }
  /* Playhead - thin vertical line at the leading edge */
  .bird-card .spectro-wrap::before {
    content: '';
    position: absolute; top: 0; bottom: 0;
    left: var(--prog, 0%);
    width: 1px;
    background: var(--ink);
    opacity: var(--playhead, 0);
    pointer-events: none;
    transition: opacity 200ms ease;
  }
  .bird-card[data-playing="true"] .spectro-wrap::before { --playhead: 0.6; }

/* ============ AvianVisitors mode toggle ============
 * Default install (body.av-local): hide the lock-row, show drawer items
 * directly when the menu opens. The PHP shim at /avian/api/menu.php
 * returns items immediately with no auth, so the live JS skips the lock
 * flow entirely.
 *
 * Forwarded install (body.av-forwarded): show the lock row so users
 * type the basic-auth password before items unlock. Pair with
 * AV_REQUIRE_AUTH=1 in php-fpm env + Caddy basic_auth on /avian/api/.
 * See avian/forwarding/. */
body.av-local #dd-locked { display: none; }
body.av-local #dd-items  { display: block; }
