/* Tagged Out — same design system as the original hub:
   dark bark-brown ground, blaze orange #ff6b1a as the one loud color,
   photorealistic hunting-woodland camo (the real texture in camo.jpg)
   full strength only where no text sits; text lives on solid or
   heavily veiled surfaces. */
:root {
  color-scheme: dark;
  --bg: #171208;
  --surface: #211a10;
  --surface2: #2c2214;
  --text: #f3ede0;
  --dim: #b3a284;
  --border: #453723;
  --accent: #ff6b1a;
  --accent-dim: #9a4a12;
  --accent-ink: #1a0e02;
  --buck: #ffb03a;
  --danger: #ff7a6b;
  --camo-pattern: url(/camo.jpg);
  --radius: 8px;
  --font-display: "Barlow Condensed", "Arial Narrow", system-ui, sans-serif;
  --font-body: "Barlow", system-ui, sans-serif;
  /* Shell v189: the labeled filter rows' shared label column —
     one fixed width for every .club-label in a filter stack,
     sized to the widest label in the idiom (WIND FOR, ~84px at
     the label's 15.5px display cut) with a breath of room, so
     CLUB / CAMERAS / BUCK / DEER / WIND FOR rows all start
     their controls on the same left edge. */
  --label-col: 88px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* ---- app frame (shell v37) ----
   The shell is a true app frame, not a scrolling document with a bar
   floating over it. html/body are exactly the viewport tall (100dvh,
   so iOS chrome and viewport changes can't shift the frame) and NEVER
   scroll. body is a vertical flex column; #root takes the flexible
   middle and is itself a flex column — who-bar, brand band, then one
   .scroll region — and the tab bar is the frame's LAST element, in
   flow after #root. Only .scroll scrolls. The bar therefore sits at
   the bottom of the screen in every state — scrolling, rubber-band,
   keyboard open, rotation, full-page screenshots — structurally,
   because there is no other place in the frame it can be. Stephen's
   call (2026-10-27): the bar in his Camp screenshot sat mid-page and
   he wants it AT THE BOTTOM, not an explanation of the overlay. */
html, body { height: 100vh; height: 100dvh; height: var(--app-h, 100dvh); overflow: hidden; }
/* The var above is the frame's MEASURED height (shell v44): app.js
   writes --app-h on <html> from a live measurement at boot and on
   every viewport event, and it wins over the dvh declaration once
   set (100dvh stays as the pre-JS / no-JS fallback). Stephen's
   Alerts screenshot (2026-10-27 08:14) showed the bar riding ~a
   bar-height off the bottom minutes after it sat flush: three
   shell updates had force-reloaded his open app that morning, and
   iOS standalone webviews can latch a SHORT layout viewport for a
   document that arrives by programmatic reload — 100dvh then sizes
   the frame to that latched height and nothing ever re-measures.
   app.js's fitFrame is the re-measure; see its note. The bar's own
   construction is unchanged: last element of this flex frame, no
   positioning, home-indicator inset inside its own height. */
body { display: flex; flex-direction: column; }
#root { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }
body {
  background-color: var(--bg);
  background-image: linear-gradient(rgba(23,18,8,.84), rgba(23,18,8,.84)), var(--camo-pattern);
  background-size: auto, 480px;
  color: var(--text);
  font-family: var(--font-body);
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
}
/* The boot placeholder is NOT styled here (v180): it carries the
   swinging ear tag and no text (Stephen: "why does it have to say
   loading now"), and its rules live with the splash's own in
   index.html's inline block, so the swing's first frame is
   painted. The v179 boot-line rules that stood here (the line
   held invisible until the fonts gate settled, with its CSS-only
   self-reveal timer) died with the words they protected — there
   is no boot text left to swap faces, and the reveal class they
   keyed on has no other consumer.
   v182 hardens this into law: NO boot geometry lives here at
   all — no position, size, or animation for the boot or its
   children may come from this file (dom-safearea pins it). The
   v181 boot sized itself from this file's frame rules and the
   app-height variable app.js writes after the cut, and the tag
   visibly re-laid itself out mid-swing when those landed; the
   boot's whole geometry is now pinned in the document's own
   critical styles, identical whether this file has arrived
   or not. */
h1, h2, h3, .display { font-family: var(--font-display); text-transform: uppercase; letter-spacing: .03em; margin: 0; }
button { font-family: inherit; cursor: pointer; }
img { max-width: 100%; display: block; }

/* ---------- brand / camo bands ---------- */
.brand-band {
  background-image: linear-gradient(rgba(23,18,8,.30), rgba(23,18,8,.55)), var(--camo-pattern);
  background-size: cover;
  border-bottom: 3px solid var(--accent);
  padding: 18px 16px 14px;
  display: flex; align-items: center; gap: 12px;
  flex: none; /* a fixed piece of the app frame's top, never shrunk */
}
.brand-band img.tag { width: 44px; height: 44px; filter: drop-shadow(0 2px 4px rgba(0,0,0,.6)); }
/* On the sign-in / setup screens the brand band is the FIRST element
   on screen — no who-bar above it — so there it must clear the iOS
   status bar itself: the camo runs full-bleed under the translucent
   bar, the wordmark sits below the top safe-area inset. In the
   signed-in shell the who-bar is first and already carries the
   inset; padding the band there too would open a double gap. */
#root > .brand-band:first-child { padding-top: calc(18px + env(safe-area-inset-top)); }
.wordmark { font-family: var(--font-display); font-size: 34px; font-weight: 700; text-transform: uppercase; line-height: 1; color: #fff; text-shadow: 0 2px 8px rgba(0,0,0,.8); }
.wordmark span { color: var(--accent); }
.brand-sub { color: #e8dcc2; font-size: 14.5px; text-shadow: 0 1px 4px rgba(0,0,0,.9); }

/* ---------- layout ---------- */
/* The tab column's 12px inset is load-bearing: the Today
   conditions grid's cell budgets (the v127 readability sizes)
   are measured against it — do not widen. Shell v189 lines the
   filter stack up INSIDE this column instead: the club row
   above the scroll region takes the same 12px inset (below). */
.wrap { max-width: 560px; margin: 0 auto; padding: 0 12px 28px; }
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; margin-top: 12px; }
.panel h2 { font-size: 24px; font-weight: 600; }
.panel h3 { font-size: 19px; font-weight: 600; }
.sub { color: var(--dim); font-size: 15px; margin-top: 2px; }
/* Camp folds (shell v98): a full-width tap row in the panel-head
   voice — title at h3 size, caret at the right edge. The body
   under it stays in the page but hidden until tapped open. */
.fold-head { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 10px; background: none; border: 0; padding: 2px 0; min-height: 44px; color: var(--text); text-align: left; }
.fold-title { font-size: 19px; font-weight: 600; }
.fold-caret { font-size: 17px; color: var(--dim); flex: none; }
.fold-body { margin-top: 8px; }
.fold-body[hidden] { display: none; }
.fold-block { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 10px; }
.call-panel {
  background-image: linear-gradient(rgba(23,18,8,.88), rgba(23,18,8,.88)), var(--camo-pattern);
  background-size: cover;
  border: 1px solid var(--accent-dim);
}
.section-title { font-size: 26px; font-weight: 600; margin: 20px 2px 0; }

/* ---------- conditions (shell v106) ---------- */
/* One card, weather on top (Stephen's approved v9 look): a hero
   — temperature big, condition words under it — over a compact
   4×2 mini-grid carrying every metric at a glance. Fixed grid,
   no horizontal scrolling anywhere in the header. */
.wx-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; margin-top: 12px; }
.wx-hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.wx-temp { font-family: var(--font-display); font-size: 76px; font-weight: 700; line-height: .95; }
.wx-cond { font-size: 17px; color: var(--text); margin-top: 2px; line-height: 1.15; }
.wx-icon { flex: none; width: 46px; height: 46px; margin-top: 6px; }
.wx-icon svg { display: block; width: 100%; height: 100%; }
.wx-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; margin-top: 12px; }
/* Shell v124: the LEGAL LIGHT cell spans the grid's full width
   under the eight metric cells — the 4x2 set keeps its
   proportions and the ninth fact gets a row of its own rather
   than stranding a lone narrow cell on a third row. */
.wx-cell-wide { grid-column: 1 / -1; }
.wx-light-halves { display: flex; gap: 5px; }
.wx-half { flex: 1; min-width: 0; }
.wx-cell { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 6px; min-width: 0; }
.wx-cell .k { font-size: 13px; font-weight: 700; letter-spacing: .08em; line-height: 1.15; color: var(--dim); }
.wx-cell .v { font-family: var(--font-display); font-size: 16.5px; font-weight: 700; line-height: 1.15; }
.wx-cell .v.fit { font-size: 14.5px; }
.wx-cell .v.fit2 { font-size: 14px; }
.wx-cell .s { font-family: var(--font-display); font-size: 13px; font-weight: 500; letter-spacing: .02em; color: var(--dim); line-height: 1.15; }
.wx-cell .s.fit { font-size: 13px; }
.wx-cell .s .st { display: block; }

/* ---------- call windows ---------- */
.range-row { display: flex; gap: 6px; margin-top: 10px; }
/* Shell v106 (Stephen: "Make tomorrow fit in the button
   better"): the four range labels sit on ONE line at phone
   widths — nowrap, a size the longest label (TOMORROW) clears
   with room at 360px, no letter-spacing eating the margin. */
.range-btn { flex: 1; min-width: 0; white-space: nowrap; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 8px 1px; font-family: var(--font-display); font-size: 15.5px; text-transform: uppercase; }
.range-btn.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 700; }
.win-row { display: flex; gap: 10px; margin-top: 10px; }
.win-box { flex: 1 1 0; min-width: 0; background: rgba(33,26,16,.92); border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; }
.win-when { font-family: var(--font-display); font-size: 15px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: var(--dim); }
.win-score { font-family: var(--font-display); font-size: 40px; font-weight: 700; line-height: 1; margin-top: 2px; }
.win-grade { font-family: var(--font-display); font-size: 18px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--accent); }
.win-hrs { font-size: 14.5px; color: var(--dim); margin-top: 2px; }
.win-why { font-size: 14.5px; line-height: 1.3; color: var(--text); margin-top: 6px; }
.grade { font-family: var(--font-display); font-size: 20px; font-weight: 700; color: var(--accent); text-transform: uppercase; }
.win-busy { margin-top: 6px; font-size: 15px; font-weight: 700; color: var(--buck); }
.win-busy small { display: block; font-size: 13px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--dim); }
.legal-light { margin-top: 10px; font-size: 16px; font-weight: 700; }
.legal-light .basis { display: block; font-size: 14px; font-weight: 400; color: var(--dim); margin-top: 2px; }
.best-area { margin-top: 10px; font-size: 15px; }
.best-area b { color: var(--buck); }

