diff --git a/CHANGELOG.md b/CHANGELOG.md
index 000f4c0f..fe489668 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,19 @@
# Changelog
-## v1.2.6 — 2026-07-19
+## v1.2.7 — 2026-07-24
+
+### Fixed
+- **The detail modal no longer collapses to a strip of padding in Chrome and
+ the Home Assistant companion app.** The `.modal-card` was capped with
+ `max-height: calc(100vh - 48px)`, which the card build rewrites to
+ `calc(100cqh - 48px)`. When Home Assistant lays the card out with an
+ *indefinite* height (masonry / content-sized slots), Blink resolves the
+ container-query height `100cqh` to `0`, so the cap became `calc(0 - 48px)` →
+ clamped to `0` and the modal shrank to just its padding. Firefox and Safari
+ resolved the unit against the rendered height, so they were unaffected. The
+ cap is now `max-height: 100%` against the `inset:0` overlay (whose height is
+ always definite), which equals the old value on a normal viewport but never
+ collapses. Fixes #58.
### Added
- **64 Australian species.** Generated kachō-e illustrations (128 renders,
diff --git a/dist/habird-card.js b/dist/habird-card.js
index 7f23ca87..5505d9c7 100644
--- a/dist/habird-card.js
+++ b/dist/habird-card.js
@@ -3,7 +3,7 @@
(function () {
'use strict';
var HABIRD_TEMPLATE = "
A tiny microphone identifies every passing bird with BirdNET-Go, built on Cornell's BirdNET. Each species shows up as an illustration in the collage, sized by how often it's been heard. Confident detections perch; uncertain ones fly past.
\n \n
\n
\n\n\n\n\n\n\n
\n \n \n
\n\n\n\n\n\n
";
-var HABIRD_CSS = " :host {\n color-scheme: light;\n /* Type is variable-driven so the card build can swap the editorial\n serif/mono pairing for the Home Assistant system font. */\n --av-font-display: ui-serif, \"Iowan Old Style\", \"Apple Garamond\", \"Bookman Old Style\", Georgia, serif;\n --av-font-mono: ui-monospace, \"SF Mono\", Menlo, monospace;\n --paper: #fcfcfb; /* near-pure white, faintest warm undertone */\n --paper-2: #f3f2ee; /* recessed track */\n --paper-3: #e8e6df; /* placeholder fill */\n --ink: #1a1612;\n --ink-2: #4a3f31;\n --ink-soft: #908576;\n --accent: #4a3f31; /* warm dark grey - matches ink-2, no longer red */\n --accent-2: #1a1612; /* near-black ink - same hue as the title text */\n --hairline: rgba(26,22,18,0.14); /* faint divider lines + chart gridlines */\n --pill: #fcfcfb; /* active-toggle sliding pill */\n /* Depth shadow recipes - replace flat 1px borders */\n --edge: inset 0 0 0 1px rgba(255,255,255,0.55), 0 0 0 1px rgba(26,22,18,0.04);\n --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);\n --recess: inset 0 1px 2px rgba(26,22,18,0.06), inset 0 0 0 1px rgba(26,22,18,0.04);\n --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);\n }\n /* ---- Charcoal dark theme. Set via data-theme=\"dark\" on (an\n inline script in index.html applies the saved choice before paint;\n the switcher lives in Settings). The whole UI is variable-driven, so\n flipping ink<->paper inverts cleanly: text reads light, the stats\n squares become light marks on dark, the collage cutouts (transparent\n PNGs) sit on the charcoal directly. Shadow recipes are re-cut for a\n dark surface (faint light edges, deeper drop shadows). ---- */\n :host([data-theme=\"dark\"]) {\n color-scheme: dark;\n --paper: #1a1a1a; /* neutral charcoal page bg (no blue tint) */\n --paper-2: #242424; /* recessed track */\n --paper-3: #303030; /* placeholder fill */\n --ink: #ece8e1; /* warm off-white text / marks */\n --ink-2: #b7afa2; /* secondary text */\n --ink-soft: #837c70; /* muted text */\n --accent: #d8d2c6; /* light warm - pills read light-on-dark */\n --accent-2: #ece8e1;\n --hairline: rgba(236,232,225,0.20); /* faint light divider on charcoal */\n --pill: #404040; /* raised, clearly lighter than the track (neutral) */\n --edge: inset 0 0 0 1px rgba(255,255,255,0.05), 0 0 0 1px rgba(0,0,0,0.4);\n --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);\n --recess: inset 0 1px 2px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,255,255,0.04);\n --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);\n }\n * { box-sizing: border-box; }\n .av-shell { margin: 0; padding: 0; height: 100%; background: var(--paper); color: var(--ink); }\n .av-shell {\n font-family: var(--av-font-display);\n -webkit-font-smoothing: antialiased;\n overflow: hidden;\n }\n .mono { font-family: var(--av-font-mono); letter-spacing: 0.02em; }\n\n /* ============ Stage / views ============\n Stage is a flex column: a static title block sits on top, the views\n slider fills the remaining height. The title NEVER moves when the\n view changes - only its text fades when the new view's title differs\n from the current one. */\n .stage {\n position: absolute; inset: 0; overflow: hidden;\n display: flex; flex-direction: column;\n }\n .static-head {\n flex: 0 0 auto;\n padding: 84px 32px 22px;\n text-align: center; position: relative; z-index: 5;\n }\n .static-head .pre {\n display: inline-block;\n background: none; border: 0; padding: 0;\n font: italic 400 clamp(13px, 1.4cqw, 18px)/1 var(--av-font-display);\n color: var(--ink-2); letter-spacing: 0.06em; margin-bottom: 6px;\n cursor: pointer; transition: color 140ms ease;\n }\n .static-head .pre:hover {\n color: var(--ink);\n text-decoration: underline; text-underline-offset: 3px;\n text-decoration-thickness: 1px;\n }\n .static-head h1 {\n margin: 0; padding: 0;\n font: 700 clamp(24px, 3.2cqw, 40px)/1 var(--av-font-display);\n letter-spacing: 0.06em; color: var(--ink); text-transform: uppercase;\n font-style: normal;\n transition: opacity 240ms cubic-bezier(.7,.05,.2,1);\n }\n .static-head.swap-out h1 { opacity: 0; }\n /* Stats + Atlas hide the shared title so their content uses the full\n height (the collage keeps it). */\n body.av-view-stats .static-head, body.av-view-atlas .static-head { display: none; }\n @container av-card (max-width: 700px) {\n .static-head { padding: 96px 18px 22px; }\n }\n\n .views {\n flex: 1 1 auto; min-height: 0;\n position: relative; display: flex;\n transition: transform 480ms cubic-bezier(.7,.05,.2,1);\n }\n .view { flex: 0 0 100%; height: 100%; overflow: auto; padding: 8px 56px 132px; position: relative; }\n /* The collage view fits the viewport (no scroll). Atlas keeps normal\n scroll; stats scrolls when its grid outgrows the box (overridden at\n the .stats-grid rules below). */\n .view#v0, .view#v1 { overflow: hidden; padding: 8px 32px 88px; }\n .view#v0 { display: flex; }\n .view#v1 { display: flex; flex-direction: column; }\n @container av-card (max-width: 700px) { .view { padding: 8px 18px 132px; } .view#v0, .view#v1 { padding: 8px 16px 88px; } }\n\n /* ============ Top bar - window picker (left) + menu button (right). ============\n Visible on every view; the picker is the universal time-window control. */\n .top {\n position: absolute; top: 22px; left: 0; right: 0;\n display: flex; justify-content: space-between; align-items: center;\n padding: 0 28px; z-index: 30; pointer-events: none;\n }\n .top > * { pointer-events: auto; }\n .top .window-pick {\n display: inline-flex; gap: 0; padding: 4px;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n position: relative;\n }\n .top .window-pick button {\n background: transparent; border: 0; color: var(--ink-soft);\n font: 10px/1 var(--av-font-mono); letter-spacing: 0.18em;\n padding: 9px 16px; border-radius: 999px; cursor: pointer; text-transform: uppercase;\n transition: color 200ms ease;\n position: relative; z-index: 1;\n }\n .top .window-pick button:hover { color: var(--ink); }\n .top .window-pick button[aria-current=\"true\"] { color: var(--ink); }\n .menu-btn {\n background: var(--paper); border: 0; color: var(--ink);\n font: 11px/1 var(--av-font-mono); padding: 9px 14px 8px;\n border-radius: 999px; cursor: pointer; letter-spacing: 0.18em; text-transform: lowercase;\n box-shadow: var(--raised);\n transition: transform 160ms ease;\n }\n .menu-btn:hover { transform: translateY(-1px); }\n .menu-btn:active { transform: translateY(0); }\n /* Compact top bar on mobile - the time-window pill goes from\n spacious to thumb-tight without losing legibility. */\n @container av-card (max-width: 700px) {\n .top { top: 12px; padding: 0 12px; }\n .top .window-pick { padding: 3px; }\n .top .window-pick button {\n font-size: 8.5px; padding: 6px 10px; letter-spacing: 0.12em;\n }\n .menu-btn { font-size: 9px; padding: 6px 11px 5px; letter-spacing: 0.14em; }\n .return-to-atlas { top: 10px; left: 10px; padding: 5px 10px; font-size: 8.5px; }\n }\n @container av-card (max-width: 420px) {\n .top .window-pick button { font-size: 8px; padding: 5px 7px; letter-spacing: 0.08em; }\n }\n\n /* ============ Bottom slider - recessed track, raised active pill ============ */\n .slider {\n position: absolute; bottom: 28px; left: 50%; transform: translateX(-50%);\n display: flex; gap: 0; padding: 4px; z-index: 30;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n position: absolute; /* re-emit for the pill positioning */\n }\n .slider button {\n background: transparent; border: 0; color: var(--ink-soft);\n font: 10px/1 var(--av-font-mono); letter-spacing: 0.2em;\n padding: 11px 24px; border-radius: 999px; cursor: pointer; text-transform: uppercase;\n transition: color 200ms ease;\n position: relative; z-index: 1;\n }\n .slider button:hover { color: var(--ink); }\n .slider button[aria-current=\"true\"] { color: var(--ink); }\n\n /* Shared sliding pill - JS animates transform and width to the\n active button's position. The pill is the only element with the\n raised-paper look; buttons just toggle their text colour. */\n .seg-pill {\n position: absolute; top: 4px; bottom: 4px; left: 0;\n background: var(--pill); border-radius: 999px;\n box-shadow: var(--raised);\n transition: transform 320ms cubic-bezier(.7,.05,.2,1), width 320ms cubic-bezier(.7,.05,.2,1);\n will-change: transform, width;\n pointer-events: none; z-index: 0;\n }\n\n /* ============ Menu dropdown ============ */\n #menu-dd {\n position: absolute; top: 58px; right: 28px; z-index: 25;\n width: 360px; max-width: calc(100cqw - 24px);\n max-height: calc(100cqh - 80px); overflow: auto;\n background: var(--paper); border-radius: 12px;\n box-shadow: var(--edge-lg);\n padding: 14px; opacity: 0; transform: translateY(-6px);\n pointer-events: none; transition: opacity 200ms ease, transform 200ms ease;\n }\n #menu-dd.open { opacity: 1; transform: translateY(0); pointer-events: auto; }\n\n .lock-row {\n display: flex; gap: 6px; align-items: stretch;\n background: var(--paper-2); border-radius: 8px;\n box-shadow: var(--recess);\n padding: 4px;\n }\n .lock-row input {\n flex: 1; background: transparent; border: 0; outline: 0;\n color: var(--ink); padding: 8px 10px;\n font: 13px var(--av-font-mono);\n }\n .lock-row input::placeholder { color: var(--ink-soft); }\n .lock-row button {\n background: var(--paper); border: 0; color: var(--ink);\n width: 32px; padding: 0; border-radius: 6px; cursor: pointer;\n font: 14px/1 var(--av-font-mono);\n box-shadow: var(--raised);\n display: flex; align-items: center; justify-content: center;\n }\n .lock-row button:hover { color: var(--accent); }\n .lock-hint {\n margin: 8px 4px 0; font: 10px var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.04em;\n }\n .lock-err { color: var(--accent); }\n /* Tiny \"built by teddy\" attribution - appears under the lock-screen\n password field AND at the foot of the about-card. Same monospace\n micro-type used by .lock-hint so the two contexts read consistent. */\n .built-by {\n margin: 14px 4px 0;\n font: 10px var(--av-font-mono);\n color: var(--ink-soft);\n letter-spacing: 0.04em;\n }\n .built-by a {\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n .built-by a:hover { color: var(--ink); }\n .about-card .built-by { margin-top: 18px; }\n\n .menu-items { padding: 4px 4px; display: none; }\n .menu-items.show { display: block; }\n .menu-items h3 {\n font: 9px/1 var(--av-font-mono); letter-spacing: 0.24em;\n color: var(--ink-soft); text-transform: uppercase; margin: 12px 4px 4px;\n }\n .menu-items a {\n display: block; padding: 9px 8px; color: var(--ink); text-decoration: none;\n font: 14px var(--av-font-display); border-radius: 6px;\n transition: background 120ms ease;\n }\n .menu-items a:hover { background: var(--paper-2); color: var(--accent); }\n\n /* ============ Settings cards inside the drawer ============\n Form controls match the time-picker's design language: recessed\n track + raised paper element. Toggles, sliders, and segmented\n pickers share the same depth-shadow recipe. */\n .menu-section {\n margin-top: 14px; padding-top: 12px;\n border-top: 1px solid rgba(26,22,18,0.07);\n }\n .menu-section:first-of-type { margin-top: 6px; border-top: 0; padding-top: 0; }\n .menu-section h3 {\n font: 700 9px var(--av-font-mono); letter-spacing: 0.20em;\n color: var(--ink); text-transform: uppercase;\n margin: 0 0 8px 4px;\n }\n /* Collapsible section - header is a button row with caret on the right.\n Default state is collapsed so accidental clicks don't touch settings. */\n .menu-section.collapsible > .section-header {\n display: flex; align-items: center; justify-content: space-between;\n cursor: pointer; user-select: none;\n padding: 4px 6px 6px 0;\n margin: 0 0 4px 0;\n }\n .menu-section.collapsible > .section-header h3 { margin: 0; }\n .menu-section.collapsible > .section-header .caret {\n width: 10px; height: 10px; transition: transform 200ms cubic-bezier(.7,.05,.2,1);\n color: var(--ink-soft);\n }\n .menu-section.collapsible[data-open=\"true\"] > .section-header .caret { transform: rotate(90deg); }\n .menu-section.collapsible > .section-body {\n max-height: 0; overflow: hidden;\n transition: max-height 280ms cubic-bezier(.7,.05,.2,1);\n }\n .menu-section.collapsible[data-open=\"true\"] > .section-body {\n max-height: 800px;\n }\n .menu-row {\n display: grid; grid-template-columns: 1fr auto; gap: 12px;\n align-items: center;\n padding: 8px 6px 8px 10px;\n border-radius: 6px;\n }\n .menu-row + .menu-row { box-shadow: inset 0 1px 0 rgba(26,22,18,0.06); }\n .menu-row .label {\n font: 12.5px var(--av-font-display); color: var(--ink);\n }\n .hint {\n display: block;\n font: 9px var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.04em; margin-top: 1px;\n }\n\n /* Toggle switch (raised pill that slides) */\n .switch {\n position: relative; width: 38px; height: 20px;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n cursor: pointer;\n border: 0; padding: 0;\n }\n .switch::after {\n content: ''; position: absolute; top: 2px; left: 2px;\n width: 16px; height: 16px;\n background: var(--pill); border-radius: 999px;\n box-shadow: var(--raised);\n transition: transform 220ms cubic-bezier(.7,.05,.2,1), background 200ms ease;\n }\n .switch[aria-checked=\"true\"] {\n background: var(--ink-2);\n }\n .switch[aria-checked=\"true\"]::after {\n transform: translateX(18px);\n background: var(--pill);\n }\n\n /* Slider - title row (label + value badge with breathing room) and\n a track row underneath. Value sits at the far right with a wide\n gap so it never crowds the label text. */\n .slider-row {\n display: block;\n padding: 8px 6px 10px 10px;\n }\n .slider-row .head {\n display: flex; align-items: baseline; gap: 24px;\n margin-bottom: 6px;\n }\n .slider-row .head .label-block { flex: 1; min-width: 0; }\n .slider-row .label {\n font: 12.5px var(--av-font-display); color: var(--ink);\n }\n .slider-row .value {\n flex: 0 0 auto;\n font: 11px var(--av-font-mono);\n color: var(--ink); font-variant-numeric: tabular-nums;\n background: var(--paper-2); padding: 3px 10px; border-radius: 4px;\n box-shadow: var(--recess);\n min-width: 48px; text-align: center;\n }\n .slider-row .slider-track {\n margin-top: 2px;\n }\n .slider-track input[type=\"range\"] {\n -webkit-appearance: none; appearance: none;\n width: 100%; height: 18px; background: transparent; cursor: pointer;\n }\n .slider-track input[type=\"range\"]::-webkit-slider-runnable-track {\n height: 4px; background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n }\n .slider-track input[type=\"range\"]::-webkit-slider-thumb {\n -webkit-appearance: none; appearance: none;\n width: 16px; height: 16px; margin-top: -6px;\n background: var(--paper); border-radius: 999px;\n box-shadow: var(--raised);\n cursor: pointer;\n }\n .slider-track input[type=\"range\"]::-moz-range-track {\n height: 4px; background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n }\n .slider-track input[type=\"range\"]::-moz-range-thumb {\n width: 16px; height: 16px;\n background: var(--paper); border-radius: 999px;\n box-shadow: var(--raised); border: 0;\n cursor: pointer;\n }\n\n /* Segmented picker (same pill-on-recess pattern as the time picker) */\n .seg {\n display: inline-flex; padding: 4px;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n position: relative;\n }\n .seg button {\n background: transparent; border: 0; color: var(--ink-soft);\n font: 9.5px var(--av-font-mono);\n letter-spacing: 0.16em; text-transform: uppercase;\n padding: 6px 12px; border-radius: 999px; cursor: pointer;\n position: relative; z-index: 1;\n transition: color 200ms ease;\n }\n .seg button[aria-current=\"true\"] {\n color: var(--ink);\n background: var(--pill);\n box-shadow: var(--raised);\n }\n\n /* Inline live-audio player - sits at the top of the unlocked drawer.\n A single play/stop button with a quiet running-indicator dot. */\n .live-audio {\n display: flex; align-items: center; gap: 12px;\n padding: 10px 12px; margin: 0 0 6px;\n background: var(--paper-2); border-radius: 8px;\n box-shadow: var(--recess);\n }\n .live-audio .pulse {\n width: 8px; height: 8px; border-radius: 50%;\n background: var(--ink-soft); flex-shrink: 0;\n transition: background 200ms ease;\n }\n .live-audio[data-on=\"true\"] .pulse {\n background: #c44a3b;\n animation: pulsate 1.6s ease-in-out infinite;\n }\n @keyframes pulsate {\n 0%, 100% { opacity: 0.4; transform: scale(1); }\n 50% { opacity: 1; transform: scale(1.25); }\n }\n .live-audio .label {\n flex: 1; font: 12.5px var(--av-font-display); color: var(--ink);\n }\n .live-audio .label .hint {\n font: 9px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.04em;\n }\n .live-audio button {\n background: var(--paper); border: 0; color: var(--ink);\n padding: 6px 12px; border-radius: 4px;\n font: 9.5px var(--av-font-mono);\n letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer;\n box-shadow: var(--raised);\n display: inline-flex; align-items: center; gap: 6px;\n }\n .live-audio button svg { width: 9px; height: 9px; }\n .live-audio button + button { margin-left: 4px; }\n /* Live spectrogram - hidden until the user clicks LISTEN. We\n collapse via max-height (not display:none) so the canvas inside\n keeps its layout dimensions and paints correctly on expand. */\n .live-spectro {\n width: 100%; height: 0;\n background: var(--paper-2); border-radius: 6px;\n margin: 0; overflow: hidden;\n box-shadow: var(--recess);\n transition: height 240ms cubic-bezier(.7,.05,.2,1), margin 240ms ease;\n }\n .live-audio[data-on=\"true\"] + .live-spectro {\n height: 70px; margin: 0 0 8px;\n }\n .live-status {\n font: 9.5px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.04em; text-transform: lowercase;\n margin: -4px 0 6px 0; padding: 0 4px;\n min-height: 13px;\n }\n .live-status.err { color: #a83a2d; }\n .menu-links a .l-hint {\n display: block; font: 8.5px var(--av-font-display); text-transform: none;\n letter-spacing: 0; color: var(--ink-soft); margin-top: 2px;\n }\n .menu-links a.full { grid-column: 1 / -1; }\n\n /* External BirdNET pages - small grid of cards */\n .menu-links {\n display: grid; grid-template-columns: 1fr 1fr; gap: 6px;\n margin: 4px 0 2px;\n }\n .menu-links a {\n display: flex; align-items: center; justify-content: center;\n padding: 9px 10px;\n background: var(--paper-2);\n color: var(--ink); text-decoration: none;\n font: 10.5px var(--av-font-mono);\n letter-spacing: 0.10em; text-transform: lowercase;\n border-radius: 6px;\n box-shadow: var(--raised);\n transition: transform 160ms ease, color 160ms ease;\n }\n .menu-links a:hover { transform: translateY(-1px); color: var(--ink); background: var(--paper); }\n .menu-save-row {\n display: flex; gap: 8px; align-items: center; justify-content: flex-end;\n margin-top: 10px; padding: 0 4px;\n }\n .menu-save-row .save-state {\n flex: 1; font: 9px var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.04em;\n }\n .menu-save-row .save-state.ok { color: #2d5d3a; }\n .menu-save-row .save-state.err { color: var(--accent); }\n .menu-save-row button {\n background: var(--ink); color: var(--paper); border: 0;\n padding: 7px 14px; border-radius: 6px;\n font: 10px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: uppercase; cursor: pointer;\n box-shadow: var(--raised);\n }\n .menu-save-row button:disabled { opacity: 0.4; cursor: default; }\n\n /* ============ View 1 - Collage (Galliformes-style poster) ============\n Viewport-fit, no scroll. The shared static-head provides the title;\n this view's content is just the cluster of bird cutouts which fills\n the remaining viewport height. */\n .gcollage {\n /* No max-width: the packing area spans the full viewport so the flock\n can use the whole screen on wide laptops/monitors. */\n flex: 1 1 auto; width: 100%; margin: 0 auto;\n position: relative; min-height: 0;\n }\n /* Centre-out bloom: each tile fades + scales in; JS staggers the\n animation-delay by distance from the cluster centre. backwards\n fill-mode holds the hidden start state through the delay, and we keep\n no forwards transform so the hover scale still works afterward. */\n @keyframes gtile-in {\n from { opacity: 0; transform: scale(0.82); }\n to { opacity: 1; transform: scale(1); }\n }\n .gtile.entering {\n animation: gtile-in 420ms cubic-bezier(.2,.7,.3,1) backwards;\n }\n @media (prefers-reduced-motion: reduce) {\n .gtile.entering { animation: none; }\n }\n /* Atlas: cards rise + fade in, row by row (JS staggers by row). */\n @keyframes bird-card-in {\n from { opacity: 0; transform: translateY(16px) scale(0.96); }\n to { opacity: 1; transform: none; }\n }\n .bird-card.entering { animation: bird-card-in 480ms cubic-bezier(.2,.7,.3,1) backwards; }\n /* Stats: the heatmap populates top-to-bottom - the hour header leads,\n species rows stagger down the list; the side panel fades in just\n behind. Opacity only (sticky cells reposition themselves, so\n animating transform here would fight their layout). */\n @keyframes stats-fade-in { from { opacity: 0; } to { opacity: 1; } }\n .stats-hm-head.entering, .stats-hm-row.entering,\n .stats-side h3.entering, .stats-side small.entering, .stats-side li.entering {\n animation: stats-fade-in 340ms ease backwards;\n }\n @media (prefers-reduced-motion: reduce) {\n .bird-card.entering, .stats-hm-head.entering, .stats-hm-row.entering,\n .stats-side h3.entering, .stats-side small.entering, .stats-side li.entering { animation: none; }\n }\n .gtile {\n position: absolute;\n background: transparent; border: 0; padding: 0; margin: 0;\n overflow: visible; /* let drop-shadow extend past the bbox */\n /* transform stays quick for hover; the layout box glides so a\n repack (new bird, count change, resize) reads as the flock\n settling rather than a redraw. */\n transition: transform 130ms ease,\n left 760ms cubic-bezier(.3, 0, .2, 1),\n top 760ms cubic-bezier(.3, 0, .2, 1),\n width 760ms cubic-bezier(.3, 0, .2, 1),\n height 760ms cubic-bezier(.3, 0, .2, 1);\n /* Events are handled at the .collage level via alpha-mask\n hit-testing - the tile rectangles must not intercept them. */\n pointer-events: none;\n }\n /* Hover state is driven by JS alpha-mask hit-testing (.is-hover),\n NOT the CSS :hover pseudo - the rectangular tile bounding boxes\n overlap, so :hover would light up the wrong bird. .is-hover only\n ever sits on the one tile whose silhouette is actually under the\n cursor. */\n .gtile.is-hover { transform: scale(1.04); z-index: 3; }\n /* A bird leaving the window fades + shrinks away; renderCollage\n removes the node when the exit finishes. */\n .gtile.leaving {\n opacity: 0; transform: scale(.85);\n transition: opacity 520ms ease, transform 520ms ease;\n }\n .gtile img {\n width: 100%; height: 100%; object-fit: contain; display: block;\n /* Cutouts arrive as alpha-bbox-cropped transparent-bg PNGs, so\n object-fit: contain leaves no whitespace inside the tile - the\n bird touches all four sides. Soft drop shadow keeps it readable\n on paper. */\n filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10));\n transition: filter 130ms ease;\n }\n .gtile.is-hover img {\n filter: drop-shadow(0 3px 10px rgba(26,22,18,0.26)) saturate(1.1);\n }\n /* Hover pill - surfaces the windowed count for the bird under the\n cursor. Confirms (Anna's-hummingbird-is-#1 paranoia) that the\n collage scaling reflects the selected window, not all-time. */\n /* JS anchors the pill to the hovered bird (left/top set per-hover) and\n keeps it clear of the bottom switcher, so it no longer hides behind\n it. translateX(-50%) centres it on the supplied point; z-index sits\n above the slider (30) as a backstop for any residual overlap. */\n .collage-tip {\n position: absolute; left: 0; top: 0;\n transform: translate(0, 0) translateX(-50%);\n padding: 6px 14px;\n background: var(--paper);\n border-radius: 999px;\n box-shadow: 0 6px 18px rgba(26,22,18,0.12),\n inset 0 0 0 1px rgba(255,255,255,0.6);\n font: italic 13px/1.3 var(--av-font-display);\n color: var(--ink);\n letter-spacing: 0.02em;\n opacity: 0; pointer-events: none;\n transition: opacity 150ms ease,\n transform 200ms cubic-bezier(.3, 0, .2, 1);\n white-space: nowrap;\n z-index: 31;\n }\n .collage-tip[aria-hidden=\"false\"] { opacity: 1; }\n .collage-tip .ct-name { font-style: normal; font-weight: 600; }\n .collage-tip .ct-n { font-style: normal; font-weight: 700; }\n .collage-tip .ct-w { color: var(--ink-soft); }\n\n .empty {\n color: var(--ink-soft); font: 10px var(--av-font-mono);\n padding: 120px 0; text-align: center; letter-spacing: 0.18em;\n text-transform: uppercase;\n }\n\n /* Page header is now shared at .stage > .static-head - see top of CSS. */\n /* Stats grid: the activity heatmap on the LEFT, text sections on the\n RIGHT. Each text section is a list with subtle separators. The\n heatmap scrolls within its panel; the view itself can also scroll\n if the whole grid outgrows the viewport. */\n .stats-grid {\n flex: 1 1 auto; min-height: 0;\n display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 48px;\n max-width: 1400px; margin: 0 auto; width: 100%;\n align-items: stretch;\n }\n /* Anything that doesn't fit (short desktop window, stacked mobile\n layout) scrolls instead of clipping. */\n .view#v1 { overflow-y: auto; overflow-x: hidden; }\n @container av-card (max-width: 900px) {\n /* Stacked single column: take the content's natural (tall) height instead\n of shrinking to the view (flex: 1 1 auto + min-height: 0 would let the\n grid collapse, clipping the heatmap), so #v1 actually scrolls on phones\n and portrait displays. */\n .stats-grid { grid-template-columns: 1fr; gap: 40px; flex: 0 0 auto; min-height: 0; }\n /* Stacked order: tables first, the heatmap underneath, and a buffer\n below it so its bottom row scrolls clear of the floating view\n switcher. */\n .stats-side { order: 1; }\n /* Full-height heatmap (no inner scroll) so the whole view is ONE scroll on\n a phone, not a nested one. Selector outranks the base max-height rule\n that follows it in source order. */\n .view#v1 .stats-timeline { order: 2; margin-bottom: 64px; max-height: none; }\n }\n /* Desktop two-column: size the grid to the side panel's content height\n (the heatmap pane stretches to match it, scrolling internally) and\n float it in the centre of the view. */\n @container av-card (min-width: 901px) {\n .stats-grid { flex: 0 1 auto; margin: auto; }\n }\n\n /* Activity heatmap: one row per species (most-heard first), one cell\n per hour of day, BirdNET-Go dashboard style. Cell tint deepens with\n detection count - mixed from paper toward ink so both themes work -\n and the count prints inside. The hour header sticks to the top of\n the scroll; species names stick to the left edge when the plot\n scrolls sideways on phones. */\n .stats-timeline {\n position: relative;\n flex: 1 1 auto; min-height: 120px;\n max-height: min(88cqh, 1600px);\n overflow: auto; -webkit-overflow-scrolling: touch;\n }\n .stats-hm {\n --hm-name: 200px;\n display: flex; flex-direction: column;\n min-width: 100%;\n }\n .stats-hm-head, .stats-hm-row {\n display: grid;\n grid-template-columns: var(--hm-name) repeat(24, minmax(22px, 1fr)) 48px;\n gap: 3px;\n padding: 1px 0;\n }\n .stats-hm-head {\n position: sticky; top: 0; z-index: 3;\n background: var(--paper);\n }\n .stats-hm-hr {\n font: 10px/1 var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.04em;\n text-align: center; padding: 3px 0 4px;\n }\n .stats-hm-name {\n position: sticky; left: 0; z-index: 2;\n background: var(--paper);\n display: flex; align-items: center;\n padding-right: 10px; overflow: hidden;\n transition: background 140ms ease;\n }\n .stats-hm-name .com {\n font: 600 14px/1.2 var(--av-font-display); color: var(--ink);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n transition: color 130ms ease;\n }\n .stats-hm-cell {\n display: flex; align-items: center; justify-content: center;\n min-height: 26px;\n border-radius: 2px;\n background: var(--paper-2);\n font: 11px/1 var(--av-font-mono); color: var(--ink);\n font-variant-numeric: tabular-nums;\n overflow: hidden;\n }\n /* Hours outside the current sub-day window get no tray tint, so the\n window's extent reads as the band of filled trays. */\n .stats-hm-cell.off { background: transparent; box-shadow: inset 0 0 0 1px var(--paper-2); }\n /* Five count steps (log scale, set in renderActivity), paper -> ink. */\n .stats-hm-cell.l1 { background: color-mix(in srgb, var(--ink) 12%, var(--paper)); }\n .stats-hm-cell.l2 { background: color-mix(in srgb, var(--ink) 26%, var(--paper)); }\n .stats-hm-cell.l3 { background: color-mix(in srgb, var(--ink) 45%, var(--paper)); color: var(--paper); }\n .stats-hm-cell.l4 { background: color-mix(in srgb, var(--ink) 68%, var(--paper)); color: var(--paper); }\n .stats-hm-cell.l5 { background: var(--ink); color: var(--paper); }\n .stats-hm-tot {\n display: flex; align-items: center; justify-content: flex-end;\n padding-right: 2px;\n font: 12px/1 var(--av-font-mono); color: var(--ink-soft);\n font-variant-numeric: tabular-nums;\n }\n .stats-hm-head .stats-hm-tot {\n align-items: flex-start; padding-top: 3px;\n font-size: 9.5px; letter-spacing: 0.04em;\n }\n /* Cross-highlight: hovering a heatmap row or a table row lights up the\n matching species on the other side - the name shifts to the accent\n ink. Click opens the species' detail modal (data-sci delegation). */\n .stats-hm-row { cursor: pointer; }\n .stats-hm-row:hover .stats-hm-name,\n .stats-hm-row.sync-hi .stats-hm-name { background: var(--paper-2); }\n .stats-hm-row:hover .com,\n .stats-hm-row.sync-hi .com { color: var(--accent); }\n .stats-hm-empty {\n position: absolute; inset: 0;\n display: flex; align-items: center; justify-content: center;\n font: 11px var(--av-font-mono); color: var(--ink-soft);\n text-transform: lowercase; letter-spacing: 0.18em;\n }\n /* Caption under the matrix: what the columns cover (rolling window vs\n last-7-days aggregate) and whether the rows were trimmed. Sticky so\n it doesn't wander off when the plot scrolls sideways. */\n .stats-hm-cap {\n position: sticky; left: 0;\n padding: 8px 0 2px;\n font: 8.5px/1 var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.04em;\n }\n /* Phones: fixed-width cells -> the plot grows past the viewport and\n scrolls horizontally under the sticky name column. */\n @container av-card (max-width: 700px) {\n .stats-hm { --hm-name: 104px; min-width: max-content; }\n .stats-hm-head, .stats-hm-row {\n grid-template-columns: var(--hm-name) repeat(24, 26px) 40px;\n }\n }\n\n /* (Old chart-card / .stats-charts CSS removed - replaced by the\n activity heatmap above.) */\n\n .stats-side {\n display: flex; flex-direction: column; justify-content: center;\n align-self: stretch; max-height: 100%;\n padding: 6px 0;\n }\n .stats-side h3 {\n font: 700 11.5px var(--av-font-mono); color: var(--ink);\n letter-spacing: 0.18em; text-transform: uppercase; margin: 0 0 1px;\n border-left: 2px solid var(--accent); padding-left: 10px;\n }\n /* Tight, content-sized groups - they sit just below their headers\n rather than spreading to fill the column. */\n .stats-side .grp { flex: 0 0 auto; }\n .stats-side .grp + .grp { margin-top: 22px; }\n .stats-side .grp small {\n display: block; font: 10px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.06em; margin: 0 0 3px 12px;\n }\n /* The whole list is one grid so counts share an aligned column, sized\n to the longest name (max-content) so that column sits right beside\n the names instead of stretching to a far edge. Each row is a subgrid\n so it still gets its own hover background + separator. */\n .stats-side ol {\n list-style: none; padding: 0; margin: 0;\n display: grid; grid-template-columns: 40px max-content 1fr;\n column-gap: 14px; align-content: start;\n }\n .stats-side li {\n display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;\n align-items: baseline; padding: 3px 0;\n box-shadow: inset 0 -1px 0 rgba(26,22,18,0.05);\n font: 14px/1.3 var(--av-font-display); color: var(--ink);\n transition: background 140ms ease, color 140ms ease;\n }\n .stats-side li[data-sci] { cursor: pointer; }\n .stats-side li[data-sci]:hover { background: var(--paper-2); }\n /* Cross-highlight from a hovered timeline square. */\n .stats-side li.sync-hi { background: var(--paper-2); }\n .stats-side li.sync-hi span:not(.yr):not(.ct) { color: var(--accent); }\n .stats-side li .yr { font: 11px var(--av-font-mono); color: var(--accent-2); letter-spacing: 0.04em; }\n .stats-side li .ct { font: 13px var(--av-font-mono); color: var(--ink-soft); font-variant-numeric: tabular-nums; justify-self: end; }\n\n /* (Histogram strip removed - charts moved into the stats-grid LEFT\n column as full-bleed chart cards. See .stats-charts above.) */\n\n /* ============ View 3 - Atlas ============\n Field-guide grid: one card per species, each card holds the cutout,\n names, window/all-time detection counts, and small action chips for\n audio playback, Wikipedia, and eBird. */\n .atlas-controls {\n max-width: 1280px; margin: 0 auto 18px;\n display: flex; justify-content: flex-end; align-items: center;\n }\n .atlas-sort {\n display: inline-flex; padding: 3px; position: relative;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n }\n .atlas-sort button {\n background: transparent; border: 0; color: var(--ink-soft);\n width: 30px; height: 26px; padding: 0; border-radius: 999px;\n cursor: pointer; position: relative; z-index: 1;\n display: inline-flex; align-items: center; justify-content: center;\n transition: color 200ms ease;\n }\n .atlas-sort button svg { width: 13px; height: 13px; display: block; }\n .atlas-sort button:hover { color: var(--ink); }\n .atlas-sort button[aria-current=\"true\"] { color: var(--ink); }\n .atlas-sort button .tip {\n position: absolute; top: calc(100% + 6px); left: 50%;\n transform: translateX(-50%); white-space: nowrap;\n font: 9px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: lowercase; color: var(--ink-soft);\n background: var(--paper); padding: 3px 8px; border-radius: 4px;\n box-shadow: var(--raised);\n opacity: 0; pointer-events: none; transition: opacity 160ms ease;\n }\n .atlas-sort button:hover .tip { opacity: 1; }\n .atlas-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n gap: 22px;\n max-width: 1280px; margin: 0 auto;\n }\n /* Mobile atlas: a compact 2-up grid instead of one giant card per row,\n so you can scan many birds and tap easily. Inline actions are hidden -\n tapping the card opens the detail modal, which has play/wiki/ebird +\n recordings - making the whole card one big touch target. */\n @container av-card (max-width: 700px) {\n /* minmax(0,1fr) lets the columns shrink below their content's intrinsic\n width (otherwise the chips force overflow). Selectors are scoped under\n .atlas-grid so they out-specify the base .bird-card rules that appear\n later in the file. */\n .atlas-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }\n .atlas-grid .bird-card { min-height: 0; padding: 12px 12px 14px; border-radius: 9px; }\n .atlas-grid .bird-card .stat { top: 10px; left: 11px; font-size: 8px; }\n .atlas-grid .bird-card .stat .n { font-size: 11px; }\n .atlas-grid .bird-card .img-wrap { margin: 30px 0 8px; }\n .atlas-grid .bird-card .img-wrap img { max-height: 96px; }\n .atlas-grid .bird-card h3 { font-size: 12.5px; }\n .atlas-grid .bird-card .sci { font-size: 9.5px; }\n .atlas-grid .bird-card .actions { display: none; }\n .atlas-grid .lifer-badge { top: 9px; right: 9px; font-size: 7px; padding: 3px 5px; letter-spacing: 0.1em; }\n }\n .bird-card {\n position: relative;\n background: var(--paper);\n border-radius: 10px;\n padding: 16px 16px 12px;\n box-shadow: var(--edge-lg);\n display: flex; flex-direction: column;\n min-height: 320px;\n cursor: pointer;\n transition: transform 200ms ease, box-shadow 200ms ease;\n }\n .bird-card:hover { transform: translateY(-1px); box-shadow: var(--edge-xl, 0 6px 16px rgba(26,22,18,0.12)); }\n .bird-card .actions, .bird-card .spectro-wrap { cursor: auto; }\n .bird-card .actions button, .bird-card .actions a { cursor: pointer; }\n .bird-card .stat {\n position: absolute; top: 14px; left: 16px;\n font: 9px/1.5 var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.04em;\n }\n /* \"lifer\" badge - shown top-right when a species' first-ever detection\n falls inside the selected window (set in renderAtlas). pointer-events\n off so it never blocks the card's click-to-open. */\n .lifer-badge {\n position: absolute; top: 12px; right: 12px; z-index: 2;\n font: 700 8px/1 var(--av-font-mono);\n letter-spacing: 0.14em; text-transform: uppercase;\n color: var(--paper); background: var(--accent);\n padding: 4px 7px; border-radius: 999px;\n box-shadow: 0 2px 6px rgba(26,22,18,0.18);\n pointer-events: none;\n }\n .bird-card .stat .n {\n color: var(--ink); font: 700 13px/1 var(--av-font-display);\n font-variant-numeric: tabular-nums; margin-right: 4px;\n letter-spacing: 0;\n }\n .bird-card .stat .lbl { display: block; }\n .bird-card .img-wrap {\n flex: 1 1 auto; min-height: 0;\n display: flex; align-items: center; justify-content: center;\n margin: 38px 0 10px;\n padding: 4px;\n }\n .bird-card .img-wrap img {\n max-width: 100%; max-height: 160px; display: block;\n filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10));\n }\n .bird-card h3 {\n margin: 0;\n font: 700 14.5px/1.15 var(--av-font-display);\n color: var(--ink); letter-spacing: 0.01em;\n }\n .bird-card .sci {\n font: italic 11px/1.2 var(--av-font-display);\n color: var(--ink-2); margin: 2px 0 0;\n }\n .bird-card .actions {\n display: flex; gap: 6px; align-items: stretch;\n margin-top: 12px; padding-top: 10px;\n box-shadow: inset 0 1px 0 rgba(26,22,18,0.07);\n }\n .bird-card .chip {\n flex: 0 0 auto;\n background: var(--paper-2);\n border: 0; color: var(--ink);\n padding: 5px 9px;\n border-radius: 4px;\n font: 9.5px/1 var(--av-font-mono);\n text-transform: lowercase; letter-spacing: 0.10em;\n text-decoration: none;\n cursor: pointer;\n display: inline-flex; align-items: center; gap: 5px;\n box-shadow: var(--raised);\n transition: transform 160ms ease, color 160ms ease;\n white-space: nowrap;\n }\n .bird-card .chip:hover {\n color: var(--ink); transform: translateY(-1px);\n }\n .bird-card .chip[data-active=\"true\"] {\n color: var(--paper);\n background: var(--ink);\n }\n .bird-card .chip svg { width: 10px; height: 10px; display: block; }\n .bird-card .chip.play { padding-left: 7px; }\n .bird-card .chip.ext::after { content: '↗'; opacity: 0.6; font-size: 11px; }\n\n /* Selected card highlight - matches the page-slider and time-window\n pill aesthetic: the active element is a slightly *brighter* paper\n surface (raised) on top of a slightly *recessed* background.\n No accent colour, no glowing ring - just the same depth shadow\n recipe the rest of the chrome uses. */\n .bird-card { transition: transform 280ms cubic-bezier(.7,.05,.2,1), box-shadow 280ms ease, background 200ms ease; }\n .bird-card[data-active=\"true\"] {\n background: var(--paper);\n box-shadow:\n inset 0 0 0 1px rgba(255,255,255,0.85),\n 0 0 0 1px rgba(26,22,18,0.06),\n 0 10px 26px rgba(26,22,18,0.10);\n transform: translateY(-1px);\n }\n .bird-card[data-pulse=\"true\"] {\n animation: bird-card-pulse 520ms cubic-bezier(.45,.05,.2,1);\n }\n /* Soft fade-in of the raised state, no ring pulse */\n @keyframes bird-card-pulse {\n 0% { transform: translateY(2px); }\n 100% { transform: translateY(-1px); }\n }\n\n .atlas-empty {\n grid-column: 1 / -1;\n padding: 80px 16px; text-align: center;\n font: 12px/1.6 var(--av-font-display); color: var(--ink-2);\n }\n .atlas-empty p { margin: 0 0 4px; }\n .atlas-empty p.hint {\n font: 10px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.06em;\n }\n\n /* ============ Admin screen (system / logs / tools) ============\n Overlays the slider when activated via #admin=... hash. Lives\n inside the same shell as the rest of the app so the header,\n menu button, and time-window pill stay put - feels native. */\n .admin-screen {\n position: absolute; inset: 60px 0 60px 0;\n background: var(--bg);\n overflow-y: auto;\n z-index: 5;\n opacity: 0; pointer-events: none;\n transition: opacity 200ms ease;\n }\n .admin-screen[aria-hidden=\"false\"] { opacity: 1; pointer-events: auto; }\n .admin-frame {\n max-width: 1100px; margin: 0 auto; padding: 0 28px 40px;\n }\n .admin-title {\n text-align: center; margin: 0 auto 28px;\n }\n .admin-title h1 {\n margin: 0;\n font: 32px/1.1 var(--av-font-display);\n color: var(--ink); letter-spacing: 0.005em;\n }\n /* Return-to-atlas pill in the same slot the time-window pill\n normally lives - matches its visual language. */\n .return-to-atlas {\n position: absolute; top: 18px; left: 20px; z-index: 6;\n display: none; align-items: center; gap: 6px;\n padding: 7px 14px; border-radius: 999px;\n background: var(--paper-2);\n color: var(--ink-soft);\n font: 10px var(--av-font-mono); letter-spacing: 0.18em;\n text-transform: lowercase; cursor: pointer;\n text-decoration: none;\n box-shadow: var(--recess);\n transition: color 200ms ease;\n }\n .av-shell.admin-on .return-to-atlas { display: inline-flex; }\n .av-shell.admin-on #slider { visibility: hidden; }\n .av-shell.admin-on .static-head { opacity: 0; pointer-events: none; }\n /* Hide the time-window pill on admin pages (it doesn't apply\n there) - the return-to-atlas link takes its slot. */\n .av-shell.admin-on #winPick { visibility: hidden; }\n .return-to-atlas:hover { color: var(--ink); }\n .return-to-atlas svg { width: 10px; height: 10px; }\n .admin-body { padding-top: 4px; }\n .admin-grid {\n display: grid; gap: 14px;\n grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n margin-bottom: 22px;\n }\n .admin-card {\n background: var(--paper); border-radius: 8px;\n padding: 14px 16px; box-shadow: var(--edge);\n }\n .admin-card h3 {\n margin: 0 0 6px; font: 10px var(--av-font-mono);\n letter-spacing: 0.18em; text-transform: uppercase;\n color: var(--ink-soft); font-weight: 500;\n }\n .admin-card .v { font: 20px var(--av-font-display); color: var(--ink); }\n .admin-card .sub { font: 11px var(--av-font-mono); color: var(--ink-soft); margin-top: 4px; }\n .admin-card.alert { box-shadow: 0 0 0 1px var(--accent), var(--edge); }\n .admin-card.alert h3 { color: var(--accent); }\n .admin-card.warn { box-shadow: 0 0 0 1px #c47a1f, var(--edge); }\n .admin-card.warn h3 { color: #c47a1f; }\n .admin-section-head {\n font: 700 11px var(--av-font-mono);\n letter-spacing: 0.18em; text-transform: uppercase;\n color: var(--ink); margin: 24px 0 10px;\n border-left: 2px solid var(--accent-2); padding-left: 10px;\n }\n table.admin-tbl {\n width: 100%; border-collapse: collapse;\n font: 12px var(--av-font-mono);\n background: var(--paper); border-radius: 8px; overflow: hidden;\n box-shadow: var(--edge);\n }\n table.admin-tbl th, table.admin-tbl td {\n border-bottom: 1px solid var(--rule, #c8bfae);\n padding: 9px 12px; text-align: left;\n }\n table.admin-tbl tr:last-child th, table.admin-tbl tr:last-child td { border-bottom: 0; }\n table.admin-tbl th {\n color: var(--ink-soft); text-transform: uppercase;\n font-size: 9.5px; letter-spacing: 0.14em; font-weight: 500;\n }\n table.admin-tbl .pill {\n display: inline-block; padding: 1px 8px; border-radius: 999px;\n font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase;\n }\n table.admin-tbl .pill.active { background: rgba(90,122,58,.18); color: #4d6730; }\n table.admin-tbl .pill.inactive { background: rgba(168,52,29,.18); color: var(--accent); }\n table.admin-tbl .pill.failed { background: rgba(168,52,29,.18); color: var(--accent); }\n table.admin-tbl button.restart {\n font: 9.5px var(--av-font-mono);\n letter-spacing: 0.04em; text-transform: lowercase;\n padding: 4px 10px; border: 0; background: var(--paper-2);\n border-radius: 4px; cursor: pointer; color: var(--ink-soft);\n box-shadow: var(--raised);\n }\n table.admin-tbl button.restart:hover { color: var(--ink); }\n table.admin-tbl button.restart[disabled] { opacity: .5; cursor: wait; }\n .admin-logs-toolbar {\n display: flex; gap: 8px; align-items: center; margin-bottom: 14px;\n }\n .admin-logs-toolbar select, .admin-logs-toolbar input[type=\"number\"] {\n font: 12px var(--av-font-mono);\n padding: 6px 10px; border: 1px solid var(--rule, #c8bfae);\n border-radius: 4px; background: var(--paper); color: var(--ink);\n }\n .admin-logs-toolbar label {\n font: 9.5px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: uppercase; color: var(--ink-soft);\n }\n .admin-logs-pane {\n background: #1c1a14; color: #d7cba8;\n font: 12px/1.5 var(--av-font-mono);\n padding: 14px 16px; border-radius: 8px;\n max-height: 65cqh; overflow: auto; white-space: pre-wrap;\n }\n .admin-actions-grid {\n display: grid; gap: 12px;\n grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));\n }\n .admin-action {\n background: var(--paper); border-radius: 8px;\n padding: 14px 16px; box-shadow: var(--edge);\n }\n .admin-action h4 { margin: 0 0 6px; font: 15px var(--av-font-display); color: var(--ink); }\n .admin-action p { margin: 0 0 10px; font: 12.5px var(--av-font-display); color: var(--ink-soft); }\n .admin-action button.run {\n font: 9.5px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: lowercase; padding: 6px 12px; border: 0;\n background: var(--ink); color: var(--paper); border-radius: 999px;\n cursor: pointer;\n }\n .admin-action button.run[disabled] { opacity: .5; cursor: wait; }\n .admin-action .out {\n margin-top: 10px; font: 10.5px var(--av-font-mono);\n color: var(--ink-soft); white-space: pre-wrap;\n }\n .admin-action.deploy pre {\n background: #1c1a14; color: #d7cba8;\n font: 11px var(--av-font-mono);\n padding: 12px 14px; border-radius: 6px;\n overflow: auto; white-space: pre-wrap; word-break: break-all;\n margin: 0;\n }\n .admin-action.deploy button.copy {\n font: 9.5px var(--av-font-mono); letter-spacing: 0.06em;\n text-transform: lowercase; padding: 5px 10px;\n background: var(--paper-2); color: var(--ink-soft);\n border: 0; border-radius: 4px; cursor: pointer; box-shadow: var(--raised);\n margin-top: 8px;\n }\n .admin-action.deploy button.copy:hover { color: var(--ink); }\n .admin-unreachable {\n background: var(--paper); border-radius: 8px;\n padding: 14px 16px; color: var(--accent);\n font: 12px var(--av-font-mono);\n margin-bottom: 18px; box-shadow: 0 0 0 1px var(--accent), var(--edge);\n }\n /* When admin screen is shown, the slider underneath is hidden so it\n doesn't compete for scrolling. The static title gets swapped out\n too - admin is its own section with its own visual identity.\n Menu button + time-window pill stay visible (still useful). */\n .av-shell.admin-on #views { display: none; }\n .av-shell.admin-on .nav-band,\n .av-shell.admin-on #slider { visibility: hidden; }\n .av-shell.admin-on .static-head { opacity: 0; pointer-events: none; transition: opacity 200ms ease; }\n\n /* Mobile compact: drawer takes near-full width, admin pads tighter,\n section heads + cards downsize. */\n @container av-card (max-width: 700px) {\n #menu-dd { top: 50px; right: 8px; left: 8px; width: auto; padding: 10px; }\n .admin-screen { inset: 52px 0 60px 0; }\n .admin-frame { padding: 0 14px 32px; }\n .admin-title { margin-bottom: 18px; }\n .admin-title h1 { font-size: 22px; }\n .admin-grid { gap: 10px; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }\n .admin-card { padding: 12px 14px; }\n .admin-card .v { font-size: 18px; }\n .admin-section-head { margin: 18px 0 8px; font-size: 10px; letter-spacing: 0.12em; }\n table.admin-tbl th, table.admin-tbl td { padding: 7px 8px; font-size: 11px; }\n .admin-actions-grid { gap: 10px; grid-template-columns: 1fr; }\n .admin-action { padding: 12px 14px; }\n /* Slider track bottom nav: tuck closer to bottom edge */\n .slider { bottom: 14px; padding: 3px; }\n .slider button { font-size: 9.5px; padding: 6px 11px; }\n }\n @container av-card (max-width: 700px) {\n /* Detail-modal: full-screen on small screens. */\n #detail-modal { padding: 12px; }\n /* Extra top padding = a clear strip for the close button, so the X\n sits in its own corner instead of on the artwork. */\n .modal-card { padding: 44px 18px 18px; border-radius: 10px; }\n .modal-close { top: 10px; right: 10px; }\n .modal-info h2 { font-size: 22px; }\n .modal-img { height: 160px; padding: 10px; }\n .modal-grid { gap: 18px; }\n /* Keep the description short so the recordings are in reach - the\n full text is one tap away on the wiki link. */\n .modal-info .desc {\n font-size: 12px; line-height: 1.5;\n display: -webkit-box; -webkit-line-clamp: 4;\n -webkit-box-orient: vertical; overflow: hidden;\n cursor: pointer;\n }\n .modal-info .desc.expanded { -webkit-line-clamp: unset; display: block; }\n .rec-row .when { font-size: 10.5px; }\n .rec-row .when small { font-size: 8.5px; }\n }\n\n /* ============ Detail modal (atlas card expansion) ============\n Click a bird card -> this fades in over a soft ink backdrop with\n the full sketch, taxonomic info, Wikipedia summary, and a\n scrollable list of every past audio capture for the species. */\n /* /about - a brief explainer popup floated over the index page.\n Reuses .modal-backdrop / .modal-close; opened by the #about hash. */\n #about-modal {\n position: absolute; inset: 0; z-index: 60;\n display: flex; align-items: center; justify-content: center;\n padding: 24px;\n pointer-events: none; opacity: 0;\n transition: opacity 200ms ease;\n }\n #about-modal[aria-hidden=\"false\"] { pointer-events: auto; opacity: 1; }\n .about-card {\n position: relative;\n max-width: 432px; width: 100%;\n background: var(--paper);\n border-radius: 14px;\n box-shadow:\n inset 0 0 0 1px rgba(255,255,255,0.6),\n 0 30px 80px rgba(26,22,18,0.24);\n padding: 34px 34px 28px;\n transform: translateY(10px) scale(0.985);\n transition: transform 260ms cubic-bezier(.32,.72,.32,1);\n }\n #about-modal[aria-hidden=\"false\"] .about-card { transform: none; }\n .about-card .about-eyebrow {\n margin: 0 0 7px;\n font: italic 400 13px/1 var(--av-font-display);\n color: var(--ink-2); letter-spacing: 0.06em;\n }\n .about-card h2 {\n margin: 0 0 13px;\n font: 700 22px/1.25 var(--av-font-display);\n letter-spacing: 0.01em; color: var(--ink);\n }\n .about-card .about-body {\n margin: 0;\n font: 400 14.5px/1.62 var(--av-font-display);\n color: var(--ink-2);\n }\n /* Plain inline link - same ink as the prose, a simple underline, no\n hover treatment. */\n .about-card .about-body a {\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n .about-card .about-explore {\n margin-top: 20px;\n background: none; border: 0; padding: 0;\n font: 700 11px/1 var(--av-font-mono);\n letter-spacing: 0.08em; text-transform: lowercase;\n color: var(--ink-soft); cursor: pointer;\n transition: color 140ms ease;\n }\n .about-card .about-explore:hover { color: var(--ink); }\n #detail-modal {\n position: absolute; inset: 0; z-index: 50;\n display: flex; align-items: center; justify-content: center;\n padding: 24px;\n pointer-events: none; opacity: 0;\n /* One opacity fade for backdrop + card together; the card also\n morphs via transform (see morphModalOpen). Driven by .is-open so\n the fade can run *during* the close morph, not after it. */\n transition: opacity 260ms ease;\n }\n #detail-modal.is-open { pointer-events: auto; opacity: 1; }\n .modal-backdrop {\n position: absolute; inset: 0;\n background: rgba(26,22,18,0.32);\n backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);\n }\n .modal-card {\n position: relative;\n max-width: 920px; max-height: calc(100cqh - 48px);\n width: 100%;\n background: var(--paper);\n border-radius: 14px;\n /* A real border, not an inset shadow: borders paint outside the\n scrolling content box, so scrolled images/spectrograms can never\n slide over the card's outline. */\n border: 1px solid var(--hairline);\n box-shadow: 0 30px 80px rgba(26,22,18,0.22);\n padding: 30px 34px 26px;\n overflow: auto;\n /* The expand-from-card animation is driven by inline transform\n and opacity set on open/close - see openDetailModal. CSS\n transition handles the in-between glide. */\n transform-origin: 50% 50%;\n will-change: transform, opacity;\n }\n .modal-card.is-morphing {\n transition: transform 300ms cubic-bezier(.22,.61,.36,1);\n }\n .modal-close {\n position: absolute; top: 16px; right: 16px;\n background: var(--paper-2); border: 0;\n width: 30px; height: 30px; border-radius: 999px;\n font: 16px/1 var(--av-font-mono); color: var(--ink);\n box-shadow: var(--raised);\n cursor: pointer; z-index: 1;\n }\n .modal-close:hover { color: var(--accent-2); }\n .modal-grid {\n display: grid; grid-template-columns: 1fr 1.15fr; gap: 32px;\n align-items: start;\n }\n @container av-card (max-width: 700px) {\n .modal-grid { grid-template-columns: 1fr; gap: 22px; }\n .modal-card { padding: 22px; }\n }\n .modal-img {\n position: relative;\n background: var(--paper-2); border-radius: 10px;\n display: flex; align-items: center; justify-content: center;\n padding: 16px;\n /* Fixed frame: the illustration fits INSIDE it (object-fit) instead\n of the frame growing to the illustration's aspect. */\n height: 230px;\n box-shadow: var(--recess);\n }\n .modal-img img {\n max-width: 100%; max-height: 100%;\n width: auto; height: auto; object-fit: contain;\n filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10));\n transition: opacity 220ms ease;\n }\n .modal-img img.swapping { opacity: 0; }\n /* Pose toggle - bottom-left corner of the modal sketch, icon strip.\n Same recipe as the time-window slider: recessed paper-2 well, a\n white sliding pill behind the active button (via .seg-pill), and\n a color shift on the icon itself. */\n .pose-toggle {\n position: absolute; left: 10px; bottom: 10px;\n display: inline-flex; padding: 3px;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n z-index: 2;\n }\n .pose-toggle button {\n background: transparent; border: 0; color: var(--ink-soft);\n width: 30px; height: 26px; padding: 0; border-radius: 999px;\n cursor: pointer; position: relative; z-index: 1;\n display: inline-flex; align-items: center; justify-content: center;\n transition: color 200ms ease;\n }\n .pose-toggle button svg { width: 13px; height: 13px; display: block; }\n .pose-toggle button:hover { color: var(--ink); }\n .pose-toggle button[aria-current=\"true\"] { color: var(--ink); }\n .pose-toggle button .tip {\n position: absolute; bottom: calc(100% + 6px); left: 50%;\n transform: translateX(-50%); white-space: nowrap;\n font: 9px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: lowercase; color: var(--ink-soft);\n background: var(--paper); padding: 3px 8px; border-radius: 4px;\n box-shadow: var(--raised);\n opacity: 0; pointer-events: none; transition: opacity 160ms ease;\n }\n .pose-toggle button:hover .tip { opacity: 1; }\n .pose-toggle[data-unavailable=\"true\"] { display: none; }\n .pose-toggle button[data-unavailable=\"true\"] { display: none; }\n .modal-info h2 {\n margin: 4px 36px 0 0;\n font: 700 26px/1.1 var(--av-font-display);\n color: var(--ink); letter-spacing: 0.01em;\n }\n .modal-info .sci {\n font: italic 13.5px/1.3 var(--av-font-display);\n color: var(--ink-2); margin: 4px 0 6px;\n }\n .modal-stats {\n display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;\n margin-bottom: 18px;\n }\n .modal-stats > div {\n background: var(--paper-2);\n border-radius: 6px; padding: 10px 12px;\n box-shadow: var(--recess);\n }\n .modal-stats .n {\n display: block;\n font: 800 17px/1 var(--av-font-display);\n color: var(--ink); font-variant-numeric: tabular-nums;\n }\n .modal-stats .lbl {\n display: block; margin-top: 4px;\n font: 8.5px var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.12em;\n text-transform: uppercase;\n }\n .modal-info .desc {\n font: 13px/1.55 var(--av-font-display);\n color: var(--ink); margin: 0 0 10px;\n }\n .modal-info .desc.placeholder { color: var(--ink-soft); font-style: italic; }\n .modal-meta {\n display: flex; flex-wrap: wrap; gap: 16px;\n font: 9.5px var(--av-font-mono); color: var(--ink-soft);\n margin: 0 0 14px;\n }\n .modal-meta .k { letter-spacing: 0.12em; text-transform: uppercase; }\n .modal-meta .v { color: var(--ink); margin-left: 6px; }\n .modal-meta .v.rare { color: var(--accent-2); }\n .modal-recordings {\n margin-top: 14px; padding-top: 14px;\n box-shadow: inset 0 1px 0 rgba(26,22,18,0.07);\n }\n .modal-recordings .rec-head {\n display: flex; align-items: baseline; justify-content: space-between;\n margin-bottom: 8px;\n }\n .modal-recordings h3 {\n font: 700 10px var(--av-font-mono);\n letter-spacing: 0.18em; text-transform: uppercase;\n color: var(--ink); margin: 0;\n border-left: 2px solid var(--accent-2); padding-left: 10px;\n }\n .modal-recordings .rec-count {\n font: 9px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.06em;\n }\n /* Reference-call (Xeno-Canto) button + its attribution line. */\n .modal-recordings .rec-head { gap: 8px; align-items: center; }\n .modal-recordings .rec-count { margin-left: auto; }\n .ref-call-btn {\n display: inline-flex; align-items: center; gap: 5px;\n font: 700 9px var(--av-font-mono); letter-spacing: 0.1em; text-transform: uppercase;\n color: var(--ink-soft); background: var(--paper); white-space: nowrap;\n border: 1px solid var(--hairline); border-radius: 999px;\n padding: 4px 11px 4px 9px; cursor: pointer;\n transition: color 160ms ease, background 160ms ease, border-color 160ms ease, transform 120ms ease;\n }\n .ref-call-btn svg { width: 9px; height: 9px; display: block; }\n .ref-call-btn:hover { color: var(--ink); background: var(--paper-2); transform: translateY(-1px); }\n .ref-call-btn[data-active=\"true\"] { color: var(--accent-2); border-color: var(--ink-soft); }\n .ref-call-btn.loading { opacity: 0.65; cursor: progress; }\n .ref-credit {\n font: 9px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.04em; line-height: 1.55; margin: -2px 0 8px;\n }\n .ref-credit a { color: var(--ink-2); text-decoration: underline; text-underline-offset: 2px; }\n .ref-credit a:hover { color: var(--ink); }\n .modal-recordings ol {\n list-style: none; padding: 0; margin: 0;\n max-height: 220px; overflow-y: auto;\n display: flex; flex-direction: column; gap: 4px;\n padding-right: 4px;\n }\n .rec-row {\n display: grid; grid-template-columns: 30px 1fr auto auto; gap: 10px;\n align-items: center;\n padding: 6px 10px; border-radius: 6px;\n background: var(--paper);\n box-shadow: var(--edge);\n transition: background 160ms ease;\n }\n .rec-row:hover { background: var(--paper-2); }\n .rec-row.expanded { grid-template-columns: 30px 1fr auto; }\n /* Spectrogram strip - inline below the row header. Click on the row\n toggles open/closed independent of playback so you can scan\n spectrograms visually without listening. */\n .rec-spectro {\n grid-column: 1 / -1;\n margin-top: 8px;\n height: 0; overflow: hidden;\n border-radius: 4px;\n /* Paper background - the spectrogram canvas paints ink ON it,\n matching the sketch palette. */\n background: var(--paper-2);\n box-shadow: var(--recess);\n position: relative;\n transition: height 240ms cubic-bezier(.7,.05,.2,1);\n }\n .rec-row.expanded .rec-spectro { height: 88px; }\n /* The spectrogram is rendered client-side onto a canvas - we fetch\n the mp3, decode via Web Audio, run an STFT, and paint columns\n with our ink palette. No BirdNET-Pi PNG involved. */\n .rec-spectro canvas {\n position: absolute; inset: 0;\n width: 100%; height: 100%;\n display: block;\n opacity: 0; transition: opacity 240ms ease;\n }\n .rec-spectro canvas.ready { opacity: 1; }\n .rec-spectro .rec-spectro-loading {\n position: absolute; inset: 0;\n display: flex; align-items: center; justify-content: center;\n color: var(--ink-soft);\n font: 9px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: lowercase;\n pointer-events: none;\n }\n /* Played portion - soft ink wash over the paper ground. */\n .rec-spectro .rec-spectro-played {\n position: absolute; left: 0; top: 0; bottom: 0; width: 0%;\n background: linear-gradient(90deg, rgba(26,22,18,0.16), rgba(26,22,18,0.06));\n pointer-events: none;\n }\n /* Cursor - sliding paper pill with a raised shadow, same recipe as\n the time-window slider's seg-pill. Pops against both the bright\n paper regions of the spectrogram and the inked vocalization\n bands. */\n .rec-spectro .rec-spectro-cursor {\n position: absolute; top: 50%; left: 0;\n width: 14px; height: 20px;\n transform: translate(-50%, -50%);\n background: var(--paper);\n border-radius: 999px;\n box-shadow: 0 1px 3px rgba(26,22,18,0.30), inset 0 0 0 1px rgba(26,22,18,0.18);\n pointer-events: none;\n opacity: 0;\n }\n .rec-spectro .rec-spectro-cursor::after {\n content: '';\n position: absolute; left: 50%; top: -100%; bottom: -100%;\n width: 1.5px; transform: translateX(-50%);\n background: rgba(251,247,238,0.85);\n box-shadow: 0 0 0 0.5px rgba(26,22,18,0.20);\n }\n .rec-spectro.armed .rec-spectro-cursor { opacity: 1; }\n /* Dark mode: the spectrogram ground is charcoal (paintSpectrogram paints\n with --paper), so the dark played-veil and the --paper cursor pill would\n both vanish into it. Flip the veil light, and make the cursor a light\n pill so it still reads against the dark ground. */\n :host([data-theme=\"dark\"]) .rec-spectro .rec-spectro-played {\n background: linear-gradient(90deg, rgba(236,232,225,0.18), rgba(236,232,225,0.07));\n }\n :host([data-theme=\"dark\"]) .rec-spectro .rec-spectro-cursor {\n background: var(--ink);\n }\n .rec-spectro .rec-spectro-scrub {\n position: absolute; inset: 0;\n cursor: ew-resize;\n }\n .rec-row .play {\n background: var(--paper-2); border: 0; color: var(--ink);\n width: 24px; height: 24px; border-radius: 999px;\n display: inline-flex; align-items: center; justify-content: center;\n box-shadow: var(--raised); cursor: pointer;\n padding: 0;\n }\n .rec-row .play svg { width: 8px; height: 8px; }\n .rec-row .play[data-active=\"true\"] {\n background: var(--ink); color: var(--paper);\n }\n .rec-row .when {\n font: 11px/1.2 var(--av-font-mono); color: var(--ink);\n }\n .rec-row .when small {\n display: block; font-size: 9px; color: var(--ink-soft);\n letter-spacing: 0.06em; margin-top: 1px;\n }\n .rec-row .conf {\n font: 10px var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.06em;\n font-variant-numeric: tabular-nums;\n }\n .rec-empty {\n padding: 14px;\n font: 11px/1.5 var(--av-font-display); color: var(--ink-soft);\n text-align: center;\n }\n .modal-actions {\n display: flex; gap: 8px; margin-top: 18px;\n }\n .modal-actions .chip {\n flex: 0 0 auto;\n background: var(--paper-2);\n border: 0; color: var(--ink);\n padding: 7px 12px;\n border-radius: 4px;\n font: 10px/1 var(--av-font-mono);\n text-transform: lowercase; letter-spacing: 0.12em;\n text-decoration: none;\n display: inline-flex; align-items: center; gap: 5px;\n box-shadow: var(--raised);\n transition: transform 160ms ease;\n }\n .modal-actions .chip:hover { transform: translateY(-1px); }\n .modal-actions .chip.ext::after { content: '↗'; opacity: 0.6; }\n\n /* Spectrogram appears on first audio play. Recoloured via CSS filter\n so BirdNET-Pi's default colour spectrogram comes out as a clean\n monochrome ink strip, matching the rest of the chart. Click to\n replay; a thin progress bar tracks playback in real time. */\n .bird-card .spectro-wrap {\n position: relative;\n min-height: 0; margin: 6px 0 2px;\n cursor: pointer;\n overflow: hidden;\n border-radius: 3px;\n box-shadow: var(--recess);\n }\n .bird-card .spectro-wrap:empty { display: none; }\n /* Canvas is painted client-side in the active theme's paper/ink palette\n (paintSpectrogram reads data-theme), so it follows light/dark mode with\n no CSS filter - same renderer as the modal recording spectrograms. */\n .bird-card .spectro-wrap canvas {\n width: 100%; height: 44px; display: block;\n }\n /* Progress wash: a translucent veil that grows as the clip plays. Ink in\n light mode; in dark mode a light veil so it stays visible over the\n charcoal spectrogram ground. */\n .bird-card .spectro-wrap::after {\n content: '';\n position: absolute; top: 0; bottom: 0; left: 0;\n width: var(--prog, 0%);\n background: linear-gradient(to right, rgba(26,22,18,0.30), rgba(26,22,18,0.05));\n pointer-events: none;\n transition: width 60ms linear;\n }\n :host([data-theme=\"dark\"]) .bird-card .spectro-wrap::after {\n background: linear-gradient(to right, rgba(236,232,225,0.26), rgba(236,232,225,0.05));\n }\n /* Playhead - thin vertical line at the leading edge */\n .bird-card .spectro-wrap::before {\n content: '';\n position: absolute; top: 0; bottom: 0;\n left: var(--prog, 0%);\n width: 1px;\n background: var(--ink);\n opacity: var(--playhead, 0);\n pointer-events: none;\n transition: opacity 200ms ease;\n }\n .bird-card[data-playing=\"true\"] .spectro-wrap::before { --playhead: 0.6; }\n\n/* ============ AvianVisitors mode toggle ============\n * Default install (.av-shell.av-local): hide the lock-row, show drawer items\n * directly when the menu opens. The PHP shim at /avian/api/menu.php\n * returns items immediately with no auth, so the live JS skips the lock\n * flow entirely.\n *\n * Forwarded install (body.av-forwarded): show the lock row so users\n * type the basic-auth password before items unlock. Pair with\n * AV_REQUIRE_AUTH=1 in php-fpm env + Caddy basic_auth on /avian/api/.\n * See avian/forwarding/. */\n.av-shell.av-local #dd-locked { display: none; }\n.av-shell.av-local #dd-items { display: block; }\n\n/* ============ Wall-display widgets (HA build) ============\n Clock + weather as a quiet corner block INSIDE the collage view.\n renderCollage() stamps this element's box into the silhouette-packing\n grid, so when the flock grows the birds nest around the numerals the\n same way they nest around each other. Same editorial recipe as the\n rest of the page: a serif display line over a monospace, letterspaced\n micro-caption, on the paper/ink variables so dark theme inverts free.\n pointer-events:none keeps hover/click hit-testing on the birds. */\n.wall-widgets {\n position: absolute; z-index: 6;\n display: flex; flex-direction: column; gap: 10px;\n pointer-events: none;\n}\n.wall-widgets[hidden] { display: none; }\n.wall-widgets[data-corner=\"bottom-right\"] { right: 36px; bottom: 96px; align-items: flex-end; text-align: right; }\n.wall-widgets[data-corner=\"bottom-left\"] { left: 36px; bottom: 96px; align-items: flex-start; text-align: left; }\n.wall-widgets[data-corner=\"top-right\"] { right: 36px; top: 14px; align-items: flex-end; text-align: right; }\n.wall-widgets[data-corner=\"top-left\"] { left: 36px; top: 14px; align-items: flex-start; text-align: left; }\n.ww-block { display: flex; flex-direction: column; gap: 3px; }\n.wall-widgets[data-corner$=\"right\"] .ww-block { align-items: flex-end; }\n.wall-widgets[data-corner$=\"left\"] .ww-block { align-items: flex-start; }\n.ww-block[hidden] { display: none; }\n.ww-big {\n font: 600 clamp(26px, 3cqw, 40px)/1 var(--av-font-display);\n color: var(--ink);\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.01em;\n}\n.ww-cap {\n font: 9.5px/1.45 var(--av-font-mono);\n letter-spacing: 0.16em; text-transform: uppercase;\n color: var(--ink-soft);\n}\n.ww-cap:empty { display: none; }\n.ww-rule {\n width: 44px; height: 1px;\n background: var(--hairline);\n}\n.ww-rule[hidden] { display: none; }\n@container av-card (max-width: 700px) {\n .wall-widgets { gap: 8px; }\n .wall-widgets[data-corner$=\"right\"] { right: 18px; }\n .wall-widgets[data-corner$=\"left\"] { left: 18px; }\n .wall-widgets[data-corner^=\"bottom\"] { bottom: 92px; }\n .ww-big { font-size: 20px; }\n .ww-cap { font-size: 8px; letter-spacing: 0.1em; }\n /* The sunrise/sunset caption is the first thing to go on a phone. */\n #wwSun { display: none; }\n}\n/* Kiosk cursor hiding - applied to by apt.js after idle when the\n wall options ask for it. */\n.av-shell.ww-cursor-hidden, .av-shell.ww-cursor-hidden * { cursor: none !important; }\n\n/* ---- Card build: configured title floats over the collage ---- */\n/* When a card sets a custom title, the heading is lifted out of the\n flex flow and overlaid on the collage top; renderCollage stamps the\n h1's box into the packing grid so birds nest around the words,\n clock-style. Stats/atlas keep headroom so their content doesn't\n slide under it. */\n.av-shell.av-title-overlay .static-head {\n position: absolute; top: 6px; left: 0; right: 0; z-index: 6;\n pointer-events: none; padding: 26px 32px 0;\n}\n.av-shell.av-title-overlay .view#v1,\n.av-shell.av-title-overlay .view#v2 { padding-top: 96px; }\n\n/* Card option: view picker pinned to the top edge instead of bottom. */\n.av-shell.av-picker-top .slider { top: 10px; bottom: auto; }\n.av-shell.av-picker-top .view { padding-bottom: 28px; }\n.av-shell.av-picker-top .view#v0, .av-shell.av-picker-top .view#v1 { padding-bottom: 24px; }\n\n/* ---- Recording-row review flag ---- */\n/* Writes a false-positive review back to BirdNET-Go. A quiet ghost x\n beside the confidence; first tap arms it into a labelled pill, second\n tap confirms - a stray touch can't flag anything. */\n.rec-row .flag {\n justify-self: end;\n min-width: 22px; height: 22px;\n display: inline-flex; align-items: center; justify-content: center;\n background: transparent; border: 0; border-radius: 999px;\n font: 10px/1 var(--av-font-mono); letter-spacing: 0;\n color: var(--ink-soft);\n padding: 0 6px; cursor: pointer; white-space: nowrap;\n transition: color 140ms ease, background 140ms ease;\n}\n.rec-row .flag:hover { color: var(--ink); background: var(--paper-2); }\n.rec-row .flag[data-state=\"armed\"] {\n background: var(--accent-2); color: var(--paper);\n font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase;\n padding: 0 9px;\n}\n.rec-row .flag[data-state=\"done\"] { color: var(--ink); cursor: default; }\n.rec-row .flag[data-state=\"failed\"] {\n color: var(--accent-2); font-weight: 700;\n font-size: 8.5px; letter-spacing: 0.06em; text-transform: uppercase;\n}\n.rec-row .flag:disabled { cursor: default; }\n.rec-row.flagged .when, .rec-row.flagged .conf { text-decoration: line-through; opacity: 0.55; }\n\n/* ---- card-build additions ---- */\n:host {\n display: block; position: relative;\n width: 100%; height: 100%; min-height: 560px;\n overflow: hidden;\n border-radius: var(--ha-card-border-radius, 12px);\n /* The card box is the responsive container all @container rules and\n cq units resolve against. */\n container: av-card / size;\n}\n.av-shell { position: absolute; inset: 0; overflow: hidden; }\n/* Card default: transparent, so the collage sits directly on the HA\n dashboard. background: paper restores the page's warm ground. */\n.av-shell { background: transparent; }\n.av-shell.av-bg-paper { background: var(--paper); }\n/* font: system swaps the editorial serif/mono pairing for HA's own\n typeface (set per-theme by HA; Roboto stock). */\n.av-shell.av-font-system {\n --av-font-display: var(--ha-font-family-body, var(--mdc-typography-font-family, Roboto, \"Segoe UI\", sans-serif));\n --av-font-mono: var(--ha-font-family-body, var(--mdc-typography-font-family, Roboto, \"Segoe UI\", sans-serif));\n}\n/* The view picker hugs the card's bottom edge (the page floats it higher). */\n.slider { bottom: 10px; }\n/* The collage owns the WHOLE card - no reserved band for the picker\n (it's stamped into the packing grid as an obstacle instead), so the\n flock centres in the true middle of the card. */\n.view#v0 { padding: 12px 14px; }\n/* Clock/weather sit a finger's width off the card's true corners. */\n.wall-widgets[data-corner$=\"right\"] { right: 16px; }\n.wall-widgets[data-corner$=\"left\"] { left: 16px; }\n.wall-widgets[data-corner^=\"top\"] { top: 12px; }\n.wall-widgets[data-corner^=\"bottom\"] { bottom: 14px; }\n/* In system-font mode the empty state reads as a normal sentence in the\n configured font instead of editorial letterspaced small-caps. */\n.av-shell.av-font-system .empty {\n font: 14px/1.5 var(--av-font-mono);\n text-transform: none; letter-spacing: 0;\n}\n/* In system-font mode the picker drops the editorial small-caps treatment\n and reads like native HA tabs. */\n.av-shell.av-font-system .slider button {\n font: 500 13px/1 var(--av-font-mono);\n letter-spacing: 0; text-transform: capitalize;\n padding: 9px 16px;\n}\n/* BirdNET-Pi admin chrome has no backend here (same as the static HA build). */\n#menuBtn, #menu-dd, #returnToAtlas, #adminScreen { display: none !important; }\n";
+var HABIRD_CSS = " :host {\n color-scheme: light;\n /* Type is variable-driven so the card build can swap the editorial\n serif/mono pairing for the Home Assistant system font. */\n --av-font-display: ui-serif, \"Iowan Old Style\", \"Apple Garamond\", \"Bookman Old Style\", Georgia, serif;\n --av-font-mono: ui-monospace, \"SF Mono\", Menlo, monospace;\n --paper: #fcfcfb; /* near-pure white, faintest warm undertone */\n --paper-2: #f3f2ee; /* recessed track */\n --paper-3: #e8e6df; /* placeholder fill */\n --ink: #1a1612;\n --ink-2: #4a3f31;\n --ink-soft: #908576;\n --accent: #4a3f31; /* warm dark grey - matches ink-2, no longer red */\n --accent-2: #1a1612; /* near-black ink - same hue as the title text */\n --hairline: rgba(26,22,18,0.14); /* faint divider lines + chart gridlines */\n --pill: #fcfcfb; /* active-toggle sliding pill */\n /* Depth shadow recipes - replace flat 1px borders */\n --edge: inset 0 0 0 1px rgba(255,255,255,0.55), 0 0 0 1px rgba(26,22,18,0.04);\n --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);\n --recess: inset 0 1px 2px rgba(26,22,18,0.06), inset 0 0 0 1px rgba(26,22,18,0.04);\n --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);\n }\n /* ---- Charcoal dark theme. Set via data-theme=\"dark\" on (an\n inline script in index.html applies the saved choice before paint;\n the switcher lives in Settings). The whole UI is variable-driven, so\n flipping ink<->paper inverts cleanly: text reads light, the stats\n squares become light marks on dark, the collage cutouts (transparent\n PNGs) sit on the charcoal directly. Shadow recipes are re-cut for a\n dark surface (faint light edges, deeper drop shadows). ---- */\n :host([data-theme=\"dark\"]) {\n color-scheme: dark;\n --paper: #1a1a1a; /* neutral charcoal page bg (no blue tint) */\n --paper-2: #242424; /* recessed track */\n --paper-3: #303030; /* placeholder fill */\n --ink: #ece8e1; /* warm off-white text / marks */\n --ink-2: #b7afa2; /* secondary text */\n --ink-soft: #837c70; /* muted text */\n --accent: #d8d2c6; /* light warm - pills read light-on-dark */\n --accent-2: #ece8e1;\n --hairline: rgba(236,232,225,0.20); /* faint light divider on charcoal */\n --pill: #404040; /* raised, clearly lighter than the track (neutral) */\n --edge: inset 0 0 0 1px rgba(255,255,255,0.05), 0 0 0 1px rgba(0,0,0,0.4);\n --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);\n --recess: inset 0 1px 2px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,255,255,0.04);\n --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);\n }\n * { box-sizing: border-box; }\n .av-shell { margin: 0; padding: 0; height: 100%; background: var(--paper); color: var(--ink); }\n .av-shell {\n font-family: var(--av-font-display);\n -webkit-font-smoothing: antialiased;\n overflow: hidden;\n }\n .mono { font-family: var(--av-font-mono); letter-spacing: 0.02em; }\n\n /* ============ Stage / views ============\n Stage is a flex column: a static title block sits on top, the views\n slider fills the remaining height. The title NEVER moves when the\n view changes - only its text fades when the new view's title differs\n from the current one. */\n .stage {\n position: absolute; inset: 0; overflow: hidden;\n display: flex; flex-direction: column;\n }\n .static-head {\n flex: 0 0 auto;\n padding: 84px 32px 22px;\n text-align: center; position: relative; z-index: 5;\n }\n .static-head .pre {\n display: inline-block;\n background: none; border: 0; padding: 0;\n font: italic 400 clamp(13px, 1.4cqw, 18px)/1 var(--av-font-display);\n color: var(--ink-2); letter-spacing: 0.06em; margin-bottom: 6px;\n cursor: pointer; transition: color 140ms ease;\n }\n .static-head .pre:hover {\n color: var(--ink);\n text-decoration: underline; text-underline-offset: 3px;\n text-decoration-thickness: 1px;\n }\n .static-head h1 {\n margin: 0; padding: 0;\n font: 700 clamp(24px, 3.2cqw, 40px)/1 var(--av-font-display);\n letter-spacing: 0.06em; color: var(--ink); text-transform: uppercase;\n font-style: normal;\n transition: opacity 240ms cubic-bezier(.7,.05,.2,1);\n }\n .static-head.swap-out h1 { opacity: 0; }\n /* Stats + Atlas hide the shared title so their content uses the full\n height (the collage keeps it). */\n body.av-view-stats .static-head, body.av-view-atlas .static-head { display: none; }\n @container av-card (max-width: 700px) {\n .static-head { padding: 96px 18px 22px; }\n }\n\n .views {\n flex: 1 1 auto; min-height: 0;\n position: relative; display: flex;\n transition: transform 480ms cubic-bezier(.7,.05,.2,1);\n }\n .view { flex: 0 0 100%; height: 100%; overflow: auto; padding: 8px 56px 132px; position: relative; }\n /* The collage view fits the viewport (no scroll). Atlas keeps normal\n scroll; stats scrolls when its grid outgrows the box (overridden at\n the .stats-grid rules below). */\n .view#v0, .view#v1 { overflow: hidden; padding: 8px 32px 88px; }\n .view#v0 { display: flex; }\n .view#v1 { display: flex; flex-direction: column; }\n @container av-card (max-width: 700px) { .view { padding: 8px 18px 132px; } .view#v0, .view#v1 { padding: 8px 16px 88px; } }\n\n /* ============ Top bar - window picker (left) + menu button (right). ============\n Visible on every view; the picker is the universal time-window control. */\n .top {\n position: absolute; top: 22px; left: 0; right: 0;\n display: flex; justify-content: space-between; align-items: center;\n padding: 0 28px; z-index: 30; pointer-events: none;\n }\n .top > * { pointer-events: auto; }\n .top .window-pick {\n display: inline-flex; gap: 0; padding: 4px;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n position: relative;\n }\n .top .window-pick button {\n background: transparent; border: 0; color: var(--ink-soft);\n font: 10px/1 var(--av-font-mono); letter-spacing: 0.18em;\n padding: 9px 16px; border-radius: 999px; cursor: pointer; text-transform: uppercase;\n transition: color 200ms ease;\n position: relative; z-index: 1;\n }\n .top .window-pick button:hover { color: var(--ink); }\n .top .window-pick button[aria-current=\"true\"] { color: var(--ink); }\n .menu-btn {\n background: var(--paper); border: 0; color: var(--ink);\n font: 11px/1 var(--av-font-mono); padding: 9px 14px 8px;\n border-radius: 999px; cursor: pointer; letter-spacing: 0.18em; text-transform: lowercase;\n box-shadow: var(--raised);\n transition: transform 160ms ease;\n }\n .menu-btn:hover { transform: translateY(-1px); }\n .menu-btn:active { transform: translateY(0); }\n /* Compact top bar on mobile - the time-window pill goes from\n spacious to thumb-tight without losing legibility. */\n @container av-card (max-width: 700px) {\n .top { top: 12px; padding: 0 12px; }\n .top .window-pick { padding: 3px; }\n .top .window-pick button {\n font-size: 8.5px; padding: 6px 10px; letter-spacing: 0.12em;\n }\n .menu-btn { font-size: 9px; padding: 6px 11px 5px; letter-spacing: 0.14em; }\n .return-to-atlas { top: 10px; left: 10px; padding: 5px 10px; font-size: 8.5px; }\n }\n @container av-card (max-width: 420px) {\n .top .window-pick button { font-size: 8px; padding: 5px 7px; letter-spacing: 0.08em; }\n }\n\n /* ============ Bottom slider - recessed track, raised active pill ============ */\n .slider {\n position: absolute; bottom: 28px; left: 50%; transform: translateX(-50%);\n display: flex; gap: 0; padding: 4px; z-index: 30;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n position: absolute; /* re-emit for the pill positioning */\n }\n .slider button {\n background: transparent; border: 0; color: var(--ink-soft);\n font: 10px/1 var(--av-font-mono); letter-spacing: 0.2em;\n padding: 11px 24px; border-radius: 999px; cursor: pointer; text-transform: uppercase;\n transition: color 200ms ease;\n position: relative; z-index: 1;\n }\n .slider button:hover { color: var(--ink); }\n .slider button[aria-current=\"true\"] { color: var(--ink); }\n\n /* Shared sliding pill - JS animates transform and width to the\n active button's position. The pill is the only element with the\n raised-paper look; buttons just toggle their text colour. */\n .seg-pill {\n position: absolute; top: 4px; bottom: 4px; left: 0;\n background: var(--pill); border-radius: 999px;\n box-shadow: var(--raised);\n transition: transform 320ms cubic-bezier(.7,.05,.2,1), width 320ms cubic-bezier(.7,.05,.2,1);\n will-change: transform, width;\n pointer-events: none; z-index: 0;\n }\n\n /* ============ Menu dropdown ============ */\n #menu-dd {\n position: absolute; top: 58px; right: 28px; z-index: 25;\n width: 360px; max-width: calc(100cqw - 24px);\n max-height: calc(100cqh - 80px); overflow: auto;\n background: var(--paper); border-radius: 12px;\n box-shadow: var(--edge-lg);\n padding: 14px; opacity: 0; transform: translateY(-6px);\n pointer-events: none; transition: opacity 200ms ease, transform 200ms ease;\n }\n #menu-dd.open { opacity: 1; transform: translateY(0); pointer-events: auto; }\n\n .lock-row {\n display: flex; gap: 6px; align-items: stretch;\n background: var(--paper-2); border-radius: 8px;\n box-shadow: var(--recess);\n padding: 4px;\n }\n .lock-row input {\n flex: 1; background: transparent; border: 0; outline: 0;\n color: var(--ink); padding: 8px 10px;\n font: 13px var(--av-font-mono);\n }\n .lock-row input::placeholder { color: var(--ink-soft); }\n .lock-row button {\n background: var(--paper); border: 0; color: var(--ink);\n width: 32px; padding: 0; border-radius: 6px; cursor: pointer;\n font: 14px/1 var(--av-font-mono);\n box-shadow: var(--raised);\n display: flex; align-items: center; justify-content: center;\n }\n .lock-row button:hover { color: var(--accent); }\n .lock-hint {\n margin: 8px 4px 0; font: 10px var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.04em;\n }\n .lock-err { color: var(--accent); }\n /* Tiny \"built by teddy\" attribution - appears under the lock-screen\n password field AND at the foot of the about-card. Same monospace\n micro-type used by .lock-hint so the two contexts read consistent. */\n .built-by {\n margin: 14px 4px 0;\n font: 10px var(--av-font-mono);\n color: var(--ink-soft);\n letter-spacing: 0.04em;\n }\n .built-by a {\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n .built-by a:hover { color: var(--ink); }\n .about-card .built-by { margin-top: 18px; }\n\n .menu-items { padding: 4px 4px; display: none; }\n .menu-items.show { display: block; }\n .menu-items h3 {\n font: 9px/1 var(--av-font-mono); letter-spacing: 0.24em;\n color: var(--ink-soft); text-transform: uppercase; margin: 12px 4px 4px;\n }\n .menu-items a {\n display: block; padding: 9px 8px; color: var(--ink); text-decoration: none;\n font: 14px var(--av-font-display); border-radius: 6px;\n transition: background 120ms ease;\n }\n .menu-items a:hover { background: var(--paper-2); color: var(--accent); }\n\n /* ============ Settings cards inside the drawer ============\n Form controls match the time-picker's design language: recessed\n track + raised paper element. Toggles, sliders, and segmented\n pickers share the same depth-shadow recipe. */\n .menu-section {\n margin-top: 14px; padding-top: 12px;\n border-top: 1px solid rgba(26,22,18,0.07);\n }\n .menu-section:first-of-type { margin-top: 6px; border-top: 0; padding-top: 0; }\n .menu-section h3 {\n font: 700 9px var(--av-font-mono); letter-spacing: 0.20em;\n color: var(--ink); text-transform: uppercase;\n margin: 0 0 8px 4px;\n }\n /* Collapsible section - header is a button row with caret on the right.\n Default state is collapsed so accidental clicks don't touch settings. */\n .menu-section.collapsible > .section-header {\n display: flex; align-items: center; justify-content: space-between;\n cursor: pointer; user-select: none;\n padding: 4px 6px 6px 0;\n margin: 0 0 4px 0;\n }\n .menu-section.collapsible > .section-header h3 { margin: 0; }\n .menu-section.collapsible > .section-header .caret {\n width: 10px; height: 10px; transition: transform 200ms cubic-bezier(.7,.05,.2,1);\n color: var(--ink-soft);\n }\n .menu-section.collapsible[data-open=\"true\"] > .section-header .caret { transform: rotate(90deg); }\n .menu-section.collapsible > .section-body {\n max-height: 0; overflow: hidden;\n transition: max-height 280ms cubic-bezier(.7,.05,.2,1);\n }\n .menu-section.collapsible[data-open=\"true\"] > .section-body {\n max-height: 800px;\n }\n .menu-row {\n display: grid; grid-template-columns: 1fr auto; gap: 12px;\n align-items: center;\n padding: 8px 6px 8px 10px;\n border-radius: 6px;\n }\n .menu-row + .menu-row { box-shadow: inset 0 1px 0 rgba(26,22,18,0.06); }\n .menu-row .label {\n font: 12.5px var(--av-font-display); color: var(--ink);\n }\n .hint {\n display: block;\n font: 9px var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.04em; margin-top: 1px;\n }\n\n /* Toggle switch (raised pill that slides) */\n .switch {\n position: relative; width: 38px; height: 20px;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n cursor: pointer;\n border: 0; padding: 0;\n }\n .switch::after {\n content: ''; position: absolute; top: 2px; left: 2px;\n width: 16px; height: 16px;\n background: var(--pill); border-radius: 999px;\n box-shadow: var(--raised);\n transition: transform 220ms cubic-bezier(.7,.05,.2,1), background 200ms ease;\n }\n .switch[aria-checked=\"true\"] {\n background: var(--ink-2);\n }\n .switch[aria-checked=\"true\"]::after {\n transform: translateX(18px);\n background: var(--pill);\n }\n\n /* Slider - title row (label + value badge with breathing room) and\n a track row underneath. Value sits at the far right with a wide\n gap so it never crowds the label text. */\n .slider-row {\n display: block;\n padding: 8px 6px 10px 10px;\n }\n .slider-row .head {\n display: flex; align-items: baseline; gap: 24px;\n margin-bottom: 6px;\n }\n .slider-row .head .label-block { flex: 1; min-width: 0; }\n .slider-row .label {\n font: 12.5px var(--av-font-display); color: var(--ink);\n }\n .slider-row .value {\n flex: 0 0 auto;\n font: 11px var(--av-font-mono);\n color: var(--ink); font-variant-numeric: tabular-nums;\n background: var(--paper-2); padding: 3px 10px; border-radius: 4px;\n box-shadow: var(--recess);\n min-width: 48px; text-align: center;\n }\n .slider-row .slider-track {\n margin-top: 2px;\n }\n .slider-track input[type=\"range\"] {\n -webkit-appearance: none; appearance: none;\n width: 100%; height: 18px; background: transparent; cursor: pointer;\n }\n .slider-track input[type=\"range\"]::-webkit-slider-runnable-track {\n height: 4px; background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n }\n .slider-track input[type=\"range\"]::-webkit-slider-thumb {\n -webkit-appearance: none; appearance: none;\n width: 16px; height: 16px; margin-top: -6px;\n background: var(--paper); border-radius: 999px;\n box-shadow: var(--raised);\n cursor: pointer;\n }\n .slider-track input[type=\"range\"]::-moz-range-track {\n height: 4px; background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n }\n .slider-track input[type=\"range\"]::-moz-range-thumb {\n width: 16px; height: 16px;\n background: var(--paper); border-radius: 999px;\n box-shadow: var(--raised); border: 0;\n cursor: pointer;\n }\n\n /* Segmented picker (same pill-on-recess pattern as the time picker) */\n .seg {\n display: inline-flex; padding: 4px;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n position: relative;\n }\n .seg button {\n background: transparent; border: 0; color: var(--ink-soft);\n font: 9.5px var(--av-font-mono);\n letter-spacing: 0.16em; text-transform: uppercase;\n padding: 6px 12px; border-radius: 999px; cursor: pointer;\n position: relative; z-index: 1;\n transition: color 200ms ease;\n }\n .seg button[aria-current=\"true\"] {\n color: var(--ink);\n background: var(--pill);\n box-shadow: var(--raised);\n }\n\n /* Inline live-audio player - sits at the top of the unlocked drawer.\n A single play/stop button with a quiet running-indicator dot. */\n .live-audio {\n display: flex; align-items: center; gap: 12px;\n padding: 10px 12px; margin: 0 0 6px;\n background: var(--paper-2); border-radius: 8px;\n box-shadow: var(--recess);\n }\n .live-audio .pulse {\n width: 8px; height: 8px; border-radius: 50%;\n background: var(--ink-soft); flex-shrink: 0;\n transition: background 200ms ease;\n }\n .live-audio[data-on=\"true\"] .pulse {\n background: #c44a3b;\n animation: pulsate 1.6s ease-in-out infinite;\n }\n @keyframes pulsate {\n 0%, 100% { opacity: 0.4; transform: scale(1); }\n 50% { opacity: 1; transform: scale(1.25); }\n }\n .live-audio .label {\n flex: 1; font: 12.5px var(--av-font-display); color: var(--ink);\n }\n .live-audio .label .hint {\n font: 9px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.04em;\n }\n .live-audio button {\n background: var(--paper); border: 0; color: var(--ink);\n padding: 6px 12px; border-radius: 4px;\n font: 9.5px var(--av-font-mono);\n letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer;\n box-shadow: var(--raised);\n display: inline-flex; align-items: center; gap: 6px;\n }\n .live-audio button svg { width: 9px; height: 9px; }\n .live-audio button + button { margin-left: 4px; }\n /* Live spectrogram - hidden until the user clicks LISTEN. We\n collapse via max-height (not display:none) so the canvas inside\n keeps its layout dimensions and paints correctly on expand. */\n .live-spectro {\n width: 100%; height: 0;\n background: var(--paper-2); border-radius: 6px;\n margin: 0; overflow: hidden;\n box-shadow: var(--recess);\n transition: height 240ms cubic-bezier(.7,.05,.2,1), margin 240ms ease;\n }\n .live-audio[data-on=\"true\"] + .live-spectro {\n height: 70px; margin: 0 0 8px;\n }\n .live-status {\n font: 9.5px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.04em; text-transform: lowercase;\n margin: -4px 0 6px 0; padding: 0 4px;\n min-height: 13px;\n }\n .live-status.err { color: #a83a2d; }\n .menu-links a .l-hint {\n display: block; font: 8.5px var(--av-font-display); text-transform: none;\n letter-spacing: 0; color: var(--ink-soft); margin-top: 2px;\n }\n .menu-links a.full { grid-column: 1 / -1; }\n\n /* External BirdNET pages - small grid of cards */\n .menu-links {\n display: grid; grid-template-columns: 1fr 1fr; gap: 6px;\n margin: 4px 0 2px;\n }\n .menu-links a {\n display: flex; align-items: center; justify-content: center;\n padding: 9px 10px;\n background: var(--paper-2);\n color: var(--ink); text-decoration: none;\n font: 10.5px var(--av-font-mono);\n letter-spacing: 0.10em; text-transform: lowercase;\n border-radius: 6px;\n box-shadow: var(--raised);\n transition: transform 160ms ease, color 160ms ease;\n }\n .menu-links a:hover { transform: translateY(-1px); color: var(--ink); background: var(--paper); }\n .menu-save-row {\n display: flex; gap: 8px; align-items: center; justify-content: flex-end;\n margin-top: 10px; padding: 0 4px;\n }\n .menu-save-row .save-state {\n flex: 1; font: 9px var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.04em;\n }\n .menu-save-row .save-state.ok { color: #2d5d3a; }\n .menu-save-row .save-state.err { color: var(--accent); }\n .menu-save-row button {\n background: var(--ink); color: var(--paper); border: 0;\n padding: 7px 14px; border-radius: 6px;\n font: 10px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: uppercase; cursor: pointer;\n box-shadow: var(--raised);\n }\n .menu-save-row button:disabled { opacity: 0.4; cursor: default; }\n\n /* ============ View 1 - Collage (Galliformes-style poster) ============\n Viewport-fit, no scroll. The shared static-head provides the title;\n this view's content is just the cluster of bird cutouts which fills\n the remaining viewport height. */\n .gcollage {\n /* No max-width: the packing area spans the full viewport so the flock\n can use the whole screen on wide laptops/monitors. */\n flex: 1 1 auto; width: 100%; margin: 0 auto;\n position: relative; min-height: 0;\n }\n /* Centre-out bloom: each tile fades + scales in; JS staggers the\n animation-delay by distance from the cluster centre. backwards\n fill-mode holds the hidden start state through the delay, and we keep\n no forwards transform so the hover scale still works afterward. */\n @keyframes gtile-in {\n from { opacity: 0; transform: scale(0.82); }\n to { opacity: 1; transform: scale(1); }\n }\n .gtile.entering {\n animation: gtile-in 420ms cubic-bezier(.2,.7,.3,1) backwards;\n }\n @media (prefers-reduced-motion: reduce) {\n .gtile.entering { animation: none; }\n }\n /* Atlas: cards rise + fade in, row by row (JS staggers by row). */\n @keyframes bird-card-in {\n from { opacity: 0; transform: translateY(16px) scale(0.96); }\n to { opacity: 1; transform: none; }\n }\n .bird-card.entering { animation: bird-card-in 480ms cubic-bezier(.2,.7,.3,1) backwards; }\n /* Stats: the heatmap populates top-to-bottom - the hour header leads,\n species rows stagger down the list; the side panel fades in just\n behind. Opacity only (sticky cells reposition themselves, so\n animating transform here would fight their layout). */\n @keyframes stats-fade-in { from { opacity: 0; } to { opacity: 1; } }\n .stats-hm-head.entering, .stats-hm-row.entering,\n .stats-side h3.entering, .stats-side small.entering, .stats-side li.entering {\n animation: stats-fade-in 340ms ease backwards;\n }\n @media (prefers-reduced-motion: reduce) {\n .bird-card.entering, .stats-hm-head.entering, .stats-hm-row.entering,\n .stats-side h3.entering, .stats-side small.entering, .stats-side li.entering { animation: none; }\n }\n .gtile {\n position: absolute;\n background: transparent; border: 0; padding: 0; margin: 0;\n overflow: visible; /* let drop-shadow extend past the bbox */\n /* transform stays quick for hover; the layout box glides so a\n repack (new bird, count change, resize) reads as the flock\n settling rather than a redraw. */\n transition: transform 130ms ease,\n left 760ms cubic-bezier(.3, 0, .2, 1),\n top 760ms cubic-bezier(.3, 0, .2, 1),\n width 760ms cubic-bezier(.3, 0, .2, 1),\n height 760ms cubic-bezier(.3, 0, .2, 1);\n /* Events are handled at the .collage level via alpha-mask\n hit-testing - the tile rectangles must not intercept them. */\n pointer-events: none;\n }\n /* Hover state is driven by JS alpha-mask hit-testing (.is-hover),\n NOT the CSS :hover pseudo - the rectangular tile bounding boxes\n overlap, so :hover would light up the wrong bird. .is-hover only\n ever sits on the one tile whose silhouette is actually under the\n cursor. */\n .gtile.is-hover { transform: scale(1.04); z-index: 3; }\n /* A bird leaving the window fades + shrinks away; renderCollage\n removes the node when the exit finishes. */\n .gtile.leaving {\n opacity: 0; transform: scale(.85);\n transition: opacity 520ms ease, transform 520ms ease;\n }\n .gtile img {\n width: 100%; height: 100%; object-fit: contain; display: block;\n /* Cutouts arrive as alpha-bbox-cropped transparent-bg PNGs, so\n object-fit: contain leaves no whitespace inside the tile - the\n bird touches all four sides. Soft drop shadow keeps it readable\n on paper. */\n filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10));\n transition: filter 130ms ease;\n }\n .gtile.is-hover img {\n filter: drop-shadow(0 3px 10px rgba(26,22,18,0.26)) saturate(1.1);\n }\n /* Hover pill - surfaces the windowed count for the bird under the\n cursor. Confirms (Anna's-hummingbird-is-#1 paranoia) that the\n collage scaling reflects the selected window, not all-time. */\n /* JS anchors the pill to the hovered bird (left/top set per-hover) and\n keeps it clear of the bottom switcher, so it no longer hides behind\n it. translateX(-50%) centres it on the supplied point; z-index sits\n above the slider (30) as a backstop for any residual overlap. */\n .collage-tip {\n position: absolute; left: 0; top: 0;\n transform: translate(0, 0) translateX(-50%);\n padding: 6px 14px;\n background: var(--paper);\n border-radius: 999px;\n box-shadow: 0 6px 18px rgba(26,22,18,0.12),\n inset 0 0 0 1px rgba(255,255,255,0.6);\n font: italic 13px/1.3 var(--av-font-display);\n color: var(--ink);\n letter-spacing: 0.02em;\n opacity: 0; pointer-events: none;\n transition: opacity 150ms ease,\n transform 200ms cubic-bezier(.3, 0, .2, 1);\n white-space: nowrap;\n z-index: 31;\n }\n .collage-tip[aria-hidden=\"false\"] { opacity: 1; }\n .collage-tip .ct-name { font-style: normal; font-weight: 600; }\n .collage-tip .ct-n { font-style: normal; font-weight: 700; }\n .collage-tip .ct-w { color: var(--ink-soft); }\n\n .empty {\n color: var(--ink-soft); font: 10px var(--av-font-mono);\n padding: 120px 0; text-align: center; letter-spacing: 0.18em;\n text-transform: uppercase;\n }\n\n /* Page header is now shared at .stage > .static-head - see top of CSS. */\n /* Stats grid: the activity heatmap on the LEFT, text sections on the\n RIGHT. Each text section is a list with subtle separators. The\n heatmap scrolls within its panel; the view itself can also scroll\n if the whole grid outgrows the viewport. */\n .stats-grid {\n flex: 1 1 auto; min-height: 0;\n display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 48px;\n max-width: 1400px; margin: 0 auto; width: 100%;\n align-items: stretch;\n }\n /* Anything that doesn't fit (short desktop window, stacked mobile\n layout) scrolls instead of clipping. */\n .view#v1 { overflow-y: auto; overflow-x: hidden; }\n @container av-card (max-width: 900px) {\n /* Stacked single column: take the content's natural (tall) height instead\n of shrinking to the view (flex: 1 1 auto + min-height: 0 would let the\n grid collapse, clipping the heatmap), so #v1 actually scrolls on phones\n and portrait displays. */\n .stats-grid { grid-template-columns: 1fr; gap: 40px; flex: 0 0 auto; min-height: 0; }\n /* Stacked order: tables first, the heatmap underneath, and a buffer\n below it so its bottom row scrolls clear of the floating view\n switcher. */\n .stats-side { order: 1; }\n /* Full-height heatmap (no inner scroll) so the whole view is ONE scroll on\n a phone, not a nested one. Selector outranks the base max-height rule\n that follows it in source order. */\n .view#v1 .stats-timeline { order: 2; margin-bottom: 64px; max-height: none; }\n }\n /* Desktop two-column: size the grid to the side panel's content height\n (the heatmap pane stretches to match it, scrolling internally) and\n float it in the centre of the view. */\n @container av-card (min-width: 901px) {\n .stats-grid { flex: 0 1 auto; margin: auto; }\n }\n\n /* Activity heatmap: one row per species (most-heard first), one cell\n per hour of day, BirdNET-Go dashboard style. Cell tint deepens with\n detection count - mixed from paper toward ink so both themes work -\n and the count prints inside. The hour header sticks to the top of\n the scroll; species names stick to the left edge when the plot\n scrolls sideways on phones. */\n .stats-timeline {\n position: relative;\n flex: 1 1 auto; min-height: 120px;\n max-height: min(88cqh, 1600px);\n overflow: auto; -webkit-overflow-scrolling: touch;\n }\n .stats-hm {\n --hm-name: 200px;\n display: flex; flex-direction: column;\n min-width: 100%;\n }\n .stats-hm-head, .stats-hm-row {\n display: grid;\n grid-template-columns: var(--hm-name) repeat(24, minmax(22px, 1fr)) 48px;\n gap: 3px;\n padding: 1px 0;\n }\n .stats-hm-head {\n position: sticky; top: 0; z-index: 3;\n background: var(--paper);\n }\n .stats-hm-hr {\n font: 10px/1 var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.04em;\n text-align: center; padding: 3px 0 4px;\n }\n .stats-hm-name {\n position: sticky; left: 0; z-index: 2;\n background: var(--paper);\n display: flex; align-items: center;\n padding-right: 10px; overflow: hidden;\n transition: background 140ms ease;\n }\n .stats-hm-name .com {\n font: 600 14px/1.2 var(--av-font-display); color: var(--ink);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n transition: color 130ms ease;\n }\n .stats-hm-cell {\n display: flex; align-items: center; justify-content: center;\n min-height: 26px;\n border-radius: 2px;\n background: var(--paper-2);\n font: 11px/1 var(--av-font-mono); color: var(--ink);\n font-variant-numeric: tabular-nums;\n overflow: hidden;\n }\n /* Hours outside the current sub-day window get no tray tint, so the\n window's extent reads as the band of filled trays. */\n .stats-hm-cell.off { background: transparent; box-shadow: inset 0 0 0 1px var(--paper-2); }\n /* Five count steps (log scale, set in renderActivity), paper -> ink. */\n .stats-hm-cell.l1 { background: color-mix(in srgb, var(--ink) 12%, var(--paper)); }\n .stats-hm-cell.l2 { background: color-mix(in srgb, var(--ink) 26%, var(--paper)); }\n .stats-hm-cell.l3 { background: color-mix(in srgb, var(--ink) 45%, var(--paper)); color: var(--paper); }\n .stats-hm-cell.l4 { background: color-mix(in srgb, var(--ink) 68%, var(--paper)); color: var(--paper); }\n .stats-hm-cell.l5 { background: var(--ink); color: var(--paper); }\n .stats-hm-tot {\n display: flex; align-items: center; justify-content: flex-end;\n padding-right: 2px;\n font: 12px/1 var(--av-font-mono); color: var(--ink-soft);\n font-variant-numeric: tabular-nums;\n }\n .stats-hm-head .stats-hm-tot {\n align-items: flex-start; padding-top: 3px;\n font-size: 9.5px; letter-spacing: 0.04em;\n }\n /* Cross-highlight: hovering a heatmap row or a table row lights up the\n matching species on the other side - the name shifts to the accent\n ink. Click opens the species' detail modal (data-sci delegation). */\n .stats-hm-row { cursor: pointer; }\n .stats-hm-row:hover .stats-hm-name,\n .stats-hm-row.sync-hi .stats-hm-name { background: var(--paper-2); }\n .stats-hm-row:hover .com,\n .stats-hm-row.sync-hi .com { color: var(--accent); }\n .stats-hm-empty {\n position: absolute; inset: 0;\n display: flex; align-items: center; justify-content: center;\n font: 11px var(--av-font-mono); color: var(--ink-soft);\n text-transform: lowercase; letter-spacing: 0.18em;\n }\n /* Caption under the matrix: what the columns cover (rolling window vs\n last-7-days aggregate) and whether the rows were trimmed. Sticky so\n it doesn't wander off when the plot scrolls sideways. */\n .stats-hm-cap {\n position: sticky; left: 0;\n padding: 8px 0 2px;\n font: 8.5px/1 var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.04em;\n }\n /* Phones: fixed-width cells -> the plot grows past the viewport and\n scrolls horizontally under the sticky name column. */\n @container av-card (max-width: 700px) {\n .stats-hm { --hm-name: 104px; min-width: max-content; }\n .stats-hm-head, .stats-hm-row {\n grid-template-columns: var(--hm-name) repeat(24, 26px) 40px;\n }\n }\n\n /* (Old chart-card / .stats-charts CSS removed - replaced by the\n activity heatmap above.) */\n\n .stats-side {\n display: flex; flex-direction: column; justify-content: center;\n align-self: stretch; max-height: 100%;\n padding: 6px 0;\n }\n .stats-side h3 {\n font: 700 11.5px var(--av-font-mono); color: var(--ink);\n letter-spacing: 0.18em; text-transform: uppercase; margin: 0 0 1px;\n border-left: 2px solid var(--accent); padding-left: 10px;\n }\n /* Tight, content-sized groups - they sit just below their headers\n rather than spreading to fill the column. */\n .stats-side .grp { flex: 0 0 auto; }\n .stats-side .grp + .grp { margin-top: 22px; }\n .stats-side .grp small {\n display: block; font: 10px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.06em; margin: 0 0 3px 12px;\n }\n /* The whole list is one grid so counts share an aligned column, sized\n to the longest name (max-content) so that column sits right beside\n the names instead of stretching to a far edge. Each row is a subgrid\n so it still gets its own hover background + separator. */\n .stats-side ol {\n list-style: none; padding: 0; margin: 0;\n display: grid; grid-template-columns: 40px max-content 1fr;\n column-gap: 14px; align-content: start;\n }\n .stats-side li {\n display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;\n align-items: baseline; padding: 3px 0;\n box-shadow: inset 0 -1px 0 rgba(26,22,18,0.05);\n font: 14px/1.3 var(--av-font-display); color: var(--ink);\n transition: background 140ms ease, color 140ms ease;\n }\n .stats-side li[data-sci] { cursor: pointer; }\n .stats-side li[data-sci]:hover { background: var(--paper-2); }\n /* Cross-highlight from a hovered timeline square. */\n .stats-side li.sync-hi { background: var(--paper-2); }\n .stats-side li.sync-hi span:not(.yr):not(.ct) { color: var(--accent); }\n .stats-side li .yr { font: 11px var(--av-font-mono); color: var(--accent-2); letter-spacing: 0.04em; }\n .stats-side li .ct { font: 13px var(--av-font-mono); color: var(--ink-soft); font-variant-numeric: tabular-nums; justify-self: end; }\n\n /* (Histogram strip removed - charts moved into the stats-grid LEFT\n column as full-bleed chart cards. See .stats-charts above.) */\n\n /* ============ View 3 - Atlas ============\n Field-guide grid: one card per species, each card holds the cutout,\n names, window/all-time detection counts, and small action chips for\n audio playback, Wikipedia, and eBird. */\n .atlas-controls {\n max-width: 1280px; margin: 0 auto 18px;\n display: flex; justify-content: flex-end; align-items: center;\n }\n .atlas-sort {\n display: inline-flex; padding: 3px; position: relative;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n }\n .atlas-sort button {\n background: transparent; border: 0; color: var(--ink-soft);\n width: 30px; height: 26px; padding: 0; border-radius: 999px;\n cursor: pointer; position: relative; z-index: 1;\n display: inline-flex; align-items: center; justify-content: center;\n transition: color 200ms ease;\n }\n .atlas-sort button svg { width: 13px; height: 13px; display: block; }\n .atlas-sort button:hover { color: var(--ink); }\n .atlas-sort button[aria-current=\"true\"] { color: var(--ink); }\n .atlas-sort button .tip {\n position: absolute; top: calc(100% + 6px); left: 50%;\n transform: translateX(-50%); white-space: nowrap;\n font: 9px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: lowercase; color: var(--ink-soft);\n background: var(--paper); padding: 3px 8px; border-radius: 4px;\n box-shadow: var(--raised);\n opacity: 0; pointer-events: none; transition: opacity 160ms ease;\n }\n .atlas-sort button:hover .tip { opacity: 1; }\n .atlas-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n gap: 22px;\n max-width: 1280px; margin: 0 auto;\n }\n /* Mobile atlas: a compact 2-up grid instead of one giant card per row,\n so you can scan many birds and tap easily. Inline actions are hidden -\n tapping the card opens the detail modal, which has play/wiki/ebird +\n recordings - making the whole card one big touch target. */\n @container av-card (max-width: 700px) {\n /* minmax(0,1fr) lets the columns shrink below their content's intrinsic\n width (otherwise the chips force overflow). Selectors are scoped under\n .atlas-grid so they out-specify the base .bird-card rules that appear\n later in the file. */\n .atlas-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }\n .atlas-grid .bird-card { min-height: 0; padding: 12px 12px 14px; border-radius: 9px; }\n .atlas-grid .bird-card .stat { top: 10px; left: 11px; font-size: 8px; }\n .atlas-grid .bird-card .stat .n { font-size: 11px; }\n .atlas-grid .bird-card .img-wrap { margin: 30px 0 8px; }\n .atlas-grid .bird-card .img-wrap img { max-height: 96px; }\n .atlas-grid .bird-card h3 { font-size: 12.5px; }\n .atlas-grid .bird-card .sci { font-size: 9.5px; }\n .atlas-grid .bird-card .actions { display: none; }\n .atlas-grid .lifer-badge { top: 9px; right: 9px; font-size: 7px; padding: 3px 5px; letter-spacing: 0.1em; }\n }\n .bird-card {\n position: relative;\n background: var(--paper);\n border-radius: 10px;\n padding: 16px 16px 12px;\n box-shadow: var(--edge-lg);\n display: flex; flex-direction: column;\n min-height: 320px;\n cursor: pointer;\n transition: transform 200ms ease, box-shadow 200ms ease;\n }\n .bird-card:hover { transform: translateY(-1px); box-shadow: var(--edge-xl, 0 6px 16px rgba(26,22,18,0.12)); }\n .bird-card .actions, .bird-card .spectro-wrap { cursor: auto; }\n .bird-card .actions button, .bird-card .actions a { cursor: pointer; }\n .bird-card .stat {\n position: absolute; top: 14px; left: 16px;\n font: 9px/1.5 var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.04em;\n }\n /* \"lifer\" badge - shown top-right when a species' first-ever detection\n falls inside the selected window (set in renderAtlas). pointer-events\n off so it never blocks the card's click-to-open. */\n .lifer-badge {\n position: absolute; top: 12px; right: 12px; z-index: 2;\n font: 700 8px/1 var(--av-font-mono);\n letter-spacing: 0.14em; text-transform: uppercase;\n color: var(--paper); background: var(--accent);\n padding: 4px 7px; border-radius: 999px;\n box-shadow: 0 2px 6px rgba(26,22,18,0.18);\n pointer-events: none;\n }\n .bird-card .stat .n {\n color: var(--ink); font: 700 13px/1 var(--av-font-display);\n font-variant-numeric: tabular-nums; margin-right: 4px;\n letter-spacing: 0;\n }\n .bird-card .stat .lbl { display: block; }\n .bird-card .img-wrap {\n flex: 1 1 auto; min-height: 0;\n display: flex; align-items: center; justify-content: center;\n margin: 38px 0 10px;\n padding: 4px;\n }\n .bird-card .img-wrap img {\n max-width: 100%; max-height: 160px; display: block;\n filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10));\n }\n .bird-card h3 {\n margin: 0;\n font: 700 14.5px/1.15 var(--av-font-display);\n color: var(--ink); letter-spacing: 0.01em;\n }\n .bird-card .sci {\n font: italic 11px/1.2 var(--av-font-display);\n color: var(--ink-2); margin: 2px 0 0;\n }\n .bird-card .actions {\n display: flex; gap: 6px; align-items: stretch;\n margin-top: 12px; padding-top: 10px;\n box-shadow: inset 0 1px 0 rgba(26,22,18,0.07);\n }\n .bird-card .chip {\n flex: 0 0 auto;\n background: var(--paper-2);\n border: 0; color: var(--ink);\n padding: 5px 9px;\n border-radius: 4px;\n font: 9.5px/1 var(--av-font-mono);\n text-transform: lowercase; letter-spacing: 0.10em;\n text-decoration: none;\n cursor: pointer;\n display: inline-flex; align-items: center; gap: 5px;\n box-shadow: var(--raised);\n transition: transform 160ms ease, color 160ms ease;\n white-space: nowrap;\n }\n .bird-card .chip:hover {\n color: var(--ink); transform: translateY(-1px);\n }\n .bird-card .chip[data-active=\"true\"] {\n color: var(--paper);\n background: var(--ink);\n }\n .bird-card .chip svg { width: 10px; height: 10px; display: block; }\n .bird-card .chip.play { padding-left: 7px; }\n .bird-card .chip.ext::after { content: '↗'; opacity: 0.6; font-size: 11px; }\n\n /* Selected card highlight - matches the page-slider and time-window\n pill aesthetic: the active element is a slightly *brighter* paper\n surface (raised) on top of a slightly *recessed* background.\n No accent colour, no glowing ring - just the same depth shadow\n recipe the rest of the chrome uses. */\n .bird-card { transition: transform 280ms cubic-bezier(.7,.05,.2,1), box-shadow 280ms ease, background 200ms ease; }\n .bird-card[data-active=\"true\"] {\n background: var(--paper);\n box-shadow:\n inset 0 0 0 1px rgba(255,255,255,0.85),\n 0 0 0 1px rgba(26,22,18,0.06),\n 0 10px 26px rgba(26,22,18,0.10);\n transform: translateY(-1px);\n }\n .bird-card[data-pulse=\"true\"] {\n animation: bird-card-pulse 520ms cubic-bezier(.45,.05,.2,1);\n }\n /* Soft fade-in of the raised state, no ring pulse */\n @keyframes bird-card-pulse {\n 0% { transform: translateY(2px); }\n 100% { transform: translateY(-1px); }\n }\n\n .atlas-empty {\n grid-column: 1 / -1;\n padding: 80px 16px; text-align: center;\n font: 12px/1.6 var(--av-font-display); color: var(--ink-2);\n }\n .atlas-empty p { margin: 0 0 4px; }\n .atlas-empty p.hint {\n font: 10px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.06em;\n }\n\n /* ============ Admin screen (system / logs / tools) ============\n Overlays the slider when activated via #admin=... hash. Lives\n inside the same shell as the rest of the app so the header,\n menu button, and time-window pill stay put - feels native. */\n .admin-screen {\n position: absolute; inset: 60px 0 60px 0;\n background: var(--bg);\n overflow-y: auto;\n z-index: 5;\n opacity: 0; pointer-events: none;\n transition: opacity 200ms ease;\n }\n .admin-screen[aria-hidden=\"false\"] { opacity: 1; pointer-events: auto; }\n .admin-frame {\n max-width: 1100px; margin: 0 auto; padding: 0 28px 40px;\n }\n .admin-title {\n text-align: center; margin: 0 auto 28px;\n }\n .admin-title h1 {\n margin: 0;\n font: 32px/1.1 var(--av-font-display);\n color: var(--ink); letter-spacing: 0.005em;\n }\n /* Return-to-atlas pill in the same slot the time-window pill\n normally lives - matches its visual language. */\n .return-to-atlas {\n position: absolute; top: 18px; left: 20px; z-index: 6;\n display: none; align-items: center; gap: 6px;\n padding: 7px 14px; border-radius: 999px;\n background: var(--paper-2);\n color: var(--ink-soft);\n font: 10px var(--av-font-mono); letter-spacing: 0.18em;\n text-transform: lowercase; cursor: pointer;\n text-decoration: none;\n box-shadow: var(--recess);\n transition: color 200ms ease;\n }\n .av-shell.admin-on .return-to-atlas { display: inline-flex; }\n .av-shell.admin-on #slider { visibility: hidden; }\n .av-shell.admin-on .static-head { opacity: 0; pointer-events: none; }\n /* Hide the time-window pill on admin pages (it doesn't apply\n there) - the return-to-atlas link takes its slot. */\n .av-shell.admin-on #winPick { visibility: hidden; }\n .return-to-atlas:hover { color: var(--ink); }\n .return-to-atlas svg { width: 10px; height: 10px; }\n .admin-body { padding-top: 4px; }\n .admin-grid {\n display: grid; gap: 14px;\n grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n margin-bottom: 22px;\n }\n .admin-card {\n background: var(--paper); border-radius: 8px;\n padding: 14px 16px; box-shadow: var(--edge);\n }\n .admin-card h3 {\n margin: 0 0 6px; font: 10px var(--av-font-mono);\n letter-spacing: 0.18em; text-transform: uppercase;\n color: var(--ink-soft); font-weight: 500;\n }\n .admin-card .v { font: 20px var(--av-font-display); color: var(--ink); }\n .admin-card .sub { font: 11px var(--av-font-mono); color: var(--ink-soft); margin-top: 4px; }\n .admin-card.alert { box-shadow: 0 0 0 1px var(--accent), var(--edge); }\n .admin-card.alert h3 { color: var(--accent); }\n .admin-card.warn { box-shadow: 0 0 0 1px #c47a1f, var(--edge); }\n .admin-card.warn h3 { color: #c47a1f; }\n .admin-section-head {\n font: 700 11px var(--av-font-mono);\n letter-spacing: 0.18em; text-transform: uppercase;\n color: var(--ink); margin: 24px 0 10px;\n border-left: 2px solid var(--accent-2); padding-left: 10px;\n }\n table.admin-tbl {\n width: 100%; border-collapse: collapse;\n font: 12px var(--av-font-mono);\n background: var(--paper); border-radius: 8px; overflow: hidden;\n box-shadow: var(--edge);\n }\n table.admin-tbl th, table.admin-tbl td {\n border-bottom: 1px solid var(--rule, #c8bfae);\n padding: 9px 12px; text-align: left;\n }\n table.admin-tbl tr:last-child th, table.admin-tbl tr:last-child td { border-bottom: 0; }\n table.admin-tbl th {\n color: var(--ink-soft); text-transform: uppercase;\n font-size: 9.5px; letter-spacing: 0.14em; font-weight: 500;\n }\n table.admin-tbl .pill {\n display: inline-block; padding: 1px 8px; border-radius: 999px;\n font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase;\n }\n table.admin-tbl .pill.active { background: rgba(90,122,58,.18); color: #4d6730; }\n table.admin-tbl .pill.inactive { background: rgba(168,52,29,.18); color: var(--accent); }\n table.admin-tbl .pill.failed { background: rgba(168,52,29,.18); color: var(--accent); }\n table.admin-tbl button.restart {\n font: 9.5px var(--av-font-mono);\n letter-spacing: 0.04em; text-transform: lowercase;\n padding: 4px 10px; border: 0; background: var(--paper-2);\n border-radius: 4px; cursor: pointer; color: var(--ink-soft);\n box-shadow: var(--raised);\n }\n table.admin-tbl button.restart:hover { color: var(--ink); }\n table.admin-tbl button.restart[disabled] { opacity: .5; cursor: wait; }\n .admin-logs-toolbar {\n display: flex; gap: 8px; align-items: center; margin-bottom: 14px;\n }\n .admin-logs-toolbar select, .admin-logs-toolbar input[type=\"number\"] {\n font: 12px var(--av-font-mono);\n padding: 6px 10px; border: 1px solid var(--rule, #c8bfae);\n border-radius: 4px; background: var(--paper); color: var(--ink);\n }\n .admin-logs-toolbar label {\n font: 9.5px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: uppercase; color: var(--ink-soft);\n }\n .admin-logs-pane {\n background: #1c1a14; color: #d7cba8;\n font: 12px/1.5 var(--av-font-mono);\n padding: 14px 16px; border-radius: 8px;\n max-height: 65cqh; overflow: auto; white-space: pre-wrap;\n }\n .admin-actions-grid {\n display: grid; gap: 12px;\n grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));\n }\n .admin-action {\n background: var(--paper); border-radius: 8px;\n padding: 14px 16px; box-shadow: var(--edge);\n }\n .admin-action h4 { margin: 0 0 6px; font: 15px var(--av-font-display); color: var(--ink); }\n .admin-action p { margin: 0 0 10px; font: 12.5px var(--av-font-display); color: var(--ink-soft); }\n .admin-action button.run {\n font: 9.5px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: lowercase; padding: 6px 12px; border: 0;\n background: var(--ink); color: var(--paper); border-radius: 999px;\n cursor: pointer;\n }\n .admin-action button.run[disabled] { opacity: .5; cursor: wait; }\n .admin-action .out {\n margin-top: 10px; font: 10.5px var(--av-font-mono);\n color: var(--ink-soft); white-space: pre-wrap;\n }\n .admin-action.deploy pre {\n background: #1c1a14; color: #d7cba8;\n font: 11px var(--av-font-mono);\n padding: 12px 14px; border-radius: 6px;\n overflow: auto; white-space: pre-wrap; word-break: break-all;\n margin: 0;\n }\n .admin-action.deploy button.copy {\n font: 9.5px var(--av-font-mono); letter-spacing: 0.06em;\n text-transform: lowercase; padding: 5px 10px;\n background: var(--paper-2); color: var(--ink-soft);\n border: 0; border-radius: 4px; cursor: pointer; box-shadow: var(--raised);\n margin-top: 8px;\n }\n .admin-action.deploy button.copy:hover { color: var(--ink); }\n .admin-unreachable {\n background: var(--paper); border-radius: 8px;\n padding: 14px 16px; color: var(--accent);\n font: 12px var(--av-font-mono);\n margin-bottom: 18px; box-shadow: 0 0 0 1px var(--accent), var(--edge);\n }\n /* When admin screen is shown, the slider underneath is hidden so it\n doesn't compete for scrolling. The static title gets swapped out\n too - admin is its own section with its own visual identity.\n Menu button + time-window pill stay visible (still useful). */\n .av-shell.admin-on #views { display: none; }\n .av-shell.admin-on .nav-band,\n .av-shell.admin-on #slider { visibility: hidden; }\n .av-shell.admin-on .static-head { opacity: 0; pointer-events: none; transition: opacity 200ms ease; }\n\n /* Mobile compact: drawer takes near-full width, admin pads tighter,\n section heads + cards downsize. */\n @container av-card (max-width: 700px) {\n #menu-dd { top: 50px; right: 8px; left: 8px; width: auto; padding: 10px; }\n .admin-screen { inset: 52px 0 60px 0; }\n .admin-frame { padding: 0 14px 32px; }\n .admin-title { margin-bottom: 18px; }\n .admin-title h1 { font-size: 22px; }\n .admin-grid { gap: 10px; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }\n .admin-card { padding: 12px 14px; }\n .admin-card .v { font-size: 18px; }\n .admin-section-head { margin: 18px 0 8px; font-size: 10px; letter-spacing: 0.12em; }\n table.admin-tbl th, table.admin-tbl td { padding: 7px 8px; font-size: 11px; }\n .admin-actions-grid { gap: 10px; grid-template-columns: 1fr; }\n .admin-action { padding: 12px 14px; }\n /* Slider track bottom nav: tuck closer to bottom edge */\n .slider { bottom: 14px; padding: 3px; }\n .slider button { font-size: 9.5px; padding: 6px 11px; }\n }\n @container av-card (max-width: 700px) {\n /* Detail-modal: full-screen on small screens. */\n #detail-modal { padding: 12px; }\n /* Extra top padding = a clear strip for the close button, so the X\n sits in its own corner instead of on the artwork. */\n .modal-card { padding: 44px 18px 18px; border-radius: 10px; }\n .modal-close { top: 10px; right: 10px; }\n .modal-info h2 { font-size: 22px; }\n .modal-img { height: 160px; padding: 10px; }\n .modal-grid { gap: 18px; }\n /* Keep the description short so the recordings are in reach - the\n full text is one tap away on the wiki link. */\n .modal-info .desc {\n font-size: 12px; line-height: 1.5;\n display: -webkit-box; -webkit-line-clamp: 4;\n -webkit-box-orient: vertical; overflow: hidden;\n cursor: pointer;\n }\n .modal-info .desc.expanded { -webkit-line-clamp: unset; display: block; }\n .rec-row .when { font-size: 10.5px; }\n .rec-row .when small { font-size: 8.5px; }\n }\n\n /* ============ Detail modal (atlas card expansion) ============\n Click a bird card -> this fades in over a soft ink backdrop with\n the full sketch, taxonomic info, Wikipedia summary, and a\n scrollable list of every past audio capture for the species. */\n /* /about - a brief explainer popup floated over the index page.\n Reuses .modal-backdrop / .modal-close; opened by the #about hash. */\n #about-modal {\n position: absolute; inset: 0; z-index: 60;\n display: flex; align-items: center; justify-content: center;\n padding: 24px;\n pointer-events: none; opacity: 0;\n transition: opacity 200ms ease;\n }\n #about-modal[aria-hidden=\"false\"] { pointer-events: auto; opacity: 1; }\n .about-card {\n position: relative;\n max-width: 432px; width: 100%;\n background: var(--paper);\n border-radius: 14px;\n box-shadow:\n inset 0 0 0 1px rgba(255,255,255,0.6),\n 0 30px 80px rgba(26,22,18,0.24);\n padding: 34px 34px 28px;\n transform: translateY(10px) scale(0.985);\n transition: transform 260ms cubic-bezier(.32,.72,.32,1);\n }\n #about-modal[aria-hidden=\"false\"] .about-card { transform: none; }\n .about-card .about-eyebrow {\n margin: 0 0 7px;\n font: italic 400 13px/1 var(--av-font-display);\n color: var(--ink-2); letter-spacing: 0.06em;\n }\n .about-card h2 {\n margin: 0 0 13px;\n font: 700 22px/1.25 var(--av-font-display);\n letter-spacing: 0.01em; color: var(--ink);\n }\n .about-card .about-body {\n margin: 0;\n font: 400 14.5px/1.62 var(--av-font-display);\n color: var(--ink-2);\n }\n /* Plain inline link - same ink as the prose, a simple underline, no\n hover treatment. */\n .about-card .about-body a {\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n .about-card .about-explore {\n margin-top: 20px;\n background: none; border: 0; padding: 0;\n font: 700 11px/1 var(--av-font-mono);\n letter-spacing: 0.08em; text-transform: lowercase;\n color: var(--ink-soft); cursor: pointer;\n transition: color 140ms ease;\n }\n .about-card .about-explore:hover { color: var(--ink); }\n #detail-modal {\n position: absolute; inset: 0; z-index: 50;\n display: flex; align-items: center; justify-content: center;\n padding: 24px;\n pointer-events: none; opacity: 0;\n /* One opacity fade for backdrop + card together; the card also\n morphs via transform (see morphModalOpen). Driven by .is-open so\n the fade can run *during* the close morph, not after it. */\n transition: opacity 260ms ease;\n }\n #detail-modal.is-open { pointer-events: auto; opacity: 1; }\n .modal-backdrop {\n position: absolute; inset: 0;\n background: rgba(26,22,18,0.32);\n backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);\n }\n .modal-card {\n position: relative;\n /* Cap to the overlay, NOT the viewport. #detail-modal is inset:0 with\n 24px padding, so 100% here == its content box == the old\n calc(100cqh - 48px) on a normal viewport - but robustly. In the card\n build 100cqh becomes 100cqh, and when Home Assistant gives the card an\n indefinite height (masonry / content-sized slots) Blink resolves cqh\n to 0, so calc(100cqh - 48px) collapsed to 0 and the modal shrank to a\n strip of padding (Chrome / HA companion WebView only; Firefox & Safari\n resolved it fine). % of the definite inset:0 overlay sidesteps that. */\n max-width: 920px; max-height: 100%;\n width: 100%;\n background: var(--paper);\n border-radius: 14px;\n /* A real border, not an inset shadow: borders paint outside the\n scrolling content box, so scrolled images/spectrograms can never\n slide over the card's outline. */\n border: 1px solid var(--hairline);\n box-shadow: 0 30px 80px rgba(26,22,18,0.22);\n padding: 30px 34px 26px;\n overflow: auto;\n /* The expand-from-card animation is driven by inline transform\n and opacity set on open/close - see openDetailModal. CSS\n transition handles the in-between glide. */\n transform-origin: 50% 50%;\n will-change: transform, opacity;\n }\n .modal-card.is-morphing {\n transition: transform 300ms cubic-bezier(.22,.61,.36,1);\n }\n .modal-close {\n position: absolute; top: 16px; right: 16px;\n background: var(--paper-2); border: 0;\n width: 30px; height: 30px; border-radius: 999px;\n font: 16px/1 var(--av-font-mono); color: var(--ink);\n box-shadow: var(--raised);\n cursor: pointer; z-index: 1;\n }\n .modal-close:hover { color: var(--accent-2); }\n .modal-grid {\n display: grid; grid-template-columns: 1fr 1.15fr; gap: 32px;\n align-items: start;\n }\n @container av-card (max-width: 700px) {\n .modal-grid { grid-template-columns: 1fr; gap: 22px; }\n .modal-card { padding: 22px; }\n }\n .modal-img {\n position: relative;\n background: var(--paper-2); border-radius: 10px;\n display: flex; align-items: center; justify-content: center;\n padding: 16px;\n /* Fixed frame: the illustration fits INSIDE it (object-fit) instead\n of the frame growing to the illustration's aspect. */\n height: 230px;\n box-shadow: var(--recess);\n }\n .modal-img img {\n max-width: 100%; max-height: 100%;\n width: auto; height: auto; object-fit: contain;\n filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10));\n transition: opacity 220ms ease;\n }\n .modal-img img.swapping { opacity: 0; }\n /* Pose toggle - bottom-left corner of the modal sketch, icon strip.\n Same recipe as the time-window slider: recessed paper-2 well, a\n white sliding pill behind the active button (via .seg-pill), and\n a color shift on the icon itself. */\n .pose-toggle {\n position: absolute; left: 10px; bottom: 10px;\n display: inline-flex; padding: 3px;\n background: var(--paper-2); border-radius: 999px;\n box-shadow: var(--recess);\n z-index: 2;\n }\n .pose-toggle button {\n background: transparent; border: 0; color: var(--ink-soft);\n width: 30px; height: 26px; padding: 0; border-radius: 999px;\n cursor: pointer; position: relative; z-index: 1;\n display: inline-flex; align-items: center; justify-content: center;\n transition: color 200ms ease;\n }\n .pose-toggle button svg { width: 13px; height: 13px; display: block; }\n .pose-toggle button:hover { color: var(--ink); }\n .pose-toggle button[aria-current=\"true\"] { color: var(--ink); }\n .pose-toggle button .tip {\n position: absolute; bottom: calc(100% + 6px); left: 50%;\n transform: translateX(-50%); white-space: nowrap;\n font: 9px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: lowercase; color: var(--ink-soft);\n background: var(--paper); padding: 3px 8px; border-radius: 4px;\n box-shadow: var(--raised);\n opacity: 0; pointer-events: none; transition: opacity 160ms ease;\n }\n .pose-toggle button:hover .tip { opacity: 1; }\n .pose-toggle[data-unavailable=\"true\"] { display: none; }\n .pose-toggle button[data-unavailable=\"true\"] { display: none; }\n .modal-info h2 {\n margin: 4px 36px 0 0;\n font: 700 26px/1.1 var(--av-font-display);\n color: var(--ink); letter-spacing: 0.01em;\n }\n .modal-info .sci {\n font: italic 13.5px/1.3 var(--av-font-display);\n color: var(--ink-2); margin: 4px 0 6px;\n }\n .modal-stats {\n display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;\n margin-bottom: 18px;\n }\n .modal-stats > div {\n background: var(--paper-2);\n border-radius: 6px; padding: 10px 12px;\n box-shadow: var(--recess);\n }\n .modal-stats .n {\n display: block;\n font: 800 17px/1 var(--av-font-display);\n color: var(--ink); font-variant-numeric: tabular-nums;\n }\n .modal-stats .lbl {\n display: block; margin-top: 4px;\n font: 8.5px var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.12em;\n text-transform: uppercase;\n }\n .modal-info .desc {\n font: 13px/1.55 var(--av-font-display);\n color: var(--ink); margin: 0 0 10px;\n }\n .modal-info .desc.placeholder { color: var(--ink-soft); font-style: italic; }\n .modal-meta {\n display: flex; flex-wrap: wrap; gap: 16px;\n font: 9.5px var(--av-font-mono); color: var(--ink-soft);\n margin: 0 0 14px;\n }\n .modal-meta .k { letter-spacing: 0.12em; text-transform: uppercase; }\n .modal-meta .v { color: var(--ink); margin-left: 6px; }\n .modal-meta .v.rare { color: var(--accent-2); }\n .modal-recordings {\n margin-top: 14px; padding-top: 14px;\n box-shadow: inset 0 1px 0 rgba(26,22,18,0.07);\n }\n .modal-recordings .rec-head {\n display: flex; align-items: baseline; justify-content: space-between;\n margin-bottom: 8px;\n }\n .modal-recordings h3 {\n font: 700 10px var(--av-font-mono);\n letter-spacing: 0.18em; text-transform: uppercase;\n color: var(--ink); margin: 0;\n border-left: 2px solid var(--accent-2); padding-left: 10px;\n }\n .modal-recordings .rec-count {\n font: 9px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.06em;\n }\n /* Reference-call (Xeno-Canto) button + its attribution line. */\n .modal-recordings .rec-head { gap: 8px; align-items: center; }\n .modal-recordings .rec-count { margin-left: auto; }\n .ref-call-btn {\n display: inline-flex; align-items: center; gap: 5px;\n font: 700 9px var(--av-font-mono); letter-spacing: 0.1em; text-transform: uppercase;\n color: var(--ink-soft); background: var(--paper); white-space: nowrap;\n border: 1px solid var(--hairline); border-radius: 999px;\n padding: 4px 11px 4px 9px; cursor: pointer;\n transition: color 160ms ease, background 160ms ease, border-color 160ms ease, transform 120ms ease;\n }\n .ref-call-btn svg { width: 9px; height: 9px; display: block; }\n .ref-call-btn:hover { color: var(--ink); background: var(--paper-2); transform: translateY(-1px); }\n .ref-call-btn[data-active=\"true\"] { color: var(--accent-2); border-color: var(--ink-soft); }\n .ref-call-btn.loading { opacity: 0.65; cursor: progress; }\n .ref-credit {\n font: 9px var(--av-font-mono); color: var(--ink-soft);\n letter-spacing: 0.04em; line-height: 1.55; margin: -2px 0 8px;\n }\n .ref-credit a { color: var(--ink-2); text-decoration: underline; text-underline-offset: 2px; }\n .ref-credit a:hover { color: var(--ink); }\n .modal-recordings ol {\n list-style: none; padding: 0; margin: 0;\n max-height: 220px; overflow-y: auto;\n display: flex; flex-direction: column; gap: 4px;\n padding-right: 4px;\n }\n .rec-row {\n display: grid; grid-template-columns: 30px 1fr auto auto; gap: 10px;\n align-items: center;\n padding: 6px 10px; border-radius: 6px;\n background: var(--paper);\n box-shadow: var(--edge);\n transition: background 160ms ease;\n }\n .rec-row:hover { background: var(--paper-2); }\n .rec-row.expanded { grid-template-columns: 30px 1fr auto; }\n /* Spectrogram strip - inline below the row header. Click on the row\n toggles open/closed independent of playback so you can scan\n spectrograms visually without listening. */\n .rec-spectro {\n grid-column: 1 / -1;\n margin-top: 8px;\n height: 0; overflow: hidden;\n border-radius: 4px;\n /* Paper background - the spectrogram canvas paints ink ON it,\n matching the sketch palette. */\n background: var(--paper-2);\n box-shadow: var(--recess);\n position: relative;\n transition: height 240ms cubic-bezier(.7,.05,.2,1);\n }\n .rec-row.expanded .rec-spectro { height: 88px; }\n /* The spectrogram is rendered client-side onto a canvas - we fetch\n the mp3, decode via Web Audio, run an STFT, and paint columns\n with our ink palette. No BirdNET-Pi PNG involved. */\n .rec-spectro canvas {\n position: absolute; inset: 0;\n width: 100%; height: 100%;\n display: block;\n opacity: 0; transition: opacity 240ms ease;\n }\n .rec-spectro canvas.ready { opacity: 1; }\n .rec-spectro .rec-spectro-loading {\n position: absolute; inset: 0;\n display: flex; align-items: center; justify-content: center;\n color: var(--ink-soft);\n font: 9px var(--av-font-mono); letter-spacing: 0.14em;\n text-transform: lowercase;\n pointer-events: none;\n }\n /* Played portion - soft ink wash over the paper ground. */\n .rec-spectro .rec-spectro-played {\n position: absolute; left: 0; top: 0; bottom: 0; width: 0%;\n background: linear-gradient(90deg, rgba(26,22,18,0.16), rgba(26,22,18,0.06));\n pointer-events: none;\n }\n /* Cursor - sliding paper pill with a raised shadow, same recipe as\n the time-window slider's seg-pill. Pops against both the bright\n paper regions of the spectrogram and the inked vocalization\n bands. */\n .rec-spectro .rec-spectro-cursor {\n position: absolute; top: 50%; left: 0;\n width: 14px; height: 20px;\n transform: translate(-50%, -50%);\n background: var(--paper);\n border-radius: 999px;\n box-shadow: 0 1px 3px rgba(26,22,18,0.30), inset 0 0 0 1px rgba(26,22,18,0.18);\n pointer-events: none;\n opacity: 0;\n }\n .rec-spectro .rec-spectro-cursor::after {\n content: '';\n position: absolute; left: 50%; top: -100%; bottom: -100%;\n width: 1.5px; transform: translateX(-50%);\n background: rgba(251,247,238,0.85);\n box-shadow: 0 0 0 0.5px rgba(26,22,18,0.20);\n }\n .rec-spectro.armed .rec-spectro-cursor { opacity: 1; }\n /* Dark mode: the spectrogram ground is charcoal (paintSpectrogram paints\n with --paper), so the dark played-veil and the --paper cursor pill would\n both vanish into it. Flip the veil light, and make the cursor a light\n pill so it still reads against the dark ground. */\n :host([data-theme=\"dark\"]) .rec-spectro .rec-spectro-played {\n background: linear-gradient(90deg, rgba(236,232,225,0.18), rgba(236,232,225,0.07));\n }\n :host([data-theme=\"dark\"]) .rec-spectro .rec-spectro-cursor {\n background: var(--ink);\n }\n .rec-spectro .rec-spectro-scrub {\n position: absolute; inset: 0;\n cursor: ew-resize;\n }\n .rec-row .play {\n background: var(--paper-2); border: 0; color: var(--ink);\n width: 24px; height: 24px; border-radius: 999px;\n display: inline-flex; align-items: center; justify-content: center;\n box-shadow: var(--raised); cursor: pointer;\n padding: 0;\n }\n .rec-row .play svg { width: 8px; height: 8px; }\n .rec-row .play[data-active=\"true\"] {\n background: var(--ink); color: var(--paper);\n }\n .rec-row .when {\n font: 11px/1.2 var(--av-font-mono); color: var(--ink);\n }\n .rec-row .when small {\n display: block; font-size: 9px; color: var(--ink-soft);\n letter-spacing: 0.06em; margin-top: 1px;\n }\n .rec-row .conf {\n font: 10px var(--av-font-mono);\n color: var(--ink-soft); letter-spacing: 0.06em;\n font-variant-numeric: tabular-nums;\n }\n .rec-empty {\n padding: 14px;\n font: 11px/1.5 var(--av-font-display); color: var(--ink-soft);\n text-align: center;\n }\n .modal-actions {\n display: flex; gap: 8px; margin-top: 18px;\n }\n .modal-actions .chip {\n flex: 0 0 auto;\n background: var(--paper-2);\n border: 0; color: var(--ink);\n padding: 7px 12px;\n border-radius: 4px;\n font: 10px/1 var(--av-font-mono);\n text-transform: lowercase; letter-spacing: 0.12em;\n text-decoration: none;\n display: inline-flex; align-items: center; gap: 5px;\n box-shadow: var(--raised);\n transition: transform 160ms ease;\n }\n .modal-actions .chip:hover { transform: translateY(-1px); }\n .modal-actions .chip.ext::after { content: '↗'; opacity: 0.6; }\n\n /* Spectrogram appears on first audio play. Recoloured via CSS filter\n so BirdNET-Pi's default colour spectrogram comes out as a clean\n monochrome ink strip, matching the rest of the chart. Click to\n replay; a thin progress bar tracks playback in real time. */\n .bird-card .spectro-wrap {\n position: relative;\n min-height: 0; margin: 6px 0 2px;\n cursor: pointer;\n overflow: hidden;\n border-radius: 3px;\n box-shadow: var(--recess);\n }\n .bird-card .spectro-wrap:empty { display: none; }\n /* Canvas is painted client-side in the active theme's paper/ink palette\n (paintSpectrogram reads data-theme), so it follows light/dark mode with\n no CSS filter - same renderer as the modal recording spectrograms. */\n .bird-card .spectro-wrap canvas {\n width: 100%; height: 44px; display: block;\n }\n /* Progress wash: a translucent veil that grows as the clip plays. Ink in\n light mode; in dark mode a light veil so it stays visible over the\n charcoal spectrogram ground. */\n .bird-card .spectro-wrap::after {\n content: '';\n position: absolute; top: 0; bottom: 0; left: 0;\n width: var(--prog, 0%);\n background: linear-gradient(to right, rgba(26,22,18,0.30), rgba(26,22,18,0.05));\n pointer-events: none;\n transition: width 60ms linear;\n }\n :host([data-theme=\"dark\"]) .bird-card .spectro-wrap::after {\n background: linear-gradient(to right, rgba(236,232,225,0.26), rgba(236,232,225,0.05));\n }\n /* Playhead - thin vertical line at the leading edge */\n .bird-card .spectro-wrap::before {\n content: '';\n position: absolute; top: 0; bottom: 0;\n left: var(--prog, 0%);\n width: 1px;\n background: var(--ink);\n opacity: var(--playhead, 0);\n pointer-events: none;\n transition: opacity 200ms ease;\n }\n .bird-card[data-playing=\"true\"] .spectro-wrap::before { --playhead: 0.6; }\n\n/* ============ AvianVisitors mode toggle ============\n * Default install (.av-shell.av-local): hide the lock-row, show drawer items\n * directly when the menu opens. The PHP shim at /avian/api/menu.php\n * returns items immediately with no auth, so the live JS skips the lock\n * flow entirely.\n *\n * Forwarded install (body.av-forwarded): show the lock row so users\n * type the basic-auth password before items unlock. Pair with\n * AV_REQUIRE_AUTH=1 in php-fpm env + Caddy basic_auth on /avian/api/.\n * See avian/forwarding/. */\n.av-shell.av-local #dd-locked { display: none; }\n.av-shell.av-local #dd-items { display: block; }\n\n/* ============ Wall-display widgets (HA build) ============\n Clock + weather as a quiet corner block INSIDE the collage view.\n renderCollage() stamps this element's box into the silhouette-packing\n grid, so when the flock grows the birds nest around the numerals the\n same way they nest around each other. Same editorial recipe as the\n rest of the page: a serif display line over a monospace, letterspaced\n micro-caption, on the paper/ink variables so dark theme inverts free.\n pointer-events:none keeps hover/click hit-testing on the birds. */\n.wall-widgets {\n position: absolute; z-index: 6;\n display: flex; flex-direction: column; gap: 10px;\n pointer-events: none;\n}\n.wall-widgets[hidden] { display: none; }\n.wall-widgets[data-corner=\"bottom-right\"] { right: 36px; bottom: 96px; align-items: flex-end; text-align: right; }\n.wall-widgets[data-corner=\"bottom-left\"] { left: 36px; bottom: 96px; align-items: flex-start; text-align: left; }\n.wall-widgets[data-corner=\"top-right\"] { right: 36px; top: 14px; align-items: flex-end; text-align: right; }\n.wall-widgets[data-corner=\"top-left\"] { left: 36px; top: 14px; align-items: flex-start; text-align: left; }\n.ww-block { display: flex; flex-direction: column; gap: 3px; }\n.wall-widgets[data-corner$=\"right\"] .ww-block { align-items: flex-end; }\n.wall-widgets[data-corner$=\"left\"] .ww-block { align-items: flex-start; }\n.ww-block[hidden] { display: none; }\n.ww-big {\n font: 600 clamp(26px, 3cqw, 40px)/1 var(--av-font-display);\n color: var(--ink);\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.01em;\n}\n.ww-cap {\n font: 9.5px/1.45 var(--av-font-mono);\n letter-spacing: 0.16em; text-transform: uppercase;\n color: var(--ink-soft);\n}\n.ww-cap:empty { display: none; }\n.ww-rule {\n width: 44px; height: 1px;\n background: var(--hairline);\n}\n.ww-rule[hidden] { display: none; }\n@container av-card (max-width: 700px) {\n .wall-widgets { gap: 8px; }\n .wall-widgets[data-corner$=\"right\"] { right: 18px; }\n .wall-widgets[data-corner$=\"left\"] { left: 18px; }\n .wall-widgets[data-corner^=\"bottom\"] { bottom: 92px; }\n .ww-big { font-size: 20px; }\n .ww-cap { font-size: 8px; letter-spacing: 0.1em; }\n /* The sunrise/sunset caption is the first thing to go on a phone. */\n #wwSun { display: none; }\n}\n/* Kiosk cursor hiding - applied to by apt.js after idle when the\n wall options ask for it. */\n.av-shell.ww-cursor-hidden, .av-shell.ww-cursor-hidden * { cursor: none !important; }\n\n/* ---- Card build: configured title floats over the collage ---- */\n/* When a card sets a custom title, the heading is lifted out of the\n flex flow and overlaid on the collage top; renderCollage stamps the\n h1's box into the packing grid so birds nest around the words,\n clock-style. Stats/atlas keep headroom so their content doesn't\n slide under it. */\n.av-shell.av-title-overlay .static-head {\n position: absolute; top: 6px; left: 0; right: 0; z-index: 6;\n pointer-events: none; padding: 26px 32px 0;\n}\n.av-shell.av-title-overlay .view#v1,\n.av-shell.av-title-overlay .view#v2 { padding-top: 96px; }\n\n/* Card option: view picker pinned to the top edge instead of bottom. */\n.av-shell.av-picker-top .slider { top: 10px; bottom: auto; }\n.av-shell.av-picker-top .view { padding-bottom: 28px; }\n.av-shell.av-picker-top .view#v0, .av-shell.av-picker-top .view#v1 { padding-bottom: 24px; }\n\n/* ---- Recording-row review flag ---- */\n/* Writes a false-positive review back to BirdNET-Go. A quiet ghost x\n beside the confidence; first tap arms it into a labelled pill, second\n tap confirms - a stray touch can't flag anything. */\n.rec-row .flag {\n justify-self: end;\n min-width: 22px; height: 22px;\n display: inline-flex; align-items: center; justify-content: center;\n background: transparent; border: 0; border-radius: 999px;\n font: 10px/1 var(--av-font-mono); letter-spacing: 0;\n color: var(--ink-soft);\n padding: 0 6px; cursor: pointer; white-space: nowrap;\n transition: color 140ms ease, background 140ms ease;\n}\n.rec-row .flag:hover { color: var(--ink); background: var(--paper-2); }\n.rec-row .flag[data-state=\"armed\"] {\n background: var(--accent-2); color: var(--paper);\n font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase;\n padding: 0 9px;\n}\n.rec-row .flag[data-state=\"done\"] { color: var(--ink); cursor: default; }\n.rec-row .flag[data-state=\"failed\"] {\n color: var(--accent-2); font-weight: 700;\n font-size: 8.5px; letter-spacing: 0.06em; text-transform: uppercase;\n}\n.rec-row .flag:disabled { cursor: default; }\n.rec-row.flagged .when, .rec-row.flagged .conf { text-decoration: line-through; opacity: 0.55; }\n\n/* ---- card-build additions ---- */\n:host {\n display: block; position: relative;\n width: 100%; height: 100%; min-height: 560px;\n overflow: hidden;\n border-radius: var(--ha-card-border-radius, 12px);\n /* The card box is the responsive container all @container rules and\n cq units resolve against. */\n container: av-card / size;\n}\n.av-shell { position: absolute; inset: 0; overflow: hidden; }\n/* Card default: transparent, so the collage sits directly on the HA\n dashboard. background: paper restores the page's warm ground. */\n.av-shell { background: transparent; }\n.av-shell.av-bg-paper { background: var(--paper); }\n/* font: system swaps the editorial serif/mono pairing for HA's own\n typeface (set per-theme by HA; Roboto stock). */\n.av-shell.av-font-system {\n --av-font-display: var(--ha-font-family-body, var(--mdc-typography-font-family, Roboto, \"Segoe UI\", sans-serif));\n --av-font-mono: var(--ha-font-family-body, var(--mdc-typography-font-family, Roboto, \"Segoe UI\", sans-serif));\n}\n/* The view picker hugs the card's bottom edge (the page floats it higher). */\n.slider { bottom: 10px; }\n/* The collage owns the WHOLE card - no reserved band for the picker\n (it's stamped into the packing grid as an obstacle instead), so the\n flock centres in the true middle of the card. */\n.view#v0 { padding: 12px 14px; }\n/* Clock/weather sit a finger's width off the card's true corners. */\n.wall-widgets[data-corner$=\"right\"] { right: 16px; }\n.wall-widgets[data-corner$=\"left\"] { left: 16px; }\n.wall-widgets[data-corner^=\"top\"] { top: 12px; }\n.wall-widgets[data-corner^=\"bottom\"] { bottom: 14px; }\n/* In system-font mode the empty state reads as a normal sentence in the\n configured font instead of editorial letterspaced small-caps. */\n.av-shell.av-font-system .empty {\n font: 14px/1.5 var(--av-font-mono);\n text-transform: none; letter-spacing: 0;\n}\n/* In system-font mode the picker drops the editorial small-caps treatment\n and reads like native HA tabs. */\n.av-shell.av-font-system .slider button {\n font: 500 13px/1 var(--av-font-mono);\n letter-spacing: 0; text-transform: capitalize;\n padding: 9px 16px;\n}\n/* BirdNET-Pi admin chrome has no backend here (same as the static HA build). */\n#menuBtn, #menu-dd, #returnToAtlas, #adminScreen { display: none !important; }\n";
// Generated by avian/scripts/build_masks.py - DO NOT EDIT BY HAND.
// DIMS[slug] = [w, h]; MASKS[slug] = { w, h, bits } 1-bit silhouettes
@@ -5080,7 +5080,7 @@ function runHABirdApp(__root, __shell, __cardConfig, __imgBase) {
// you copied the artwork locally (homeassistant/install.sh layout).
var HABIRD_CDN_ASSETS = 'https://cdn.jsdelivr.net/gh/adamoberley/HABirdDashboard@HABirdDashboard/avian/assets/';
-var HABIRD_VERSION = '1.2.6';
+var HABIRD_VERSION = '1.2.7';
var HABIRD_EDITOR_SCHEMA = [
{ name: 'dashboard', type: 'expandable', flatten: true, title: 'Dashboard', expanded: true, schema: [
diff --git a/homeassistant/card/build.js b/homeassistant/card/build.js
index ec35f8a9..f728c51e 100644
--- a/homeassistant/card/build.js
+++ b/homeassistant/card/build.js
@@ -197,7 +197,7 @@ const wrapper = `
// you copied the artwork locally (homeassistant/install.sh layout).
var HABIRD_CDN_ASSETS = 'https://cdn.jsdelivr.net/gh/adamoberley/HABirdDashboard@HABirdDashboard/avian/assets/';
-var HABIRD_VERSION = '1.2.6';
+var HABIRD_VERSION = '1.2.7';
var HABIRD_EDITOR_SCHEMA = [
{ name: 'dashboard', type: 'expandable', flatten: true, title: 'Dashboard', expanded: true, schema: [
diff --git a/homeassistant/www/styles.css b/homeassistant/www/styles.css
index e51cfaab..1d33889e 100644
--- a/homeassistant/www/styles.css
+++ b/homeassistant/www/styles.css
@@ -1276,7 +1276,15 @@
}
.modal-card {
position: relative;
- max-width: 920px; max-height: calc(100vh - 48px);
+ /* Cap to the overlay, NOT the viewport. #detail-modal is inset:0 with
+ 24px padding, so 100% here == its content box == the old
+ calc(100vh - 48px) on a normal viewport - but robustly. In the card
+ build 100vh becomes 100cqh, and when Home Assistant gives the card an
+ indefinite height (masonry / content-sized slots) Blink resolves cqh
+ to 0, so calc(100cqh - 48px) collapsed to 0 and the modal shrank to a
+ strip of padding (Chrome / HA companion WebView only; Firefox & Safari
+ resolved it fine). % of the definite inset:0 overlay sidesteps that. */
+ max-width: 920px; max-height: 100%;
width: 100%;
background: var(--paper);
border-radius: 14px;
diff --git a/package-lock.json b/package-lock.json
index fdedac2c..3d03f1f6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "habird-dashboard",
- "version": "1.2.6",
+ "version": "1.2.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "habird-dashboard",
- "version": "1.2.6",
+ "version": "1.2.7",
"license": "CC-BY-NC-SA-4.0",
"devDependencies": {
"jsdom": "^24.0.0"
diff --git a/package.json b/package.json
index 37f0594f..69d11b46 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "habird-dashboard",
- "version": "1.2.6",
+ "version": "1.2.7",
"private": true,
"description": "Bird Card - a live bird collage card for Home Assistant, fed by BirdNET-Go",
"license": "CC-BY-NC-SA-4.0",