:root { color-scheme: light; --paper: #fcfcfb; /* near-pure white, faintest warm undertone */ --paper-2: #f3f2ee; /* recessed track */ --paper-3: #e8e6df; /* placeholder fill */ --ink: #1a1612; --ink-2: #4a3f31; --ink-soft: #908576; --accent: #4a3f31; /* warm dark grey — matches ink-2, no longer red */ --accent-2: #1a1612; /* near-black ink — same hue as the title text */ /* Depth shadow recipes — replace flat 1px borders */ --edge: inset 0 0 0 1px rgba(255,255,255,0.55), 0 0 0 1px rgba(26,22,18,0.04); --edge-lg: inset 0 0 0 1px rgba(255,255,255,0.6), 0 0 0 1px rgba(26,22,18,0.06), 0 8px 28px rgba(26,22,18,0.06); --recess: inset 0 1px 2px rgba(26,22,18,0.06), inset 0 0 0 1px rgba(26,22,18,0.04); --raised: inset 0 0 0 1px rgba(255,255,255,0.8), 0 0 0 1px rgba(26,22,18,0.05), 0 1px 2px rgba(26,22,18,0.07); } * { box-sizing: border-box; } html, body { margin: 0; padding: 0; height: 100%; background: var(--paper); color: var(--ink); } body { font-family: ui-serif, "Iowan Old Style", "Apple Garamond", Georgia, serif; -webkit-font-smoothing: antialiased; overflow: hidden; } .mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: 0.02em; } /* ============ Stage / views ============ Stage is a flex column: a static title block sits on top, the views slider fills the remaining height. The title NEVER moves when the view changes — only its text fades when the new view's title differs from the current one. */ .stage { position: fixed; inset: 0; overflow: hidden; display: flex; flex-direction: column; } .static-head { flex: 0 0 auto; padding: 84px 32px 22px; text-align: center; position: relative; z-index: 5; } .static-head .pre { display: inline-block; background: none; border: 0; padding: 0; font: italic 400 clamp(13px, 1.4vw, 18px)/1 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink-2); letter-spacing: 0.06em; margin-bottom: 6px; cursor: pointer; transition: color 140ms ease; } .static-head .pre:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; } .static-head h1 { margin: 0; padding: 0; font: 700 clamp(24px, 3.2vw, 40px)/1 ui-serif, "Iowan Old Style", "Bookman Old Style", Georgia, serif; letter-spacing: 0.06em; color: var(--ink); text-transform: uppercase; font-style: normal; transition: opacity 240ms cubic-bezier(.7,.05,.2,1); } .static-head.swap-out h1 { opacity: 0; } @media (max-width: 700px) { .static-head { padding: 96px 18px 22px; } } .views { flex: 1 1 auto; min-height: 0; position: relative; display: flex; transition: transform 480ms cubic-bezier(.7,.05,.2,1); } .view { flex: 0 0 100%; height: 100%; overflow: auto; padding: 8px 56px 132px; position: relative; } /* Collage and stats views both fit the viewport (no scroll). Atlas keeps normal scroll. */ .view#v0, .view#v1 { overflow: hidden; padding: 8px 32px 88px; } .view#v0 { display: flex; } .view#v1 { display: flex; flex-direction: column; } @media (max-width: 700px) { .view { padding: 8px 18px 132px; } .view#v0, .view#v1 { padding: 8px 16px 88px; } } /* ============ Top bar — window picker (left) + menu button (right). ============ Visible on every view; the picker is the universal time-window control. */ .top { position: fixed; top: 22px; left: 0; right: 0; display: flex; justify-content: space-between; align-items: center; padding: 0 28px; z-index: 30; pointer-events: none; } .top > * { pointer-events: auto; } .top .window-pick { display: inline-flex; gap: 0; padding: 4px; background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess); position: relative; } .top .window-pick button { background: transparent; border: 0; color: var(--ink-soft); font: 10px/1 ui-monospace, Menlo, monospace; letter-spacing: 0.18em; padding: 9px 16px; border-radius: 999px; cursor: pointer; text-transform: uppercase; transition: color 200ms ease; position: relative; z-index: 1; } .top .window-pick button:hover { color: var(--ink); } .top .window-pick button[aria-current="true"] { color: var(--ink); } .menu-btn { background: var(--paper); border: 0; color: var(--ink); font: 11px/1 ui-monospace, Menlo, monospace; padding: 9px 14px 8px; border-radius: 999px; cursor: pointer; letter-spacing: 0.18em; text-transform: lowercase; box-shadow: var(--raised); transition: transform 160ms ease; } .menu-btn:hover { transform: translateY(-1px); } .menu-btn:active { transform: translateY(0); } /* Compact top bar on mobile — the time-window pill goes from spacious to thumb-tight without losing legibility. */ @media (max-width: 700px) { .top { top: 12px; padding: 0 12px; } .top .window-pick { padding: 3px; } .top .window-pick button { font-size: 8.5px; padding: 6px 10px; letter-spacing: 0.12em; } .menu-btn { font-size: 9px; padding: 6px 11px 5px; letter-spacing: 0.14em; } .return-to-atlas { top: 10px; left: 10px; padding: 5px 10px; font-size: 8.5px; } } @media (max-width: 420px) { .top .window-pick button { font-size: 8px; padding: 5px 7px; letter-spacing: 0.08em; } } /* ============ Bottom slider — recessed track, raised active pill ============ */ .slider { position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%); display: flex; gap: 0; padding: 4px; z-index: 30; background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess); position: fixed; /* re-emit for the pill positioning */ } .slider button { background: transparent; border: 0; color: var(--ink-soft); font: 10px/1 ui-monospace, Menlo, monospace; letter-spacing: 0.2em; padding: 11px 24px; border-radius: 999px; cursor: pointer; text-transform: uppercase; transition: color 200ms ease; position: relative; z-index: 1; } .slider button:hover { color: var(--ink); } .slider button[aria-current="true"] { color: var(--ink); } /* Shared sliding pill — JS animates transform and width to the active button's position. The pill is the only element with the raised-paper look; buttons just toggle their text colour. */ .seg-pill { position: absolute; top: 4px; bottom: 4px; left: 0; background: var(--paper); border-radius: 999px; box-shadow: var(--raised); transition: transform 320ms cubic-bezier(.7,.05,.2,1), width 320ms cubic-bezier(.7,.05,.2,1); will-change: transform, width; pointer-events: none; z-index: 0; } /* ============ Menu dropdown ============ */ #menu-dd { position: fixed; top: 58px; right: 28px; z-index: 25; width: 360px; max-width: calc(100vw - 24px); max-height: calc(100vh - 80px); overflow: auto; background: var(--paper); border-radius: 12px; box-shadow: var(--edge-lg); padding: 14px; opacity: 0; transform: translateY(-6px); pointer-events: none; transition: opacity 200ms ease, transform 200ms ease; } #menu-dd.open { opacity: 1; transform: translateY(0); pointer-events: auto; } .lock-row { display: flex; gap: 6px; align-items: stretch; background: var(--paper-2); border-radius: 8px; box-shadow: var(--recess); padding: 4px; } .lock-row input { flex: 1; background: transparent; border: 0; outline: 0; color: var(--ink); padding: 8px 10px; font: 13px ui-monospace, Menlo, monospace; } .lock-row input::placeholder { color: var(--ink-soft); } .lock-row button { background: var(--paper); border: 0; color: var(--ink); width: 32px; padding: 0; border-radius: 6px; cursor: pointer; font: 14px/1 ui-monospace, Menlo, monospace; box-shadow: var(--raised); display: flex; align-items: center; justify-content: center; } .lock-row button:hover { color: var(--accent); } .lock-hint { margin: 8px 4px 0; font: 10px ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.04em; } .lock-err { color: var(--accent); } /* Tiny "built by teddy" attribution — appears under the lock-screen password field AND at the foot of the about-card. Same monospace micro-type used by .lock-hint so the two contexts read consistent. */ .built-by { margin: 14px 4px 0; font: 10px ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.04em; } .built-by a { color: inherit; text-decoration: underline; text-underline-offset: 2px; } .built-by a:hover { color: var(--ink); } .about-card .built-by { margin-top: 18px; } .menu-items { padding: 4px 4px; display: none; } .menu-items.show { display: block; } .menu-items h3 { font: 9px/1 ui-monospace, Menlo, monospace; letter-spacing: 0.24em; color: var(--ink-soft); text-transform: uppercase; margin: 12px 4px 4px; } .menu-items a { display: block; padding: 9px 8px; color: var(--ink); text-decoration: none; font: 14px ui-serif, Georgia, serif; border-radius: 6px; transition: background 120ms ease; } .menu-items a:hover { background: var(--paper-2); color: var(--accent); } /* ============ Settings cards inside the drawer ============ Form controls match the time-picker's design language: recessed track + raised paper element. Toggles, sliders, and segmented pickers share the same depth-shadow recipe. */ .menu-section { margin-top: 14px; padding-top: 12px; border-top: 1px solid rgba(26,22,18,0.07); } .menu-section:first-of-type { margin-top: 6px; border-top: 0; padding-top: 0; } .menu-section h3 { font: 700 9px ui-monospace, Menlo, monospace; letter-spacing: 0.20em; color: var(--ink); text-transform: uppercase; margin: 0 0 8px 4px; } /* Collapsible section — header is a button row with caret on the right. Default state is collapsed so accidental clicks don't touch settings. */ .menu-section.collapsible > .section-header { display: flex; align-items: center; justify-content: space-between; cursor: pointer; user-select: none; padding: 4px 6px 6px 0; margin: 0 0 4px 0; } .menu-section.collapsible > .section-header h3 { margin: 0; } .menu-section.collapsible > .section-header .caret { width: 10px; height: 10px; transition: transform 200ms cubic-bezier(.7,.05,.2,1); color: var(--ink-soft); } .menu-section.collapsible[data-open="true"] > .section-header .caret { transform: rotate(90deg); } .menu-section.collapsible > .section-body { max-height: 0; overflow: hidden; transition: max-height 280ms cubic-bezier(.7,.05,.2,1); } .menu-section.collapsible[data-open="true"] > .section-body { max-height: 800px; } .menu-row { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center; padding: 8px 6px 8px 10px; border-radius: 6px; } .menu-row + .menu-row { box-shadow: inset 0 1px 0 rgba(26,22,18,0.06); } .menu-row .label { font: 12.5px ui-serif, Georgia, serif; color: var(--ink); } .hint { display: block; font: 9px ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.04em; margin-top: 1px; } /* Toggle switch (raised pill that slides) */ .switch { position: relative; width: 38px; height: 20px; background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess); cursor: pointer; border: 0; padding: 0; } .switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: var(--paper); border-radius: 999px; box-shadow: var(--raised); transition: transform 220ms cubic-bezier(.7,.05,.2,1), background 200ms ease; } .switch[aria-checked="true"] { background: var(--ink-2); } .switch[aria-checked="true"]::after { transform: translateX(18px); background: var(--paper); } /* Slider — title row (label + value badge with breathing room) and a track row underneath. Value sits at the far right with a wide gap so it never crowds the label text. */ .slider-row { display: block; padding: 8px 6px 10px 10px; } .slider-row .head { display: flex; align-items: baseline; gap: 24px; margin-bottom: 6px; } .slider-row .head .label-block { flex: 1; min-width: 0; } .slider-row .label { font: 12.5px ui-serif, Georgia, serif; color: var(--ink); } .slider-row .value { flex: 0 0 auto; font: 11px ui-monospace, Menlo, monospace; color: var(--ink); font-variant-numeric: tabular-nums; background: var(--paper-2); padding: 3px 10px; border-radius: 4px; box-shadow: var(--recess); min-width: 48px; text-align: center; } .slider-row .slider-track { margin-top: 2px; } .slider-track input[type="range"] { -webkit-appearance: none; appearance: none; width: 100%; height: 18px; background: transparent; cursor: pointer; } .slider-track input[type="range"]::-webkit-slider-runnable-track { height: 4px; background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess); } .slider-track input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; margin-top: -6px; background: var(--paper); border-radius: 999px; box-shadow: var(--raised); cursor: pointer; } .slider-track input[type="range"]::-moz-range-track { height: 4px; background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess); } .slider-track input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; background: var(--paper); border-radius: 999px; box-shadow: var(--raised); border: 0; cursor: pointer; } /* Segmented picker (same pill-on-recess pattern as the time picker) */ .seg { display: inline-flex; padding: 4px; background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess); position: relative; } .seg button { background: transparent; border: 0; color: var(--ink-soft); font: 9.5px ui-monospace, Menlo, monospace; letter-spacing: 0.16em; text-transform: uppercase; padding: 6px 12px; border-radius: 999px; cursor: pointer; position: relative; z-index: 1; transition: color 200ms ease; } .seg button[aria-current="true"] { color: var(--ink); background: var(--paper); box-shadow: var(--raised); } /* Inline live-audio player — sits at the top of the unlocked drawer. A single play/stop button with a quiet running-indicator dot. */ .live-audio { display: flex; align-items: center; gap: 12px; padding: 10px 12px; margin: 0 0 6px; background: var(--paper-2); border-radius: 8px; box-shadow: var(--recess); } .live-audio .pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--ink-soft); flex-shrink: 0; transition: background 200ms ease; } .live-audio[data-on="true"] .pulse { background: #c44a3b; animation: pulsate 1.6s ease-in-out infinite; } @keyframes pulsate { 0%, 100% { opacity: 0.4; transform: scale(1); } 50% { opacity: 1; transform: scale(1.25); } } .live-audio .label { flex: 1; font: 12.5px ui-serif, Georgia, serif; color: var(--ink); } .live-audio .label .hint { font: 9px ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.04em; } .live-audio button { background: var(--paper); border: 0; color: var(--ink); padding: 6px 12px; border-radius: 4px; font: 9.5px ui-monospace, Menlo, monospace; letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer; box-shadow: var(--raised); display: inline-flex; align-items: center; gap: 6px; } .live-audio button svg { width: 9px; height: 9px; } .live-audio button + button { margin-left: 4px; } /* Live spectrogram — hidden until the user clicks LISTEN. We collapse via max-height (not display:none) so the canvas inside keeps its layout dimensions and paints correctly on expand. */ .live-spectro { width: 100%; height: 0; background: var(--paper-2); border-radius: 6px; margin: 0; overflow: hidden; box-shadow: var(--recess); transition: height 240ms cubic-bezier(.7,.05,.2,1), margin 240ms ease; } .live-audio[data-on="true"] + .live-spectro { height: 70px; margin: 0 0 8px; } .live-status { font: 9.5px ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.04em; text-transform: lowercase; margin: -4px 0 6px 0; padding: 0 4px; min-height: 13px; } .live-status.err { color: #a83a2d; } .menu-links a .l-hint { display: block; font: 8.5px ui-serif, Georgia, serif; text-transform: none; letter-spacing: 0; color: var(--ink-soft); margin-top: 2px; } .menu-links a.full { grid-column: 1 / -1; } /* External BirdNET pages — small grid of cards */ .menu-links { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 4px 0 2px; } .menu-links a { display: flex; align-items: center; justify-content: center; padding: 9px 10px; background: var(--paper-2); color: var(--ink); text-decoration: none; font: 10.5px ui-monospace, Menlo, monospace; letter-spacing: 0.10em; text-transform: lowercase; border-radius: 6px; box-shadow: var(--raised); transition: transform 160ms ease, color 160ms ease; } .menu-links a:hover { transform: translateY(-1px); color: var(--ink); background: var(--paper); } .menu-save-row { display: flex; gap: 8px; align-items: center; justify-content: flex-end; margin-top: 10px; padding: 0 4px; } .menu-save-row .save-state { flex: 1; font: 9px ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.04em; } .menu-save-row .save-state.ok { color: #2d5d3a; } .menu-save-row .save-state.err { color: var(--accent); } .menu-save-row button { background: var(--ink); color: var(--paper); border: 0; padding: 7px 14px; border-radius: 6px; font: 10px ui-monospace, Menlo, monospace; letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer; box-shadow: var(--raised); } .menu-save-row button:disabled { opacity: 0.4; cursor: default; } /* ============ View 1 — Collage (Galliformes-style poster) ============ Viewport-fit, no scroll. The shared static-head provides the title; this view's content is just the cluster of bird cutouts which fills the remaining viewport height. */ .gcollage { flex: 1 1 auto; width: 100%; max-width: 1300px; margin: 0 auto; position: relative; min-height: 0; } .gtile { position: absolute; background: transparent; border: 0; padding: 0; margin: 0; overflow: visible; /* let drop-shadow extend past the bbox */ transition: transform 130ms ease; /* Events are handled at the .collage level via alpha-mask hit-testing — the tile rectangles must not intercept them. */ pointer-events: none; } /* Hover state is driven by JS alpha-mask hit-testing (.is-hover), NOT the CSS :hover pseudo — the rectangular tile bounding boxes overlap, so :hover would light up the wrong bird. .is-hover only ever sits on the one tile whose silhouette is actually under the cursor. */ .gtile.is-hover { transform: scale(1.04); z-index: 3; } .gtile img { width: 100%; height: 100%; object-fit: contain; display: block; /* Cutouts arrive as alpha-bbox-cropped transparent-bg PNGs, so object-fit: contain leaves no whitespace inside the tile — the bird touches all four sides. Soft drop shadow keeps it readable on paper. */ filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10)); transition: filter 130ms ease; } .gtile.is-hover img { filter: drop-shadow(0 3px 10px rgba(26,22,18,0.26)) saturate(1.1); } /* Hover pill — surfaces the windowed count for the bird under the cursor. Confirms (Anna's-hummingbird-is-#1 paranoia) that the collage scaling reflects the selected window, not all-time. */ .collage-tip { position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%); padding: 6px 14px; background: var(--paper); border-radius: 999px; box-shadow: 0 6px 18px rgba(26,22,18,0.12), inset 0 0 0 1px rgba(255,255,255,0.6); font: italic 13px/1.3 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink); letter-spacing: 0.02em; opacity: 0; pointer-events: none; transition: opacity 150ms ease; white-space: nowrap; z-index: 4; } .collage-tip[aria-hidden="false"] { opacity: 1; } .collage-tip .ct-name { font-style: normal; font-weight: 600; } .collage-tip .ct-n { font-style: normal; font-weight: 700; } .collage-tip .ct-w { color: var(--ink-soft); } .empty { color: var(--ink-soft); font: 10px ui-monospace, Menlo, monospace; padding: 120px 0; text-align: center; letter-spacing: 0.18em; text-transform: uppercase; } /* Page header is now shared at .stage > .static-head — see top of CSS. */ /* Stats grid: charts stack on the LEFT, text sections on the RIGHT. Each chart is a full-bleed card; each text section is a list with subtle separators. Sized to fit the viewport without scrolling. */ .stats-grid { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 80px; max-width: 1100px; margin: 0 auto; width: 100%; align-items: stretch; } @media (max-width: 900px) { .stats-grid { grid-template-columns: 1fr; gap: 40px; } /* Single-column: the timeline has no tall grid sibling to stretch against, so pin it to a usable height instead of letting it collapse to its (zero) intrinsic height. clamp() keeps it sane on both short landscape phones and tall portrait ones. */ .stats-timeline { height: clamp(260px, 44vh, 420px); } } /* Editorial detection timeline. One column per species. Black square positioned vertically by count (y = quantity), columns ordered by detection time (x = time). Rotated two-line label sits just above its square. Y-axis count ticks on the left, X-axis time labels on the bottom. Fits the viewport — never scrolls. */ .stats-timeline { position: relative; flex: 1 1 auto; min-height: 0; padding: 0 0 30px 40px; /* bottom = x-axis, left = y-axis */ } .stats-tl-yaxis { position: absolute; left: 0; top: 0; bottom: 30px; width: 36px; border-right: 1px solid rgba(26,22,18,0.22); } .stats-tl-ytick { position: absolute; right: 5px; font: 9px/1 "SF Mono", Menlo, monospace; color: var(--ink-soft); transform: translateY(50%); } .stats-tl-ytick::after { content: ''; position: absolute; right: -5px; top: 50%; width: 4px; height: 1px; background: rgba(26,22,18,0.3); } .stats-tl-plot { position: relative; height: 100%; } /* Each species is absolutely positioned along the time axis by its last_seen; columns aren't equal-width anymore, so gaps in the timeline show as actual empty space. The width is a label/square gutter, not a layout slot. */ .stats-tl-col { position: absolute; top: 0; bottom: 0; width: 28px; transform: translateX(-50%); cursor: pointer; } /* Faint vertical hairlines at every x-axis tick — the "no detections in this slice" period reads as the empty space between marks. */ .stats-tl-gridline { position: absolute; top: 0; bottom: 0; width: 1px; background: rgba(26,22,18,0.10); pointer-events: none; } .stats-tl-square { position: absolute; left: 50%; transform: translateX(-50%); background: var(--ink); transition: background 140ms ease, transform 140ms ease; } .stats-tl-col { cursor: pointer; } /* Cross-highlight: hovering a square or a table row lights up the matching species on the other side — the square scales up and its label scales + shifts to the accent ink, so the whole column reads as selected. */ .stats-tl-col.sync-hi .stats-tl-square { background: var(--accent); transform: translateX(-50%) scale(1.18); } .stats-tl-col.sync-hi .stats-tl-label { transform: rotate(-90deg) translateY(55%) scale(1.1); } .stats-tl-col.sync-hi .stats-tl-label .com, .stats-tl-col.sync-hi .stats-tl-label .sci { color: var(--accent); } /* Two-line species label, rotated to read bottom-to-top like the reference exhibition checklist. Sits just above the square — rotate(-90deg) then translateY(50%) re-centres the stack on the column regardless of label height. Common name and scientific name sit as two adjacent columns of vertical text. The sync-hi scale uses translateY(55%) to stay centred while enlarged. */ .stats-tl-label { position: absolute; left: 50%; transform-origin: 0% 100%; transform: rotate(-90deg) translateY(50%); white-space: nowrap; font: 11px/1.2 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink); transition: transform 130ms ease; } .stats-tl-label .com { display: block; font-weight: 600; transition: color 130ms ease; } .stats-tl-label .sci { display: block; font-style: italic; color: var(--ink-soft); font-size: 9.5px; transition: color 130ms ease; } /* Ticks live inside .stats-tl-plot so their left:% aligns with the gridlines (both share the plot's content box). bottom is negative to push the label into the x-axis gutter below the plot. */ .stats-tl-xtick { position: absolute; bottom: -22px; transform: translateX(-50%); font: 8.5px/1 "SF Mono", Menlo, monospace; color: var(--ink-soft); white-space: nowrap; } .stats-tl-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font: 11px ui-monospace, Menlo, monospace; color: var(--ink-soft); text-transform: lowercase; letter-spacing: 0.18em; } /* Trim note — shown when the plot can't fit every species column and falls back to the most-heard subset. A paper background keeps it legible even if a tall label reaches the corner behind it. */ .stats-tl-cap { position: absolute; top: 0; right: 0; z-index: 2; background: var(--paper); padding: 1px 0 3px 8px; font: 8.5px/1 "SF Mono", Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.04em; } /* (Old chart-card / .stats-charts CSS removed — replaced by the editorial .stats-timeline above.) */ .stats-side { display: flex; flex-direction: column; justify-content: center; align-self: stretch; max-height: 100%; padding: 6px 0; } .stats-side h3 { font: 700 10px ui-monospace, Menlo, monospace; color: var(--ink); letter-spacing: 0.18em; text-transform: uppercase; margin: 0 0 1px; border-left: 2px solid var(--accent); padding-left: 10px; } /* Tight, content-sized groups — they sit just below their headers rather than spreading to fill the column. */ .stats-side .grp { flex: 0 0 auto; } .stats-side .grp + .grp { margin-top: 22px; } .stats-side .grp small { display: block; font: 9px ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.06em; margin: 0 0 3px 12px; } .stats-side ol { list-style: none; padding: 0; margin: 0; } .stats-side li { display: grid; grid-template-columns: 44px 1fr auto; gap: 10px; padding: 2px 0; box-shadow: inset 0 -1px 0 rgba(26,22,18,0.05); font: 12.5px/1.25 ui-serif, Georgia, serif; color: var(--ink); align-items: baseline; transition: background 140ms ease, color 140ms ease; } .stats-side li[data-sci] { cursor: pointer; } .stats-side li[data-sci]:hover { background: var(--paper-2); } /* Cross-highlight from a hovered timeline square. */ .stats-side li.sync-hi { background: var(--paper-2); } .stats-side li.sync-hi span:not(.yr):not(.ct) { color: var(--accent); } .stats-side li .yr { font: 9.5px ui-monospace, Menlo, monospace; color: var(--accent-2); letter-spacing: 0.04em; } .stats-side li .ct { font: 11px ui-monospace, Menlo, monospace; color: var(--ink-soft); font-variant-numeric: tabular-nums; } /* (Histogram strip removed — charts moved into the stats-grid LEFT column as full-bleed chart cards. See .stats-charts above.) */ /* ============ View 3 — Atlas ============ Field-guide grid: one card per species, each card holds the cutout, names, window/all-time detection counts, and small action chips for audio playback, Wikipedia, and eBird. */ .atlas-controls { max-width: 1280px; margin: 0 auto 18px; display: flex; justify-content: flex-end; align-items: center; } .atlas-sort { display: inline-flex; padding: 3px; position: relative; background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess); } .atlas-sort button { background: transparent; border: 0; color: var(--ink-soft); width: 30px; height: 26px; padding: 0; border-radius: 999px; cursor: pointer; position: relative; z-index: 1; display: inline-flex; align-items: center; justify-content: center; transition: color 200ms ease; } .atlas-sort button svg { width: 13px; height: 13px; display: block; } .atlas-sort button:hover { color: var(--ink); } .atlas-sort button[aria-current="true"] { color: var(--ink); } .atlas-sort button .tip { position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%); white-space: nowrap; font: 9px ui-monospace, Menlo, monospace; letter-spacing: 0.14em; text-transform: lowercase; color: var(--ink-soft); background: var(--paper); padding: 3px 8px; border-radius: 4px; box-shadow: var(--raised); opacity: 0; pointer-events: none; transition: opacity 160ms ease; } .atlas-sort button:hover .tip { opacity: 1; } .atlas-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 22px; max-width: 1280px; margin: 0 auto; } @media (max-width: 700px) { .atlas-grid { grid-template-columns: 1fr; gap: 16px; } } .bird-card { position: relative; background: var(--paper); border-radius: 10px; padding: 16px 16px 12px; box-shadow: var(--edge-lg); display: flex; flex-direction: column; min-height: 320px; cursor: pointer; transition: transform 200ms ease, box-shadow 200ms ease; } .bird-card:hover { transform: translateY(-1px); box-shadow: var(--edge-xl, 0 6px 16px rgba(26,22,18,0.12)); } .bird-card .actions, .bird-card .spectro-wrap { cursor: auto; } .bird-card .actions button, .bird-card .actions a { cursor: pointer; } .bird-card .stat { position: absolute; top: 14px; left: 16px; font: 9px/1.5 ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.04em; } .bird-card .stat .n { color: var(--ink); font: 700 13px/1 ui-serif, Georgia, serif; font-variant-numeric: tabular-nums; margin-right: 4px; letter-spacing: 0; } .bird-card .stat .lbl { display: block; } .bird-card .img-wrap { flex: 1 1 auto; min-height: 0; display: flex; align-items: center; justify-content: center; margin: 38px 0 10px; padding: 4px; } .bird-card .img-wrap img { max-width: 100%; max-height: 160px; display: block; filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10)); } .bird-card h3 { margin: 0; font: 700 14.5px/1.15 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink); letter-spacing: 0.01em; } .bird-card .sci { font: italic 11px/1.2 ui-serif, Georgia, serif; color: var(--ink-2); margin: 2px 0 0; } .bird-card .actions { display: flex; gap: 6px; align-items: stretch; margin-top: 12px; padding-top: 10px; box-shadow: inset 0 1px 0 rgba(26,22,18,0.07); } .bird-card .chip { flex: 0 0 auto; background: var(--paper-2); border: 0; color: var(--ink); padding: 5px 9px; border-radius: 4px; font: 9.5px/1 ui-monospace, Menlo, monospace; text-transform: lowercase; letter-spacing: 0.10em; text-decoration: none; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; box-shadow: var(--raised); transition: transform 160ms ease, color 160ms ease; white-space: nowrap; } .bird-card .chip:hover { color: var(--ink); transform: translateY(-1px); } .bird-card .chip[data-active="true"] { color: var(--paper); background: var(--ink); } .bird-card .chip svg { width: 10px; height: 10px; display: block; } .bird-card .chip.play { padding-left: 7px; } .bird-card .chip.ext::after { content: '↗'; opacity: 0.6; font-size: 11px; } /* Selected card highlight — matches the page-slider and time-window pill aesthetic: the active element is a slightly *brighter* paper surface (raised) on top of a slightly *recessed* background. No accent colour, no glowing ring — just the same depth shadow recipe the rest of the chrome uses. */ .bird-card { transition: transform 280ms cubic-bezier(.7,.05,.2,1), box-shadow 280ms ease, background 200ms ease; } .bird-card[data-active="true"] { background: var(--paper); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.85), 0 0 0 1px rgba(26,22,18,0.06), 0 10px 26px rgba(26,22,18,0.10); transform: translateY(-1px); } .bird-card[data-pulse="true"] { animation: bird-card-pulse 520ms cubic-bezier(.45,.05,.2,1); } /* Soft fade-in of the raised state, no ring pulse */ @keyframes bird-card-pulse { 0% { transform: translateY(2px); } 100% { transform: translateY(-1px); } } .atlas-empty { grid-column: 1 / -1; padding: 80px 16px; text-align: center; font: 12px/1.6 ui-serif, Georgia, serif; color: var(--ink-2); } .atlas-empty p { margin: 0 0 4px; } .atlas-empty p.hint { font: 10px ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.06em; } /* ============ Admin screen (system / logs / tools) ============ Overlays the slider when activated via #admin=... hash. Lives inside the same shell as the rest of the app so the header, menu button, and time-window pill stay put — feels native. */ .admin-screen { position: fixed; inset: 60px 0 60px 0; background: var(--bg); overflow-y: auto; z-index: 5; opacity: 0; pointer-events: none; transition: opacity 200ms ease; } .admin-screen[aria-hidden="false"] { opacity: 1; pointer-events: auto; } .admin-frame { max-width: 1100px; margin: 0 auto; padding: 0 28px 40px; } .admin-title { text-align: center; margin: 0 auto 28px; } .admin-title h1 { margin: 0; font: 32px/1.1 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink); letter-spacing: 0.005em; } /* Return-to-atlas pill in the same slot the time-window pill normally lives — matches its visual language. */ .return-to-atlas { position: fixed; top: 18px; left: 20px; z-index: 6; display: none; align-items: center; gap: 6px; padding: 7px 14px; border-radius: 999px; background: var(--paper-2); color: var(--ink-soft); font: 10px ui-monospace, Menlo, monospace; letter-spacing: 0.18em; text-transform: lowercase; cursor: pointer; text-decoration: none; box-shadow: var(--recess); transition: color 200ms ease; } body.admin-on .return-to-atlas { display: inline-flex; } body.admin-on #slider { visibility: hidden; } body.admin-on .static-head { opacity: 0; pointer-events: none; } /* Hide the time-window pill on admin pages (it doesn't apply there) — the return-to-atlas link takes its slot. */ body.admin-on #winPick { visibility: hidden; } .return-to-atlas:hover { color: var(--ink); } .return-to-atlas svg { width: 10px; height: 10px; } .admin-body { padding-top: 4px; } .admin-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); margin-bottom: 22px; } .admin-card { background: var(--paper); border-radius: 8px; padding: 14px 16px; box-shadow: var(--edge); } .admin-card h3 { margin: 0 0 6px; font: 10px ui-monospace, Menlo, monospace; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-soft); font-weight: 500; } .admin-card .v { font: 20px ui-serif, Georgia, serif; color: var(--ink); } .admin-card .sub { font: 11px ui-monospace, Menlo, monospace; color: var(--ink-soft); margin-top: 4px; } .admin-card.alert { box-shadow: 0 0 0 1px var(--accent), var(--edge); } .admin-card.alert h3 { color: var(--accent); } .admin-card.warn { box-shadow: 0 0 0 1px #c47a1f, var(--edge); } .admin-card.warn h3 { color: #c47a1f; } .admin-section-head { font: 700 11px ui-monospace, Menlo, monospace; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink); margin: 24px 0 10px; border-left: 2px solid var(--accent-2); padding-left: 10px; } table.admin-tbl { width: 100%; border-collapse: collapse; font: 12px ui-monospace, Menlo, monospace; background: var(--paper); border-radius: 8px; overflow: hidden; box-shadow: var(--edge); } table.admin-tbl th, table.admin-tbl td { border-bottom: 1px solid var(--rule, #c8bfae); padding: 9px 12px; text-align: left; } table.admin-tbl tr:last-child th, table.admin-tbl tr:last-child td { border-bottom: 0; } table.admin-tbl th { color: var(--ink-soft); text-transform: uppercase; font-size: 9.5px; letter-spacing: 0.14em; font-weight: 500; } table.admin-tbl .pill { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase; } table.admin-tbl .pill.active { background: rgba(90,122,58,.18); color: #4d6730; } table.admin-tbl .pill.inactive { background: rgba(168,52,29,.18); color: var(--accent); } table.admin-tbl .pill.failed { background: rgba(168,52,29,.18); color: var(--accent); } table.admin-tbl button.restart { font: 9.5px ui-monospace, Menlo, monospace; letter-spacing: 0.04em; text-transform: lowercase; padding: 4px 10px; border: 0; background: var(--paper-2); border-radius: 4px; cursor: pointer; color: var(--ink-soft); box-shadow: var(--raised); } table.admin-tbl button.restart:hover { color: var(--ink); } table.admin-tbl button.restart[disabled] { opacity: .5; cursor: wait; } .admin-logs-toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 14px; } .admin-logs-toolbar select, .admin-logs-toolbar input[type="number"] { font: 12px ui-monospace, Menlo, monospace; padding: 6px 10px; border: 1px solid var(--rule, #c8bfae); border-radius: 4px; background: var(--paper); color: var(--ink); } .admin-logs-toolbar label { font: 9.5px ui-monospace, Menlo, monospace; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-soft); } .admin-logs-pane { background: #1c1a14; color: #d7cba8; font: 12px/1.5 ui-monospace, Menlo, monospace; padding: 14px 16px; border-radius: 8px; max-height: 65vh; overflow: auto; white-space: pre-wrap; } .admin-actions-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); } .admin-action { background: var(--paper); border-radius: 8px; padding: 14px 16px; box-shadow: var(--edge); } .admin-action h4 { margin: 0 0 6px; font: 15px ui-serif, Georgia, serif; color: var(--ink); } .admin-action p { margin: 0 0 10px; font: 12.5px ui-serif, Georgia, serif; color: var(--ink-soft); } .admin-action button.run { font: 9.5px ui-monospace, Menlo, monospace; letter-spacing: 0.14em; text-transform: lowercase; padding: 6px 12px; border: 0; background: var(--ink); color: var(--paper); border-radius: 999px; cursor: pointer; } .admin-action button.run[disabled] { opacity: .5; cursor: wait; } .admin-action .out { margin-top: 10px; font: 10.5px ui-monospace, Menlo, monospace; color: var(--ink-soft); white-space: pre-wrap; } .admin-action.deploy pre { background: #1c1a14; color: #d7cba8; font: 11px ui-monospace, Menlo, monospace; padding: 12px 14px; border-radius: 6px; overflow: auto; white-space: pre-wrap; word-break: break-all; margin: 0; } .admin-action.deploy button.copy { font: 9.5px ui-monospace, Menlo, monospace; letter-spacing: 0.06em; text-transform: lowercase; padding: 5px 10px; background: var(--paper-2); color: var(--ink-soft); border: 0; border-radius: 4px; cursor: pointer; box-shadow: var(--raised); margin-top: 8px; } .admin-action.deploy button.copy:hover { color: var(--ink); } .admin-unreachable { background: var(--paper); border-radius: 8px; padding: 14px 16px; color: var(--accent); font: 12px ui-monospace, Menlo, monospace; margin-bottom: 18px; box-shadow: 0 0 0 1px var(--accent), var(--edge); } /* When admin screen is shown, the slider underneath is hidden so it doesn't compete for scrolling. The static title gets swapped out too — admin is its own section with its own visual identity. Menu button + time-window pill stay visible (still useful). */ body.admin-on #views { display: none; } body.admin-on .nav-band, body.admin-on #slider { visibility: hidden; } body.admin-on .static-head { opacity: 0; pointer-events: none; transition: opacity 200ms ease; } /* Mobile compact: drawer takes near-full width, admin pads tighter, section heads + cards downsize. */ @media (max-width: 700px) { #menu-dd { top: 50px; right: 8px; left: 8px; width: auto; padding: 10px; } .admin-screen { inset: 52px 0 60px 0; } .admin-frame { padding: 0 14px 32px; } .admin-title { margin-bottom: 18px; } .admin-title h1 { font-size: 22px; } .admin-grid { gap: 10px; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); } .admin-card { padding: 12px 14px; } .admin-card .v { font-size: 18px; } .admin-section-head { margin: 18px 0 8px; font-size: 10px; letter-spacing: 0.12em; } table.admin-tbl th, table.admin-tbl td { padding: 7px 8px; font-size: 11px; } .admin-actions-grid { gap: 10px; grid-template-columns: 1fr; } .admin-action { padding: 12px 14px; } /* Slider track bottom nav: tuck closer to bottom edge */ .slider { bottom: 14px; padding: 3px; } .slider button { font-size: 9.5px; padding: 6px 11px; } } @media (max-width: 700px) { /* Detail-modal: full-screen on small screens. */ #detail-modal { padding: 12px; } .modal-card { padding: 18px; border-radius: 10px; } .modal-info h2 { font-size: 22px; } .modal-img { aspect-ratio: 1 / 1; padding: 10px; } .modal-grid { gap: 18px; } .rec-row .when { font-size: 10.5px; } .rec-row .when small { font-size: 8.5px; } } /* ============ Detail modal (atlas card expansion) ============ Click a bird card → this fades in over a soft ink backdrop with the full sketch, taxonomic info, Wikipedia summary, and a scrollable list of every past audio capture for the species. */ /* /about — a brief explainer popup floated over the index page. Reuses .modal-backdrop / .modal-close; opened by the #about hash. */ #about-modal { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 24px; pointer-events: none; opacity: 0; transition: opacity 200ms ease; } #about-modal[aria-hidden="false"] { pointer-events: auto; opacity: 1; } .about-card { position: relative; max-width: 432px; width: 100%; background: var(--paper); border-radius: 14px; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.6), 0 30px 80px rgba(26,22,18,0.24); padding: 34px 34px 28px; transform: translateY(10px) scale(0.985); transition: transform 260ms cubic-bezier(.32,.72,.32,1); } #about-modal[aria-hidden="false"] .about-card { transform: none; } .about-card .about-eyebrow { margin: 0 0 7px; font: italic 400 13px/1 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink-2); letter-spacing: 0.06em; } .about-card h2 { margin: 0 0 13px; font: 700 22px/1.25 ui-serif, "Iowan Old Style", Georgia, serif; letter-spacing: 0.01em; color: var(--ink); } .about-card .about-body { margin: 0; font: 400 14.5px/1.62 ui-serif, "Iowan Old Style", Georgia, serif; color: var(--ink-2); } /* Plain inline link — same ink as the prose, a simple underline, no hover treatment. */ .about-card .about-body a { color: inherit; text-decoration: underline; text-underline-offset: 2px; } .about-card .about-explore { margin-top: 20px; background: none; border: 0; padding: 0; font: 700 11px/1 ui-monospace, Menlo, monospace; letter-spacing: 0.08em; text-transform: lowercase; color: var(--ink-soft); cursor: pointer; transition: color 140ms ease; } .about-card .about-explore:hover { color: var(--ink); } #detail-modal { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 24px; pointer-events: none; opacity: 0; transition: opacity 220ms ease; } #detail-modal[aria-hidden="false"] { pointer-events: auto; opacity: 1; } .modal-backdrop { position: absolute; inset: 0; background: rgba(26,22,18,0.32); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); } .modal-card { position: relative; max-width: 920px; max-height: calc(100vh - 48px); width: 100%; background: var(--paper); border-radius: 14px; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.6), 0 30px 80px rgba(26,22,18,0.22); padding: 30px 34px 26px; overflow: auto; /* The expand-from-card animation is driven by inline transform and opacity set on open/close — see openDetailModal. CSS transition handles the in-between glide. */ transform-origin: 50% 50%; will-change: transform, opacity; } .modal-card.is-morphing { transition: transform 380ms cubic-bezier(.32,.72,.32,1), opacity 220ms ease; } /* The static aria-hidden=false rule used to override transform. It now conflicts with our inline FLIP morph — removed. The morph handler explicitly sets the destination transform. */ .modal-close { position: absolute; top: 16px; right: 16px; background: var(--paper-2); border: 0; width: 30px; height: 30px; border-radius: 999px; font: 16px/1 ui-monospace, Menlo, monospace; color: var(--ink); box-shadow: var(--raised); cursor: pointer; z-index: 1; } .modal-close:hover { color: var(--accent-2); } .modal-grid { display: grid; grid-template-columns: 1fr 1.15fr; gap: 32px; align-items: start; } @media (max-width: 700px) { .modal-grid { grid-template-columns: 1fr; gap: 22px; } .modal-card { padding: 22px; } } .modal-img { position: relative; background: var(--paper-2); border-radius: 10px; display: flex; align-items: center; justify-content: center; padding: 18px; aspect-ratio: 4 / 3; box-shadow: var(--recess); } .modal-img img { max-width: 100%; max-height: 100%; filter: drop-shadow(0 2px 6px rgba(26,22,18,0.10)); transition: opacity 220ms ease; } .modal-img img.swapping { opacity: 0; } /* Pose toggle — bottom-left corner of the modal sketch, icon strip. Same recipe as the time-window slider: recessed paper-2 well, a white sliding pill behind the active button (via .seg-pill), and a color shift on the icon itself. */ .pose-toggle { position: absolute; left: 10px; bottom: 10px; display: inline-flex; padding: 3px; background: var(--paper-2); border-radius: 999px; box-shadow: var(--recess); z-index: 2; } .pose-toggle button { background: transparent; border: 0; color: var(--ink-soft); width: 30px; height: 26px; padding: 0; border-radius: 999px; cursor: pointer; position: relative; z-index: 1; display: inline-flex; align-items: center; justify-content: center; transition: color 200ms ease; } .pose-toggle button svg { width: 13px; height: 13px; display: block; } .pose-toggle button:hover { color: var(--ink); } .pose-toggle button[aria-current="true"] { color: var(--ink); } .pose-toggle button .tip { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); white-space: nowrap; font: 9px ui-monospace, Menlo, monospace; letter-spacing: 0.14em; text-transform: lowercase; color: var(--ink-soft); background: var(--paper); padding: 3px 8px; border-radius: 4px; box-shadow: var(--raised); opacity: 0; pointer-events: none; transition: opacity 160ms ease; } .pose-toggle button:hover .tip { opacity: 1; } .pose-toggle[data-unavailable="true"] { display: none; } .pose-toggle button[data-unavailable="true"] { display: none; } .modal-info h2 { margin: 4px 36px 0 0; font: 700 26px/1.1 ui-serif, "Iowan Old Style", "Bookman Old Style", Georgia, serif; color: var(--ink); letter-spacing: 0.01em; } .modal-info .sci { font: italic 13.5px/1.3 ui-serif, Georgia, serif; color: var(--ink-2); margin: 5px 0 18px; } .modal-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 18px; } .modal-stats > div { background: var(--paper-2); border-radius: 6px; padding: 10px 12px; box-shadow: var(--recess); } .modal-stats .n { display: block; font: 800 17px/1 ui-serif, Georgia, serif; color: var(--ink); font-variant-numeric: tabular-nums; } .modal-stats .lbl { display: block; margin-top: 4px; font: 8.5px ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.12em; text-transform: uppercase; } .modal-info .desc { font: 13px/1.55 ui-serif, Georgia, serif; color: var(--ink); margin: 0 0 14px; } .modal-info .desc.placeholder { color: var(--ink-soft); font-style: italic; } .modal-meta { display: flex; flex-wrap: wrap; gap: 16px; font: 9.5px ui-monospace, Menlo, monospace; color: var(--ink-soft); } .modal-meta .k { letter-spacing: 0.12em; text-transform: uppercase; } .modal-meta .v { color: var(--ink); margin-left: 6px; } .modal-meta .v.rare { color: var(--accent-2); } .modal-recordings { margin-top: 24px; padding-top: 16px; box-shadow: inset 0 1px 0 rgba(26,22,18,0.07); } .modal-recordings .rec-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8px; } .modal-recordings h3 { font: 700 10px ui-monospace, Menlo, monospace; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink); margin: 0; border-left: 2px solid var(--accent-2); padding-left: 10px; } .modal-recordings .rec-count { font: 9px ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.06em; } .modal-recordings ol { list-style: none; padding: 0; margin: 0; max-height: 220px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; padding-right: 4px; } .rec-row { display: grid; grid-template-columns: 30px 1fr auto; gap: 10px; align-items: center; padding: 6px 10px; border-radius: 6px; background: var(--paper); box-shadow: var(--edge); transition: background 160ms ease; } .rec-row:hover { background: var(--paper-2); } .rec-row.expanded { grid-template-columns: 30px 1fr auto; } /* Spectrogram strip — inline below the row header. Click on the row toggles open/closed independent of playback so you can scan spectrograms visually without listening. */ .rec-spectro { grid-column: 1 / -1; margin-top: 8px; height: 0; overflow: hidden; border-radius: 4px; /* Paper background — the spectrogram canvas paints ink ON it, matching the sketch palette. */ background: var(--paper-2); box-shadow: var(--recess); position: relative; transition: height 240ms cubic-bezier(.7,.05,.2,1); } .rec-row.expanded .rec-spectro { height: 88px; } /* The spectrogram is rendered client-side onto a canvas — we fetch the mp3, decode via Web Audio, run an STFT, and paint columns with our ink palette. No BirdNET-Pi PNG involved. */ .rec-spectro canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; opacity: 0; transition: opacity 240ms ease; } .rec-spectro canvas.ready { opacity: 1; } .rec-spectro .rec-spectro-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--ink-soft); font: 9px ui-monospace, Menlo, monospace; letter-spacing: 0.14em; text-transform: lowercase; pointer-events: none; } /* Played portion — soft ink wash over the paper ground. */ .rec-spectro .rec-spectro-played { position: absolute; left: 0; top: 0; bottom: 0; width: 0%; background: linear-gradient(90deg, rgba(26,22,18,0.16), rgba(26,22,18,0.06)); pointer-events: none; } /* Cursor — sliding paper pill with a raised shadow, same recipe as the time-window slider's seg-pill. Pops against both the bright paper regions of the spectrogram and the inked vocalization bands. */ .rec-spectro .rec-spectro-cursor { position: absolute; top: 50%; left: 0; width: 14px; height: 20px; transform: translate(-50%, -50%); background: var(--paper); border-radius: 999px; box-shadow: 0 1px 3px rgba(26,22,18,0.30), inset 0 0 0 1px rgba(26,22,18,0.18); pointer-events: none; opacity: 0; } .rec-spectro .rec-spectro-cursor::after { content: ''; position: absolute; left: 50%; top: -100%; bottom: -100%; width: 1.5px; transform: translateX(-50%); background: rgba(251,247,238,0.85); box-shadow: 0 0 0 0.5px rgba(26,22,18,0.20); } .rec-spectro.armed .rec-spectro-cursor { opacity: 1; } .rec-spectro .rec-spectro-scrub { position: absolute; inset: 0; cursor: ew-resize; } .rec-row .play { background: var(--paper-2); border: 0; color: var(--ink); width: 24px; height: 24px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; box-shadow: var(--raised); cursor: pointer; padding: 0; } .rec-row .play svg { width: 8px; height: 8px; } .rec-row .play[data-active="true"] { background: var(--ink); color: var(--paper); } .rec-row .when { font: 11px/1.2 ui-monospace, Menlo, monospace; color: var(--ink); } .rec-row .when small { display: block; font-size: 9px; color: var(--ink-soft); letter-spacing: 0.06em; margin-top: 1px; } .rec-row .conf { font: 10px ui-monospace, Menlo, monospace; color: var(--ink-soft); letter-spacing: 0.06em; font-variant-numeric: tabular-nums; } .rec-empty { padding: 14px; font: 11px/1.5 ui-serif, Georgia, serif; color: var(--ink-soft); text-align: center; } .modal-actions { display: flex; gap: 8px; margin-top: 18px; } .modal-actions .chip { flex: 0 0 auto; background: var(--paper-2); border: 0; color: var(--ink); padding: 7px 12px; border-radius: 4px; font: 10px/1 ui-monospace, Menlo, monospace; text-transform: lowercase; letter-spacing: 0.12em; text-decoration: none; display: inline-flex; align-items: center; gap: 5px; box-shadow: var(--raised); transition: transform 160ms ease; } .modal-actions .chip:hover { transform: translateY(-1px); } .modal-actions .chip.ext::after { content: '↗'; opacity: 0.6; } /* Spectrogram appears on first audio play. Recoloured via CSS filter so BirdNET-Pi's default colour spectrogram comes out as a clean monochrome ink strip, matching the rest of the chart. Click to replay; a thin progress bar tracks playback in real time. */ .bird-card .spectro-wrap { position: relative; min-height: 0; margin: 6px 0 2px; cursor: pointer; overflow: hidden; border-radius: 3px; box-shadow: var(--recess); } .bird-card .spectro-wrap:empty { display: none; } .bird-card .spectro-wrap img { width: 100%; max-height: 44px; display: block; object-fit: cover; filter: grayscale(1) brightness(0.94) contrast(1.16); opacity: 0.78; transition: opacity 200ms ease; } .bird-card .spectro-wrap:hover img { opacity: 1; } /* Progress wash: a translucent ink veil that grows as the clip plays */ .bird-card .spectro-wrap::after { content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: var(--prog, 0%); background: linear-gradient(to right, rgba(26,22,18,0.30), rgba(26,22,18,0.05)); pointer-events: none; transition: width 60ms linear; } /* Playhead — thin vertical line at the leading edge */ .bird-card .spectro-wrap::before { content: ''; position: absolute; top: 0; bottom: 0; left: var(--prog, 0%); width: 1px; background: var(--ink); opacity: var(--playhead, 0); pointer-events: none; transition: opacity 200ms ease; } .bird-card[data-playing="true"] .spectro-wrap::before { --playhead: 0.6; } /* ============ AvianVisitors mode toggle ============ * Default install (body.av-local): hide the lock-row, show drawer items * directly when the menu opens. The PHP shim at /avian/api/menu.php * returns items immediately with no auth, so the live JS skips the lock * flow entirely. * * Forwarded install (body.av-forwarded): show the lock row so users * type the basic-auth password before items unlock. Pair with * AV_REQUIRE_AUTH=1 in php-fpm env + Caddy basic_auth on /avian/api/. * See avian/forwarding/. */ body.av-local #dd-locked { display: none; } body.av-local #dd-items { display: block; }