/* Avian Visitors — Helsinki. Collage-only stylesheet, ported from the
   original AvianVisitors project's frontend/styles.css. Only the rules the
   collage container/tiles/labels/empty-state actually use are kept — the
   stats/atlas/admin/menu/stamp-batch styles from the original are all
   out of scope and dropped. Base page chrome (header/status line) is a
   trimmed-down version of the original's .stage/.static-head, since the
   window-picker/menu/theme-toggle UI they supported isn't part of this
   app. */

:root {
  --paper:    #fcfcfb;
  --paper-2:  #f3f2ee;
  --paper-3:  #e8e6df;
  --ink:      #1a1612;
  --ink-2:    #4a3f31;
  --ink-soft: #908576;
  --accent:   #4a3f31;
  --accent-2: #1a1612;
  --hairline: rgba(26,22,18,0.14);
  --pill:     #fcfcfb;
  /* Depth-shadow recipes, verbatim from the original — replace flat 1px
     borders with a soft inset/outset "pressed paper" look used throughout
     the pill controls (window-pick, slider, atlas-sort, chart-pick). */
  --edge:    inset 0 0 0 1px rgba(255,255,255,0.55), 0 0 0 1px rgba(26,22,18,0.04);
  --edge-lg: inset 0 0 0 1px rgba(255,255,255,0.6), 0 0 0 1px rgba(26,22,18,0.06), 0 8px 28px rgba(26,22,18,0.06);
  --recess:  inset 0 1px 2px rgba(26,22,18,0.06), inset 0 0 0 1px rgba(26,22,18,0.04);
  --raised:  inset 0 0 0 1px rgba(255,255,255,0.8), 0 0 0 1px rgba(26,22,18,0.05), 0 1px 2px rgba(26,22,18,0.07);
}

/* Dark palette applies either when the OS prefers it (and the user hasn't
   explicitly forced light), or when the user has explicitly picked dark
   via the About panel's theme toggle — see theme.js. Same token values,
   just reachable two ways. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper:    #17181c;
    --paper-2:  #212329;
    --paper-3:  #2c2f36;
    --ink:      #ece8e1;
    --ink-2:    #b7afa2;
    --ink-soft: #837c70;
    --accent:   #d8d2c6;
    --accent-2: #ece8e1;
    --hairline: rgba(236,232,225,0.20);
    --pill:     #3a3e48;
    --edge:    inset 0 0 0 1px rgba(255,255,255,0.05), 0 0 0 1px rgba(0,0,0,0.4);
    --edge-lg: inset 0 0 0 1px rgba(255,255,255,0.06), 0 0 0 1px rgba(0,0,0,0.5), 0 10px 30px rgba(0,0,0,0.5);
    --recess:  inset 0 1px 2px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,255,255,0.04);
    --raised:  inset 0 0 0 1px rgba(255,255,255,0.07), 0 0 0 1px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.5);
  }
}
:root[data-theme="dark"] {
  --paper:    #17181c;
  --paper-2:  #212329;
  --paper-3:  #2c2f36;
  --ink:      #ece8e1;
  --ink-2:    #b7afa2;
  --ink-soft: #837c70;
  --accent:   #d8d2c6;
  --accent-2: #ece8e1;
  --hairline: rgba(236,232,225,0.20);
  --pill:     #3a3e48;
  --edge:    inset 0 0 0 1px rgba(255,255,255,0.05), 0 0 0 1px rgba(0,0,0,0.4);
  --edge-lg: inset 0 0 0 1px rgba(255,255,255,0.06), 0 0 0 1px rgba(0,0,0,0.5), 0 10px 30px rgba(0,0,0,0.5);
  --recess:  inset 0 1px 2px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,255,255,0.04);
  --raised:  inset 0 0 0 1px rgba(255,255,255,0.07), 0 0 0 1px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.5);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; background: var(--paper); color: var(--ink); }
body {
  font-family: ui-serif, "Iowan Old Style", "Apple Garamond", Georgia, serif;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

/* ============ Page shell ============
   .stage is a fixed-inset flex column with a static title block on top;
   .top (window-pick + menu) and .slider (the view switcher) are both
   `position: fixed` chrome layered above it — matching the original's
   layout exactly (see docs/... reference screenshot and styles.css's
   `.top`/`.slider` rules, ported closely below). */
.stage {
  position: fixed; inset: 0; overflow: hidden;
  display: flex; flex-direction: column;
}
.static-head {
  flex: 0 0 auto;
  padding: 84px 32px 18px;
  text-align: center; position: relative; z-index: 5;
}
.static-head .pre {
  display: inline-block;
  background: none; border: 0; padding: 0 0 1px; margin: 0 0 6px;
  font: italic 400 clamp(13px, 1.4vw, 18px)/1.22 ui-serif, "Iowan Old Style", Georgia, serif;
  color: var(--ink-2); letter-spacing: 0.06em; cursor: pointer;
  transition: color 140ms ease;
}
.static-head .pre:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.static-head h1 {
  margin: 0; padding: 0;
  font: 700 clamp(24px, 3.2vw, 40px)/1 ui-serif, "Iowan Old Style", "Bookman Old Style", Georgia, serif;
  letter-spacing: 0.06em; color: var(--ink); text-transform: uppercase;
}
.collage-status {
  margin: 10px 0 0;
  font: 11px ui-monospace, "SF Mono", Menlo, monospace;
  color: var(--ink-soft); letter-spacing: 0.04em;
}
.collage-status a {
  color: var(--ink-soft); text-decoration: underline; text-underline-offset: 2px;
  text-decoration-color: var(--hairline);
}
.collage-status a:hover { color: var(--ink); text-decoration-color: currentColor; }