/* ---------- photos / cards ---------- */
.photo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 12px; }
.photo-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; text-align: left; padding: 0; color: var(--text); }
.photo-card img { width: 100%; height: 128px; object-fit: cover; }
.photo-card .meta { padding: 8px 10px; }
.photo-card .what { font-family: var(--font-display); font-size: 18px; text-transform: uppercase; line-height: 1.05; }
.photo-card .where { color: var(--dim); font-size: 15px; }
.buck-card { display: flex; gap: 12px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px; margin-top: 10px; text-align: left; color: var(--text); width: 100%; }
/* The Alerts size is the app-wide FLOOR for sighting photos
   (Stephen, 2026-10-27: "Make sure all pages photos are at least
   this big") — no page may render a sighting photo smaller than
   Alerts. The floor is declared once, on the .ph box below.
   The photo BOX (shell v41 — Stephen's Alerts screenshot: slow
   photos rendered as thin slivers because the img owned its box
   and an unloaded img has no size of its own). Geometry now lives
   on this container and NEVER on the img: a fixed 42%-width,
   true-16:9 box that measures the same whether the photo is
   loaded, loading, or dead. The img absolutely fills the box;
   until it lands (and if it never does) the box shows the bark
   surface — and the phase-1 fallback panel fills it the same way. */
.ph { position: relative; display: block; flex: none; width: 42%; aspect-ratio: 16 / 9; border-radius: 6px; overflow: hidden; background: var(--surface2); }
.ph img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Full-width photo (shell v96 — Stephen, 2026-10-29: "it's hard
   to see the photos for older hunters"; Reveal shows each photo
   full screen width, stacked, caption under it). The .ph-full
   variant keeps every other .ph property — container-owned
   16:9 geometry, img absolutely filling it — and widens the box
   to 100% of its card. v96 used it in Today + Alerts only;
   shell v97 (Stephen: "What about the animals and pattern
   tabs") extends it to every photo card: the buck cards
   (.buck-card-stack), the Alerts visit rows (.alert-row), the
   Animals proposal / linked-photo cards (.proposal) and the
   Patterns stand target (.stand-buck). The base 42% .ph box
   above stays as the shared box definition the variant widens.
   (Shell v130: the Patterns stand card dropped its photo —
   text-only now, see the stands section below. Every other
   surface named here keeps the full-width photo.) */
.ph.ph-full { width: 100%; }
/* Tracked-buck card, stacked (shell v96 for Today's board;
   shell v97 the ONLY buck-card form — Animals renders it too):
   photo on top at full card width, name/stats below in
   .card-body. The card clips its corners so the edge-to-edge
   photo follows the radius. */
.buck-card.buck-card-stack { flex-direction: column; align-items: stretch; padding: 0; overflow: hidden; gap: 0; }
.buck-card.buck-card-stack .ph { border-radius: 0; }
.buck-card.buck-card-stack .card-body { padding: 10px 12px 12px; text-align: left; }
/* Split board card (shell v111 — Stephen, 2026-10-30: show
   both the best buck and the most recent): the photo band
   splits into two halves — best buck left, latest right —
   each photo still true 16:9 in its half-width box (the .ph
   geometry, widened to its half), so the band stands about
   half as tall as the single full-width photo. A hairline of
   the card's own surface separates the two photos. Each
   photo wears a tag in the blaze pill the app already
   uses for its target tag — "Best" on the left, "Most
   recent" on the right (shell v113; the right tag read
   "Latest here" in v111–v112 and the left wore none).
   Under the band, one
   caption per side in the card's own name/rack styles, then
   the best buck's card body as usual. */
.buck-card-split .split-band { display: flex; gap: 2px; }
.buck-card-split .split-half { position: relative; display: block; flex: 1 1 0; min-width: 0; }
.buck-card-split .split-half .ph { width: 100%; }
.buck-card-split .split-tag { position: absolute; top: 6px; right: 6px; background: var(--accent); color: var(--accent-ink); font-weight: 700; font-size: 13.5px; border-radius: 999px; padding: 1px 8px; }
.buck-card-split .split-caps { display: flex; gap: 2px; }
.buck-card-split .split-cap { flex: 1 1 0; min-width: 0; padding: 6px 12px 12px; text-align: left; }
/* Shell v126: the board card's camera headline — the camera
   name directly under the photo at the buck-name size, so the
   camera is the first words read on every board card. Single
   cards take their inset from .card-body; split cards pad the
   headline itself (their captions carry the bottom inset). */
.buck-card .cam-head { font-family: var(--font-display); font-size: 22px; text-transform: uppercase; line-height: 1; margin-bottom: 4px; }
.buck-card-split .cam-head { padding: 10px 12px 0; }
.buck-card .nm { font-family: var(--font-display); font-size: 22px; text-transform: uppercase; line-height: 1; }
.buck-card .stats { color: var(--buck); font-weight: 600; font-size: 16px; margin-top: 2px; }
.buck-card .seen { color: var(--dim); font-size: 15px; }
/* Animals identity strip (shell v129 — Stephen, 2026-10-31:
   "This gets lost and it's confusing what deer it is for").
   Each tracked buck's Animals block is wrapped in .buck-block
   and led by this strip — his name + short rack line on a
   SOLID bar (words never sit on photo or camo). The strip is
   sticky at top:0 of the .scroll region — the frame's only
   scroll container, with nothing else pinned inside it, so
   top:0 sits flush at the scroll area's top edge. Sticky is
   contained by the .buck-block parent: the strip pins while
   any part of his block is on screen and is pushed off by
   his block's end, where the next buck's strip takes over.
   In flow, never an overlay — at rest it reads as the block's
   heading bar, with the card's own margin below it. The line
   is one line at any width: nowrap + hidden overflow +
   ellipsis on the inner line, so a long name truncates
   instead of wrapping. z-index keeps the stuck strip above
   the block's photos as they slide under it. */
.buck-strip { position: sticky; top: 0; z-index: 6; margin-top: 14px; background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius); padding: 7px 12px; font-family: var(--font-display); font-size: 19px; font-weight: 600; letter-spacing: .03em; text-transform: uppercase; line-height: 1.15; }
.buck-strip-line { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.buck-strip-rack { color: var(--buck); }
.tagline { display: inline-block; background: var(--surface2); border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px; font-size: 13.5px; color: var(--dim); margin-top: 4px; }
.tagline.target { color: var(--accent-ink); background: var(--accent); border-color: var(--accent); font-weight: 700; }
.why-line { display: block; background: none; border: none; color: var(--buck); font-size: 15px; font-weight: 600; padding: 8px 2px 2px; text-align: left; }
.why-body { margin-top: 6px; }
.why-body[hidden] { display: none; }

/* ---------- stands ---------- */
/* One card per stand: name big, the target buck's name + rack
   line in the .stand-buck block, then the two plain lines a
   guide reads to a client — what to look for, and how he's
   been coming lately. Shell v130: TEXT ONLY — the card carries
   no photo and no photo tap (through v129 the .stand-buck
   button carried the target's photo; Stephen found the stand
   photos duplicative of Animals/Alerts and burying the travel
   visual, which now leads the tab). The .stand-buck rule below
   is unchanged — it now styles the plain name/rack div. */
.stand-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; margin-top: 10px; }
.stand-name { font-family: var(--font-display); font-size: 24px; text-transform: uppercase; line-height: 1.05; }
.stand-buck { display: flex; flex-direction: column; gap: 8px; background: none; border: none; padding: 8px 0 2px; text-align: left; color: var(--text); width: 100%; align-items: stretch; }
/* The stand target's photo (shell v97): on top of the button at
   the full width of the stand card's content (.ph-full, true
   16:9), his name and rack line below it — the same big-photo
   treatment as the other tabs (v96/v97). A guide holds this
   card up to a client, and it already carried the data-photo
   viewer tap. (Through v96 the photo rode beside the name at
   the old 42% Alerts geometry.) RETIRED in shell v130: stand
   cards render no photo at all — the words above are history,
   kept so the v97 pins in the test headers still read true. */