/* ============ Top bar — window picker (left) + menu (right) ============
   Ported closely from the original's `.top`/`.window-pick`/`.menu-btn`:
   fixed, full-width, pointer-events split so only the two controls (not
   the gap between them) are clickable. */
:root { --ctl-h: 36px; }
.top {
  position: fixed; top: 22px; left: 0; right: 0;
  display: flex; justify-content: space-between; align-items: center;
  padding: 0 28px; z-index: 30; pointer-events: none;
}
.top > * { pointer-events: auto; }
.top-right { display: flex; align-items: center; gap: 8px; }
.window-pick {
  display: inline-flex; gap: 0; padding: 4px; position: relative;
  background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess);
  min-height: var(--ctl-h); box-sizing: border-box; align-items: center;
}
.window-pick button {
  background: transparent; border: 0; color: var(--ink-soft);
  font: 10px/1 ui-monospace, Menlo, monospace; letter-spacing: 0.18em;
  padding: 9px 16px; border-radius: 999px; cursor: pointer; text-transform: uppercase;
  transition: color 200ms ease; position: relative; z-index: 1;
}
.window-pick button:hover, .window-pick button[aria-current="true"] { color: var(--ink); }
.menu-btn {
  background: var(--paper); border: 0; color: var(--ink);
  font: 11px/1 ui-monospace, Menlo, monospace; padding: 0 14px;
  border-radius: 999px; cursor: pointer; letter-spacing: 0.18em; text-transform: lowercase;
  box-shadow: var(--raised); transition: transform 160ms ease;
  min-height: var(--ctl-h); box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
}
.menu-btn:hover { transform: translateY(-1px); }
.menu-btn:active { transform: translateY(0); }

/* Quick theme toggle — a single header click cycles auto -> light -> dark
   -> auto (see theme.js); the fuller 3-way picker stays in the About
   panel too, for anyone who wants to jump straight to a mode instead of
   cycling through it. Icon reflects the currently-resolved appearance,
   not the raw preference (the tooltip shows that instead). */
.theme-quick-btn {
  background: var(--paper); border: 0; color: var(--ink); position: relative;
  width: var(--ctl-h); height: var(--ctl-h); border-radius: 999px; cursor: pointer;
  box-shadow: var(--raised); transition: transform 160ms ease;
  display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box;
}
.theme-quick-btn:hover { transform: translateY(-1px); }
.theme-quick-btn:active { transform: translateY(0); }
.theme-quick-btn svg { width: 15px; height: 15px; position: absolute; transition: opacity 200ms ease, transform 200ms ease; }
.theme-icon-sun { opacity: 1; transform: scale(1) rotate(0deg); }
.theme-icon-moon { opacity: 0; transform: scale(.6) rotate(-20deg); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-icon-sun { opacity: 0; transform: scale(.6) rotate(20deg); }
  :root:not([data-theme="light"]) .theme-icon-moon { opacity: 1; transform: scale(1) rotate(0deg); }
}
:root[data-theme="dark"] .theme-icon-sun { opacity: 0; transform: scale(.6) rotate(20deg); }
:root[data-theme="dark"] .theme-icon-moon { opacity: 1; transform: scale(1) rotate(0deg); }
@media (max-width: 700px) {
  :root { --ctl-h: 27px; }
  .top { top: 12px; padding: 0 12px; }
  .window-pick { padding: 3px; }
  .window-pick button { font-size: 8.5px; padding: 6px 10px; letter-spacing: 0.12em; }
  .menu-btn { font-size: 9px; padding: 0 11px; letter-spacing: 0.14em; }
  .static-head { padding: 70px 18px 18px; }
}

/* Shared sliding pill — JS animates transform+width to the active
   button's rect. Ported verbatim from the original's `.seg-pill`. */
.seg-pill {
  position: absolute; top: 4px; bottom: 4px; left: 0;
  background: var(--pill); border-radius: 999px; box-shadow: var(--raised);
  transition: transform 320ms cubic-bezier(.7,.05,.2,1), width 320ms cubic-bezier(.7,.05,.2,1);
  will-change: transform, width; pointer-events: none; z-index: 0;
}

/* ============ Bottom slider — the three-view switcher ============ */
.slider {
  position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 0; padding: 4px; z-index: 30;
  min-height: 40px; box-sizing: border-box;
  background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess);
}
.slider button {
  background: transparent; border: 0; color: var(--ink-soft);
  font: 10px/1 ui-monospace, Menlo, monospace; letter-spacing: 0.2em;
  padding: 11px 24px; border-radius: 999px; cursor: pointer; text-transform: uppercase;
  transition: color 200ms ease; position: relative; z-index: 1;
}
.slider button:hover, .slider button[aria-current="true"] { color: var(--ink); }

/* ============ Views — horizontal slider, one panel per top-level view ===
   Matches the original's structure: three `.view` panels inside `.views`,
   which translateX()s between them (JS sets the transform) instead of
   toggling `hidden` — the actual "three separate top-level views" the
   previous pass's two-tab merge didn't have. */
.views {
  flex: 1 1 auto; min-height: 0; position: relative; display: flex;
  transition: transform 480ms cubic-bezier(.7,.05,.2,1);
}
.view {
  flex: 0 0 100%; height: 100%; min-width: 0;
  overflow-y: auto; overflow-x: hidden;
  padding: 8px 32px 100px;
}
.view#v0 { display: flex; } /* collage centers itself */
@media (max-width: 700px) { .view { padding: 8px 16px 100px; } }

/* ============ View 1 - Collage (Galliformes-style poster) ============
   Viewport-fit, no scroll: fills the remaining space beneath the header. */
.gcollage {
  flex: 1 1 auto; width: 100%; max-width: 1300px; margin: 0 auto;
  position: relative; min-height: 0;
  transform-origin: 50% 48%;
}

/* Centre-out bloom: each tile fades + scales in; JS staggers the
   animation-delay by distance from the cluster centre. backwards fill-mode
   holds the hidden start state through the delay. */
@keyframes gtile-in {
  from { opacity: 0; transform: scale(0.82); }
  to   { opacity: 1; transform: scale(1); }
}
.gtile.entering {
  animation: gtile-in 420ms cubic-bezier(.2,.7,.3,1) backwards;
}
@media (prefers-reduced-motion: reduce) {
  .gtile.entering { animation: none; }
}

.gtile {
  position: absolute;
  background: transparent; border: 0; padding: 0; margin: 0;
  overflow: visible; /* let drop-shadow extend past the bbox */
  transition: transform 130ms ease;
  /* Events are handled at the .gcollage level via alpha-mask hit-testing -
     the tile rectangles must not intercept them. */
  pointer-events: none;
}
/* Hover state is driven by JS alpha-mask hit-testing (.is-hover), NOT the
   CSS :hover pseudo - the rectangular tile bounding boxes overlap, so
   :hover would light up the wrong bird. */
.gtile.is-hover { transform: scale(1.04); z-index: 3; }
.gtile img {
  width: 100%; height: 100%; object-fit: contain; display: block;
  filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10));
  transition: filter 130ms ease;
}
.gtile.is-hover img {
  filter: drop-shadow(0 3px 10px rgba(26,22,18,0.26)) saturate(1.1);
}

/* Hover pill - surfaces the windowed count for the bird under the cursor.
   Only ever shown when labels are off (?labels=0), since with labels on
   every bird already wears its name. */
.collage-tip {
  position: absolute; left: 50%; bottom: 14px;
  transform: translateX(-50%);
  padding: 6px 14px;
  background: var(--paper);
  border-radius: 999px;
  box-shadow: 0 6px 18px rgba(26,22,18,0.12),
              inset 0 0 0 1px rgba(255,255,255,0.6);
  font: italic 13px/1.3 ui-serif, "Iowan Old Style", Georgia, serif;
  color: var(--ink);
  letter-spacing: 0.02em;
  opacity: 0; pointer-events: none;
  transition: opacity 150ms ease;
  white-space: nowrap;
  z-index: 4;
}
.collage-tip[aria-hidden="false"] { opacity: 1; }
.collage-tip .ct-name { font-style: normal; font-weight: 600; }
.collage-tip .ct-n { font-style: normal; font-weight: 700; }
.collage-tip .ct-w { color: var(--ink-soft); }

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

/* No-birds state: an empty-nest illustration sits where the collage would
   be, with the status line beneath it. */
.empty-nest {
  height: 100%;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 28px;
}
.empty-nest .nest-img { width: 78%; max-width: 470px; height: auto; }
.empty-nest .empty { padding: 0; margin: 0; }
.empty-nest.entering .nest-img {
  animation: gtile-in 420ms cubic-bezier(.2, .7, .3, 1) backwards;
}
.empty-nest.entering .empty {
  animation: gtile-in 420ms cubic-bezier(.2, .7, .3, 1) 130ms backwards;
}
@media (prefers-reduced-motion: reduce) {
  .empty-nest.entering .nest-img,
  .empty-nest.entering .empty { animation: none; }
}

/* ============ Collage labels ============ */
/* Handwritten face (Caveat, OFL - license alongside the font file).
   Loaded lazily: nothing references 'Hand' until labels are on. */