/* A sighting photo still loading — or one whose file is gone, in the
   instant before the app-wide fallback swaps it — sits on the bark
   surface, never on the browser's broken-image presentation. */
img[src*="/photos/"] { background: var(--surface2); }
.stand-buck .nm { font-family: var(--font-display); font-size: 22px; text-transform: uppercase; line-height: 1; color: var(--buck); }
.stand-buck .stats { color: var(--buck); font-weight: 600; font-size: 16px; margin-top: 2px; }
.stand-line { font-size: 15.5px; line-height: 1.4; margin-top: 6px; }
.stand-line b { color: var(--buck); }
.stand-small { color: var(--dim); font-size: 15px; margin-top: 5px; }

/* ---------- travel maps ---------- */
/* Schematic camera maps: real relative GPS positions, big names and
   times, the picture plus at most two short lines of words. */
.travel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; margin-top: 8px; }
.travel-pick { display: flex; align-items: center; gap: 10px; margin: 12px 0 2px; }
.travel-head { font-family: var(--font-display); font-size: 17px; font-weight: 600; text-transform: uppercase; letter-spacing: .03em; color: var(--dim); }
.travel-call { font-size: 16.5px; font-weight: 600; line-height: 1.35; margin: 6px 0 8px; }
.travel-note { color: var(--dim); font-size: 15.5px; line-height: 1.4; margin: 6px 0 2px; }
.travel-back { color: var(--dim); font-size: 15px; margin: 8px 0 0; }
/* Labeled fact lines (shell v154, Stephen 2026-11-01 — his
   approved tidy mockup of the Patterns travel panel): small
   amber caps label on the left, the big cream answer on the
   right, one fact per line; a muted evidence note rides under
   its line, indented to the value column. The Patterns saved-
   deer panel and the Animals travel block share this shape. */