@font-face {
  font-family: 'Hand';
  src: url('./fonts/Caveat.ttf') format('truetype');
  font-weight: 400 700;
  font-display: swap;
}
.gtile-label {
  position: absolute;
  overflow: visible; pointer-events: none;
}
.gtile-label text {
  fill: var(--ink-soft);
  /* Paper-colored halo behind the ink fill: labels sit directly on top of
     whatever's drawn underneath (the bird's own silhouette, per the
     nesting design), not a fixed background, so a plain --ink-soft fill
     goes low-contrast/near-invisible whenever the art underneath happens
     to be a similar warm-brown tone - the placeholder tile art (currently
     most tiles, since most species have no real illustration yet) is
     exactly that tone. The stroke keeps the same "ink brush" look while
     staying legible regardless of what's underneath. */
  paint-order: stroke fill;
  stroke: var(--paper);
  stroke-width: 3px;
  stroke-linejoin: round;
  filter: url(#lbl-ink-m);
}
.gtile-label text[data-ink="s"] { filter: url(#lbl-ink-s); }
.gtile-label text[data-ink="l"] { filter: url(#lbl-ink-l); }
.gtile-label-defs { position: absolute; width: 0; height: 0; overflow: hidden; }


/* ============ Stats view ============
   `.stats-grid` layout (two-column: chart + side panel, rhythm full-width
   below) ported closely from the original's own `.stats-grid` rule —
   same grid-template-columns ratio, same max-width, same gap. */
.stats-date-nav {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  max-width: 1100px; margin: 0 auto 18px; padding: 0 4px;
}
/* :not(.day-nav-today) — the "back to today" button is a differently-
   shaped pill, not an icon button, and needs to stay [hidden]-able; this
   rule would otherwise win the cascade over [hidden] by declaring
   `display` at the same specificity. */
.stats-date-nav button:not(.day-nav-today) {
  background: transparent; border: 0; color: var(--ink-soft);
  width: 22px; height: 22px; padding: 0; border-radius: 999px;
  cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  transition: color 160ms ease, background 160ms ease;
}
.stats-date-nav button:not(.day-nav-today):hover { color: var(--ink); background: var(--paper-2); }
.stats-date-nav button:disabled { opacity: 0.3; cursor: default; }
.stats-date-nav button:disabled:hover { background: transparent; }
.stats-date-nav button svg { width: 11px; height: 11px; }
.stats-date-label {
  font: 700 11px/1 ui-monospace, Menlo, monospace; color: var(--ink);
  letter-spacing: 0.08em; text-transform: uppercase; min-width: 11em; text-align: center;
}
.day-nav-today {
  font: 10px ui-monospace, Menlo, monospace; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-soft); background: var(--paper-2); border: 0; border-radius: 999px;
  padding: 5px 12px; cursor: pointer;
}
.day-nav-today:hover { color: var(--ink); }

.stats-grid {
  position: relative;
  flex: 1 1 auto; min-height: 0;
  display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 60px;
  max-width: 1100px; margin: 0 auto; width: 100%;
  align-items: stretch;
}
@media (max-width: 900px) {
  .stats-grid { grid-template-columns: 1fr; gap: 36px; }
}
.stats-error, .stats-placeholder {
  color: var(--ink-soft); font: 12px ui-monospace, Menlo, monospace;
  padding: 20px 0;
}

/* -- Chart area: timeline / hourly heatmap toggle -- */
.stats-chart { position: relative; display: flex; flex-direction: column; min-width: 0; height: clamp(280px, 46vh, 430px); }
.chart-pick {
  align-self: flex-end;
  display: inline-flex; padding: 3px; position: relative; margin-bottom: 8px;
  background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess);
}
.chart-pick button {
  background: transparent; border: 0; color: var(--ink-soft);
  width: 28px; height: 24px; padding: 0; border-radius: 999px;
  cursor: pointer; position: relative; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color 200ms ease;
}
.chart-pick button svg { width: 13px; height: 13px; }
.chart-pick button:hover, .chart-pick button[aria-current="true"] { color: var(--ink); }

/* Shared icon-button tooltip — used by .chart-pick, (in the postcard
   modal) .postcard-pose-toggle, and the header's .theme-quick-btn. */
.chart-pick .tip, .postcard-pose-toggle .tip, .theme-quick-btn .tip {
  position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  white-space: nowrap; font: 9px ui-monospace, Menlo, monospace; letter-spacing: 0.1em;
  text-transform: lowercase; color: var(--ink-soft);
  background: var(--paper); padding: 3px 8px; border-radius: 4px; box-shadow: var(--raised);
  opacity: 0; pointer-events: none; transition: opacity 160ms ease; z-index: 2;
}
.chart-pick button:hover .tip, .postcard-pose-toggle button:hover .tip, .theme-quick-btn:hover .tip { opacity: 1; }
/* The pose toggle sits at the bottom-left corner of the art frame, and the
   postcard sheet clips overflow — a downward tooltip there renders past the
   sheet's edge and is invisible. Point it up instead, matching the original. */
.postcard-pose-toggle .tip { top: auto; bottom: calc(100% + 6px); }

/* Editorial detection timeline — one column per species, black square's
   height encodes count, rotated two-line label below. Ported closely from
   the original's `.stats-tl-*` rules. */
.stats-timeline { position: relative; flex: 1 1 auto; min-height: 0; padding: 0 0 28px 30px; }
.stats-tl-yaxis { position: absolute; left: 0; top: 0; bottom: 28px; width: 26px; border-right: 1px solid var(--hairline); }
.stats-tl-ytick {
  position: absolute; right: 5px; font: 8px/1 ui-monospace, Menlo, monospace; color: var(--ink-soft);
  transform: translateY(50%);
}
.stats-tl-plot { position: relative; height: 100%; }
.stats-tl-col { position: absolute; top: 0; bottom: 0; transform: translateX(-50%); cursor: pointer; }
.stats-tl-gridline { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--hairline); pointer-events: none; }
.stats-tl-square {
  position: absolute; left: 50%; transform: translateX(-50%);
  background: var(--ink); border-left: 1px solid var(--paper); border-right: 1px solid var(--paper);
  transition: background 140ms ease, transform 140ms ease;
}
.stats-tl-col:hover .stats-tl-square { background: var(--accent); }
/* Cross-highlight: hovering a timeline square or a side-list row lights up
   the matching species on the other side. Ported from the original's
   sync-hi mechanic (wireStatsHighlight in apt.js). */
.stats-tl-col.sync-hi .stats-tl-square { background: var(--accent); transform: translateX(-50%) scale(1.18); }
.stats-tl-col.sync-hi .stats-tl-label { transform: rotate(-90deg) translateY(55%) scale(1.1); }
.stats-tl-col.sync-hi .stats-tl-label .com, .stats-tl-col.sync-hi .stats-tl-label .sci { color: var(--accent); }
.stats-tl-label {
  position: absolute; left: 50%; transform-origin: 0% 100%;
  transform: rotate(-90deg) translateY(50%);
  white-space: nowrap; font: 10px/1.15 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink);
}
.stats-tl-label .com { display: block; font-weight: 600; }
.stats-tl-label .sci { display: block; font-style: italic; color: var(--ink-soft); font-size: 8.5px; }
.stats-tl-xtick {
  position: absolute; bottom: -19px; transform: translateX(-50%);
  font: 7px/1 ui-monospace, Menlo, monospace; color: var(--ink-soft); white-space: nowrap;
}
.stats-tl-trim {
  position: absolute; right: 0; top: 0; margin: 0;
  font: 9px ui-monospace, Menlo, monospace; color: var(--ink-soft);
  text-transform: lowercase; letter-spacing: 0.04em;
}

/* Species-by-hour ledger (the "by hour" chart-pick option) — a real grid
   this time (shaded cell per active hour, visible count, total column),
   restyled toward the original's `.stats-heatmap` look rather than the
   bar-per-species rows the previous pass shipped. */
.stats-heatmap { position: relative; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.heatmap-wrap { display: flex; flex-direction: column; }
.hm-head { display: grid; grid-template-columns: 140px repeat(24, 1fr) 34px; gap: 1px; margin-bottom: 4px; }
.hm-head span {
  font: 7px ui-monospace, Menlo, monospace; color: var(--ink-soft); text-align: center;
}
.hm-head span:first-child, .hm-head span:last-child { text-align: left; }
.hm-row { display: grid; grid-template-columns: 140px repeat(24, 1fr) 34px; gap: 1px; align-items: center; padding: 3px 0; }
.hm-name { min-width: 0; }
.hm-com { display: block; font: 600 11.5px/1.2 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hm-sci { display: block; font: italic 9px/1.1 ui-serif, Georgia, serif; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hm-cell { aspect-ratio: 1; border-radius: 2px; background: var(--paper-2); }
.hm-cell[data-n] {
  background: color-mix(in srgb, var(--ink) 60%, var(--paper-2));
  display: flex; align-items: center; justify-content: center;
  font: 700 8px ui-monospace, Menlo, monospace; color: var(--paper);
}
.hm-total { font: 11px ui-monospace, Menlo, monospace; color: var(--ink-soft); text-align: right; font-variant-numeric: tabular-nums; }

/* -- Side panel: By Period / Top Species / First Detections -- */
.stats-side { display: flex; flex-direction: column; justify-content: flex-start; padding: 4px 0 6px; }
.stats-side h3, .stats-rhythm h3 {
  font: 700 10px ui-monospace, Menlo, monospace; color: var(--ink);
  letter-spacing: 0.18em; text-transform: uppercase; margin: 0 0 1px;
  border-left: 2px solid var(--accent); padding-left: 10px;
}
.stats-side .grp { flex: 0 0 auto; }
.stats-side .grp + .grp { margin-top: 22px; }
.stats-side .grp small, .stats-rhythm small {
  display: block; font: 9px ui-monospace, Menlo, monospace; color: var(--ink-soft);
  letter-spacing: 0.06em; margin: 0 0 3px 12px;
}
.stats-side ol { list-style: none; padding: 0; margin: 0; }
.stats-side li {
  display: grid; grid-template-columns: 44px 1fr auto; gap: 10px;
  padding: 2px 0; box-shadow: inset 0 -1px 0 var(--hairline);
  font: 12.5px/1.25 ui-serif, Georgia, serif; color: var(--ink); align-items: baseline;
  transition: background 140ms ease, color 140ms ease;
}
.stats-side li[data-sci] { cursor: pointer; }
.stats-side li[data-sci]:hover { background: var(--paper-2); }
.stats-side li.sync-hi { background: var(--paper-2); }
.stats-side li.sync-hi span:not(.yr):not(.ct) { color: var(--accent); }
.stats-side li .yr { font: 9.5px ui-monospace, Menlo, monospace; color: var(--accent-2); letter-spacing: 0.04em; }
.stats-side li .ct { font: 11px ui-monospace, Menlo, monospace; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

/* -- Day's Rhythm -- */
.stats-rhythm { grid-column: 1 / -1; padding-bottom: 40px; }
.rh-plot { position: relative; }
.rh-plot svg { width: 100%; height: 150px; display: block; margin-top: 14px; }
.rh-line { fill: none; stroke-width: 1.6px; stroke-linejoin: round; stroke-linecap: round; }
.rh-today { stroke: var(--ink); }
.rh-avg { stroke: var(--ink-soft); stroke-dasharray: 3 3; opacity: 0.8; }
.rh-tick { fill: var(--ink-soft); font: 8px ui-monospace, Menlo, monospace; }
.rh-grid { stroke: var(--hairline); stroke-width: 1px; }

/* Hover/scrub crosshair — adapted from the original's attachCrosshair.
   Walks the mouse along the line, snapping to the nearest real detection
   slot and showing its time + count. */
.rh-xhair { position: absolute; inset: 0; top: 14px; pointer-events: none; opacity: 0; }
.rh-xhair.on { opacity: 1; }
@media (prefers-reduced-motion: no-preference) { .rh-xhair { transition: opacity .12s ease; } }
.rh-xh-v { position: absolute; top: 0; width: 0; height: 0; border-left: 1px dashed var(--ink-soft); }
.rh-xh-h { position: absolute; left: 0; width: 0; height: 0; border-top: 1px dashed var(--ink-soft); }
.rh-xh-dot {
  position: absolute; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px;
  border-radius: 999px; background: var(--ink); box-shadow: 0 0 0 2px var(--paper);
}
.rh-xh-rx, .rh-xh-ry {
  position: absolute; font: 500 10px/1 "SF Mono", Menlo, monospace;
  color: var(--ink); background: var(--paper); padding: 3px 6px; border-radius: 3px;
  box-shadow: var(--raised); white-space: nowrap; pointer-events: none;
}
.rh-xh-rx { bottom: 0; transform: translate(-50%, 0); }
.rh-xh-ry { left: 0; transform: translate(0, -50%); }

/* ============ Atlas view ============
   Sort control + stamp grid live in stamps.css (this file only needs to
   not fight it). See stamps.css's own header for what that ports vs
   simplifies from the original's stamp system. */

/* ============ Postcard (click-to-detail) modal ============
   Ported closely from the original's `#postcard-modal`/`.postcard-*`:
   backdrop blur with a radial mask, a fixed-size two-column card (art
   left, identity+About+Recordings right) on desktop, textured card stock.
   Not ported: the original's mobile bottom-drawer/drag-to-dismiss variant
   (this project doesn't have a separate mobile layout pass) and the
   shared-element morph from the clicked card's exact screen position —
   this uses a plain translateY+opacity entrance instead. See modal.js's
   header for the full account. */
#postcard-modal {
  position: fixed; inset: 0; z-index: 90;
  display: grid; place-items: center; padding: clamp(16px, 4vw, 54px);
  visibility: hidden; pointer-events: none;
}
#postcard-modal[aria-hidden="false"] { visibility: visible; pointer-events: auto; }
.postcard-backdrop {
  position: absolute; inset: 0;
  background: color-mix(in srgb, var(--paper) 34%, transparent);
  opacity: 0; transition: opacity 240ms cubic-bezier(.23,1,.32,1);
}
.postcard-backdrop::before {
  content: ''; position: absolute; inset: 0;
  backdrop-filter: blur(7px) saturate(.92); -webkit-backdrop-filter: blur(7px) saturate(.92);
  mask-image: radial-gradient(ellipse 72% 78% at 50% 50%, #000 0 45%, rgba(0,0,0,.8) 59%, transparent 84%);
  -webkit-mask-image: radial-gradient(ellipse 72% 78% at 50% 50%, #000 0 45%, rgba(0,0,0,.8) 59%, transparent 84%);
}
#postcard-modal[aria-hidden="false"] .postcard-backdrop { opacity: 1; }
body.postcard-open { overflow: hidden; }

.postcard-sheet {
  --card-stock: #f7f6f2; --card-ink: #29251f; --card-muted: #77736b;
  --card-rule: rgba(61,51,37,.13);
  --card-shadow: 0 2px 1px rgba(42,34,24,.07), 0 26px 68px rgba(38,30,20,.22);
  position: relative;
  width: min(880px, calc(100vw - 56px)); height: min(540px, calc(100dvh - 56px)); min-height: 460px;
  display: grid; grid-template-columns: minmax(0,.82fr) minmax(0,1.18fr); gap: 8px; padding: 8px;
  color: var(--card-ink); background-color: var(--card-stock);
  background-image: linear-gradient(color-mix(in srgb, var(--card-stock) 94%, transparent), color-mix(in srgb, var(--card-stock) 94%, transparent)),
                     url('./assets/stamp/paper-texture-grey.png');
  background-position: 0 0, center; background-size: auto, 680px 510px; background-repeat: repeat;
  background-blend-mode: normal, multiply;
  border: 1px solid color-mix(in srgb, var(--card-ink) 18%, transparent);
  border-radius: 3px; box-shadow: var(--card-shadow);
  overflow: hidden; isolation: isolate;
  transform: translateY(12px); opacity: 0;
  transition: transform 280ms cubic-bezier(.23,1,.32,1), opacity 200ms cubic-bezier(.23,1,.32,1);
}
#postcard-modal[aria-hidden="false"] .postcard-sheet { transform: translateY(0); opacity: 1; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .postcard-sheet {
    --card-stock: #202124; --card-ink: #efede8; --card-muted: #a6a5aa; --card-rule: rgba(239,238,234,.13);
    --card-shadow: 0 2px 1px rgba(0,0,0,.25), 0 30px 76px rgba(0,0,0,.48);
    background-blend-mode: normal, soft-light;
  }
}
:root[data-theme="dark"] .postcard-sheet {
  --card-stock: #202124; --card-ink: #efede8; --card-muted: #a6a5aa; --card-rule: rgba(239,238,234,.13);
  --card-shadow: 0 2px 1px rgba(0,0,0,.25), 0 30px 76px rgba(0,0,0,.48);
  background-blend-mode: normal, soft-light;
}
@media (max-width: 760px) {
  .postcard-sheet { grid-template-columns: 1fr; height: min(88vh, 720px); overflow-y: auto; }
}

.postcard-close {
  position: absolute; top: 14px; right: 14px; z-index: 10;
  width: 28px; height: 28px; border-radius: 999px; border: 0;
  background: var(--card-stock); color: var(--card-ink); box-shadow: var(--raised);
  font-size: 17px; line-height: 1; cursor: pointer;
}

.postcard-visual { position: relative; z-index: 1; min-width: 0; min-height: 0; }
.postcard-bird-frame {
  position: relative; width: 100%; height: 100%; border-radius: 2px;
  background: color-mix(in srgb, var(--card-stock) 60%, var(--paper));
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
#modalImg {
  max-width: 78%; max-height: 68%; object-fit: contain; filter: drop-shadow(0 10px 18px rgba(20,17,13,.22));
  opacity: 1; transition: opacity 180ms ease;
}
#modalImg.is-loading { opacity: 0; }
.postcard-pose-toggle {
  position: absolute; left: 12px; bottom: 12px; z-index: 3;
  display: inline-flex; padding: 3px; background: var(--card-stock); border-radius: 999px; box-shadow: var(--raised);
}
.postcard-pose-toggle button {
  background: transparent; border: 0; color: var(--card-muted);
  width: 26px; height: 24px; padding: 0; border-radius: 999px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.postcard-pose-toggle button svg { width: 12px; height: 12px; }
.postcard-pose-toggle button:hover, .postcard-pose-toggle button[aria-current="true"] { color: var(--card-ink); }
.postcard-links { position: absolute; right: 12px; bottom: 12px; z-index: 3; }
.postcard-links a {
  font: 10px ui-monospace, Menlo, monospace; letter-spacing: 0.06em;
  color: var(--card-ink); background: var(--card-stock); box-shadow: var(--raised);
  border-radius: 999px; padding: 6px 12px; text-decoration: none;
}
.postcard-content { position: relative; z-index: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: 22px 22px 18px; display: flex; flex-direction: column; }
.postcard-identity { flex: 0 0 auto; display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.postcard-heading h2 { margin: 0; font: 700 26px/1.05 ui-serif, "Iowan Old Style", Georgia, serif; }
.postcard-heading #modalSci { display: block; margin-top: 3px; font: italic 13px/1.2 ui-serif, Georgia, serif; color: var(--card-muted); }
.postcard-stats { margin-top: 10px; display: flex; gap: 16px; font: 12px ui-monospace, Menlo, monospace; color: var(--card-muted); }
.postcard-stats b { color: var(--card-ink); font-weight: 700; }
/* The corner stamp is rendered at its normal 168px card size, then scaled
   down as a unit — same trick the original's .stamp-fit uses (renders the
   full card, then transform:scale()s it) so the tiny text and art inside
   stay legible-proportioned instead of reflowing at true-miniature sizes. */
.postcard-stamp-slot { position: relative; flex: 0 0 auto; width: 46px; aspect-ratio: .8; align-self: flex-start; }
.postcard-stamp-slot .stamp {
  position: absolute; top: 0; left: 0; width: 168px;
  transform: scale(0.2738); transform-origin: top left;
  filter: drop-shadow(0 4px 8px rgba(20,17,13,.25));
  pointer-events: none;
}

/* flex: 0 0 auto (no shrink) on the closed state is load-bearing: a closed
   section's only content is its summary, so it must never be squeezed
   below that — with min-height:0 + flex-shrink>0 (the naive version of
   this rule) a growing open sibling (e.g. expanding a recording row) can
   compress the closed section toward 0 and, combined with this element's
   own overflow:hidden, clip its own summary clean off. Only the open
   section (which legitimately has more content than fits and needs its
   own internal scroll) gets shrink+min-height:0. */
.postcard-section { flex: 0 0 auto; display: flex; flex-direction: column; border-top: 1px solid var(--card-rule); margin-top: 14px; overflow: hidden; }
.postcard-section[open] { flex: 1 1 auto; min-height: 0; }
.postcard-section summary {
  flex: 0 0 auto; list-style: none; cursor: pointer; display: flex; align-items: baseline; justify-content: space-between;
  padding-top: 14px;
  font: 700 13px ui-serif, "Iowan Old Style", Georgia, serif; color: var(--card-ink);
}
.postcard-section summary::-webkit-details-marker { display: none; }
.postcard-section summary small { font: 10px ui-monospace, Menlo, monospace; color: var(--card-muted); }
.postcard-section-body { flex: 1 1 auto; min-height: 0; padding-top: 10px; display: flex; flex-direction: column; }
.desc { font: 13px/1.55 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--card-ink); }
.desc.placeholder { color: var(--card-muted); font-style: italic; }
.modal-meta { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 12px; }
.modal-meta .meta-item { font: 10px ui-monospace, Menlo, monospace; color: var(--card-muted); }
.modal-meta .k { text-transform: uppercase; letter-spacing: 0.06em; margin-right: 4px; }
.modal-meta .v { color: var(--card-ink); }

/* -- Recordings: per-row expandable player + client spectrogram --
   ADAPTED from the original's .rec-row/.rec-spectro/.rec-player-* system
   (apt.js's setRecordingExpanded/playModalRecording, styles.css). Ported:
   the collapsed row -> click-to-expand spectrogram strip -> inline
   transport-bar structure, and a custom play/pause + scrub surface
   (replacing the plain <audio controls> the previous pass shipped).
   SIMPLIFIED: no loop-region/repeat-a-section handles — a real but
   fairly niche pro-audio-editor feature judged disproportionate to port
   for a bird-detection clip player. Every row can be expanded
   independently (matching the original); only one row's audio plays at
   a time. */
.rec-table-head { flex: 0 0 auto; display: grid; grid-template-columns: 1fr 70px 100px; gap: 8px; font: 9px ui-monospace, Menlo, monospace; color: var(--card-muted); letter-spacing: 0.06em; text-transform: uppercase; padding-bottom: 4px; }
#modalRecordings { flex: 1 1 auto; min-height: 0; list-style: none; margin: 0; padding: 0 0 8px; overflow-y: auto; }
#modalRecordings li.rec-empty { padding: 14px 0; color: var(--card-muted); font-style: italic; font: 12px ui-serif, Georgia, serif; }
.rec-row { border-top: 1px solid var(--card-rule); }
.rec-row:first-child { border-top: 0; }
.rec-row-toggle {
  width: 100%; display: grid; grid-template-columns: 1fr 70px 100px; gap: 8px; align-items: center;
  padding: 7px 0; border: 0; background: transparent; color: inherit; text-align: left; cursor: pointer;
  font: 12px ui-serif, Georgia, serif; color: var(--card-ink);
}
.rec-row-toggle span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rec-spectro {
  position: relative; height: 0; margin: 0; overflow: hidden;
  border: 1px solid var(--card-rule); border-radius: 5px; background: var(--card-panel-alt, var(--paper-2));
  opacity: 0;
  transition: height 220ms cubic-bezier(.22,1,.36,1), margin 220ms ease, opacity 160ms ease;
}
.rec-row.expanded .rec-spectro { height: 108px; margin: 0 0 10px; opacity: 1; }
.rec-spectro canvas {
  position: absolute; inset: 0 0 26px; z-index: 0;
  display: block; width: 100%; height: calc(100% - 26px);
  opacity: 0; transition: opacity 200ms ease;
}
.rec-spectro canvas.ready { opacity: 1; }
.rec-spectro-loading {
  position: absolute; inset: 0 0 26px; z-index: 1; display: grid; place-items: center;
  color: var(--card-muted); font: 500 8px/1 ui-monospace, Menlo, monospace;
  letter-spacing: .08em; text-transform: uppercase; pointer-events: none; text-align: center; padding: 0 8px;
}
/* Equal specificity to the rule above, so this must come after it: the
   [hidden] attribute (set once renderSpectrogram succeeds) needs to win
   over the unconditional `display: grid` so the label actually disappears
   instead of sitting on top of the finished spectrogram. */
.rec-spectro-loading[hidden] { display: none; }
.rec-spectro-played {
  position: absolute; inset: 0 auto 26px 0; z-index: 2; width: 0;
  background: color-mix(in srgb, var(--card-ink) 6%, transparent); pointer-events: none;
}
.rec-spectro-cursor {
  position: absolute; z-index: 3; top: 6px; bottom: 26px; left: 0; width: 1px;
  background: var(--card-ink); transform: translateX(-50%); opacity: 0; pointer-events: none;
}
.rec-spectro.armed .rec-spectro-cursor, .rec-row.playing .rec-spectro-cursor { opacity: .72; }
.rec-spectro-scrub { position: absolute; inset: 0 0 26px; z-index: 4; cursor: ew-resize; touch-action: pan-y; }
.rec-spectro-scrub:focus-visible { outline: 1px solid color-mix(in srgb, var(--card-ink) 45%, transparent); outline-offset: -2px; }
.rec-player-controls {
  position: absolute; z-index: 5; inset: auto 0 0; height: 26px; padding: 0 6px;
  display: grid; grid-template-columns: 22px 1fr; align-items: center; gap: 7px;
  border-top: 1px solid var(--card-rule); background: var(--card-panel);
}
.rec-player-toggle {
  width: 22px; height: 22px; padding: 0; display: grid; place-items: center;
  border: 0; border-radius: 3px; background: transparent; color: var(--card-ink);
  cursor: pointer; opacity: .86; transition: opacity 140ms ease, transform 140ms cubic-bezier(.23,1,.32,1);
}
.rec-player-toggle svg { display: block; width: 10px; height: 10px; }
.rec-player-toggle:active { transform: scale(.92); }
.rec-player-toggle[data-error="true"] { opacity: .38; cursor: not-allowed; }
@media (hover: hover) and (pointer: fine) { .rec-player-toggle:hover { opacity: 1; } }
.rec-player-time {
  overflow: hidden; white-space: nowrap; color: var(--card-muted);
  font: 500 8.5px/1 ui-monospace, Menlo, monospace; font-variant-numeric: tabular-nums; letter-spacing: .04em;
}

/* ============ About modal ============ */
#about-modal {
  position: fixed; inset: 0; z-index: 90;
  display: grid; place-items: center; padding: 24px;
  visibility: hidden; pointer-events: none;
}
#about-modal[aria-hidden="false"] { visibility: visible; pointer-events: auto; }
.modal-backdrop {
  position: absolute; inset: 0; background: color-mix(in srgb, var(--ink) 45%, transparent);
  opacity: 0; transition: opacity 200ms ease;
}
#about-modal[aria-hidden="false"] .modal-backdrop { opacity: 1; }
.about-card {
  position: relative; z-index: 1; width: min(420px, calc(100vw - 48px));
  background: var(--paper); border-radius: 14px; box-shadow: var(--edge-lg);
  padding: 32px 28px 28px; text-align: center;
  transform: scale(0.96); opacity: 0; transition: transform 200ms ease, opacity 200ms ease;
}
#about-modal[aria-hidden="false"] .about-card { transform: scale(1); opacity: 1; }
.about-eyebrow { margin: 0 0 6px; font: italic 13px ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink-2); }
.about-card h2 { margin: 0 0 14px; font: 700 20px/1.2 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink); }
.about-body { margin: 0 0 20px; font: 14px/1.6 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink); }
.about-body a { color: var(--ink); }
.about-theme {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 0 0 20px; padding: 10px 6px 10px 14px;
  background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess);
}
.about-theme-label {
  font: 10px ui-monospace, Menlo, monospace; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-soft);
}
.about-theme .seg { display: inline-flex; gap: 0; padding: 3px; position: relative; }
.about-theme .seg button {
  background: transparent; border: 0; color: var(--ink-soft);
  font: 9.5px/1 ui-monospace, Menlo, monospace; letter-spacing: 0.1em;
  padding: 7px 12px; border-radius: 999px; cursor: pointer; text-transform: lowercase;
  transition: color 200ms ease; position: relative; z-index: 1;
}
.about-theme .seg button:hover, .about-theme .seg button[aria-current="true"] { color: var(--ink); }
.about-explore, .modal-close {
  font: 11px ui-monospace, Menlo, monospace; letter-spacing: 0.08em; cursor: pointer;
}
.about-explore { background: var(--ink); color: var(--paper); border: 0; border-radius: 999px; padding: 10px 20px; text-transform: uppercase; }
.about-card .modal-close {
  position: absolute; top: 12px; right: 12px; width: 28px; height: 28px; border-radius: 999px; border: 0;
  background: var(--paper-2); color: var(--ink); font-size: 18px; line-height: 1;
}