.fact-rows { margin: 4px 0 2px; }
.fact-row { display: flex; align-items: baseline; gap: 12px; margin: 8px 0; }
.fact-label { flex: 0 0 104px; font-family: var(--font-display); font-size: 14px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--buck); }
.fact-value { font-size: 17.5px; font-weight: 700; line-height: 1.3; }
.fact-note { margin: 0 0 2px 116px; color: var(--dim); font-size: 14.5px; line-height: 1.35; }
.travel-mapwrap { position: relative; width: 100%; aspect-ratio: 1 / 1; border-radius: 8px; overflow: hidden; background: #1b150c; border: 1px solid #453723; user-select: none; -webkit-user-select: none; }
/* Zoom + pan (shell v143): ONE transformed layer — the tiles and
   the SVG both ride inside .travel-mapview, and the gesture code
   only ever sets this element's transform, so pins, cones, arcs
   and labels can never drift apart. Scroll discipline (shell
   v185): the wrap is touch-action: none at EVERY state, so a
   hold-and-drag pans the map in both axes at any scale, the
   fitted view included — the v143 split (pan-y at rest handing
   vertical flicks to the page, drags only once zoomed) left the
   resting map undraggable once v184 put every resting view at
   k = 1. The page scrolls from the content around the panel. */
.travel-mapwrap { touch-action: none; }
.travel-mapwrap.map-zoomed { cursor: grab; }
.travel-mapview { position: absolute; inset: 0; transform-origin: 0 0; will-change: transform; }
.travel-tiles { position: absolute; inset: 0; }
.travel-tile { position: absolute; width: 256px; height: 256px; display: block; border: 0; }
.travel-map { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.map-zoomctl { position: absolute; right: 8px; bottom: 8px; z-index: 3; display: flex; flex-direction: column; gap: 6px; align-items: stretch; }
.map-zbtn { min-width: 44px; height: 44px; padding: 0 10px; border-radius: 10px; border: 1px solid #5a4630; background: rgba(15, 10, 4, .92); color: #f3ede0; font-family: var(--font-body); font-size: 24px; font-weight: 700; line-height: 1; cursor: pointer; }
.map-zbtn.map-zreset { height: 38px; font-size: 13.5px; font-weight: 600; color: #e8dcc2; }
.map-zbtn[disabled] { opacity: .4; cursor: default; }
.travel-attr { color: var(--dim); font-size: 13.5px; margin: 4px 0 0; }
/* Scent cones (shell v104): blaze-orange, translucent, drawn
   under the arcs, dots and pills — a wash of where scent
   carries, never a cover over a name or a number. The one
   wind line under the map reads in the travel block's voice. */
.travel-map .tv-cone { fill: #ff6b1a; opacity: .26; }
.scent-line { font-size: 15.5px; font-weight: 600; line-height: 1.35; margin: 8px 0 2px; }
.travel-map text { font-family: var(--font-body); paint-order: stroke; stroke: #171208; stroke-width: 3px; stroke-linejoin: round; }
.travel-map .tv-name { font-size: 13.5px; font-weight: 700; fill: #f3ede0; }
.travel-map .tv-count { font-size: 13px; font-weight: 800; fill: #241708; paint-order: normal; stroke: none; }
.travel-map .tv-leg { font-size: 13px; font-weight: 600; fill: #e8dcc2; }
.travel-map .tv-north { font-size: 13px; font-weight: 700; fill: #d8c9a3; }
.travel-map .tv-pill { fill: rgba(15, 10, 4, .92); stroke: #5a4630; stroke-width: 1; }
.travel-map .tv-legpill { fill: rgba(15, 10, 4, .9); stroke: #5a4630; stroke-width: 1; }
.travel-map .tv-leader { stroke: #e8dcc2; stroke-width: 1.2; opacity: .65; }
.route-list { margin: 2px 0 10px; }
.route-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 7px 2px; border-bottom: 1px solid var(--border); }
.route-row:last-child { border-bottom: none; }
.route-names { font-size: 16.5px; font-weight: 600; line-height: 1.3; }
.route-meta { font-size: 15.5px; color: var(--dim); white-space: nowrap; }
.lg-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin: 0 4px 0 10px; vertical-align: baseline; }

/* ---------- alerts ---------- */
/* Stephen, 2026-10-27: the river's photos and rows were "super
   small" — the 42%-beside-text enlargement dates from then.
   Shell v96 (Stephen, 2026-10-29, older hunters / Reveal
   comparison) supersedes the width for THIS river only: each
   visit row now leads with its photo at the FULL row width in
   the photo's own 16:9 shape — the camera frames are 16:9, so
   object-fit: cover shows the full frame, nothing cropped
   away — with the title and time/count lines below it. Fewer
   rows per screen is the owner's explicit call: one glance,
   big words, big photos. */
/* Shell v96: the row is now a COLUMN — full-width photo on top
   (.ph-full above), caption below — instead of photo-beside-text.
   Padding, border, title/time sizes are unchanged. */
.alert-row { display: flex; flex-direction: column; gap: 12px; align-items: stretch; padding: 14px 0; border-bottom: 1px solid var(--border); }
.alert-row:last-child { border-bottom: none; }
.alert-row > div { flex: 1; min-width: 0; }
.alert-row .alert-body { flex: none; width: 100%; min-width: 0; }
.alert-row.tappable { cursor: pointer; }
.alert-row.tappable:active { background: rgba(255, 255, 255, 0.05); }
/* The visit a notification tap landed on: blaze-orange frame so the
   eye finds it after the scroll, on any camera's row. */
.alert-row.deep-hit { background: rgba(255, 107, 26, 0.10); box-shadow: inset 0 0 0 2px var(--accent); border-radius: 6px; padding-left: 8px; padding-right: 8px; }
.deep-note { margin-bottom: 10px; }
.alert-row .t { font-weight: 700; font-size: 20px; line-height: 1.15; }
.alert-row .w { color: var(--dim); font-size: 15.5px; line-height: 1.35; margin-top: 2px; }
.buck-status { color: var(--buck); font-weight: 600; font-size: 15.5px; margin-top: 2px; }
/* Alerts day divider (shell v86): a thin blaze line across the
   river wherever the lease day changes, the day marked on it —
   small and quiet next to the 20px rows it separates. The label
   sits on the panel's own solid ground, never on camo. */
.day-divider { display: flex; align-items: center; gap: 10px; margin: 16px 0 2px; }
.day-divider::before, .day-divider::after { content: ''; flex: 1; height: 2px; border-radius: 1px; background: var(--accent); opacity: 0.55; }
.day-divider span { font-family: var(--font-display); font-size: 15px; font-weight: 700; letter-spacing: 0.04em; color: var(--accent); white-space: nowrap; }

/* ---------- unmatched bucks ---------- */
.buck-card.unmatched { cursor: default; flex-direction: column; align-items: stretch; padding: 0; overflow: hidden; gap: 0; }
/* Unmatched bucks live on Animals only — they left Today entirely
   (best-bucks purity, 2026-10-27), so no Today row rules remain. */
.buck-card .u-photo { position: relative; padding: 0; border: none; background: var(--surface2); flex: none; width: 42%; aspect-ratio: 16 / 9; border-radius: 6px; overflow: hidden; }
.buck-card .u-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Shell v97 (Stephen: "What about the animals and pattern tabs"):
   the unmatched card stacks like every other photo card — the
   photo on top at the FULL card width in its true 16:9 shape,
   the name / camera / status / buttons below in .u-body. This
   supersedes the 2026-10-27 fill-the-row geometry (photo at the
   42% floor stretched to the card's full height, from his "photo
   smaller than the row" complaint): stacked, there is no row
   beside the photo to leave dead space under, and the photo is
   far bigger than the floor ever made it. The base .u-photo
   rule above keeps declaring the standalone 16:9 box; only
   inside the unmatched card does the width widen to 100%. */
.buck-card.unmatched .u-photo { width: 100%; aspect-ratio: 16 / 9; border-radius: 0; }
/* The action buttons keep the compact scoped size settled in
   2026-10-27 (13px, tighter padding, 6px gap — one line, ~37px
   tap targets, never blocks). Under the stacked photo the body
   has the full card width, so Name him / Match him / Disregard
   (v80) sit on one line with room to spare; the wrap stays as a
   safety net for narrow screens. */
.buck-card.unmatched .u-actions { gap: 6px; flex-wrap: wrap; }
.buck-card.unmatched .u-actions .btn { padding: 9px 10px; font-size: 14.5px; }
.buck-card .u-body { flex: 1; min-width: 0; padding: 10px 12px 12px; }
.u-actions { display: flex; gap: 8px; margin-top: 10px; align-items: flex-start; }

/* ---------- animals tab: ONE photo treatment — full width ----------
   Stephen, 2026-10-27: "I want all of the photos on animals the
   size of photos on alerts page." Through v96 that size WAS the
   Alerts geometry (42% of the card width, 16:9); shell v97
   (Stephen: "What about the animals and pattern tabs") raises
   Animals to the treatment Alerts itself got in v96: every photo
   on the tab — tracked buck cards, unmatched New buck cards,
   proposal and linked photos — stacks on top of its card at the
   full card width in the true 16:9 shape, words and buttons
   below. The geometry lives on each card family's own rules
   above; this section keeps the tab's shared action sizing. */
/* Compact card actions (same 2026-10-27 fix): one line, natural
   height, never stretched and never wide enough to wrap — the
   buttons serve the photo, they don't size the card. flex: none
   + nowrap keeps "Name him" / "Match him" on one line at their
   natural width. */
.u-actions .btn, .u-form-btns .btn { padding: 9px 12px; font-size: 15px; white-space: nowrap; flex: none; }
.u-form { margin-top: 10px; }
.u-form select {
  width: 100%; margin-top: 6px; background: var(--surface2); border: 1px solid var(--border);
  border-radius: 6px; color: var(--text); padding: 12px; font-size: 17px;
}
.u-form-btns { display: flex; gap: 8px; margin-top: 8px; }

/* ---------- hunter numbers: provenance + fix editor ---------- */
.stats-src { color: var(--dim); font-size: 14.5px; margin-top: 2px; }
.stats-form { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.stats-form .sf { display: flex; flex-direction: column; gap: 4px; font-size: 14.5px; color: var(--dim); }
.stats-form input {
  width: 100%; background: var(--surface2); border: 1px solid var(--border);
  border-radius: 6px; color: var(--text); padding: 10px; font-size: 17px;
}
.stats-form .u-form-btns, .stats-form .err { grid-column: 1 / -1; }
.stats-form .sub { grid-column: 1 / -1; margin: 2px 0 0; }
/* Shell v134: the Name him form carries the same number grid
   under its name input, plus the one confirm toggle. */
.u-form .stats-form { margin-top: 8px; }
.u-confirm { display: flex; align-items: center; gap: 8px; margin-top: 10px; font-size: 15px; color: var(--text); }
.u-confirm[hidden] { display: none; }
.u-confirm input { width: 20px; height: 20px; flex: none; margin: 0; accent-color: var(--accent); }

/* ---------- buck photo proposals ---------- */
.proposal { display: flex; flex-direction: column; gap: 0; align-items: stretch; background: var(--surface); border: 1px solid var(--accent-dim); border-radius: var(--radius); padding: 0; overflow: hidden; margin-top: 8px; }
/* Shell v97: the proposal / linked-photo card stacks like every
   other photo card — photo on top at full card width (app.js
   renders it .ph-full, true 16:9, square corners against the
   card's clipped radius), the question and the answer buttons
   below in .proposal-body. This supersedes the 2026-10-27
   fill-the-row geometry (photo at 42% stretched to the card's
   full content height): the hunter is deciding from this photo,
   and full width is the biggest, clearest read of it. A proposal
   with NO photo (the match-conflict card) is a single body in
   the same column — it renders exactly as before. */
.proposal .ph { border-radius: 0; }
/* Stephen, 2026-10-27: the proposal photo was a small 72x54 box he
   couldn't tap. It carries the data-photo tap wiring (app.js),
   so it opens the viewer like every other photo. */
.proposal img { cursor: pointer; }
.proposal-body { flex: 1; min-width: 0; padding: 10px 12px 12px; }
.proposal-q { font-weight: 700; font-size: 18px; line-height: 1.2; }
.proposal .w { color: var(--dim); font-size: 15.5px; margin-top: 2px; }
.proposal-btns { display: flex; gap: 8px; margin-top: 10px; align-items: flex-start; }
.proposal-btns .btn { padding: 9px 12px; font-size: 15px; white-space: nowrap; flex: none; }

/* ---------- patterns ---------- */
table.facts { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 15.5px; }
table.facts td { padding: 6px 4px; border-bottom: 1px solid var(--border); }
table.facts td:first-child { color: var(--dim); }
table.facts tr:last-child td { border-bottom: none; }

/* ---------- forms / auth ---------- */
.auth-wrap { max-width: 460px; margin: 0 auto; padding: 0 14px 40px; }
.name-pick { display: grid; gap: 10px; margin-top: 14px; }
.name-btn { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); padding: 14px; font-family: var(--font-display); font-size: 22px; text-transform: uppercase; text-align: left; }
.name-btn.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.name-btn .role { display: block; font-family: var(--font-body); font-size: 14.5px; color: var(--dim); text-transform: none; letter-spacing: 0; }
label.f { display: block; margin-top: 14px; font-size: 15px; color: var(--dim); }
input.code, input[type="text"], input[type="password"], input[type="email"] {
  width: 100%; margin-top: 6px; background: var(--surface2); border: 1px solid var(--border);
  border-radius: 6px; color: var(--text); padding: 12px; font-size: 17px;
}
.btn { display: inline-block; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 12px 18px; font-size: 16px; text-decoration: none; text-align: center; }
.btn-accent { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 700; }
.btn-wide { width: 100%; margin-top: 16px; }
.err { background: #3a1710; border: 1px solid var(--danger); color: #ffd9d3; border-radius: 6px; padding: 10px 12px; margin-top: 12px; font-size: 15px; }
.ok-note { background: #233014; border: 1px solid #5a7038; border-radius: 6px; padding: 10px 12px; margin-top: 12px; font-size: 15px; }
/* The topmost bar of the signed-in shell (Stephen, 2026-10-27: it
   "is sitting high in the screen" — overlapping the clock/battery).
   viewport-fit=cover + the translucent status bar lay the page out
   UNDER the bar, so the bar's own solid surface extends up behind
   it while its content clears the notch via the top inset. */
.who-bar { display: flex; flex: none; justify-content: space-between; align-items: center; gap: 10px; background: var(--surface); border-bottom: 1px solid var(--border); padding: calc(8px + env(safe-area-inset-top)) 14px 8px; font-size: 15px; }
.who-bar b { color: var(--accent); }
/* Owner member preview (shell v141): the bar rides every render
   while a preview is open — blaze orange so it can never be
   mistaken for the member's own app — with the one-tap way back. */
.preview-bar { display: flex; flex: none; justify-content: space-between; align-items: center; gap: 10px; background: var(--accent); color: var(--accent-ink); padding: 8px 14px; font-size: 15px; font-weight: 600; }
.preview-bar .btn { flex: none; }
.preview-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 6px 0; font-size: 16px; }
.chip-btn { background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 999px; padding: 8px 14px; font-size: 14.5px; min-height: 40px; display: inline-flex; align-items: center; justify-content: center; }
.chip-btn.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 700; }

/* ---------- club selector ---------- */
/* Shell v55: shown only to a viewer with 2+ clubs. A big native
   dropdown in the header region (never inside the scrolling
   region, never combined with another club's name) — one club is
   active, and every tab below it shows that club's data only. */
/* Shell v189: the row's horizontal inset matches the tab
   column (.wrap, 12px) so the CLUB select shares both edges
   with the filter rows under it in every tab's stack. */
.club-row { flex: none; display: flex; align-items: center; gap: 10px; padding: 10px 12px 2px; }
.club-label { font-family: var(--font-display); font-size: 15.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--dim); flex: 0 0 var(--label-col); width: var(--label-col); white-space: nowrap; }
.club-select { flex: 1 1 auto; min-width: 0; min-height: 44px; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; font-family: var(--font-display); font-size: 20px; letter-spacing: .03em; text-transform: uppercase; }
.club-select:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---------- camera filter ---------- */
.cam-filter { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 12px; }
/* Shell v189: a LABELED .cam-filter row (Animals' Buck row) joins
   the filter stack's lineup — its chip-row gap is 6px where the
   labeled family (.club-row / .cam-drop-row / .travel-pick)
   breathes at 10px, so the select makes up the 4px difference
   and its left edge lands on the stack's shared column edge. */
.cam-filter > .club-label + .club-select { margin-left: 4px; }
.cam-scope { color: var(--dim); font-size: 14.5px; }
.cam-pick { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.cam-pick-actions { display: flex; gap: 6px; width: 100%; }
/* The Patterns visual's camera dropdown (shell v138): the select
   that moved onto the travel panel — a full-width toggle carrying
   the current scope, and under it real checkbox rows with big
   tap targets, one per camera in the club. */
.cam-drop-row { flex-wrap: nowrap; align-items: flex-start; gap: 10px; }
.cam-drop-row .club-label { padding-top: 12px; }
.cam-drop { flex: 1 1 auto; min-width: 0; }
/* Shell v189: no font-size here — the toggle carries .club-select,
   whose display font at 20px, letter-spacing and uppercase
   transform ARE the bar's type treatment (the selects' family).
   This rule only lays the bar out: full width, scope left and
   chevron right, 44px tall like the selects beside it. */
.cam-drop-toggle { width: 100%; justify-content: space-between; min-height: 44px; }
.cam-drop-panel { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.cam-drop-done { width: 100%; justify-content: center; min-height: 44px; font-size: 16px; margin-top: 2px; }
.cam-drop-quick { display: flex; flex-wrap: wrap; gap: 6px; }
.cam-check { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 6px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; font-size: 16px; }
.cam-check input { width: 22px; height: 22px; flex: 0 0 auto; accent-color: var(--accent); }

/* ---------- sharing ---------- */
.share-cam { padding: 12px 0 2px; border-bottom: 1px solid var(--border); }
.share-cam:last-child { border-bottom: none; }
.share-cam-name { font-family: var(--font-display); font-size: 19px; text-transform: uppercase; letter-spacing: .03em; }
.share-cam-name .own { font-family: var(--font-body); font-size: 14.5px; color: var(--dim); text-transform: none; letter-spacing: 0; }
.share-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 7px 0; font-size: 16px; }
.share-row select { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); padding: 10px 12px; font-size: 16px; min-height: 44px; min-width: 148px; }
/* Shell v124: the first-tie picker is one compact block inside
   the camera's Sharing block — two rows in the .share-row shape
   plus its Share button, set off by a hairline so it reads as
   the add-someone step, not another member row. */
.share-new { border-top: 1px dashed var(--border); margin-top: 6px; padding-top: 4px; }
.share-new .btn { margin: 2px 0 8px; }
.share-line { color: var(--dim); font-size: 15px; padding: 2px 0 10px; }

/* ---------- camera flow (owner only) ---------- */
.flow-row { padding: 10px 0 4px; border-bottom: 1px solid var(--border); }
.flow-row:last-child { border-bottom: none; }
.flow-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.flow-name { font-family: var(--font-display); font-size: 20px; text-transform: uppercase; letter-spacing: .03em; }
.flow-status { font-size: 17px; font-weight: 700; color: var(--buck); }
.flow-line { color: var(--dim); font-size: 15.5px; margin-top: 3px; }
.flow-fail { color: var(--danger); font-size: 15.5px; font-weight: 600; margin-top: 3px; }
/* One row per camera (shell v79): name left, flags + battery symbol
   right, a single line that never wraps into a pileup. */
.flow-cam { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 5px; font-size: 15.5px; }
.flow-cam-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.flow-cam-side { display: flex; align-items: center; gap: 8px; flex: none; }
.flow-tag { font-size: 13px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--buck); border: 1px solid var(--border); border-radius: 999px; padding: 1px 7px; white-space: nowrap; }
.flow-tag-bad { color: var(--danger); }
.batt { display: inline-flex; align-items: center; gap: 5px; color: var(--dim); font-size: 15px; white-space: nowrap; }
.batt-icon { display: block; flex: none; }
.batt-shell { fill: none; stroke: var(--dim); stroke-width: 1.2; }
.batt-nub { fill: var(--dim); }
.batt-ok { fill: var(--accent); }
.batt-warn { fill: var(--buck); }
.batt-low { fill: var(--danger); }
.flow-tip { font-size: 15px; margin-top: 2px; }
/* One row per camera in the single Cameras section (shell v85):
   name + flags/battery on the head line, one quiet meta line
   under it (owner · access · status · last photo). */
.cam-row { padding: 8px 0 6px; }
.cam-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 16px; }
.cam-name { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cam-meta { color: var(--dim); font-size: 15.5px; margin-top: 2px; }
/* Merged camera blocks (shell v144): one block per camera in
   Camp — status + sharing together — collapsed to name /
   status / sharing summary behind a full-width 44px+ header
   tap; the summary wraps, never sideways-scrolls. */
.cam-block { padding: 10px 0 4px; border-bottom: 1px solid var(--border); }
.cam-block:last-child { border-bottom: none; }
.cam-block-head { display: block; width: 100%; background: none; border: 0; padding: 4px 0; min-height: 44px; color: var(--text); text-align: left; }
.cam-block-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 16px; }
.cam-block-sub { display: block; color: var(--dim); font-size: 15px; margin-top: 2px; overflow-wrap: anywhere; }
.cam-block-body { padding: 2px 0 8px; }
.cam-share { border-top: 1px dashed var(--border); margin-top: 8px; padding-top: 4px; }
.cam-stalled { color: var(--danger); font-weight: 700; }
.cameras-card .cam-system { margin-bottom: 2px; }

/* ---------- app use (owner only) ---------- */
.usage-alerts { font-size: 15px; font-weight: 700; }
.usage-on { color: var(--buck); }
.usage-off { color: var(--dim); }

/* ---------- install card ---------- */
.install-card ol { margin: 6px 0 2px; padding-left: 22px; font-size: 17px; line-height: 1.55; }
.connect-card ol { margin: 6px 0 2px; padding-left: 22px; font-size: 17px; line-height: 1.55; }
.connect-card li { margin: 5px 0; }
.connect-card .connect-status { font-size: 17px; font-weight: 600; margin-top: 10px; }

/* ---------- camera accounts (inside the Connect card) ---------- */
.acct-box { margin-top: 16px; border-top: 1px solid var(--border); padding-top: 12px; }
.acct-box h4 { font-family: var(--font-display); font-size: 20px; text-transform: uppercase; letter-spacing: .03em; margin: 0 0 4px; }
.acct-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.acct-row:last-child { border-bottom: none; }
/* The row's left cell must be able to shrink and wrap (pre-share
   audit, 2026-10-27): with an unshrinkable side cell, a long
   account email pushed the owner's "Mark login saved" button off
   the right edge of a 390px screen — present but unreachable.
   Now the email breaks anywhere, and when the two cells still
   cannot share a line the side cell wraps whole underneath. */
.acct-row > div:first-child { flex: 1 1 140px; min-width: 0; }
.acct-name { font-size: 17px; font-weight: 600; overflow-wrap: anywhere; }
.acct-meta { color: var(--dim); font-size: 15px; margin-top: 2px; overflow-wrap: anywhere; }
.acct-side { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.acct-status { font-size: 16px; font-weight: 700; color: var(--buck); }
.acct-pickon { font-size: 15px; font-weight: 600; color: var(--dim); }
.acct-member { font-family: var(--font-display); font-size: 18px; text-transform: uppercase; letter-spacing: .03em; margin-top: 10px; }
.acct-brands { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0 4px; }
.acct-brands .btn { flex: 1 1 30%; padding: 12px 8px; font-size: 16px; }
.acct-needs { border: 2px solid var(--accent); border-radius: var(--radius); padding: 10px 12px; margin: 12px 0 4px; }
.acct-needs h4 { font-family: var(--font-display); font-size: 20px; text-transform: uppercase; letter-spacing: .03em; color: var(--accent); margin: 0 0 4px; }
.acct-need { font-size: 17px; font-weight: 700; padding: 5px 0; }
.acct-next { font-size: 17px; font-weight: 700; color: var(--buck); margin: 10px 0 2px; }
.install-card li { margin: 5px 0; }
.install-card .os { font-family: var(--font-display); font-size: 20px; font-weight: 600; text-transform: uppercase; letter-spacing: .03em; margin-top: 14px; }

/* ---------- bottom nav ---------- */
/* The LAST element of the app frame: a static body-level element in
   index.html, in flow after #root inside the 100dvh flex frame — NOT
   positioned, never floating over content, never re-created by renders.
   The frame puts it at the bottom of the screen; the scroll region
   above it ends where the bar begins, so no content can hide under it
   and no bottom-padding compensation is needed. flex: none keeps the
   frame from ever shrinking it. It still pads its bottom with the
   home-indicator inset, and stays hidden until the shell renders
   (sign-in / setup screens have no bar). */
.bottom-nav { flex: none; background: rgba(23,18,8,.97); border-top: 2px solid var(--accent-dim); display: flex; padding-bottom: env(safe-area-inset-bottom); }
.bottom-nav[hidden] { display: none; }
.bottom-nav button { flex: 1; background: none; border: none; color: var(--dim); font-family: var(--font-display); font-size: 17px; text-transform: uppercase; padding: 12px 0 10px; letter-spacing: .04em; }
.bottom-nav button.on { color: var(--accent); box-shadow: inset 0 2px 0 var(--accent); }

/* ---------- viewer / toast ---------- */
/* True full-screen overlay: solid ground, notch-safe padding on both ends
   (header clears the status bar, footer clears the home indicator), and
   the page behind it never shows through or scrolls. */
.viewer { position: fixed; inset: 0; height: 100vh; height: 100dvh; height: var(--app-h, 100dvh); background: #0a0703; z-index: 60; padding: calc(14px + env(safe-area-inset-top)) 14px calc(14px + env(safe-area-inset-bottom)); display: flex; flex-direction: column; }
.viewer[hidden] { display: none !important; pointer-events: none; }
.viewer-top { display: flex; justify-content: space-between; align-items: center; gap: 12px; min-height: 44px; color: var(--text); font-family: var(--font-display); font-size: 20px; text-transform: uppercase; }
.viewer-top #viewer-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.viewer-close { flex: 0 0 auto; width: 44px; height: 44px; padding: 0; display: flex; align-items: center; justify-content: center; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 999px; font-size: 20px; line-height: 1; }
/* The stage owns the gestures (shell v55): touch-action: none so a
   pinch is the app's to handle — it zooms the PHOTO via transform —
   and the browser never gets to zoom or scroll the page behind the
   viewer with it. The img hugs the photo (max-constraints give the
   old object-fit: contain sizing, with the element box equal to
   the photo box, so transform scale maps 1:1 onto the photo). */
.viewer-stage { flex: 1 1 auto; min-height: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; touch-action: none; margin: 12px 0; }
.viewer-stage img { flex: none; width: auto; height: auto; max-width: 100%; max-height: 100%; border-radius: 6px; will-change: transform; }
.viewer-actions { display: flex; gap: 10px; }
.viewer-actions .btn { flex: 1 1 0; min-width: 0; }
/* Shell v131: the assignment bar between the photo and the
   Share/Download row — an unmatched buck photo carries the same
   .u-actions / .u-form controls his Animals card and Alerts row
   carry (their sizing rules ride along untouched); a filed photo
   carries "Filed under <name>" plus his card's Not him. These
   rules only seat the bar: the stage flexes, so the photo gives
   up the bar's height and nothing else on the screen moves. */
/* Shell v132: a pending proposal question seats in the bar and
   on Alerts rows too — its answer pair already rides the
   .proposal-btns rule above; the question line takes the same
   top spacing the assignment controls carry in a row, and in
   the bar it sits where "Filed under" sits. */
.row-u .proposal-q { margin-top: 10px; }
.viewer-bar { flex: 0 0 auto; margin-bottom: 12px; }
.viewer-bar[hidden] { display: none; }
.viewer-bar .u-actions, .viewer-bar .u-form { margin-top: 0; }
.viewer-bar .proposal-btns { margin-top: 8px; }
.viewer-bar .w { color: var(--dim); font-size: 15px; margin-top: 8px; }
.viewer-filed { font-size: 17px; }
.viewer-actions .viewer-nav { flex: 0 0 52px; font-size: 24px; line-height: 1; padding: 12px 0; }
.viewer-actions .viewer-nav[hidden] { display: none; }
/* Shell v149: on-demand video (Stephen's approved mockup). The
   stage seats the clip and the big ▶ badge over the photo; the
   Get video button rides the Share/Download row; the line under
   the row carries the mockup's words for the standing state —
   the bucks-only chip before a request, "Requested by …" while
   one is being fetched, "This camera is set to photos only."
   when there is no clip. Proportions are the mockup's: a 78px
   blaze circle, white ring, ▶ centered. */
.viewer-stage { position: relative; }
.viewer-stage video { flex: none; width: auto; height: auto; max-width: 100%; max-height: 100%; border-radius: 6px; background: #000; }
.viewer-stage video[hidden] { display: none; }
.viewer-playbadge { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 78px; height: 78px; border-radius: 50%; background: rgba(255, 107, 26, .94); color: #fff; font-size: 34px; display: flex; align-items: center; justify-content: center; padding: 0 0 0 6px; box-shadow: 0 4px 18px rgba(0, 0, 0, .6); border: 3px solid rgba(255, 255, 255, .85); cursor: pointer; z-index: 3; }
.viewer-playbadge[hidden] { display: none; }
.viewer-actions #viewer-video[hidden] { display: none; }
/* Shell v166: a ready clip's own Share/Download row — the same
   flex row family as the photo's, seated between it and the
   video note, shown only while a clip is ready to hand out. */
.viewer-videoactions { flex: 0 0 auto; margin-top: 10px; }
.viewer-videoactions[hidden] { display: none; }
.viewer-videonote { flex: 0 0 auto; margin-top: 12px; }
.viewer-videonote[hidden] { display: none; }
.viewer-videonote .vchip { background: #241708; border: 1px solid #5a4630; border-radius: 10px; padding: 10px 12px; font-size: 14.5px; line-height: 1.45; color: var(--text); }
.viewer-videonote .vnote { font-size: 15px; color: var(--dim); line-height: 1.45; }
/* His Animals history row carries the mark too (mockup state 3):
   "Video on this sighting" with a blaze ▶ Video pill. */
.vmark { display: inline-block; background: var(--accent); color: #fff; font-size: 15px; font-weight: 800; border-radius: 9px; padding: 8px 11px; margin-top: 8px; }
/* ---- first-login welcome tour (shell v150) ----
   The approved mockup's sheet, in the app's own palette: a fixed
   overlay above the app (the app's tab bar hides beneath it; the
   mini tab bars inside the slides are illustrations, per the
   mockup), one slide at a time, blaze on dark, camo off text. */
.tour { position: fixed; inset: 0; height: 100vh; height: 100dvh; height: var(--app-h, 100dvh); background: #0a0703; z-index: 65; padding: calc(14px + env(safe-area-inset-top)) 14px calc(14px + env(safe-area-inset-bottom)); overflow-y: auto; display: flex; flex-direction: column; align-items: center; }
.tour[hidden] { display: none !important; pointer-events: none; }
.tour-frame { position: relative; width: 100%; max-width: 430px; margin: auto 0; background: #1b150c; border: 1px solid var(--border); border-radius: 22px; padding: 18px 14px 14px; box-shadow: 0 8px 28px rgba(0, 0, 0, .55); }
.tour-skip { position: absolute; top: 12px; right: 14px; min-height: 36px; font-family: var(--font-body); font-size: 15px; font-weight: 700; color: var(--dim); border: 1px solid var(--border); border-radius: 999px; padding: 5px 12px; background: var(--surface); cursor: pointer; }
.tour-dots { display: flex; gap: 6px; justify-content: center; margin-bottom: 16px; }
.tour-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--border); }
.tour-dot.on { background: var(--accent); }
.tour-big { font-size: 25px; font-weight: 800; line-height: 1.25; text-align: center; margin-bottom: 10px; color: var(--text); }
.tour-big .o { color: var(--accent); }
.tour-words { font-size: 17px; line-height: 1.5; color: #e8dcc2; text-align: center; margin: 0 6px 16px; }
.tour-note { font-size: 15px; color: var(--dim); line-height: 1.45; margin-top: 12px; text-align: center; }
.tour-bell { font-size: 56px; text-align: center; margin: 4px 0 10px; }
.tour-why { display: flex; gap: 10px; padding: 9px 2px; border-bottom: 1px solid #2c2214; text-align: left; }
.tour-why.last { border-bottom: none; margin-bottom: 6px; }
.tour-role { flex: none; width: 104px; font-size: 15px; font-weight: 800; color: var(--buck); letter-spacing: .4px; padding-top: 2px; }
.tour-gain { font-size: 16px; line-height: 1.45; color: #e8dcc2; }
.tour-btnrow { display: flex; gap: 9px; margin-top: 6px; }
.tour-btn { flex: 1; min-height: 54px; display: flex; align-items: center; justify-content: center; border-radius: 12px; border: 1px solid var(--border); background: var(--surface); color: var(--text); font-family: var(--font-body); font-size: 17px; font-weight: 700; text-align: center; padding: 8px 4px; cursor: pointer; }
.tour-btn.blaze { background: var(--accent); border-color: var(--accent); color: #fff; }
.tour-btn[disabled] { opacity: .55; }
.tour-tabbar { display: flex; border-top: 2px solid var(--border); margin: 16px -14px -14px; padding: 10px 4px 12px; }
.tour-tabbar div { flex: 1; text-align: center; font-size: 13px; font-weight: 700; color: var(--dim); letter-spacing: .4px; }
.tour-tabbar div.on { color: var(--accent); }
.toast { position: fixed; left: 50%; transform: translateX(-50%); bottom: 84px; background: var(--surface2); border: 1px solid var(--accent-dim); color: var(--text); padding: 10px 16px; border-radius: 999px; z-index: 70; font-size: 15px; pointer-events: none; }
.toast[hidden] { display: none !important; }
.empty-note { color: var(--dim); padding: 18px 4px; font-size: 15.5px; }

/* ---- member feedback (shell v151) ----
   The floating button is Stephen's approved scene: the real
   ear-tag logo on a dark circle — the shadow treatment — seated
   bottom-right above the pinned tab bar (the bar itself is never
   touched: this is a fixed layer over the content region, and
   app.js hides it whenever an overlay or the keyboard is up).
   The form is the approved mockup's sheet in the app palette:
   "Tell Tagged Out", the two big chips, one box, one blaze send. */
.feedback-fab { position: fixed; right: 14px; bottom: calc(76px + env(safe-area-inset-bottom)); width: 62px; height: 62px; border-radius: 50%; background: #261d11; border: 1px solid #5a4630; box-shadow: 0 4px 14px rgba(0, 0, 0, .55); padding: 0; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 55; }
.feedback-fab[hidden] { display: none !important; }
.feedback-fab img { width: 50px; height: 50px; display: block; pointer-events: none; }
.feedback-sheet { position: fixed; inset: 0; height: 100vh; height: 100dvh; height: var(--app-h, 100dvh); background: #0a0703; z-index: 66; padding: calc(14px + env(safe-area-inset-top)) 14px calc(14px + env(safe-area-inset-bottom)); overflow-y: auto; display: flex; flex-direction: column; align-items: center; }
.feedback-sheet[hidden] { display: none !important; pointer-events: none; }
.feedback-frame { position: relative; width: 100%; max-width: 430px; margin: auto 0; background: #1b150c; border: 1px solid var(--border); border-radius: 22px; padding: 18px 14px 14px; box-shadow: 0 8px 28px rgba(0, 0, 0, .55); }
.feedback-close { position: absolute; top: 12px; right: 14px; min-height: 36px; font-family: var(--font-body); font-size: 15px; font-weight: 700; color: var(--dim); border: 1px solid var(--border); border-radius: 999px; padding: 5px 12px; background: var(--surface); cursor: pointer; }
.feedback-big { font-size: 23px; font-weight: 800; line-height: 1.25; margin-bottom: 8px; color: var(--text); }
.feedback-big .o { color: var(--accent); }
.feedback-words { font-size: 16px; line-height: 1.5; color: #e8dcc2; margin-bottom: 14px; }
.feedback-chips { display: flex; gap: 9px; margin-bottom: 14px; }
.feedback-chip { flex: 1; min-height: 52px; display: flex; align-items: center; justify-content: center; border-radius: 12px; border: 1.5px solid var(--border); background: var(--surface); font-family: var(--font-body); font-size: 17px; font-weight: 800; color: var(--text); cursor: pointer; }
.feedback-chip.on { border-color: var(--accent); background: #3a2410; color: var(--buck); }
.feedback-text { width: 100%; box-sizing: border-box; border: 1.5px solid var(--border); border-radius: 12px; background: var(--surface); min-height: 110px; padding: 12px; font-family: var(--font-body); font-size: 16px; line-height: 1.45; color: var(--text); margin-bottom: 14px; resize: vertical; }
.feedback-text::placeholder { color: var(--dim); }
.feedback-send { width: 100%; min-height: 56px; display: flex; align-items: center; justify-content: center; border: none; border-radius: 12px; background: var(--accent); color: #fff; font-family: var(--font-body); font-size: 17px; font-weight: 800; cursor: pointer; }
.feedback-send[disabled] { opacity: .45; }
.feedback-sent { font-size: 16px; line-height: 1.5; color: #e8dcc2; text-align: center; margin-top: 12px; }
/* Privacy & terms (shell v152): the same sheet shape as the
   tour and the feedback form — a fixed overlay on solid ground
   with its own scroll, the text in one readable frame — and
   Camp's quiet copyright line at the very foot of the tab. */
.privacy-sheet { position: fixed; inset: 0; height: 100vh; height: 100dvh; height: var(--app-h, 100dvh); background: #0a0703; z-index: 66; padding: calc(14px + env(safe-area-inset-top)) 14px calc(14px + env(safe-area-inset-bottom)); overflow-y: auto; display: flex; flex-direction: column; align-items: center; }
.privacy-sheet[hidden] { display: none !important; pointer-events: none; }
.privacy-frame { position: relative; width: 100%; max-width: 430px; margin: auto 0; background: #1b150c; border: 1px solid var(--border); border-radius: 22px; padding: 18px 14px 14px; box-shadow: 0 8px 28px rgba(0, 0, 0, .55); }
.privacy-close { position: absolute; top: 12px; right: 14px; min-height: 36px; font-family: var(--font-body); font-size: 15px; font-weight: 700; color: var(--dim); border: 1px solid var(--border); border-radius: 999px; padding: 5px 12px; background: var(--surface); cursor: pointer; }
.privacy-big { font-size: 23px; font-weight: 800; line-height: 1.25; margin-bottom: 12px; color: var(--text); }
.privacy-p { font-size: 16.5px; line-height: 1.55; color: #e8dcc2; margin-bottom: 14px; }
.privacy-p b { color: var(--text); }
/* Member terms acceptance (shell v157): the I agree gate sheet —
   the tour frame's shape on its own layer, UNDER the Privacy &
   terms sheet (66) so the gate's "Read the full Privacy & terms"
   opens over it and closing returns to the gate. The gist lines
   read left-aligned in the tour sheet's words size — big type,
   few words, and one big blaze tap. */
.terms-sheet { position: fixed; inset: 0; height: 100vh; height: 100dvh; height: var(--app-h, 100dvh); background: #0a0703; z-index: 64; padding: calc(14px + env(safe-area-inset-top)) 14px calc(14px + env(safe-area-inset-bottom)); overflow-y: auto; display: flex; flex-direction: column; align-items: center; }
.terms-sheet[hidden] { display: none !important; pointer-events: none; }
.pwgate-sheet { position: fixed; inset: 0; height: 100vh; height: 100dvh; height: var(--app-h, 100dvh); background: #0a0703; z-index: 67; padding: calc(14px + env(safe-area-inset-top)) 14px calc(14px + env(safe-area-inset-bottom)); overflow-y: auto; display: flex; flex-direction: column; align-items: center; }
.pwgate-sheet[hidden] { display: none !important; pointer-events: none; }
.installgate-sheet { position: fixed; inset: 0; height: 100vh; height: 100dvh; height: var(--app-h, 100dvh); background: #0a0703; z-index: 61; padding: calc(14px + env(safe-area-inset-top)) 14px calc(14px + env(safe-area-inset-bottom)); overflow-y: auto; display: flex; flex-direction: column; align-items: center; }
.installgate-sheet[hidden] { display: none !important; pointer-events: none; }
.pwgate-signout { display: block; margin: 14px auto 0; min-height: 36px; background: none; border: 0; padding: 5px 12px; font-family: var(--font-body); font-size: 15px; font-weight: 700; color: var(--dim); text-decoration: underline; cursor: pointer; }
.pw-wrap { position: relative; display: block; }
.pw-wrap input { padding-right: 86px; }
.pw-eye { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); min-height: 40px; min-width: 58px; padding: 6px 12px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--text); font-family: var(--font-body); font-size: 14px; font-weight: 700; cursor: pointer; }
.terms-line { font-size: 17px; line-height: 1.5; color: #e8dcc2; text-align: left; margin: 0 2px 13px; }
.terms-line b { color: var(--text); }
.camp-copyright { text-align: center; font-size: 13.5px; line-height: 1.45; color: var(--dim); padding: 6px 10px 12px; }
/* Stephen's Feedback list in Camp (owner-only payload): the
   mockup's item cards — tag + sender · time on top, the text,
   and the three status chips he works the list by. The item a
   push tap named carries the highlight until he drives on. */
.feedback-item { border: 1px solid var(--border); border-radius: 12px; background: var(--surface); padding: 12px; margin-bottom: 10px; }
.feedback-item.hl { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.feedback-item .fi-top { display: flex; justify-content: space-between; gap: 10px; font-size: 13.5px; color: var(--dim); margin-bottom: 5px; }
.feedback-item .fi-tag { font-weight: 800; color: var(--buck); letter-spacing: .4px; white-space: nowrap; }
.feedback-item .fi-txt { font-size: 16px; line-height: 1.45; color: var(--text); white-space: pre-wrap; }
.reset-link-text { word-break: break-all; } /* the minted reset link on Camp's shelf (v163): one long unbroken string, it must wrap in the row */
.feedback-status { display: flex; gap: 8px; margin-top: 10px; }
.feedback-status .st { flex: 1; text-align: center; font-family: var(--font-body); font-size: 14px; font-weight: 700; padding: 8px 0; border-radius: 9px; border: 1px solid var(--border); background: none; color: var(--dim); cursor: pointer; }
.feedback-status .st.on { background: var(--accent); border-color: var(--accent); color: #fff; }
/* Install coach (shell v158): the top-of-Today card for a member
   still in a browser tab — big words, picture cues, one blaze
   line for why. It is an ordinary in-flow card, NOT a sheet: the
   app stays usable around it, "Not now" parks it for the day,
   and only real installation (standalone mode) turns it off for
   good. Words sit on the solid panel, blaze only as accent. */
.coach-card { border: 2px solid var(--accent); }
.coach-card h3 { font-size: 21px; margin-bottom: 6px; }
.coach-steps { margin: 6px 0 2px; padding-left: 24px; font-size: 17.5px; line-height: 1.55; }
.coach-steps li { margin: 7px 0; }
.coach-ic { width: 21px; height: 21px; vertical-align: -4px; fill: none; stroke: var(--accent); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.coach-why { font-size: 16.5px; font-weight: 700; color: var(--accent); margin: 10px 0 2px; }
.coach-btnrow { margin: 10px 0 2px; }
.coach-install { width: 100%; min-height: 52px; font-size: 18px; }
.coach-later { margin-top: 10px; }
