/*
 * Plain CSS component styles. Tailwind utilities come from app/assets/builds/tailwind.css.
 * Custom color values reference CSS variables defined by the Tailwind theme.
 */

/* ── Self-hosted fonts (ad-hoc 2026-04-11) ──
 *
 * Replaces the previous <link> imports from fonts.googleapis.com. Fonts
 * live in app/assets/fonts/ and are served through Propshaft; the
 * url("…") paths get digest-rewritten at build time. Material Symbols
 * is fetched from Google Fonts via the `icon_names=` parameter as a
 * pre-subsetted variable font (65KB vs. 3.8MB for the full axis).
 *
 * When adding a new Material Symbols glyph, re-fetch the subsetted
 * font with:
 *
 *   bin/icons --fetch
 *
 * The script scans ERB templates, JS controllers (innerHTML, textContent,
 * glyph variables) and the OWM weather icon map.
 * Run `bin/icons` (without --fetch) to just list the detected icons.
 */
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/outfit-latin-400-9c6ecee0.woff2") format('woff2');
}
/* Michroma (self-hosted, OFL) — the parent-brand face: futuristic/Star-Trek console, chosen for
   the "by StarPaw" byline. The byline is PARKED (2026-07-22, see docs/brand.md) until StarPaw has
   a second product; the face stays loaded and reserved so branding can pick it straight back up. */
@font-face {
  font-family: 'Michroma';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/michroma-latin-400-d6aa33cd.woff2") format('woff2');
}
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/assets/outfit-latin-500-9c6ecee0.woff2") format('woff2');
}
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/outfit-latin-600-9c6ecee0.woff2") format('woff2');
}
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/assets/outfit-latin-700-9c6ecee0.woff2") format('woff2');
}
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url("/assets/outfit-latin-800-9c6ecee0.woff2") format('woff2');
}
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/dm-sans-latin-400-c3a50c50.woff2") format('woff2');
}
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/assets/dm-sans-latin-500-c3a50c50.woff2") format('woff2');
}
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/dm-sans-latin-600-c3a50c50.woff2") format('woff2');
}
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/assets/dm-sans-latin-700-c3a50c50.woff2") format('woff2');
}
@font-face {
  font-family: 'DM Sans';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/dm-sans-latin-400-italic-dbade5b5.woff2") format('woff2');
}
@font-face {
  font-family: 'Material Symbols Outlined';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url("/assets/material-symbols-outlined-subset-ad95abd4.woff2") format('woff2');
}

/* ── Theme surfaces (Plan 260) ────────────────────────────────────────────────
   The token families in `app/assets/tailwind/application.css` cover everything
   addressed by a utility class. These cover what plain CSS in THIS file paints:
   the tints, veils and scrims that were written as literals because no utility
   was involved.

   Two different mechanisms, because two different problems:

   `--tint-shade` is a BASE, mixed at the call site — `color-mix(in srgb,
   var(--tint-shade) 8%, transparent)` is exactly the old 8%-of-ink literal.
   Hairlines, dividers and recessed fills each keep their own alpha (they vary
   from 3% to 15% and those differences are deliberate), so a per-alpha token
   would mean thirty tokens. Flipping the base to white inverts all of them at
   once: "darken this surface" becomes "lighten this surface", which is what the
   same rule means on a dark ground.

   The `--color-*` entries below are FULL VALUES, because their light and dark
   forms differ in alpha as well as hue — a 50%-white inset panel reads as a
   raised card on cream and as a blown-out slab on ink; the dark form needs ~6%.
   A flippable base cannot express that, so both values are written out. */
:root {
  /* Mixed into a SURFACE to push it away from the reader. Flips to white on dark,
     because "darken this" and "lighten this" are the same instruction on opposite
     grounds. Hairlines, dividers, recessed fills, press states. */
  --tint-shade: #1A1A2E;

  /* Mixed into a DROP SHADOW. Does NOT flip — light falls the same way in both
     themes, and a white blur under a card is a halo, not a shadow. The dark theme
     deepens it instead (a shadow needs more alpha to read on ink than on cream). */
  --tint-shadow: #1A1A2E;

  --color-inset:        rgba(255, 255, 255, 0.5);   /* raised panel on the canvas */
  --color-inset-strong: rgba(255, 255, 255, 0.92);  /* opaque floating dropdown  */
  --color-inset-bar:    rgba(245, 240, 235, 0.8);   /* the sticky logistics bar  */
  --color-inset-handle: rgba(248, 249, 252, 0.82);  /* the nav drawer handle     */
  --color-scrim:        rgba(0, 0, 0, 0.3);         /* modal backdrop            */
  --color-scrim-deep:   rgba(26, 26, 46, 0.5);      /* dialog backdrop           */

  /* The gallery overlay's own ground. Not `--color-canvas` because the 2% of
     transparency is doing work — the blurred page reads through it. */
  --color-overlay-canvas: rgba(245, 240, 235, 0.98);

  /* THE TICKET PALETTE — deliberately NOT themed.
     `.account-segment` is dark stock in both themes (a constant rgb(12 20 42)), so an
     accent painted on it must not follow the theme: Plan 260 brightened `--color-mint`
     for dark SURFACES and the paid fill inherited it, making the fill brighter while the
     ground it sits on never moved. It read as stark, and rendered two different greens on
     an identical background depending on theme. These are the ORIGINAL accent values;
     `accounts/_cell.html.erb` builds `--seg-color` from `--color-ticket-*`, so the ticket
     is identical in both themes. A per-service legend returning here needs a token added. */
  --color-ticket-mint: #34C7A0;

  /* The sticky filter/search bar that floats over a scrolling list. */
  --color-canvas-veil: rgba(245, 240, 235, 0.75);

  /* Mixed INTO an accent to make a pale chip of it. White on cream; the surface
     colour on ink — "tint toward the ground", which is the ground-dependent half
     of the same idea as --tint-shade. */
  --tint-chip: #FFFFFF;

  /* Elevation. A token rather than three rules, because the dark theme does not
     merely re-colour the shadow — it changes the RECIPE (a catch-light and a ring
     replace a shadow that cannot read on ink). Keeping the whole value in a token
     means the dark form lives in the theme block with everything else, and
     bin/check-css covers it for free. */
  --shadow-depth-sm: 0 2px 8px color-mix(in srgb, var(--tint-shadow) 4%, transparent),
                     0 0 0 1px color-mix(in srgb, var(--tint-shade) 3%, transparent);
  --shadow-depth-md: 0 4px 20px color-mix(in srgb, var(--tint-shadow) 6%, transparent),
                     0 0 0 1px color-mix(in srgb, var(--tint-shade) 3%, transparent);
  --shadow-depth-lg: 0 8px 40px color-mix(in srgb, var(--tint-shadow) 8%, transparent),
                     0 0 0 1px color-mix(in srgb, var(--tint-shade) 2%, transparent);

  /* Values whose light form encodes a MEASURED contrast decision (the ratios are
     named at each call site). Measured against cream, so they do not transfer —
     the dark theme restates them rather than deriving them. */
  --color-status-prepaid: #117D66;  /* deepened mint — 5.0:1 on glass */
  --color-status-settled: #87879B;  /* quietest by design */
  --color-panel-error:    #b91c1c;
  --color-amt-in:         color-mix(in srgb, var(--color-mint) 70%, #000);
  --shadow-bonus-float:   0 1px 0 rgba(255, 255, 255, 0.8);
}

/* ── Base ── */
html {
  height: 100%;
  overflow: hidden;
  /* The iOS safe-area strips and the overscroll gutter paint the ROOT, not the
     body — without this they stay white in dark mode while the app is ink. The
     marketing layout learned the same lesson (Plan 248) and hardcoded #12121F. */
  background: var(--color-canvas);
}
body {
  background: var(--color-canvas);
  font-family: var(--font-body);
  -webkit-font-smoothing: antialiased;
  height: 100%;
  color: var(--color-ink);
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: none;
}

/* Disable native image/link drag and iOS touch-callout previews */
img, a {
  -webkit-user-drag: none;
  user-drag: none;
  -webkit-touch-callout: none;
}

/* ── Material Symbols ──
 *
 * Plan 196: the whole rule lives in @layer base so Tailwind utilities can
 * override it. Unlayered author CSS beats ALL layered CSS regardless of
 * specificity — with this rule unlayered, its font-size: 24px silently
 * defeated every text-[..px] utility on an icon app-wide (utilities live in
 * the built tailwind.css's @layer utilities). Layer names are document-wide,
 * so this `base` merges before the build's `utilities` and text-[..px] /
 * align-[..] / block etc. now genuinely apply to icons. Unlayered rules
 * elsewhere in this file (.starpaw-* icon sizes, .filled-icon, …) still win
 * over both — unchanged behavior. */
@layer base {
  .material-symbols-outlined {
    font-family: 'Material Symbols Outlined';
    font-weight: normal;
    font-style: normal;
    font-size: 24px;
    line-height: 1;
    letter-spacing: normal;
    text-transform: none;
    white-space: nowrap;
    word-wrap: normal;
    direction: ltr;
    -webkit-font-feature-settings: 'liga';
    font-feature-settings: 'liga';
    -webkit-font-smoothing: antialiased;
    font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
    display: inline-block;
    vertical-align: middle;
  }

  /* iOS Safari gives <input type="date"> an intrinsic min-width and its own
     appearance chrome, so w-full fields OVERFLOW narrow containers (the away
     sheets' glass windows — founder screenshot, 2026-08-16) and the value hugs
     the right edge. Normalized here in base (Plan 342 round) so utilities can
     still override; the native picker on tap is untouched. */
  input[type="date"] {
    -webkit-appearance: none;
    appearance: none;
    min-width: 0;
  }
  input[type="date"]::-webkit-date-and-time-value {
    text-align: left;
  }
}
.filled-icon {
  font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}

/* ── Loading spinner ── */
.loading-spinner {
  width: 40px;
  height: 40px;
  border: 3px solid var(--color-brand-light, rgba(0,0,0,0.1));
  border-top-color: var(--color-brand);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Localized in-modal upload overlay. Each upload surface (hero photo; the home-access
   photos placeholder tile; the contact-avatar spinner) wraps itself in
   [data-upload-scope] and holds one of these, revealed on file pick by
   dog-modal#uploading. Toggled via `.is-uploading` — plain classes (not the `hidden`
   utility) so the display state can't collide with Tailwind's `flex`/`hidden` ordering.
   Spinner recolored white for contrast on the dark scrim/tile. */
.dog-upload-overlay { display: none; }
.dog-upload-overlay.is-uploading { display: flex; }
.dog-upload-overlay .loading-spinner {
  border-color: rgba(255, 255, 255, 0.35);
  border-top-color: #fff;
}

/* ── Scrollbar ── */
.hide-scrollbar::-webkit-scrollbar { display: none; }
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* Edge-fade scroll cues (design panel, 2026-08-24): a hidden scrollbar's replacement —
   token-gradient scrims on a scroller's wrapper, toggled by JS (_syncScrollCues), so
   "there is more" is said by dissolving content, not chrome. --color-canvas is the dialog
   surface's own token, so both themes inherit. */
.scroll-cue { position: absolute; left: 0; right: 0; height: 28px; pointer-events: none; z-index: 1; }
.scroll-cue-top { top: 0; background: linear-gradient(to bottom, var(--color-canvas), transparent); }
.scroll-cue-bottom { bottom: 0; background: linear-gradient(to top, var(--color-canvas), transparent); }

/* ── Glass surfaces ── */
.glass {
  background: var(--color-glass);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--color-line-soft);
}
/* Bottom nav corners: rounded top only (square bottom sits flush on the safe-area strip). */
.app-bottom-nav { border-radius: 1.25rem 1.25rem 0 0; }

/* Environment label in the bottom safe-area gap — only when installed to the home screen
   (standalone), where the gap actually exists. Hidden in a normal browser tab. */
.env-badge { display: none; }
@media (display-mode: standalone) { .env-badge { display: inline; } }

.glass-strong {
  background: var(--color-glass-strong);
  backdrop-filter: blur(24px) saturate(1.5);
  -webkit-backdrop-filter: blur(24px) saturate(1.5);
  border: 1px solid var(--color-line-soft);
}

/* ── Depth ── */
.depth-sm { box-shadow: var(--shadow-depth-sm); }
.depth-md { box-shadow: var(--shadow-depth-md); }
.depth-lg { box-shadow: var(--shadow-depth-lg); }

/* ── Tactile button ── */
.tactile {
  transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  cursor: pointer;
}
.tactile:active { transform: scale(0.94); }

/* ── Action button press darken ── */
.btn-press:active { filter: brightness(0.85); }

/* ── Icon pill ── */
.icon-pill {
  width: 2.25rem; height: 2.25rem;
  display: flex; align-items: center; justify-content: center;
  border-radius: 9999px;
  background: rgba(255, 107, 74, 0.08);
  color: var(--color-brand-text);
  border: none; cursor: pointer;
  transition: all 0.2s;
  flex-shrink: 0;
}
.icon-pill:hover { background: rgba(255, 107, 74, 0.15); transform: scale(1.06); }

/* ── Label ── */
.label {
  font-family: var(--font-display);
  font-size: 0.6875rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-ink-faint);
}

/* ── Note block ── */
.note-block {
  padding: 0.875rem 1rem;
  background: rgba(255, 107, 74, 0.04);
  border-radius: 0.875rem;
  font-size: 0.8125rem;
  line-height: 1.65;
  color: var(--color-ink-soft);
  font-style: italic;
  border-left: 3px solid var(--color-brand-soft);
}

/* ── Entrance animation ── */
@keyframes rise {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}
.animate-rise { animation: rise 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; }

/* Bottom sheets that rise from the edge. TWO call sites have asked for this animation by
   name for months and it did not exist — the recurring-removal sheet (application.js) and
   the Logistics ETA sheet (logistics/_eta_sheet) both declare
   `animation: slide-up 0.3s ...` inline, so both simply appeared in one frame. Found by the
   Plan 356 panel, which went looking for the idiom the plan wanted to reuse.

   The house's OTHER sheets (.acct-keyboard, .acct-history-sheet) slide by transitioning
   `transform` with an `.is-up` class instead, which is interruptible and is what new sheets
   should use (ui-conventions: name the string as a class, don't retype it in JS). This
   keyframe exists so the two existing callers do what they already say they do; it is not
   the pattern to copy. */
@keyframes slide-up {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

/* THE DOG MENU (Plan 356) — the sheet that asks what a tap on a dog meant.
   Body-appended, so it owns its own stacking; z-index 300 matches the removal sheet it sits
   beside in the same role. It must NEVER be open at the same time as the day dialog, which
   uses showModal() and therefore the native top layer no z-index can reach — the controller
   closes the menu before opening the dialog. */
.dog-day-menu { position: fixed; inset: 0; z-index: 300; }
.dog-day-menu-backdrop { position: absolute; inset: 0; background: color-mix(in srgb, var(--tint-shadow) 30%, transparent); }
.dog-day-menu-sheet {
  position: absolute; inset-inline: 0; bottom: 0;
  padding: 12px 20px calc(32px + env(safe-area-inset-bottom, 0px));
  border-start-start-radius: 1.5rem; border-start-end-radius: 1.5rem;
  background: var(--color-glass-strong);
  box-shadow: var(--shadow-depth-lg);
  /* The house sheet slide: transform + .is-up, interruptible, never a keyframe. */
  transform: translateY(110%);
  transition: transform 0.28s cubic-bezier(0, 0, 0.2, 1);
}
.dog-day-menu.is-up .dog-day-menu-sheet { transform: none; }

/* Woof pop: playful tap feedback on empty dog-card icons */
@keyframes woof-pop {
  0%   { opacity: 0; transform: translate(-50%, 0) scale(0.5); }
  20%  { opacity: 1; transform: translate(-50%, -6px) scale(1.1); }
  50%  { opacity: 1; transform: translate(-50%, -10px) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -18px) scale(0.8); }
}
.woof-pop {
  position: absolute;
  bottom: 100%;
  left: 50%;
  pointer-events: none;
  font-family: 'Comic Sans MS', 'Chalkboard SE', cursive;
  font-size: 11px;
  font-weight: 700;
  color: var(--color-ink-faint);
  white-space: nowrap;
  animation: woof-pop 0.6s ease-out forwards;
}

/* Logistics: hide checkboxes on non-today dates */
.logistics-past .log-check { display: none; }

/* Driving card: colored left rail encodes pick-up (mint) vs drop-off (brand) —
   readable peripherally before any text (Plan 202 design review). */
.log-row.is-pickup { border-left: 4px solid var(--color-mint); }
.log-row.is-dropoff { border-left: 4px solid var(--color-brand); }
/* Reception + Feeding share the driving card language (Plan 204). */
.log-row.is-checkin { border-left: 4px solid var(--color-mint); }
.log-row.is-checkout { border-left: 4px solid var(--color-brand); }
.feed-card { border-left: 4px solid var(--color-amber); }
.feed-card.is-boarding { border-left-color: var(--color-rose); }

/* The logistics sticky bars (main tabs, Driving's Stops/Select sub-bar, Feeding's
   meal sub-bar) share one frosted treatment — was duplicated inline styles. */
.log-sticky-bar {
  -webkit-backdrop-filter: blur(16px) saturate(1.8);
  backdrop-filter: blur(16px) saturate(1.8);
  background: var(--color-inset-bar);
}

/* The hero's DOCK (Plan 288). The hero is the top of the run and stays there — the stops
   scroll under it, so the next stop is always on screen while the walker reads ahead. The
   blur/background live here rather than on .log-hero because the card is glass: without an
   opaque-enough backing, every row passing beneath it shows through. Empty collapses to
   nothing (the card is client-rendered, and _dissolveHero empties this outright). */
.log-hero-dock {
  -webkit-backdrop-filter: blur(16px) saturate(1.8);
  backdrop-filter: blur(16px) saturate(1.8);
  background: var(--color-inset-bar);
}
.log-hero-dock:empty { display: none; }

/* Next-stop hero (Plan 203): the first incomplete stop, rendered rich. Themed by
   direction via --dir. The source row is MARKED (.is-current), not hidden — the list is a
   stable itinerary and the hero mirrors whichever row the cursor is on. */
/* The CURSOR (founder, 2026-07-31). The row the walker is on is MARKED, not hidden: nothing
   in the run ever moves now, so the list is a stable itinerary and this says where you are in
   it. The hero at the top mirrors the same row, and that duplication is deliberate — hiding
   the source row is what produced the zero-height-row bugs (a spec silently passing on it for
   weeks, the sortable's geometry filter, the reveal dance on dissolve). */
/* The cursor row is MARKED, and auto-scroll parks it just ABOVE the fold rather than hiding it
   (founder 2026-07-31, revised the same hour). Hiding was tried: it is a duplicate of the hero
   and costs a row of screen. But the row is the ONLY place two things live — the drag grip, so
   the current stop can be reordered at all (Plan 288 deleted the hero's grip precisely because
   this row is draggable), and, when the cursor is an anchor, its picker and its leg's Optimize,
   which the hero does not mirror. Hidden, the current stop could be neither moved nor edited.
   Scrolling past it costs nothing and keeps both. */
.log-dest.is-current::after {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  border-radius: 3px; background: var(--color-brand);
}
.log-dest.is-current { box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--color-brand) 45%, transparent); }
/* Hero promotion: when the current next-stop is marked done, the following stop fades +
   rises into the hero slot so the swap is noticeable (otherwise it's instant/unnoticeable). */
@keyframes log-hero-enter {
  from { opacity: 0; transform: translateY(16px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
/* ease-out (not the app's aggressive rise curve, which front-loaded opacity to ~90% in the
   first ~100ms and read as instant) over ~0.5s so the fade-in is actually perceptible. */
.log-hero-enter { animation: log-hero-enter 0.5s ease-out both; }
.log-hero { --dir: var(--color-brand); --dir-text: var(--color-brand-text); --dir-light: var(--color-brand-light);
  padding: 10px 12px 10px 16px; }
.log-hero.is-pickup { --dir: var(--color-mint); --dir-text: var(--color-mint-text); --dir-light: var(--color-mint-light); }
.log-hero::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px; background: var(--dir); }
.log-hero-away { font-size: 11.5px; font-weight: 800; color: var(--dir-text); background: var(--dir-light); padding: 2px 9px; border-radius: 999px; flex: 0 0 auto; }
.log-hero-away:empty { display: none; }
.log-hero-group { display: none; align-items: center; gap: 5px; font-size: 11px; font-weight: 800; flex: 0 0 auto; }
.log-hero-dot { width: 7px; height: 7px; border-radius: 999px; flex: 0 0 auto; }
.log-hero-dog { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
.log-hero-ident { flex: 1; min-width: 0; }
/* The identity line: "Ziggy · Pick-up #1". NAME FIRST and it wins every shrink — the first cut
   put the direction first and let the name absorb the loss, which rendered "Zig…" on a 393px
   phone. The direction is two fixed words that the colour rail and the list row both repeat;
   the name is the only thing that says which stop this is, so the direction is what gives way.
   Nothing fixed-width lives here: the pack and distance chips sit on the meta line below,
   where they cost the name nothing. */
.log-hero-line { display: flex; align-items: baseline; gap: 4px; margin: 0; white-space: nowrap; }
.log-hero-dir { font-size: 15px; font-weight: 800; letter-spacing: -.02em; color: var(--dir-text); line-height: 1.1; flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.log-hero-line .n { font-size: 15px; font-weight: 800; color: var(--color-ink-faint); flex: 0 1 auto; }
.log-hero-sep { font-size: 13px; color: var(--color-ink-faint); flex: 0 0 auto; }
.log-hero-meta { display: flex; align-items: center; gap: 8px; margin: 2px 0 0; min-width: 0; overflow: hidden; }
/* An <a> since 2026-08-18 (it opens the dog's card): text-decoration + color like
   its siblings .log-hero-addr / .log-hero-ico, or the fallback paw ligature — which is
   TEXT to the browser — arrives underlined and link-coloured. */
.log-hero-photo { width: 40px; height: 40px; border-radius: 11px; overflow: hidden; flex: 0 0 auto; background: var(--color-canvas-warm); display: grid; place-items: center; text-decoration: none; color: inherit; }
.log-hero-photo img { width: 100%; height: 100%; object-fit: cover; }
/* flex-shrink 0: the name is never the thing that gets clipped. */
.log-hero-name { font-size: 17px; font-weight: 800; letter-spacing: -.02em; line-height: 1.05; color: var(--color-ink); flex: 0 0 auto; }
.log-hero-time { font-size: 13px; color: var(--color-ink-soft); font-weight: 600; margin: 0; flex: 0 0 auto; }
.log-hero-addr { display: flex; align-items: center; gap: 9px; text-decoration: none; color: inherit; background: var(--color-inset); border: 1px solid var(--color-line); border-radius: 11px; padding: 7px 10px; margin-bottom: 8px; }
.log-hero-pin { font-size: 17px; color: var(--dir-text); flex: 0 0 auto; }
.log-hero-street { flex: 1; font-size: 14px; font-weight: 700; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--color-ink); }
.log-hero-go { display: flex; align-items: center; gap: 3px; font-size: 11.5px; font-weight: 800; color: var(--dir-text); flex: 0 0 auto; }
/* The entry code line (2026-09-02) — a sibling of .log-hero-addr in the same inset box
   language, never inside it (that box is the Directions tap). A <button>: the whole line is
   the reveal tap, hence the font/text/width resets. Wide tracking on the code: 0/O and 1/l
   at arm's length are the difference between in and locked out. [hidden] is spelled out
   because this rule's display:flex would otherwise beat the preflight. */
.log-hero-access { display: flex; align-items: center; gap: 9px; width: 100%; text-align: left; font-family: inherit; color: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; background: var(--color-inset); border: 1px solid var(--color-line); border-radius: 11px; padding: 6px 10px; margin-bottom: 8px; }
.log-hero-access[hidden] { display: none; }
.log-hero-key { font-size: 17px; color: var(--dir-text); flex: 0 0 auto; }
.log-hero-eye { font-size: 18px; color: var(--color-ink-faint); flex: 0 0 auto; margin-left: auto; }
.log-hero-access-label { font-size: 11.5px; font-weight: 800; color: var(--color-ink-soft); flex: 0 0 auto; }
.log-hero-code { font-size: 16px; font-weight: 700; letter-spacing: .18em; color: var(--color-ink); }
.log-hero-code[hidden] { display: none; }
.log-hero-actions { display: flex; gap: 7px; }
.log-hero-done { flex: 1; display: flex; align-items: center; justify-content: center; gap: 7px; background: var(--dir); color: #fff; border: none; font-family: inherit; font-weight: 800; font-size: 13.5px; border-radius: 11px; padding: 10px; cursor: pointer; -webkit-tap-highlight-color: transparent; text-decoration: none; }
.log-hero-ico { width: 42px; flex: 0 0 auto; display: grid; place-items: center; background: var(--color-inset); border: 1px solid var(--color-line); border-radius: 11px; color: var(--dir-text); cursor: pointer; text-decoration: none; }

/* Logistics select-to-optimize (Plan 200). The selected badge is hidden until a
   stop is picked; in select mode incomplete rows read as tappable. Completed rows
   are never selectable (they stay sunk at the bottom). */
.log-selected-badge { display: none; }
.log-row.is-selected .log-selected-badge { display: flex; }
/* Inset outline (not an outward ring) so adjacent selected rows never touch, and
   it keeps the row's depth shadow. Follows the rounded corners in modern WebViews. */
.log-row.is-selected { outline: 1.5px solid var(--color-brand); outline-offset: -1.5px; }
.log-selecting .log-row:not([data-log-done]) { cursor: pointer; }
.log-selecting .log-row[data-log-done] { opacity: 0.4; }
/* The grip's VISUAL stays a slim icon column; its TOUCH TARGET must not (founder,
   2026-08-01: "hard to drag"). The real column is w-9 (36px) and the pseudo-element
   adds hit slop to ~44px wide and ~44px tall — inside the row only (the driving row is
   overflow-hidden, so outward slop would be clipped from hit-testing anyway), and the
   8px left slop stays within the flex gap so it cannot steal taps from the checkmark. */
.log-drag { position: relative; }
.log-drag::before { content: ""; position: absolute; inset: -10px 0 -10px -8px; }

.log-selecting .log-drag { pointer-events: none; opacity: 0.25; }
/* Completed stops are parked at the bottom and locked — can't be dragged/
   repositioned (un-complete them by tapping their check again). */
.log-row[data-log-done] .log-drag { pointer-events: none; opacity: 0.2; }

/* Upcoming badge: alarm icon on today's dog card icons */
.upcoming-badge {
  position: absolute;
  bottom: 1px;
  right: 1px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--color-canvas-warm);
  display: flex;
  align-items: center;
  justify-content: center;
}
.upcoming-badge .material-symbols-outlined {
  font-size: 12px !important;
  width: 12px;
  height: 12px;
  line-height: 1;
  color: var(--color-ink-faint);
  font-variation-settings: "FILL" 0, "wght" 500, "opsz" 20;
}
@keyframes frame-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.frame-fade-in { animation: frame-fade 100ms ease-out both; }

/* ── Month picker overlay ── */
.month-picker-backdrop { position: fixed; inset: 0; z-index: 50; background: transparent; opacity: 0; transition: opacity 0.15s ease; pointer-events: none; }
.month-picker-backdrop.open { opacity: 1; pointer-events: auto; }
.month-picker { position: fixed; z-index: 51; background: var(--color-glass-strong); border-radius: 16px; padding: 14px 16px 18px; box-shadow: 0 8px 24px color-mix(in srgb, var(--tint-shadow) 18%, transparent); transform: translateY(-8px); opacity: 0; transition: transform 0.2s ease, opacity 0.15s ease; pointer-events: none; overflow: hidden; }
.month-picker.open { transform: translateY(0); opacity: 1; pointer-events: auto; }
.month-picker-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.month-picker-title { font-family: var(--font-display); font-weight: 700; font-size: 18px; color: var(--color-ink); }
.month-picker-nav { background: none; border: none; padding: 6px; color: var(--color-ink-faint); cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
.month-grid { display: grid; grid-template-columns: repeat(7, 1fr); grid-template-rows: auto repeat(6, 72px); gap: 2px 0; }
.month-cell:not(:nth-child(7n)) { border-right: 0.5px solid color-mix(in srgb, var(--tint-shade) 6%, transparent); }
.month-day-label { font-family: var(--font-display); font-weight: 600; font-size: 12px; color: var(--color-ink-faint); text-align: center; padding: 2px 0 6px; }
.month-cell { display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 2px; padding: 5px 2px; border-radius: 10px; cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation; box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--tint-shade) 6%, transparent); overflow: hidden; }
.month-cell-other { box-shadow: none; }
.month-cell:active { background: color-mix(in srgb, var(--tint-shade) 6%, transparent); }
.month-cell-today { box-shadow: inset 0 0 0 1.5px var(--color-brand); }
.month-cell-selected { background: var(--color-brand); color: #fff; }
.month-cell-selected.month-cell-today { box-shadow: none; }

/* The add dialog's multi-date picker (Plan 361, drag-select rider) — the month picker's
   scaffold at dialog scale. touch-action: pan-y (design panel, 2026-08-24): the browser owns
   VERTICAL pans — the grid is half the visible form and must scroll like the rest of it —
   while horizontal paint-drags engage past an 8px slop (datesPointerMove) and pan-y still
   forbids the browser stealing them mid-sweep. Previously: none — the DRAG paints days, so the
   grid must never hand the gesture to the scroller mid-sweep. Selection classes are toggled
   from JS on built cells, so they live here as plain CSS (Tailwind emits nothing for classes
   that only appear on createElement'd nodes). */
.date-pick-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; touch-action: pan-y;
  /* The HOLD gesture must never become a text selection of the day numbers (founder
     report, 2026-08-23): no selection, no iOS callout. The contextmenu handler on the
     grid is the other half (Android's long-press). */
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
/* showModal() focuses the dialog's first button (the header's back chevron), and the
   browser paints it "selected" on open (founder screenshot, 2026-08-24). Only KEYBOARD
   focus earns the ring — :focus-visible stays untouched. (.new-dog-dialog, not the
   dialog's id: check-css's hex regex reads the id's first letters as a colour.) */
.new-dog-dialog button:focus:not(:focus-visible) { outline: none; }
.date-pick-day { height: 34px; border-radius: 10px; border: none; background: transparent; font-family: var(--font-display); font-weight: 600; font-size: 13px; color: var(--color-ink); display: flex; align-items: center; justify-content: center; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.date-pick-day.is-other { color: var(--color-ink-faint); opacity: 0.45; }
/* The grid's day states (Plan 364; ONE COLOR FAMILY, in THE TAPE'S LANGUAGE — founder,
   2026-08-24, final flip + same-day follow-up: "recurring filled, one-offs outlined",
   then "when I'm adding one-offs there should be outlines, not filled in"): the standing
   pattern is a SOFT brand fill; a ONE-OFF wears the RING — the already-booked and the
   freshly picked alike, because a pick SAVES as a one-off (a pick's ring is merely at
   full strength; the count line and Save carry the selection); a cancelled occurrence is
   the fill GRAYED with a crisp strike. One hue, and shape carries every distinction
   (fill/ring/strike), so nothing is hue-only. The marks show the POST-SAVE state —
   pending and persisted look identical. Precedence is applied by the renderer
   (pick > cancel mark > restore mark > base): one state per cell. */
.date-pick-day { position: relative; }
/* THE MULTI-SERVICE ENGINE (founder, 2026-08-24: "add the boarding and day care
   information using the same logic as from the schedule tape"): the painter feeds
   service colors through --fa/--fb (fills) and --ra/--rb (rings) — walk brand,
   day care amber, boarding purple; picks at full strength, saved marks at 75%.
   FILLS carry recurrings: one is a soft wash, two SPLIT HORIZONTALLY — walk on the
   BOTTOM half, day care on the top (founder, 2026-08-24), the schedule tape's own lane
   order, so the two surfaces stack their services the same way. RINGS carry one-offs
   and stays: one is that service's outline, two NEST with the walk outside ("the walk
   outside and the boarding outline inside"). --fa/--ra is always the WALK when a walk
   is in play; the is-* classes stay as the gesture/spec vocabulary, these own the look. */
.date-pick-day.fills-1 { background: color-mix(in srgb, var(--fa) 22%, transparent); }
.date-pick-day.fills-2 { background: linear-gradient(180deg,
  color-mix(in srgb, var(--fb) 22%, transparent) 0 50%,
  color-mix(in srgb, var(--fa) 22%, transparent) 50% 100%); }
/* A SKIPPED occurrence is diagonal stripes in that service's own colour, in that
   service's own half (founder, 2026-08-24) — the other service keeps its solid fill, so
   a skipped walk can never read as a cancelled day care. Only the ACTIVE service can be
   skipped here, so at most one half is ever striped. */
.date-pick-day.fills-1.stripe-a {
  background: repeating-linear-gradient(135deg,
    color-mix(in srgb, var(--fa) 45%, transparent) 0 2px, transparent 2px 5px);
}
.date-pick-day.fills-2.stripe-a {
  background:
    linear-gradient(color-mix(in srgb, var(--fb) 22%, transparent),
                    color-mix(in srgb, var(--fb) 22%, transparent)) top / 100% 50% no-repeat,
    repeating-linear-gradient(135deg,
      color-mix(in srgb, var(--fa) 45%, transparent) 0 2px, transparent 2px 5px)
      bottom / 100% 50% no-repeat;
}
.date-pick-day.fills-2.stripe-b {
  background:
    repeating-linear-gradient(135deg,
      color-mix(in srgb, var(--fb) 45%, transparent) 0 2px, transparent 2px 5px)
      top / 100% 50% no-repeat,
    linear-gradient(color-mix(in srgb, var(--fa) 22%, transparent),
                    color-mix(in srgb, var(--fa) 22%, transparent)) bottom / 100% 50% no-repeat;
}
/* A SKIPPED recurring occurrence on the tape: the GRAYED-OUT circle with ONE diagonal
   stripe through it (founder, 2026-08-24 — repeating stripes were tried first and are
   mush at 12px; one stripe is the whole signal). The stripe keeps the service's colour
   (--skip, set inline per lane) so the mark still says WHICH service, while the grey
   ground says it isn't happening. Before any of this a skipped day drew nothing at all,
   so a cancelled Tuesday looked like a Tuesday the dog never had. */
.tape-skip {
  background-color: color-mix(in srgb, var(--color-ink-faint) 15%, transparent);
  /* 135deg = the stripe leans "/" (top-right to bottom-left) — the founder's slope for
     CANCELLATIONS (Plan 373; it leaned "\" until then). Away is the mirror below. */
  background-image: linear-gradient(135deg,
    transparent 0 42%, color-mix(in srgb, var(--skip) 70%, transparent) 42% 58%,
    transparent 58% 100%);
}
/* AWAY (Plan 373 F1): same ground, same stops, OPPOSITE slope ("\") and a grey stripe
   (--skip is set to ink-faint at the call site). Hue AND slope distinguish it from a
   cancellation, so the two survive each other at 12px. */
/* The SPAN reads as one thing before the dots do: a faint wash over each away column
   (Plan 373 preview, shipped pending the founder's manual test). */
.tape-away-col { background: color-mix(in srgb, var(--color-ink-faint) 8%, transparent); border-radius: 6px; }
.tape-skip.tape-away {
  background-image: linear-gradient(45deg,
    transparent 0 42%, color-mix(in srgb, var(--skip) 70%, transparent) 42% 58%,
    transparent 58% 100%);
}
/* AWAY (Plan 373 F2): whole-cell neutral stripes at the OPPOSITE slope to a
   cancellation's ("\" vs "/"), date dimmed but not struck — away is whole-dog, a span
   over the dog rather than a cancellation of one service, so it wears no service colour
   and takes the whole cell. Pickable like an excluded day: booking into a span is
   allowed ("warn, don't sweep"); it just must never look free. */
.date-pick-day.is-away {
  background: repeating-linear-gradient(45deg,
    color-mix(in srgb, var(--color-ink-faint) 40%, transparent) 0 2px, transparent 2px 5px);
  color: color-mix(in srgb, var(--color-ink) 55%, transparent);
}
.date-pick-day.rings-1, .date-pick-day.rings-2 { border: 2px solid var(--ra); }
.date-pick-day.rings-2::before {
  content: ""; position: absolute; inset: 4px; border-radius: 7px;
  border: 2px solid var(--rb); pointer-events: none;
}
.date-pick-day.is-picked { font-weight: 700; }
/* A cancelled RECURRENCE — the one cancellation that must stay visible (a standing
   pattern hides behind it; the hold brings it back). The STRIKE lives here; the grayed
   FILL rides the same --fa/--fb engine as every other mark, so on a day that also holds
   another service only the skipped service's HALF goes gray (founder, 2026-08-24: an
   all-over gray made skipping a walk read as "cancelling daycare"). Pending and
   persisted look identical — both are the post-save truth. A cancelled one-off has no
   state at all: the day goes blank. */
.date-pick-day.is-excluded {
  color: var(--color-ink-soft);
  text-decoration: line-through 1.5px var(--color-ink);
}
/* The billed/locked cell states were tried in four visual forms and RETIRED for
   simplicity (founder, 2026-08-24: "the system has gotten a bit too complicated…
   wait for user input") — the interface closes the PAST wholesale instead: gray,
   inert, no per-day distinctions. Git has the versions. */
/* TODAY carries the grid's one underline (founder, 2026-08-24: "quickly see today").
   currentColor keeps it visible on every state — ink on plain and ringed days, faint
   on the dimmed ones. */
.date-pick-day.is-today::after {
  content: ""; position: absolute; left: 28%; right: 28%; bottom: 3px; height: 2px;
  border-radius: 2px; background: currentColor; opacity: 0.85;
}
/* Past days read as history: a gentle dim under whatever base marks they carry — and
   they are INERT; the interface closed the past (founder, 2026-08-24). */
.date-pick-day.is-past { opacity: 0.6; }
@media (prefers-reduced-motion: no-preference) {
  .date-pick-day.is-holding { transform: scale(0.9); transition: transform 0.45s ease; }
}
.month-cell-other { opacity: 0.25; }
.month-cell-num { font-family: var(--font-display); font-size: 16px; font-weight: 600; line-height: 1.2; }
/* Plan 226: columns (icon over counts). Group walks · Solo · Stays. Daycare + boarding stack into
   one column only when a day would otherwise need a fourth (all four activities present). */
.month-dots { display: flex; flex-direction: row; justify-content: center; gap: 3px; }
.month-dots-col { display: flex; flex-direction: column; align-items: center; gap: 0; }
.month-dot-icon { font-size: 12px; line-height: 1; }
.month-dot-count { font-family: var(--font-display); font-size: 9px; font-weight: 700; line-height: 1; text-align: right; font-variant-numeric: tabular-nums; justify-self: end; }
.month-dot-counts { display: flex; flex-direction: column; align-items: center; gap: 0; }
.month-dot-comma { font-size: 8px; color: var(--color-ink-faint); margin: 0 0.5px; }
.month-cell-selected .month-dot-count { color: rgba(255,255,255,0.8) !important; }
.month-cell-selected .month-dot-comma { color: rgba(255,255,255,0.5) !important; }
/* Service accents shared by the month dots and the request context's range chart
   (Plan 343): CONSTANT across themes, like the dots always were. Outside the
   --color- namespace on purpose — the parity check governs inverting tokens, and
   these deliberately do not invert. */
:root { --service-daycare: #f59e0b; --service-boarding: #8b5cf6; }
.month-dot-daycare { color: var(--service-daycare); }
.month-dot-boarding { color: var(--service-boarding); }
.ctx-bar-walks { background: var(--color-brand); }
.ctx-bar-daycare { background: var(--service-daycare); }
.ctx-bar-boarding { background: var(--service-boarding); }
.month-dot-charge { color: var(--color-mint-text); } /* Account month-jump picker (Plan 194): the money colour */
.month-cell-selected .month-dot { color: rgba(255,255,255,0.8) !important; }
.month-picker-spinner { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.month-picker-spin-icon { font-size: 28px; color: var(--color-ink-faint); animation: spin 1s linear infinite; }

/* ── Date Range Picker ── */
.drp-backdrop { position: fixed; inset: 0; z-index: 50; background: transparent; opacity: 0; transition: opacity 0.15s ease; pointer-events: none; }
.drp-backdrop.open { opacity: 1; pointer-events: auto; }
.drp-card { position: fixed; z-index: 51; background: var(--color-glass-strong); -webkit-backdrop-filter: blur(16px) saturate(1.8); backdrop-filter: blur(16px) saturate(1.8); border-radius: 16px; padding: 12px 12px 14px; box-shadow: 0 8px 24px color-mix(in srgb, var(--tint-shadow) 18%, transparent); transform: translateY(-8px); opacity: 0; transition: transform 0.2s ease, opacity 0.15s ease; pointer-events: none; overflow: hidden; }
.drp-card.open { transform: translateY(0); opacity: 1; pointer-events: auto; }
.drp-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.drp-title { font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--color-ink); }
.drp-nav { background: none; border: none; padding: 6px; color: var(--color-ink-faint); cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
.drp-months { display: flex; gap: 8px; }
.drp-month { flex: 1; min-width: 0; }
.drp-month-label { font-family: var(--font-display); font-weight: 600; font-size: 12px; color: var(--color-ink-soft); text-align: center; margin-bottom: 4px; }
.drp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; }
.drp-day-label { font-family: var(--font-display); font-weight: 600; font-size: 10px; color: var(--color-ink-faint); text-align: center; padding: 2px 0 4px; }
.drp-cell { display: flex; align-items: center; justify-content: center; aspect-ratio: 1; border-radius: 8px; cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation; border: none; background: none; padding: 0; }
.drp-cell:active { background: color-mix(in srgb, var(--tint-shade) 6%, transparent); }
.drp-cell-num { font-family: var(--font-display); font-size: 13px; font-weight: 600; line-height: 1; color: var(--color-ink); }
.drp-cell-other { opacity: 0.2; }
.drp-cell-past { opacity: 0.25; pointer-events: none; }
.drp-cell-today { box-shadow: inset 0 0 0 1.5px var(--color-brand); }
.drp-cell-endpoint { background: var(--color-brand); border-radius: 8px; }
.drp-cell-endpoint .drp-cell-num { color: #fff; }
.drp-cell-between { background: rgba(255, 107, 74, 0.12); border-radius: 0; }
.drp-cell-single { border-radius: 8px; }
.drp-cell-pending { animation: drp-pulse 1.2s ease-in-out infinite; }
@keyframes drp-pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 107, 74, 0.3); } 50% { box-shadow: 0 0 0 4px rgba(255, 107, 74, 0.15); } }
.drp-footer { display: flex; justify-content: center; margin-top: 10px; }
.drp-today-btn { font-family: var(--font-display); font-weight: 700; font-size: 13px; color: var(--color-brand-text); background: none; border: none; padding: 6px 16px; cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
.drp-today-btn:active { opacity: 0.6; }

turbo-frame#main-content { display: block; }
turbo-frame#dogs-list { display: block; }

/* ── Walk animation ── */
.walk-scene {
  position: relative;
  overflow: visible;
}

/* ── Tabs ── */
.tab-active {
  color: var(--color-brand-text);
  font-weight: 700;
  position: relative;
}
.tab-active::after {
  content: '';
  position: absolute;
  bottom: 0; left: 50%; transform: translateX(-50%);
  width: 20px; height: 3px;
  background: var(--color-brand);
  border-radius: 3px;
}
.tab-inactive {
  color: var(--color-ink-faint);
  font-weight: 600;
  transition: color 0.2s;
}
.tab-inactive:hover { color: var(--color-ink-soft); }
/* An EMPTY tab says so before the tap (Plan 377, founder: "some indicator (grayed out text?)
   for tabs that are in empty state so we don't have to click to find out"). A third marker
   beside active/inactive — walk_controller#_showTab rewrites only those two, so it survives
   switching. Active + empty keeps the brand underline, dimmed: looking at nothing, on purpose. */
.tab-empty:not(.tab-active) { color: color-mix(in srgb, var(--color-ink-faint) 55%, transparent); font-weight: 500; }
.tab-empty.tab-active { opacity: .6; }

/* ── Day column tap highlight (corner brackets) ── */
.day-tap-highlight {
  position: relative;
  transition: opacity 0.35s ease-out;
}
.day-tap-highlight::before {
  --arm: 6px;
  --r: 2px;
  --c: rgba(255,107,74,0.35);
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    /* top-left horizontal */ linear-gradient(var(--c), var(--c)) 0 0 / var(--arm) 1.5px no-repeat,
    /* top-left vertical */   linear-gradient(var(--c), var(--c)) 0 0 / 1.5px var(--arm) no-repeat,
    /* top-right horizontal */ linear-gradient(var(--c), var(--c)) 100% 0 / var(--arm) 1.5px no-repeat,
    /* top-right vertical */   linear-gradient(var(--c), var(--c)) 100% 0 / 1.5px var(--arm) no-repeat,
    /* bottom-left horizontal */ linear-gradient(var(--c), var(--c)) 0 100% / var(--arm) 1.5px no-repeat,
    /* bottom-left vertical */   linear-gradient(var(--c), var(--c)) 0 100% / 1.5px var(--arm) no-repeat,
    /* bottom-right horizontal */ linear-gradient(var(--c), var(--c)) 100% 100% / var(--arm) 1.5px no-repeat,
    /* bottom-right vertical */   linear-gradient(var(--c), var(--c)) 100% 100% / 1.5px var(--arm) no-repeat;
}
.day-tap-highlight {
  box-shadow: none !important;
}
.day-tap-highlight.day-tap-fade {
  opacity: 0;
}

/* ── Dog card body transition ── */
.dog-card-body {
  transition: all 0.3s ease;
}

/* Schedule tape nav buttons */
.tape-nav-btn {
  flex-shrink: 0;
  width: 24px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-ink-soft);
  opacity: 0.5;
  transition: opacity 0.15s;
}
.tape-nav-btn:active:not(.tape-nav-disabled) {
  opacity: 1;
}
.tape-nav-btn.tape-nav-disabled {
  opacity: 0.15;
  pointer-events: none;
}

.tape-month-sep::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -4.5px;
  width: 1px;
  background: color-mix(in srgb, var(--color-line-strong) 30%, transparent);
  pointer-events: none;
}

/* An inactive dog's schedule tape reads as muted: desaturate the colored activity dots/icons to
   gray. Keyed on the card's data-status so it covers both the initial tape and the lazy-appended
   days (dogs_index.js appends them into the same [data-schedule-tape] container). */
article[data-status="inactive"] [data-schedule-tape] {
  filter: grayscale(1);
  opacity: 0.7;
}

/* ── Modal ── */
/* ── New Dog Dialog ── */
.new-dog-dialog {
  margin: auto;
  padding: 1.25rem;
  border: none;
  border-radius: 1rem;
  background: var(--color-canvas);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
  max-width: 20rem;
  width: calc(100% - 2rem);
  max-height: calc(100dvh - 4rem);
  overflow-y: auto; /* short screens scroll the grown form (Plan 234) */
  /* Vertical pans are the SCROLL's (2026-08-22): the browser takes them natively, so a
     thumb-swipe on a form taller than the panel scrolls it — on every .new-dog-dialog
     surface. Where dialog-fling is attached (dogs index, dog picker) it engages only on a
     horizontal-dominant drag, so the two gestures never fight. (The date-pick grid inside
     the calendar's dialog sets touch-action: none for its paint drags.) */
  /* pan-y, ONCE (design panel, 2026-08-24): a fling-era `touch-action: none` re-declared
     three lines below and silently won the cascade — chrome swipes were dead. dialog_fling
     engages only on horizontal-dominant drags, so it coexists with native vertical pans. */
  touch-action: pan-y;
}
.new-dog-dialog input,
.new-dog-dialog textarea {
  touch-action: auto;
}
/* iOS keyboard (2026-08-04): with the dialog centered, focusing the dog search made
   iOS push the page up to reveal the input — sliding the title and search bar clear
   out of the visual viewport. While the search field has focus, anchor the dialog to
   the top (margin-bottom stays auto) and shorten the roster list so title + search +
   a few rows all sit above the keyboard; blur re-centers it. */
/* iOS keyboard (2026-08-04): with the dialog centered, focusing the dog search made
   iOS push the page up to reveal the input — sliding the title and search bar clear
   out of the visual viewport. calendar_controller watches visualViewport and adds
   kb-raised only while the keyboard has ACTUALLY shrunk the viewport with the search
   focused (a :focus-driven CSS rule moved the dialog on every desktop click too, and
   broke headless row taps). All geometry — margin-top anchored to visualViewport
   offsetTop (iOS pans the visual viewport on fresh-page focus), the dialog and
   roster-list height caps — is set inline by that handler; the class is the marker.
   margin-bottom stays auto from the base rule, so a fixed top margin = top-anchored. */
.new-dog-dialog.kb-raised {
  margin-bottom: auto;
}
/* Walk widget (Plan 234): Solo|Group is the StarView mini switch (the Photos
   panel preview toggle's 28×16 track + sliding 12px thumb), light palette,
   inline with the Walks label. One checkbox, :has()-driven, no JS; the form
   reset unchecks it (thumb slides home to Solo). */
.walk-mode-track {
  position: relative;
  width: 28px;
  height: 16px;
  flex-shrink: 0;
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-ink-faint) 35%, transparent);
  transition: background 0.18s ease;
}
.walk-mode-track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
  transition: transform 0.18s ease;
}
.walk-mode-switch:has(input:checked) .walk-mode-track { background: var(--color-brand); }
.walk-mode-switch:has(input:checked) .walk-mode-track::after { transform: translateX(12px); }
.walk-mode-side { transition: color 0.18s ease; }
.walk-mode-solo { color: var(--color-ink-soft); }
.walk-mode-group { color: var(--color-ink-faint); }
.walk-mode-switch:has(input:checked) .walk-mode-solo { color: var(--color-ink-faint); }
.walk-mode-switch:has(input:checked) .walk-mode-group { color: var(--color-brand-text); }

/* The pack pills exist only for Group walks — the switch reveals them via
   :has() (no JS; reset hides them again). */
.new-dog-walk-widget .pack-pills {
  display: none;
}
.new-dog-walk-widget:has(input[name="walk_mode"]:checked) .pack-pills {
  display: flex;
}
.new-dog-dialog::backdrop {
  background: var(--color-scrim-deep);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}


.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: var(--color-scrim);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}


.modal-panel {
  position: fixed;
  inset: 0.75rem;
  display: flex;
  flex-direction: column;
  background: var(--color-glass-strong);
  backdrop-filter: blur(24px) saturate(1.5);
  -webkit-backdrop-filter: blur(24px) saturate(1.5);
  border: 1px solid var(--color-line-soft);
  z-index: 301;
  max-width: 32rem;
  margin-inline: auto;
  border-radius: 1.5rem;
  overflow: hidden;
  box-shadow: 0 8px 24px rgba(0,0,0,0.18);
}

/* Ad-hoc 2026-04-11: fling-to-dismiss hero photo. Disable native
   touch-scroll and text selection on the photo area so the fling
   drag doesn't fight vertical scroll or select nearby text. */
.dog-modal-hero {
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

/* Hero photo overlay buttons — press feedback */
.hero-overlay-btn:active {
  background: rgba(255,255,255,0.3);
}

.modal-panel > form {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.modal-header {
  flex-shrink: 0;
}

.modal-scroll {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  scrollbar-width: none;
}
.modal-scroll::-webkit-scrollbar { display: none; }

.modal-tabs {
  background: var(--color-glass-strong);
}

/* ── Unlock-visible elements (add/remove buttons) ── */
.modal-panel .unlock-visible { display: none; }
.modal-panel.unlocked .unlock-visible { display: inline-flex; }
.modal-panel.unlocked div.unlock-visible { display: block; }
.modal-panel.unlocked div.unlock-visible.flex { display: flex; }
.modal-panel.unlocked p.unlock-visible { display: block; }
.modal-panel.unlocked .unlock-visible[data-controller~="address-autocomplete"] { display: block; width: 100%; }
.modal-panel.unlocked .unlock-show.hidden { display: block; }

/* ── Lock-visible elements (read-mode only) ── */
.modal-panel.unlocked .lock-visible { display: none; }

/* ── Mode selects (pickup/checkin, dropoff/checkout) ── */
.modal-panel.unlocked .mode-select {
  pointer-events: auto;
  cursor: pointer;
  appearance: auto;
  -webkit-appearance: menulist;
}

/* ── Day toggles ── */
.modal-panel.unlocked .day-toggle { cursor: pointer; }

/* ── Swap fields ── */
/* Wrapper for fields where read/edit are fundamentally different widgets
   (badge→select, pills→text, masked→plain). Activates on click when unlocked. */
.swap-field .edit-field { display: none; }
.swap-field.active .read-field { display: none; }
.swap-field.active .edit-field { display: revert; }
.modal-panel.unlocked .swap-field { cursor: pointer; }

/* ── Live inputs ── */
/* Always in DOM at same position as their text. Transparent + non-interactive
   in read mode; clickable in unlocked mode.
   Font/color inherit is in @layer components so Tailwind utilities override it
   when explicit classes are present; otherwise the input inherits from its parent. */

/* Structural — unlayered so nothing overrides these */
.live-input {
  background: transparent;
  border: none;
  outline: none;
  padding: 0;
  margin: 0;
  display: block;
  width: 100%;
  -webkit-appearance: none;
  appearance: none;
  pointer-events: none;
  resize: none;
}
.live-input::-webkit-inner-spin-button,
.live-input::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.live-input[type="number"] {
  -moz-appearance: textfield;
}

/* Typography — in components layer so Tailwind utilities win when present */
@layer components {
  .live-input {
    font-family: inherit;
    font-size: inherit;
    font-weight: inherit;
    font-style: inherit;
    line-height: inherit;
    letter-spacing: inherit;
    color: inherit;
  }
}

.modal-panel.unlocked .live-input {
  pointer-events: auto;
  cursor: text;
}

.modal-panel.unlocked .live-input:hover {
  background: color-mix(in srgb, var(--tint-shade) 4%, transparent);
  border-radius: 3px;
}

.modal-panel.unlocked .live-input:focus {
  outline: none;
  background: rgba(255, 107, 74, 0.06);
  border-radius: 3px;
}

/* ── Edit inputs ── */
/* Base class handles box only — no font-size/family so Tailwind utilities aren't clobbered. */
.edit-input {
  width: 100%;
  padding: 0.3rem 0.6rem;
  background: var(--color-glass-strong);
  border: 1.5px solid color-mix(in srgb, var(--tint-shade) 10%, transparent);
  border-radius: 0.5rem;
  font-size: 0.875rem;        /* default for body inputs */
  font-family: var(--font-body);
  color: var(--color-ink);
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
  -webkit-appearance: none;
  appearance: none;
}
.edit-input:focus {
  border-color: var(--color-brand);
  box-shadow: 0 0 0 3px rgba(255, 107, 74, 0.15);
}
.edit-input::placeholder { color: var(--color-ink-faint); }

/* Modifier: matches the modal header h2 (name field) */
.edit-input-title {
  font-size: 1.5rem;
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: -0.025em;
  color: var(--color-ink);
  padding-top: 0.1rem;
  padding-bottom: 0.1rem;
}

/* Modifier: matches the breed/age subheading (text-sm font-600 text-brand) */
.edit-input-sub {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--color-brand-text);
}

/* ── Field errors ── */
.field-error { font-size: 0.75rem; color: var(--color-rose-text); margin-top: 0.25rem; }

/* (.error-banner removed — Plan 196: the dog modal renders shared/_error_summary.) */

/* ── Radial photo menu ── */
.starpaw-overlay {
  background: transparent;
  transition: background 0.2s ease, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.5s ease;
}
/* Star visible — overflow clip + header z-index (starview and listening) */
body[data-quick-photo-state-value="starview"] .starpaw-overlay,
body[data-quick-photo-state-value="listening"] .starpaw-overlay {
  overflow: hidden;
}
body[data-quick-photo-state-value="starview"] > header,
body[data-quick-photo-state-value="listening"] > header {
  z-index: 201;
}

/* StarView: background + dimming + panels — blocks page interaction */
/* Deep space base with nebula clouds */
body[data-quick-photo-state-value="starview"] .starpaw-overlay,
body[data-quick-photo-state-value="listening"] .starpaw-overlay {
  background:
    /* (Removed: a "dark mask behind corona" radial-gradient that was pinned to the
       screen centre — its --star-cx/--star-cy were never set, so it rendered as a
       dark hole in the middle of the starfield rather than tracking the star.) */
    radial-gradient(ellipse 55% 45% at 15% 20%,
      rgba(120, 60, 160, 0.25) 0%, rgba(80, 40, 130, 0.12) 40%, transparent 70%),
    radial-gradient(ellipse 50% 55% at 80% 75%,
      rgba(40, 140, 160, 0.2) 0%, rgba(30, 100, 140, 0.1) 40%, transparent 70%),
    radial-gradient(ellipse 35% 40% at 70% 40%,
      rgba(160, 60, 100, 0.15) 0%, rgba(120, 40, 80, 0.06) 50%, transparent 70%),
    radial-gradient(ellipse 40% 35% at 25% 80%,
      rgba(50, 80, 180, 0.18) 0%, rgba(40, 60, 140, 0.08) 45%, transparent 70%),
    rgba(12, 20, 42, 0.92);
  pointer-events: auto !important;
}
/* Twinkling star layers — injected dynamically by showStarView() */
.starview-stars {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
}
.starview-stars-1 { animation: starfield-twinkle-1 4s ease-in-out infinite alternate; }
.starview-stars-2 { animation: starfield-twinkle-2 5s ease-in-out infinite alternate; }
@keyframes starfield-twinkle-1 {
  0%   { opacity: 0.5; }
  50%  { opacity: 1; }
  100% { opacity: 0.6; }
}
@keyframes starfield-twinkle-2 {
  0%   { opacity: 0.9; }
  50%  { opacity: 0.3; }
  100% { opacity: 0.8; }
}
body > main,
body > nav {
  transition: opacity 0.5s ease, transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Nav drawer handle — a small grip above the bar to slide it away for more space.
   It's a child of <nav>, positioned just above the bar's top edge, so it rides
   down with the bar when collapsed and peeks at the bottom viewport edge. */
.nav-drawer-handle {
  position: absolute;
  left: 50%;
  bottom: 100%;
  transform: translateX(-50%);
  width: 56px;
  height: 20px;
  /* The handle is the bring-it-back affordance, shown only while collapsed; when
     the bar is up you hide it with a downward drag instead. */
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 11px 11px 0 0;
  background: var(--color-inset-handle);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  box-shadow: 0 -3px 10px color-mix(in srgb, var(--tint-shadow) 10%, transparent);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.nav-drawer-grip {
  width: 38px;
  height: 5px;
  border-radius: 3px;
  background: var(--color-ink-faint);
  transition: background 0.15s ease;
}
.nav-drawer-handle:active .nav-drawer-grip { background: var(--color-ink-soft); }
body.nav-collapsed .nav-drawer-handle { display: flex; }

/* Collapsed: slide the whole bar (handle included) down by its own height, and
   let <main> reclaim the freed bottom space (overrides the inline padding). */
body.nav-no-anim > nav { transition: none; }
body.nav-collapsed > nav { transform: translateY(100%); }
body.nav-collapsed > main {
  padding-bottom: calc(1.75rem + env(safe-area-inset-bottom)) !important;
}
body > main { transition: padding-bottom 0.32s cubic-bezier(0.4, 0, 0.2, 1); }
body.nav-no-anim > main { transition: none; }

/* Walk-screen map controls (recenter / report / Paw·Pin) anchor above the bottom
   nav via var(--map-ctrl-bottom); when the nav is dragged away they drop down to
   the freed space. (Inline JS styles read this var — see mapbox_map_controller.) */
body { --map-ctrl-bottom: calc(3.5rem + env(safe-area-inset-bottom, 0px) + 12px); }
body.nav-collapsed { --map-ctrl-bottom: calc(env(safe-area-inset-bottom, 0px) + 12px); }
body[data-quick-photo-state-value="starview"] > main,
body[data-quick-photo-state-value="listening"] > main {
  opacity: 0.1;
}
body[data-quick-photo-state-value="starview"] > nav,
body[data-quick-photo-state-value="listening"] > nav {
  opacity: 0;
  pointer-events: none;
}
/* Plan 166 (experiment): keep the top header visible + usable while StarView is open. The header is
   z-50, but the StarView overlay rides at z-1000 (window_stack base), so it'd otherwise sit behind
   the nebula. Lift it above the StarView layers (action bar = base+1), below a stacked modal (2000).
   Listening (AI mode) gets the same lift (Plan 193) — it uses the same overlay, and the stale z-201
   rule left the header BEHIND the nebula there. */
body[data-quick-photo-state-value="starview"] > header,
body[data-quick-photo-state-value="listening"] > header {
  z-index: 1500;
}
body[data-quick-photo-state-value="starview"] .starpaw-context-bar,
body[data-quick-photo-state-value="starview"] .starpaw-context-panel {
  will-change: transform;
}

.starpaw-swipe-zone-line {
  position: fixed;
  bottom: calc(3.5rem + env(safe-area-inset-bottom));
  left: 12px;
  right: 12px;
  height: 0;
  border-top: 1px dashed rgba(255, 255, 255, 0.35);
  pointer-events: none;
}

/* Bottom action bar — panel-attached verbs (photos Share/Publish/Slideshow, …)
   in the repurposed gesture strip. The strip stays gesture-transparent; only the
   buttons are pointer-events islands, so the paw's fling/pull gestures still work
   in the gaps. Visible only in starview (no actions during listening). */
.starpaw-action-bar {
  position: fixed;
  bottom: env(safe-area-inset-bottom);
  left: 0; right: 0;
  height: calc(3.5rem - 7px);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  pointer-events: none;
  z-index: 201;
  opacity: 0;
  transition: opacity 0.2s;
}
body[data-quick-photo-state-value="starview"] .starpaw-action-bar {
  opacity: 1;
}
.starpaw-action-bar-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 38px;
  padding: 0 14px;
  border-radius: 19px;
  /* Glassy brand-blue pill — reads as part of the StarView (matches the core's
     electric blue) rather than a bright cream chip floating on the nebula. */
  background: rgba(74, 139, 255, 0.22);
  color: #fff;
  border: 1px solid rgba(74, 139, 255, 0.55);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.28),
              0 4px 14px rgba(0, 0, 0, 0.24);
  font-family: var(--font-display, inherit);
  font-weight: 600;
  font-size: 13px;
  line-height: 1;
  pointer-events: none;
  cursor: pointer;
  flex-shrink: 0;
  -webkit-user-select: none;
  user-select: none;
}
body[data-quick-photo-state-value="starview"] .starpaw-action-bar-btn {
  pointer-events: auto;
}
.starpaw-action-bar-btn .material-symbols-outlined {
  font-size: 20px;
  line-height: 1;
}
.starpaw-action-bar-btn.is-active {
  background: var(--color-brand-light, #e0e7ff);
  color: var(--color-brand, #4f46e5);
}
/* Always visible, but dimmed + inert when nothing is selected. */
.starpaw-action-bar-btn.is-disabled {
  opacity: 0.4;
}
.starpaw-action-bar-btn:not(.is-disabled):active {
  filter: brightness(0.9);
}

/* Recording indicator — glow on paw control while mic is live */
.star-group.ai-recording {
  box-shadow: 0 0 10px 3px rgba(99, 102, 241, 0.3) !important;
  animation: ai-record-pulse 2s ease-in-out infinite;
}
.star-group.ai-recording::after {
  border-color: rgba(99, 102, 241, 0.5);
  box-shadow: 0 0 8px 2px rgba(99, 102, 241, 0.3);
}
.star-group.ai-recording.ai-voice-active {
  box-shadow: 0 0 16px 6px rgba(99, 102, 241, 0.55) !important;
  animation: ai-voice-pulse 0.8s ease-in-out infinite;
}
.star-group.ai-recording.ai-voice-active::after {
  border-color: rgba(99, 102, 241, 0.7);
  box-shadow: 0 0 12px 4px rgba(99, 102, 241, 0.5);
}
.star-group.ai-recording .orbit-slot-group-dots {
  display: none;
}
@keyframes ai-record-pulse {
  0%, 100% { box-shadow: 0 0 10px 3px rgba(99, 102, 241, 0.2); }
  50% { box-shadow: 0 0 14px 5px rgba(99, 102, 241, 0.4); }
}
@keyframes ai-voice-pulse {
  0%, 100% { box-shadow: 0 0 12px 4px rgba(99, 102, 241, 0.35); }
  50% { box-shadow: 0 0 22px 8px rgba(99, 102, 241, 0.65); }
}

/* Equalizer — vertical bars in the gesture zone strip at the bottom */
.core-eq-container {
  position: fixed;
  bottom: calc(3.5rem + env(safe-area-inset-bottom));
  left: 12px;
  right: 12px;
  height: 3rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  pointer-events: none;
  z-index: 201;
}
.core-eq-bar {
  width: 3px;
  border-radius: 1px;
  background: rgba(99, 102, 241, 0.8);
  will-change: height;
  flex-shrink: 0;
}

/* AI transcript text — inside the gesture area */
.ai-gesture-text {
  position: fixed;
  bottom: calc(env(safe-area-inset-bottom) + (3.5rem / 2) - 10px);
  left: 16px;
  right: 16px;
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.3;
  color: rgba(255, 255, 255, 0.8);
  pointer-events: none;
  transition: opacity 0.2s ease-out;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ai-gesture-text:has(.ai-gesture-action) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  overflow: visible;
}
.ai-gesture-action {
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  margin: -10px 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.15);
  border: none;
  color: white;
  cursor: pointer;
  flex-shrink: 0;
}
.ai-gesture-action:active {
  background: rgba(255, 255, 255, 0.3);
}
.ai-gesture-action .material-symbols-outlined {
  font-size: 20px;
  line-height: 1;
}

/* ── Listening mode ── */

/* Art deco ornament ring — solid silver/platinum sunburst bezel.
   Inspired by 1930s wall clocks: bold stepped wedges, thick bands, raised studs.
   Hidden by default, fades in during conversation/listening mode.
   Slowly counter-rotates against the starburst for visual depth. */
.star-deco-ring {
  position: absolute;
  width: 260px;
  height: 260px;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.6s ease-out;
  animation: deco-ring-spin 120s linear infinite;
  /* Combined mask: radial (center hole + edge fade) intersected with
     conic (5 segments at 72° intervals, each ~52° wide, 20° gaps).
     Pentagonal symmetry echoes the starburst rays. */
  -webkit-mask-image:
    radial-gradient(circle, transparent 22%, white 26%, white 88%, transparent 95%),
    conic-gradient(
      from 0deg,
      white 0deg, white 26deg,
      transparent 26deg, transparent 46deg,
      white 46deg, white 98deg,
      transparent 98deg, transparent 118deg,
      white 118deg, white 170deg,
      transparent 170deg, transparent 190deg,
      white 190deg, white 242deg,
      transparent 242deg, transparent 262deg,
      white 262deg, white 314deg,
      transparent 314deg, transparent 334deg,
      white 334deg, white 360deg
    );
  -webkit-mask-composite: destination-in;
  mask-image:
    radial-gradient(circle, transparent 22%, white 26%, white 88%, transparent 95%),
    conic-gradient(
      from 0deg,
      white 0deg, white 26deg,
      transparent 26deg, transparent 46deg,
      white 46deg, white 98deg,
      transparent 98deg, transparent 118deg,
      white 118deg, white 170deg,
      transparent 170deg, transparent 190deg,
      white 190deg, white 242deg,
      transparent 242deg, transparent 262deg,
      white 262deg, white 314deg,
      transparent 314deg, transparent 334deg,
      white 334deg, white 360deg
    );
  mask-composite: intersect;
}
@keyframes deco-ring-spin {
  from { transform: translate(-50%, -50%) rotate(0deg); }
  to   { transform: translate(-50%, -50%) rotate(-360deg); }
}

/* ── The winter sky (Plan 350) ──────────────────────────────────────────────
   Sirius' real neighbours — Orion, Canis Minor, Monoceros, Lepus, Puppis,
   Columba — drawn behind the conversation. CANIS MAJOR IS ABSENT on purpose:
   the Dog Star IS Sirius, so the figures ring an empty middle and the star
   occupies it (Orion's belt points at it, exactly as it does in the real sky).

   Listening ONLY: StarView's panels and strips need a quiet backdrop, and the
   conversation is the screen with room to look up. The layer is inert scenery —
   pointer-events: none, no twinkle competing with the star. */
.starview-constellations {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  transition: opacity 1.2s ease-out;
  /* The sky EMERGES from the glow: the corona washes out anything near the
     centre anyway, so fade the layer there instead of letting figure lines
     muddy the starburst. Radius tracks CENTRE_RADIUS in winter_sky.js. */
  -webkit-mask-image: radial-gradient(circle at 50% 50%, transparent 14%, rgba(0, 0, 0, 0.55) 24%, #000 34%);
  mask-image: radial-gradient(circle at 50% 50%, transparent 14%, rgba(0, 0, 0, 0.55) 24%, #000 34%);
}
body.listening-adorned .starview-constellations {
  opacity: 1;
}
.constellation-line {
  stroke: rgba(190, 210, 255, 0.26);
  stroke-width: 0.12;
  stroke-linecap: round;
}
.constellation-star {
  fill: rgba(226, 236, 255, 0.58);
}
/* The named stars — Betelgeuse, Rigel, Procyon — carry the figure; a hair
   brighter, never bright enough to rival the corona. */
.constellation-star.is-bright {
  fill: rgba(240, 246, 255, 0.88);
}

/* Shooting stars — brief luminous streaks across the sky during conversation */
.shooting-star {
  position: absolute;
  width: var(--shoot-len, 60px);
  height: 1.5px;
  border-radius: 1px;
  pointer-events: none;
  z-index: 1;
  transform-origin: 100% 50%;
  transform: rotate(var(--shoot-angle, 30deg));
  background: linear-gradient(90deg, transparent 0%, rgba(220, 230, 255, 0.15) 20%, rgba(240, 245, 255, 0.9) 85%, #fff 100%);
  animation: shooting-star-fly 1s ease-in forwards;
}
.shooting-star::after {
  content: "";
  position: absolute;
  right: -1px;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 4px 1px rgba(200, 220, 255, 0.8);
}
@keyframes shooting-star-fly {
  0% {
    opacity: 0;
    translate: 0 0;
  }
  10% {
    opacity: 1;
  }
  70% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    translate: var(--shoot-dx, -30vw) var(--shoot-dy, 20vh);
  }
}

/* Speaking ripples — concentric refractive waves emanating from the star body
   during RESPONDING state. Simulates spacetime distortion via layered ring
   gradients with mix-blend-mode for a light-bending look. */
.star-ripple-container {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 0;
  height: 0;
  pointer-events: none;
  z-index: 0;
}
.star-ripple {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 40px;
  height: 40px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  pointer-events: none;
  /* Double ring: bright leading edge + dark trough behind it, like a water wave
     refracting light. The bright/dark bands create a lens-like distortion illusion. */
  background: radial-gradient(
    circle,
    transparent 40%,
    rgba(180, 200, 240, 0.04) 55%,
    rgba(100, 120, 160, 0.08) 62%,
    rgba(200, 220, 255, 0.15) 68%,
    rgba(255, 255, 255, 0.10) 72%,
    rgba(200, 220, 255, 0.06) 78%,
    rgba(100, 120, 160, 0.03) 85%,
    transparent 95%
  );
  mix-blend-mode: screen;
  animation: star-ripple-expand var(--ripple-duration, 2s) ease-out forwards;
}
@keyframes star-ripple-expand {
  0% {
    width: 40px;
    height: 40px;
    opacity: 0;
  }
  10% {
    opacity: 1;
  }
  60% {
    opacity: 0.7;
  }
  100% {
    width: var(--ripple-size, 350px);
    height: var(--ripple-size, 350px);
    opacity: 0;
  }
}

/* Body centering transition */

/* Fade out StarView UI during listening mode (tabs, panels, empty state,
   date, joystick arrows). Starburst rays and corona stay visible. */
body[data-quick-photo-state-value="listening"] .starpaw-context-bar,
body[data-quick-photo-state-value="listening"] .starpaw-empty-state,
body[data-quick-photo-state-value="listening"] .starpaw-context-panel,
body[data-quick-photo-state-value="listening"] .starpaw-joystick-arrows {
  opacity: 0 !important;
  pointer-events: none !important;
  transition: opacity 0.4s ease-out;
}

/* Dialogue bubble shared */
.listening-bubble {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  max-width: 85vw;
  width: max-content;
  padding: 10px 18px;
  border-radius: 18px;
  background: #fff;
  border: 2px solid #222;
  color: #1a1a1a;
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.01em;
  text-align: left;
  pointer-events: none;
  z-index: 10;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

/* Clickable links in conversation bubbles (phone numbers, addresses) */
.conv-link {
  color: var(--color-brand, #6366f1);
  text-decoration: underline;
  text-underline-offset: 2px;
  pointer-events: auto;
}


/* Star bubble — above body */
.listening-bubble-star {
  bottom: calc(50% + 60px);
}

/* User bubble — below paw control */
.listening-bubble-user {
  top: calc(50% + 100px);
}
.listening-bubble-user .listening-bubble-text {
  display: block;
  max-height: calc(1.4em * 4);
  overflow: hidden;
}

/* Tail — curved comic-book style via SVG */
.listening-bubble-tail {
  position: absolute;
  width: 20px;
  height: 18px;
  overflow: visible;
}
.listening-bubble-tail svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: #fff;
  stroke: #222;
  stroke-width: 1.5;
  stroke-linejoin: round;
}
/* Star bubble tail — bottom-center, curves down toward the core */
.listening-bubble-star .listening-bubble-tail {
  bottom: -16px;
  left: 50%;
  transform: translateX(-50%);
}
/* User bubble tail — bottom, horizontal position set by JS */
.listening-bubble-user .listening-bubble-tail {
  bottom: -16px;
  left: 50%;
  transform: translateX(-50%);
}

/* Entrance animation */
.listening-bubble-enter {
  animation: bubble-pop 0.2s ease-out both;
}
@keyframes bubble-pop {
  from { opacity: 0; transform: translateX(-50%) scale(0.8); }
  to   { opacity: 1; transform: translateX(-50%) scale(1); }
}

/* Exit animation */
.listening-bubble-exit {
  animation: bubble-fade 0.15s ease-in both;
}
@keyframes bubble-fade {
  to { opacity: 0; transform: translateX(-50%) scale(0.9); }
}

/* Thinking bubble — cloud-shaped thought bubble */
.thinking-bubble {
  padding: 0;
  border: none;
  border-radius: 0;
  background: none;
  box-shadow: none;
  min-width: 0;
  width: auto;
  transform: translateX(-25%);
  /* Cloud SVG: single fluffy outline path, white fill, no internal lines */
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 140 90'%3E%3Cpath d='M32 72 C18 72 8 64 10 54 C4 48 4 38 12 30 C14 20 24 12 38 12 C42 4 56 0 68 4 C76 -2 90 -1 100 6 C108 4 120 10 124 20 C134 26 138 38 132 50 C138 60 132 72 118 72 C112 80 96 84 84 78 C74 84 56 82 46 76 C40 78 34 76 32 72 Z' fill='%23fff' stroke='%23222' stroke-width='2'/%3E%3C/svg%3E") no-repeat center / 100% 100%;
  overflow: visible;
}
.thinking-bubble.listening-bubble-enter {
  animation: think-pop 0.2s ease-out both;
}
.thinking-bubble.listening-bubble-exit {
  animation: think-fade 0.15s ease-in both;
}
@keyframes think-pop {
  from { opacity: 0; transform: translateX(-25%) scale(0.8); }
  to   { opacity: 1; transform: translateX(-25%) scale(1); }
}
@keyframes think-fade {
  to { opacity: 0; transform: translateX(-25%) scale(0.9); }
}
.thinking-bubble .listening-bubble-tail { display: none; }

.thinking-dodecahedron {
  display: block;
  width: 120px;
  height: 60px;
  /* Inset the canvas well inside the cloud outline */
  margin: 22px 26px 22px;
}

/* Thought-cloud dots — angled trail from bubble down to star.
   Comic-book style: well-spaced, ~1 dot-diameter gap between each. */
.thinking-tail {
  position: absolute;
  bottom: -52px;
  left: 0;
  right: 0;
  height: 46px;
  pointer-events: none;
}
.thinking-dot {
  position: absolute;
  border-radius: 50%;
  background: #fff;
  border: 1.5px solid #222;
}
/* Dot 1 (largest) — near left third of bubble, gap from bubble edge */
.thinking-dot-1 { width: 12px; height: 12px; top: 0; left: 33%; transform: translateX(-50%); }
/* Dot 2 (medium) — angled toward star, ~1 diameter gap from dot 1 */
.thinking-dot-2 { width: 8px; height: 8px; top: 20px; left: 28%; transform: translateX(-50%); }
/* Dot 3 (smallest) — near star center, ~1 diameter gap from dot 2 */
.thinking-dot-3 { width: 5px; height: 5px; top: 36px; left: 25%; transform: translateX(-50%); }

/* Body action icon in listening mode — must sit above the core circuit overlay (z-index: 2).
   The ::before disc covers the circuit traces so the icon reads clearly. */
.listening-action-icon {
  font-size: 28px;
  color: #000;
  position: relative;
  z-index: 3;
}

/* ── Confirm summary box (plan 127) ──
   A plain, factual "what you're about to confirm" receipt, shown above the star
   while a confirm-required command's icon is pending on the core. Distinct from
   the white spoken bubble: a dark, monospace-ish ticket so it reads as a system
   summary of the change, not conversation. Positioned above the star bubble. */
.listening-confirm-summary {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(50% + 132px);
  /* width: max-content (like .listening-bubble) so the box sizes to its text
     instead of collapsing to the small radial container and wrapping into a
     tall column. max-width keeps a long summary from spanning the whole screen. */
  width: max-content;
  max-width: 85vw;
  padding: 8px 13px;
  border-radius: 12px;
  background: rgba(20, 24, 33, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.16);
  color: #eef2f8;
  font-family: var(--font-display, ui-sans-serif);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: 0.01em;
  text-align: center;
  pointer-events: none;
  z-index: 11;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.32);
  white-space: pre-line;
  opacity: 0;
  transition: opacity 0.18s ease;
}
.listening-confirm-summary.visible { opacity: 1; }
.listening-confirm-summary .lcs-label {
  display: block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #7f93ad;
  margin-bottom: 2px;
}

/* ── StarPaw context bar ── */
.starpaw-context-bar {
  display: flex;
  gap: 8px;
}
.starpaw-context-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 5px 12px;
  border-radius: 20px;
  border: none;
  background: #1e3a5f;
  color: #8aa8c4;
  font-size: 13px;
  font-weight: 600;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.15s, color 0.15s;
}
.starpaw-context-btn.active {
  background: #1e3a5f;
  color: #fff;
  box-shadow: 0 0 0 1px rgba(255,255,255,0.7), 0 0 0 3px #4A8BFF;
}
.starpaw-context-btn:active {
  background: rgba(255, 255, 255, 0.3);
}
.starpaw-context-btn.starpaw-tab-disabled {
  opacity: 0.3;
  pointer-events: none;
}

.starpaw-sound-toggle {
  position: fixed;
  left: 16px;
  bottom: calc(4rem + env(safe-area-inset-bottom));
  z-index: 201;
  background: rgba(255, 255, 255, 0.1);
  border: none;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  /* AI-only (2026-07-08): the toggle mutes the ASSISTANT's voice, so it shows only
     in listening mode — in the StarView console it controlled nothing visible. */
  display: none;
  align-items: center;
  justify-content: center;
  color: rgba(255, 255, 255, 0.6);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  pointer-events: auto;
}
body[data-quick-photo-state-value="listening"] .starpaw-sound-toggle {
  display: flex;
}
.starpaw-sound-toggle .material-symbols-outlined {
  font-size: 18px;
}
.starpaw-sound-toggle:active {
  color: rgba(255, 255, 255, 1);
  background: rgba(255, 255, 255, 0.2);
}
.starpaw-datetime {
  position: fixed;
  right: 16px;
  bottom: calc(4rem + env(safe-area-inset-bottom));
  z-index: 201;
  color: rgba(255, 255, 255, 0.5);
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 36px;
  pointer-events: none;
  white-space: nowrap;
}

/* ── StarPaw panel height ──
   ONE value shared by all four StarView panels (logistics / photos / contacts / home), so they
   cannot drift apart. Everything below the header (104px), above the action bar (3.5rem), inside
   the safe areas.

   Plan 262 briefly capped this to ~55dvh so panel content could never reach the star's park zone
   — the usability panel's answer to planets overlapping the strips, and the structural
   replacement for the auto-contract timer. The founder REVERSED it after testing (2026-07-25),
   and the reasoning is worth keeping because it will look like a regression otherwise:

     - The cap's real cost landed on the preview drawer. Clamped to fit, it resolved to ~210px
       against an ~83px thumbnail — not a preview, just a slightly bigger thumbnail.
     - Sharing the screen is acceptable. The planets are legible over photos now that they carry
       the achromatic casing (.orbit-slot-img), so the overlap is occlusion, not illegibility.
     - There is already a good escape hatch: conceal the planets with a radial pull on the paw
       control. The joystick keeps working while concealed, so nothing is lost by tucking them
       away — and it is a deliberate act with an obvious undo, which is exactly what the old
       auto-contract timer was trying to guess on the walker's behalf and getting wrong.

   So the timer stays deleted and the cap does not replace it: the walker decides when the
   planets are in the way. Do not reintroduce a timer here. */
:root {
  --starpaw-panel-max: calc(100dvh - env(safe-area-inset-top) - 104px - 12px - 3.5rem - env(safe-area-inset-bottom));
}

/* ── StarPaw logistics panel ── */
.starpaw-logistics-panel {
  background: rgba(255, 255, 255, 0.12);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-radius: 12px;
  padding: 0;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
  max-height: var(--starpaw-panel-max);
  touch-action: pan-y;
  overflow-y: auto;
  overscroll-behavior: contain;
  scroll-behavior: smooth;
}
.starpaw-logistics-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* ── Shared dog badge (thumbnail + name column on left of sections) ── */
.starpaw-dog-badge {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  width: 48px;
  flex-shrink: 0;
  padding: 4px 4px 4px 8px;
  background: rgba(255, 255, 255, 0.05);
  border-right: 1px solid rgba(255, 255, 255, 0.08);
}
.starpaw-dog-badge-thumb {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}
.starpaw-dog-badge-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.15);
  color: rgba(255, 255, 255, 0.6);
}
.starpaw-section-content {
  flex: 1;
  min-width: 0;
  align-self: stretch;
  /* Vertically center the contact rows against the (full-height) dog badge so a
     short card (one contact) reads as centered rather than top-aligned. */
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.starpaw-logistics-title {
  font-size: 12px;
  font-weight: 600;
  color: #fff;
  letter-spacing: 0.02em;
}
.starpaw-logistics-chevron {
  color: rgba(255, 255, 255, 0.7);
}
.starpaw-logistics-body {
  padding: 4px 12px 10px;
}
.starpaw-logistics-row {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 8px;
  padding: 6px 10px;
  margin-bottom: 6px;
}
.starpaw-logistics-row:last-child {
  margin-bottom: 0;
}
.starpaw-logistics-row-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.starpaw-logistics-dog-name {
  font-size: 12px;
  font-weight: 600;
  color: #fff;
  padding-left: 8px;
  border-left: 2px solid var(--color-brand);
}
.starpaw-logistics-check {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.3);
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255, 255, 255, 0.5);
  flex-shrink: 0;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.starpaw-logistics-check:active {
  background: var(--color-mint);
  border-color: var(--color-mint);
  color: #fff;
}
.starpaw-logistics-detail {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.7);
  margin-top: 2px;
}
.starpaw-logistics-detail a {
  color: rgba(255, 255, 255, 0.85);
}
.starpaw-logistics-contact {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 4px;
}
.starpaw-logistics-contact-thumb {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}
.starpaw-logistics-contact-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.15);
  color: rgba(255, 255, 255, 0.6);
}
.starpaw-logistics-contact-name {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.8);
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.starpaw-logistics-action-btn {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: rgba(52, 199, 160, 0.25);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #5ee8c5;
  flex-shrink: 0;
  -webkit-tap-highlight-color: transparent;
}
.starpaw-logistics-action-btn:active {
  background: rgba(255, 255, 255, 0.3);
}
.starpaw-logistics-eta-btn {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px 8px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  flex-shrink: 0;
  border: none;
  -webkit-tap-highlight-color: transparent;
}
.starpaw-logistics-eta-btn:active {
  background: rgba(255, 255, 255, 0.3);
}

/* ── StarPaw Photos Panel ─────────────────────────── */
.starpaw-photos-panel {
  /* Dark glass (Plan 166): the photos read more vividly on a dark surround — like the lightbox's
     near-black backdrop — than on the old bright white-tint, which washed them out by contrast.
     Tone matches the dark sticky header. */
  background: rgba(18, 20, 28, 0.55);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-radius: 12px;
  padding: 0;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
  max-height: var(--starpaw-panel-max);
  overflow-y: auto;
  overflow-anchor: none;
  overscroll-behavior: contain;
  touch-action: pan-y;
}
.starpaw-photos-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 12px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  position: sticky;
  top: 0;
  z-index: 1;
  background: rgba(20, 22, 30, 0.97);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}
.starpaw-photos-title {
  font-size: 14px;
  font-weight: 600;
  color: #fff;
  letter-spacing: 0.02em;
}
.starpaw-photos-chevron {
  color: rgba(255, 255, 255, 0.7);
}
/* The multi-select toggle is a header affordance, tapped directly — opt it back out of
   the "panel buttons are joystick-gated" rule (.starpaw-context-panel button →
   pointer-events:none). The .starpaw-context-panel prefix is needed to beat its specificity. */
.starpaw-context-panel .starpaw-photos-previewtoggle,
.starpaw-context-panel .starpaw-photos-mediafilter button,
.starpaw-context-panel .starpaw-photos-clear,
.starpaw-context-panel .starpaw-photos-preview-nav,
.starpaw-context-panel .starpaw-photos-preview-handle,
.starpaw-context-panel .starpaw-alerts-inc-nav,
.starpaw-context-panel .starpaw-alerts-inc-nav button,
.starpaw-context-panel .starpaw-photos-upload-tile,
.starpaw-context-panel .starpaw-stats-drawer-handle {
  pointer-events: auto;
}
/* The upload tile joined the list 2026-08-02 (Plan 301 exposure of a Plan 206 gap):
   its design contract is "tap → native picker, no joystick focus needed", but the
   panel's button gate had it pointer-events:none, so a DIRECT first tap resolved to
   the strip and only parked focus — the picker needed a second tap nobody was told
   about. Tiles are direct-tap affordances like the header buttons above. */
.starpaw-photos-header-actions {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}
.starpaw-photos-header-actions /* Media-type filter (Plan 303): a compact three-segment pill — All | photos | videos.
   Header affordance, direct-tap (exempted from the panel button gate above). */
.starpaw-photos-mediafilter {
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  padding: 2px;
  gap: 1px;
}
.starpaw-photos-mediafilter button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: rgba(255, 255, 255, 0.55);
  border-radius: 999px;
  padding: 2px 7px;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 10px;
  line-height: 16px;
  cursor: pointer;
}
.starpaw-photos-mediafilter button .material-symbols-outlined {
  font-size: 14px;
  line-height: 16px;
}
.starpaw-photos-mediafilter button.is-on {
  background: rgba(255, 255, 255, 0.22);
  color: #fff;
}
.starpaw-photos-previewtoggle { margin-left: 0; }
.starpaw-photos-body {
  display: flex;
  flex-direction: column;
  padding: 4px 12px 10px;
}
.starpaw-photos-row {
  background: rgba(0, 0, 0, 0.22);
  border: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: 8px;
  padding: 4px 0;
  margin-bottom: 6px;
  overflow: hidden;
}
.starpaw-photos-row:last-child {
  margin-bottom: 0;
}
/* Plan 166 — inline preview drawer: slides down below a strip, showing the latest-selected photo
   at size. Inline (inside the row) so it pushes the strips below it down. */
.starpaw-photos-preview {
  /* Full strip width (the 8px side margins match the strip) and SQUARE for now: aspect-ratio makes
     the height track the width, so the preview is a 1:1 box (Plan 166 — trying square first; the
     max-height-of-width variant is the alternative). max-height animates the reveal and caps the
     square at 78vh so a wide (landscape) strip can't grow taller than the viewport. */
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  margin: 0 8px;
  border-radius: 8px;
  aspect-ratio: 1 / 1;
  position: relative;
  display: flex;
  flex-direction: column;
  transition: max-height 0.22s ease, opacity 0.18s ease, margin-top 0.22s ease;
}
.starpaw-photos-preview.is-open {
  /* Opens to FULL size — a square the width of the strip (2026-07-25). Tried clamping it to
     fit inside the capped panel and it resolved to ~210px against an ~83px thumbnail: not a
     preview, just a slightly bigger thumbnail, so the clamp bought nothing and cost the whole
     point of the drawer. The panel scrolls instead, and a walker who wants an uninterrupted
     look conceals the planets (radial pull on the paw control) — a deliberate act with an
     obvious undo, which is what the old auto-contract timer was trying and failing to guess. */
  max-height: min(100vw, 78vh);
  opacity: 1;
  margin-top: 6px;
}
.starpaw-photos-preview-handle {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding: 6px 0 4px;
  background: transparent;
  border: 0;
  cursor: pointer;
  touch-action: manipulation;
}
.starpaw-photos-preview-handle > span {
  display: block;
  width: 40px;
  height: 4px;
  border-radius: 9999px;
  background: rgba(255, 255, 255, 0.55);
  transition: background 0.15s ease;
}
.starpaw-photos-preview-handle:active > span { background: rgba(255, 255, 255, 0.85); }
/* Wraps the photo + its overlays so the selection ring and chevrons frame exactly the image box
   (the handle sits below, outside this). */
.starpaw-photos-preview-imgwrap {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
}
.starpaw-photos-preview-img {
  display: block;
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  object-fit: contain;
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.25);
}
/* Plan 212 — the drawer's inline clip player; same box as the preview img. */
.starpaw-photos-preview-video {
  display: block;
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  object-fit: contain;
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.25);
}
/* Selected previewed photo gets the same dashed white "marching ants" ring as a selected thumb. */
.starpaw-photos-preview.is-photo-selected .starpaw-photos-preview-imgwrap::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 8px;
  pointer-events: none;
  z-index: 1;
  background:
    linear-gradient(90deg, #fff 50%, transparent 50%) repeat-x,
    linear-gradient(90deg, #fff 50%, transparent 50%) repeat-x,
    linear-gradient(0deg, #fff 50%, transparent 50%) repeat-y,
    linear-gradient(0deg, #fff 50%, transparent 50%) repeat-y;
  background-size: 12px 2px, 12px 2px, 2px 12px, 2px 12px;
  background-position: 0 0, 0 100%, 0 0, 100% 0;
  animation: starpaw-sel-ants-lg 0.6s linear infinite;
}
@keyframes starpaw-sel-ants-lg {
  to { background-position: 12px 0, -12px 100%, 0 -12px, 100% 12px; }
}
/* Spinner overlaid on the photo while a slow swap loads (toggled by .is-loading after 200ms). */
.starpaw-photos-preview-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 34px;
  height: 34px;
  margin: -17px 0 0 -17px;
  border: 3px solid rgba(255, 255, 255, 0.25);
  border-top-color: rgba(255, 255, 255, 0.9);
  border-radius: 50%;
  z-index: 3;
  opacity: 0;
  pointer-events: none;
  animation: spin 0.7s linear infinite;
  transition: opacity 0.15s ease;
}
.starpaw-photos-preview.is-loading .starpaw-photos-preview-spinner { opacity: 1; }
/* Prev / next chevrons overlaid on the photo, centered on the image box. */
.starpaw-photos-preview-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 9999px;
  background: rgba(8, 10, 16, 0.5);
  color: #fff;
  cursor: pointer;
  touch-action: manipulation;
  transition: background 0.15s ease;
}
.starpaw-photos-preview-nav:active { background: rgba(8, 10, 16, 0.8); }
.starpaw-photos-preview-nav .material-symbols-outlined { font-size: 26px; }
.starpaw-photos-preview-nav-prev { left: 8px; }
.starpaw-photos-preview-nav-next { right: 8px; }
.starpaw-photos-dog {
  position: sticky;
  left: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 36px;
  flex-shrink: 0;
  align-self: stretch;
  pointer-events: auto;
  cursor: pointer;
  background: linear-gradient(90deg, rgba(30, 40, 60, 0.95) 70%, transparent);
  padding-right: 4px;
}
.starpaw-photos-dog-thumb {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}
.starpaw-photos-dog-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.15);
  color: rgba(255, 255, 255, 0.6);
}
.starpaw-photos-dog-name {
  font-size: 10px;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.9);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 36px;
  text-align: center;
}
.starpaw-photos-strip {
  position: relative;
  /* Query container so thumbs can size to a fraction of the strip's VISIBLE width
     (100cqw) and dynamically fit ~4 across, responsive to the strip width. */
  container-type: inline-size;
  display: flex;
  gap: 4px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  align-items: flex-end;
  touch-action: pan-x pan-y;
  /* On release, always settle on a thumb's left edge so none is ever left
     half-hidden under the sticky dog badge; the padding offsets past the badge. */
  scroll-snap-type: x mandatory;
  scroll-padding-left: 44px;
  background:
    repeating-linear-gradient(
      90deg,
      transparent 0px, transparent 3px,
      rgba(60, 55, 45, 0.7) 3px, rgba(60, 55, 45, 0.7) 9px,
      transparent 9px, transparent 14px
    ) top 2px left / 14px 6px repeat-x,
    repeating-linear-gradient(
      90deg,
      transparent 0px, transparent 3px,
      rgba(60, 55, 45, 0.7) 3px, rgba(60, 55, 45, 0.7) 9px,
      transparent 9px, transparent 14px
    ) bottom 2px left / 14px 6px repeat-x,
    rgba(20, 18, 15, 0.8);
  background-attachment: local, local, scroll;
  border-radius: 4px;
  padding: 12px 8px;
  border-top: 3px solid rgba(40, 35, 30, 0.9);
  border-bottom: 3px solid rgba(40, 35, 30, 0.9);
}
.starpaw-photos-strip::-webkit-scrollbar {
  display: none;
}
.starpaw-photos-date-group {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  gap: 2px;
}
.starpaw-photos-date-group-thumbs {
  display: flex;
  gap: 4px;
}
.starpaw-photos-thumb {
  /* Dynamically fit ~4 across the strip's visible width: subtract the sticky dog
     badge (~44px incl. its gap) and the 3 inter-thumb gaps (~12px), divide by 4. */
  width: calc((100cqw - 56px) / 4);
  height: calc((100cqw - 56px) / 4);
  border-radius: 3px;
  object-fit: cover;
  flex-shrink: 0;
  cursor: pointer;
  transition: transform 0.15s ease-out, opacity 0.15s;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
  /* Suppress the iOS long-press image callout so hold-to-peek isn't hijacked. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
/* Per-strip upload tile (Plan 206) — the leading "add photo" cell of every stream strip.
   Sized to match a thumb so it reads as the strip's first slot; tapping it opens the native
   picker and uploads straight to THIS strip's dog stream. JS-created, so it must be plain
   CSS (Tailwind utilities won't emit for it). */
.starpaw-photos-upload-tile {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
  padding: 0;
  font: inherit;
  width: calc((100cqw - 56px) / 4);
  height: calc((100cqw - 56px) / 4);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 3px;
  border: 1.5px dashed rgba(255, 255, 255, 0.34);
  background: rgba(255, 255, 255, 0.06);
  color: rgba(255, 255, 255, 0.8);
  cursor: pointer;
  scroll-snap-align: start;
  transition: transform 0.12s ease-out, background 0.12s, border-color 0.12s;
  -webkit-user-select: none;
  user-select: none;
}
.starpaw-photos-upload-tile .material-symbols-outlined {
  font-size: 20px;
}
/* Plan 212 — live percentage while the tile's direct upload streams. */
.starpaw-photos-upload-tile .starpaw-upload-pct {
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.starpaw-photos-upload-tile:active {
  transform: scale(0.92);
  background: rgba(255, 255, 255, 0.15);
  border-color: rgba(255, 255, 255, 0.55);
}

/* Hold-to-peek preview overlay — full image, no buttons, closes on release. */
.starpaw-photo-peek {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.72);
  pointer-events: none; /* preview only — the release (pointerup) closes it */
  opacity: 0;
  transition: opacity 0.12s ease-out;
}
.starpaw-photo-peek.is-visible { opacity: 1; }
.starpaw-photo-peek-img {
  max-width: 88vw;
  max-height: 80vh;
  object-fit: contain;
  border-radius: 12px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
}
.starpaw-photos-thumb:active {
  transform: scale(0.92);
  opacity: 0.8;
}
/* Selected photo (multi-select) — the Photos-tab verbs (Slideshow/Share/Publish)
   act on the whole selection. Tapping a thumb toggles this. */
/* Selected thumbs get a dashed white "marching ants" ring — the dashes rotate around
   the perimeter. It lives on the WRAP (an <img> can't host ::after); the joystick's
   blue focus ring stays on the thumb itself, so a selected+focused thumb shows both.
   Four edge gradients (top/bottom/left/right) animate their positions for the march. */
.starpaw-photos-thumb-wrap:has(> .starpaw-photos-thumb.is-selected)::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 4px;
  pointer-events: none;
  z-index: 1;
  background:
    linear-gradient(90deg, #fff 50%, transparent 50%) repeat-x,
    linear-gradient(90deg, #fff 50%, transparent 50%) repeat-x,
    linear-gradient(0deg, #fff 50%, transparent 50%) repeat-y,
    linear-gradient(0deg, #fff 50%, transparent 50%) repeat-y;
  background-size: 8px 2px, 8px 2px, 2px 8px, 2px 8px;
  background-position: 0 0, 0 100%, 0 0, 100% 0;
  animation: starpaw-sel-ants 0.6s linear infinite;
}
@keyframes starpaw-sel-ants {
  to { background-position: 8px 0, -8px 100%, 0 -8px, 100% 8px; }
}
/* Header multi-select toggle — a label + a sliding switch (Photos panel). */
.starpaw-photos-previewtoggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
  padding: 3px 2px;
  background: transparent;
  border: none;
  font-size: 11px;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.7);
  cursor: pointer;
}
.starpaw-toggle-label { white-space: nowrap; }
/* The switch: a pill track with a thumb that slides left (off) → right (on). */
.starpaw-toggle-switch {
  position: relative;
  width: 28px;
  height: 16px;
  flex-shrink: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.22);
  transition: background 0.18s ease;
}
.starpaw-toggle-switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
  transition: transform 0.18s ease;
}
.is-on .starpaw-toggle-switch { background: #4A8BFF; }
.is-on .starpaw-toggle-switch::after { transform: translateX(12px); }
.starpaw-photos-clear {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  /* Dimmed by default (nothing selected → clearing is a no-op). */
  color: rgba(255, 255, 255, 0.4);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.1);
  cursor: pointer;
}
/* Enabled-looking when there's a selection (toggled by refreshActionBar). */
.starpaw-photos-clear.is-active {
  color: #fff;
  background: rgba(255, 255, 255, 0.16);
  border-color: rgba(255, 255, 255, 0.32);
}
.starpaw-photos-clear:active { filter: brightness(0.85); }
/* Published filter — hide thumbs (and their now-empty date groups) that don't match.
   `data-published` is "1"/"0" on each thumb; CSS `:has()` re-evaluates live, so
   publishing/unpublishing a photo while filtered makes it appear/vanish in place. */
/* Collapse a date group with no matching thumbs (drop its separator too). */
/* Published indicator on a strip thumb (Plan 136) */
.starpaw-photos-thumb-wrap {
  position: relative;
  flex-shrink: 0;
  line-height: 0;
  /* Always snap the thumb's LEFT edge (just past the badge). No end-align needed:
     the joystick wraps around at the end and the strip keeps the focused thumb
     visible, so the last thumb never needs to settle flush-right. */
  scroll-snap-align: start;
}
/* Plan 212 — video clips in the strips: centered play badge + duration chip, so a
   clip's poster frame reads as playable rather than as another photo. */
.starpaw-photos-thumb-play {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.starpaw-photos-thumb-play .material-symbols-outlined {
  font-size: 24px;
  color: #fff;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
}
.starpaw-photos-thumb-duration {
  position: absolute;
  right: 3px;
  bottom: 3px;
  font-family: var(--font-display, inherit);
  font-size: 9px;
  font-weight: 600;
  line-height: 1.2;
  color: rgba(255, 255, 255, 0.9);
  background: rgba(0, 0, 0, 0.5);
  border-radius: 3px;
  padding: 0 3px;
}

/* Plan 182 — Pool | Stream tab toggle in the photos panel header. */
.starpaw-photos-tabs {
  display: inline-flex;
  gap: 2px;
  background: rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  padding: 2px;
}
.starpaw-photos-tab {
  border: none;
  background: transparent;
  color: rgba(255, 255, 255, 0.6);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  padding: 3px 12px;
  border-radius: 999px;
  cursor: pointer;
  touch-action: manipulation;
}
.starpaw-photos-tab.is-active {
  background: var(--color-brand, #ff7a59);
  color: #fff;
}

/* Plan 182 — Pool: a small paw marks walk-sourced candidates; already-published ones recede. */
.starpaw-photos-thumb-paw {
  position: absolute;
  left: 3px;
  bottom: 3px;
  display: flex;
  pointer-events: none;
}
.starpaw-photos-thumb-paw .material-symbols-outlined {
  font-size: 13px;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
}
.starpaw-photos-thumb-wrap.is-pool-published {
  opacity: 0.45;
}
/* Plan 183 — in the Streams tab, drafts (in the stream but not yet owner-visible) recede so the
   published (live) photos stand out. */
.starpaw-photos-thumb-wrap.is-stream-draft {
  opacity: 0.5;
}
/* Photo modal: expanded image + Publish/Share (Plan 136) */
.starpaw-action-modal {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none; /* dim only — a joystick core-tap must reach the star body */
}
.starpaw-action-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.62);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  pointer-events: none;
}
.starpaw-action-modal-card {
  position: relative;
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 84vw;
  padding: 10px;
  border-radius: 16px;
  background: rgba(20, 22, 28, 0.95);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
  /* Start hidden; revealed (faded in) only once the image content has loaded, so the
     window never flashes an empty/loading frame. Opacity-only — a transform would
     distort getBoundingClientRect while the card is positioned over the star core. */
  opacity: 0;
  transition: opacity 0.16s ease-out;
}
.starpaw-action-modal-card.is-ready {
  opacity: 1;
}
/* Tile variant (e.g. contacts): a fixed-width card so every modal of this kind is
   identical in size regardless of the photo or name length. */
.starpaw-action-modal-card-tile {
  width: 256px;
  max-width: 84vw;
}
.starpaw-action-modal-close {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 1;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  cursor: pointer;
  touch-action: manipulation;
}
.starpaw-action-modal-close .material-symbols-outlined {
  font-size: 20px;
}
.starpaw-action-modal-close:active {
  background: rgba(0, 0, 0, 0.75);
}
.starpaw-action-modal-img {
  display: block;
  max-width: 100%;
  max-height: 50vh;
  object-fit: contain;
  border-radius: 10px;
}
/* Slideshow chrome — prev/next chevrons + "n / total" counter over the image. */
.starpaw-action-modal-navwrap {
  position: relative;
  display: inline-block;
  line-height: 0;
  align-self: center;
}
.starpaw-action-modal-chevron {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  border: none;
  cursor: pointer;
  pointer-events: auto;
}
.starpaw-action-modal-chevron .material-symbols-outlined { font-size: 24px; }
.starpaw-action-modal-chevron-left { left: 6px; }
.starpaw-action-modal-chevron-right { right: 6px; }
.starpaw-action-modal-chevron:active { background: rgba(0, 0, 0, 0.65); }
.starpaw-action-modal-counter {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, 0.85);
  font-size: 11px;
  font-weight: 600;
  background: rgba(0, 0, 0, 0.45);
  padding: 2px 10px;
  border-radius: 999px;
  line-height: 1.4;
  pointer-events: none;
  white-space: nowrap;
}
/* Tapping the image advances to the next photo (photo modal only). */
.starpaw-action-modal-img-tappable {
  cursor: pointer;
}
/* Fixed standard-size square photo (contacts) — matted with proportional padding and
   framed, so every modal looks the same whether the photo is portrait, landscape, or
   absent. Padding on the <img> insets the photo and reveals the frame background. */
.starpaw-action-modal-img-tile {
  width: 160px;
  height: 160px;
  box-sizing: border-box;
  padding: 14px;
  align-self: center;
  object-fit: cover;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.06);
}
/* Icon tile shown when there's no image (e.g. a contact with no photo). Same frame. */
.starpaw-action-modal-img-placeholder {
  width: 160px;
  height: 160px;
  box-sizing: border-box;
  align-self: center;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.06);
}
.starpaw-action-modal-img-placeholder .material-symbols-outlined {
  font-size: 84px;
  color: rgba(255, 255, 255, 0.45);
}
.starpaw-action-modal-caption {
  text-align: center;
  color: #fff;
  font-weight: 700;
  font-size: 15px;
  margin-top: -2px;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.starpaw-action-modal-actions {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.starpaw-action-menu-item {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 11px 10px;
  border: none;
  background: rgba(255, 255, 255, 0.08);
  border-radius: 10px;
  color: rgba(255, 255, 255, 0.9);
  font-family: inherit;
  font-weight: 700;
  font-size: 15px;
  cursor: pointer;
  touch-action: manipulation;
}
.starpaw-action-menu-item .material-symbols-outlined {
  font-size: 19px;
}
.starpaw-action-menu-item:active {
  background: rgba(255, 255, 255, 0.16);
}
.starpaw-action-menu-item.is-focused {
  background: var(--color-brand);
  color: #fff;
}
.starpaw-photos-date-sep {
  font-size: 10px;
  font-weight: 600;
  color: rgba(200, 180, 140, 0.7);
  letter-spacing: 0.03em;
  white-space: nowrap;
  padding-left: 1px;
  margin-top: -8px;
  /* Don't let a press/drag over the date label start a text selection. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.starpaw-photos-empty {
  font-size: 13px;
  color: rgba(255, 255, 255, 0.4);
  font-style: italic;
  padding: 10px 0;
  min-height: 64px;
  display: flex;
  align-items: center;
}
.starpaw-photos-spinner {
  font-size: 20px;
  color: rgba(255, 255, 255, 0.3);
  animation: spin 1s linear infinite;
}
/* Skeleton loading boxes — the thumbs' exact size, so the strip reserves its height (no expansion
   when photos arrive) and each photo fades in over its box (the reveal in _buildThumb). */
.starpaw-photos-empty.starpaw-photos-skeletons {
  display: flex; align-items: flex-end;
  padding: 0; min-height: 64px; font-style: normal;
}
.starpaw-photos-skeleton {
  width: 64px; height: 64px; border-radius: 3px; flex-shrink: 0;
  background: linear-gradient(100deg, rgba(255, 255, 255, 0.05) 30%, rgba(255, 255, 255, 0.14) 50%, rgba(255, 255, 255, 0.05) 70%);
  background-size: 200% 100%;
  animation: starpaw-skeleton-shimmer 1.3s ease-in-out infinite;
}
/* Date-separator placeholder — keeps .starpaw-photos-date-sep's height/offset (so no jump) but
   renders as a short shimmer bar instead of text. */
.starpaw-photos-skeleton-bar {
  width: 42px; color: transparent; border-radius: 3px;
  background: linear-gradient(100deg, rgba(255, 255, 255, 0.05) 30%, rgba(255, 255, 255, 0.14) 50%, rgba(255, 255, 255, 0.05) 70%);
  background-size: 200% 100%;
  animation: starpaw-skeleton-shimmer 1.3s ease-in-out infinite;
}
@keyframes starpaw-skeleton-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* ── Contacts panel ── */
.starpaw-contacts-panel {
  background: rgba(255, 255, 255, 0.12);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-radius: 12px;
  padding: 0;
  overflow-y: auto;
  overflow-anchor: none;
  max-height: var(--starpaw-panel-max);
  touch-action: pan-y;
}
.starpaw-contacts-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  position: sticky;
  top: 0;
  z-index: 1;
  background: inherit;
}
.starpaw-contacts-title {
  font-size: 14px;
  font-weight: 700;
  color: white;
  display: flex;
  align-items: center;
  gap: 4px;
}
.starpaw-contacts-body {
  display: flex;
  flex-direction: column;
  padding: 4px 12px 10px;
}
.starpaw-contacts-section {
  display: flex;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 8px;
  margin-bottom: 6px;
}
.starpaw-contacts-section:last-child {
  margin-bottom: 0;
}
/* Contact row (Plan 152): tapping a phone-bearing contact selects it (single-select);
   the bottom action bar's Text/Call then act on it. */
.starpaw-contacts-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 8px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.starpaw-contacts-item:last-child {
  border-bottom: none;
}
.starpaw-contacts-thumb {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}
.starpaw-contacts-icon {
  font-size: 18px;
  color: rgba(255, 255, 255, 0.5);
  flex-shrink: 0;
}
.starpaw-contacts-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: row;
  align-items: baseline;
  gap: 6px;
}
.starpaw-contacts-name {
  font-size: 14px;
  font-weight: 600;
  color: white;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.starpaw-contacts-role {
  font-size: 12px;
  color: rgba(255, 255, 255, 0.5);
  text-transform: capitalize;
  flex-shrink: 0;
}
/* Inline per-contact actions — round Call / Text / Email buttons at the right of each
   row (Plan 153). Real tel:/sms:/mailto: anchors; each is also a joystick L2 element. */
.starpaw-contacts-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}
.starpaw-contacts-action {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.starpaw-contacts-action .material-symbols-outlined { font-size: 17px; }
.starpaw-contacts-action:active { background: rgba(255, 255, 255, 0.22); }
.starpaw-contacts-empty {
  font-size: 13px;
  color: rgba(255, 255, 255, 0.4);
  font-style: italic;
  padding: 10px 0;
}

/* StarPaw Home panel */
.starpaw-home-panel {
  background: rgba(255, 255, 255, 0.12);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-radius: 12px;
  padding: 0;
  /* Header + a separately-scrolling body below it: the body owns the scroll clip so
     cards never show through the (glass) header. The panel itself doesn't scroll. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  max-height: var(--starpaw-panel-max);
  touch-action: pan-y;
}
.starpaw-home-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  flex-shrink: 0;
  position: relative;
  z-index: 1;
  /* Brand-blue glass (black is reserved for the Photos header). Stays translucent —
     nothing slides behind it; the body owns the scroll clip. */
  background: rgba(36, 58, 120, 0.55);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}
.starpaw-home-title {
  font-size: 14px;
  font-weight: 700;
  color: white;
  display: flex;
  align-items: center;
  gap: 4px;
}
.starpaw-home-body {
  display: flex;
  flex-direction: column;
  padding: 4px 12px 10px;
  /* The scroll + clip region, below the glass header — scrolling content clips at this
     container's top edge instead of showing through the header. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overflow-anchor: none;
}
.starpaw-home-section {
  display: flex;
  position: relative; /* anchors the marching-ants selection ring (::after) */
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 8px;
  margin-bottom: 6px;
}
.starpaw-home-section:last-child {
  margin-bottom: 0;
}
/* Contacts lead each Home section (Plan 134); divide them from the home rows
   below. No divider when the section is contacts-only. */
.starpaw-home-contacts:not(:last-child) {
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}
.starpaw-home-row {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  padding: 5px 8px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.starpaw-home-row:last-of-type {
  border-bottom: none;
}
.starpaw-home-icon {
  font-size: 16px;
  color: rgba(255, 255, 255, 0.5);
  flex-shrink: 0;
  margin-top: 1px;
}
/* Countdown digit replaces the lock icon while PIN is revealed */
.starpaw-countdown {
  font-family: var(--font-display), system-ui, sans-serif;
  font-size: 14px;
  font-weight: 700;
  min-width: 1em;
  text-align: center;
}
.starpaw-home-link {
  font-size: 14px;
  font-weight: 600;
  color: #8bbcff;
  text-decoration: underline;
  text-decoration-color: rgba(139, 188, 255, 0.35);
}
.starpaw-home-link:active {
  color: inherit;
}
.starpaw-home-code {
  font-size: 15px;
  font-weight: 700;
  color: rgba(255, 255, 255, 0.85);
  font-family: monospace;
  letter-spacing: 2px;
}
.starpaw-home-reveal {
  background: none;
  border: none;
  color: rgba(255, 255, 255, 0.5);
  padding: 0 2px;
  cursor: pointer;
  flex-shrink: 0;
}
.starpaw-home-reveal:active {
  color: white;
}
.starpaw-home-instructions .starpaw-home-text {
  font-size: 14px;
  color: rgba(255, 255, 255, 0.7);
  line-height: 1.4;
  white-space: pre-wrap;
}
.starpaw-home-photos {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 5px 8px;
}
.starpaw-home-photo {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: 8px;
  cursor: pointer;
}
.starpaw-home-photo:active {
  opacity: 0.7;
}
.starpaw-home-empty {
  font-size: 13px;
  color: rgba(255, 255, 255, 0.4);
  font-style: italic;
  padding: 10px 0;
}
/* Alerts panel rows (Plan 300) — the walk summary's Medical/Behavior vocabulary on a
   StarView card. StarView is dark in both themes (excluded from the theme lint), so
   these are fixed night-surface values like their .starpaw-home-* siblings. */
/* ── Stats panel (Plan 305) ────────────────────────────────────────────
   A 2×2 grid of stat cells sized for the top third of the screen; each cell
   opens a weekly-bars graph in ONE shared drawer beneath the card (the Plan 303
   drawer pattern). Cells are DIVs with role=button — the panel button gate
   (pointer-events:none) must not eat direct first taps (the Plan 206/301 trap). */
.starpaw-stats-grid {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1 1 auto; /* fill the card — the section is a flex row and content-sized grids hugged left */
  padding: 7px;
}
.starpaw-stats-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr; /* 3×2 since daycare + boarding joined (founder) */
  gap: 6px;
}
.starpaw-stats-cell {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  column-gap: 6px;
  align-items: baseline;
  padding: 7px 8px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.07);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.starpaw-stats-cell.is-graph-open {
  background: rgba(74, 139, 255, 0.18); /* joystick blue family — an OPEN state, not selection */
}
.starpaw-stats-icon {
  grid-row: 1 / 3;
  align-self: center;
  font-size: 20px;
  color: rgba(255, 255, 255, 0.55);
}
.starpaw-stats-value {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 16px;
  line-height: 1.1;
  color: #fff;
}
.starpaw-stats-label {
  grid-column: 2;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.65);
}
.starpaw-stats-qualifier {
  grid-column: 1 / 3;
  font-size: 10px;
  color: rgba(255, 255, 255, 0.45);
  padding-top: 2px;
}
/* The cells MULTI-select, Plan 306 chip pattern: a chip we label ("Stats · N") with
   the real multi-select INVISIBLE on top, so a tap still opens the OS picker — a
   collapsed native multi-select's own text is browser-rendered (iOS shows the first
   selected option, truncated; it read as a random panel name — founder, 2026-08-08). */
.starpaw-stats-cells-wrap {
  position: relative;
  display: inline-flex;
  margin-left: auto;
}
.starpaw-stats-cells-wrap .starpaw-stats-range {
  margin-left: 0;
  display: inline-flex;
  align-items: center;
}
.starpaw-stats-cells {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  border: none;
  appearance: none;
  -webkit-appearance: none;
}
/* The picker dropdowns — native selects styled as compact pills ("just make them
   dropdowns" — founder, 2026-08-03; the action-modal versions read as unharmonized). */
.starpaw-stats-range {
  appearance: none;
  -webkit-appearance: none;
  margin-left: auto;
  border: none;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 11px;
  line-height: 1;
  padding: 5px 22px 5px 10px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='white' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 10px 6px;
  cursor: pointer;
}
/* The shared graph drawer — max-height slide like the photos preview. */
.starpaw-stats-drawer {
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  margin-top: 6px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.07);
  transition: max-height 0.22s ease, opacity 0.22s ease;
}
.starpaw-stats-drawer.is-open {
  max-height: 320px;
  opacity: 1;
}
.starpaw-stats-chart-emptynote {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.45);
  margin: 4px 0 10px;
}
.starpaw-stats-chart { padding: 8px 10px 0; }
.starpaw-stats-chart-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.6);
  margin: 0 0 4px;
}
.starpaw-stats-svg { display: block; width: 100%; height: auto; }
/* The graph's own controls row inside the drawer (Plan 325) — each graph tailors its
   own; today one range pill, right-aligned above the chart. */
.starpaw-stats-graph-controls {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
  padding: 6px 10px 0;
}
.starpaw-stats-graph-controls .starpaw-stats-range { margin-left: 0; }
/* The deviation clock chart (Plan 324): the actual per-day line + its dots, and the
   dashed window-average reference beneath them. StarView is dark in both themes. */
.starpaw-stats-devline { fill: none; stroke: #4A8BFF; stroke-width: 1.5; stroke-linejoin: round; }
.starpaw-stats-devdot { fill: #4A8BFF; }
.starpaw-stats-avgline { stroke: rgba(255, 255, 255, 0.4); stroke-width: 1; stroke-dasharray: 3 3; }
.starpaw-stats-axis { stroke: rgba(255, 255, 255, 0.25); stroke-width: 1; }
.starpaw-stats-grid-line { stroke: rgba(255, 255, 255, 0.12); stroke-width: 1; stroke-dasharray: 2 3; }
.starpaw-stats-tick { fill: rgba(255, 255, 255, 0.45); font-size: 9px; }
.starpaw-stats-drawer-handle {
  display: flex;
  justify-content: center;
  width: 100%;
  padding: 6px 0 8px;
  background: transparent;
  border: none;
  cursor: pointer;
}
.starpaw-stats-drawer-handle span {
  width: 34px;
  height: 4px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.35);
}
.starpaw-alerts-heading {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.45);
  padding: 6px 0 2px;
}
.starpaw-alerts-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 5px 0;
}
.starpaw-alerts-icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
.starpaw-alerts-label {
  font-size: 13px;
  font-weight: 600;
  color: white;
}
.starpaw-alerts-detail {
  font-size: 12px;
  color: rgba(255, 255, 255, 0.55);
  line-height: 1.35;
}
/* The Incidents pager: arrows FLANK the incident they page (one visual unit — a
   nav row below read as paging the whole card). Always tappable (opted out of the
   joystick gate above, like the photo preview arrows). */
.starpaw-alerts-inc { align-items: center; gap: 6px; }
.starpaw-alerts-inc-nav {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
.starpaw-alerts-inc-nav button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.06);
  color: rgba(255, 255, 255, 0.8);
  cursor: pointer;
}
.starpaw-alerts-inc-nav button:disabled {
  opacity: 0.3;
  cursor: default;
}
.starpaw-alerts-inc-counter {
  font-size: 10px;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.45);
  margin-left: 6px;
}
/* The walk strip's identity slot IS a pager (Plan 301): ‹ date › in the dog-badge
   position. Arrow wrappers reuse .starpaw-alerts-inc-nav (pointer-events exemption +
   the round button look). */
.starpaw-walk-pager {
  display: flex;
  align-items: center;
  gap: 4px;
}
.starpaw-walk-strip-glyph {
  font-size: 18px;
  color: rgba(255, 255, 255, 0.65);
}
/* The selected dog's name in a panel header (Plan 302 — per-card dog badges died;
   the panel states identity once). */
.starpaw-header-dog {
  font-weight: 600;
  color: rgba(255, 255, 255, 0.55);
}

/* Freeze underlying page scroll while a Call-in animation is playing
   (ad-hoc 2026-04-10). Scoped narrowly to the arrival animation — not
   the whole visible lifetime — so regular page scroll still works once
   the Dog Star has landed. `overflow: hidden` covers desktop + Android;
   `touch-action: none` covers iOS Safari, which otherwise ignores
   `overflow: hidden` for touch-driven scroll. Applied to both <html>
   and <body> because browsers differ on which element actually owns
   the viewport scroll. */
html:has(body.star-incoming),
body.star-incoming {
  overflow: hidden;
  touch-action: none;
  overscroll-behavior: none;
}

/* ── Radial photo menu orbit ── */
[data-quick-photo-target="radial"],
.orbit-ring,
.orbit-ring ~ button,
[data-dog-thumb] {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
}
[data-dog-thumb] img {
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
  user-select: none;
  pointer-events: none;
}

/* Body (shutter) button — centering transform is inline (must stay inline
   so .tactile:active doesn't replace it). Collapse rules use !important
   to override during the hide/show animation. */

.orbit-ring {
  position: relative;
  width: 272px;
  height: 272px;
  pointer-events: none;
}

/* Plan 038: inner-gap hit target. A transparent circular layer centered
   on the radial, sized to fill the annulus between the Body (96px
   diameter → 48px radius) and Ring 1 (slot inner edge at ~80px radius).
   In edit mode it becomes a rotation start zone; in view mode it's
   inert so it doesn't swallow taps that would otherwise extend/contract
   the Dog Star through the Body. */
.star-inner-gap {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 160px;
  height: 160px;
  margin: -80px 0 0 -80px;
  border-radius: 50%;
  pointer-events: none;
  /* No z-index: rely on DOM paint order. The Body button is a later
     sibling, so its 96px Body naturally wins hit-testing inside the
     inner-gap's disc (the core + crust stay draggable in edit mode),
     while the inner-gap still claims the surrounding annulus up to
     Ring 1's inner edge. With an explicit z-index, the inner-gap
     floated above the Body button and swallowed every core/crust
     tap in edit mode. */
}
body.photo-orbit-edit .star-inner-gap {
  pointer-events: auto;
}

/* Gather dot — red pip on the action planet when dogs are gathered.
   Centered on top of the action planet. */
.orbit-slot-preselect-dot {
  position: absolute;
  top: -3px;
  left: 50%;
  transform: translateX(-50%);
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #ef4444;
  box-shadow: 0 0 0 1.5px var(--color-brand), 0 0 0 3px rgba(255, 255, 255, 0.9);
  pointer-events: none;
  z-index: 2;
}



/* Anchor button (plan 032) — pins the Dog Star in place so accidental drags
   while scrolling can't move it. Positioned just outside the Body at the
   bottom-right (~315°, offset 46px from radial center). Visible only when
   the rings are retracted; the `:has()` rule hides it whenever the orbit
   ring is visible (no `.hidden` class). Base look is a small brand circle;
   active (anchored) state takes on the frosted-glass look from the Body
   core so the state is instantly readable. */
/* Schedule tape day tap — small radiating ring from the tapped dot */
@keyframes tape-day-pulse {
  0%   { box-shadow: 0 0 0 0 var(--color-brand-a40, rgba(255, 107, 74, 0.5)); }
  100% { box-shadow: 0 0 0 10px transparent; }
}

/* Plan 037: satellite mirror of the anchor button. Cycles the active
   dog group on tap, toggles alphabetical on long-press (manual groups
   only), slides along a fixed-radius arc in edit mode. Default 225°
   (bottom-left) — diagonally opposite the anchor's default 45°. Icon
   glyph (digit/letter/paw) stays upright regardless of orbit angle. */
/* Ad-hoc 2026-04-11: `@property` registration for the group button's
   radial-pull offset, same rationale as `--anchor-pull`. */
@property --group-pull {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
/* Register orbit/group radii so they can be transitioned for the
   retract-to-center animation when planets are hidden. */
@property --orbit-radius {
  syntax: "<length>";
  inherits: false;
  initial-value: -152px;
}
@property --group-radius {
  syntax: "<length>";
  inherits: false;
  initial-value: 65px;
}
.star-group {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 26px;
  height: 26px;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  border-radius: 9999px;
  --group-angle: 45deg;
  --group-radius: 65px;
  /* Ad-hoc 2026-04-11: radial-pull offset added to the base radius.
     In edit mode the walker can drag the group button outward along
     its angle; on release the value transitions back to 0 via
     `.group-pull-return`. Pulling past the commit threshold clears
     the currently active dog group. */
  --group-pull: 0px;
  transform: translate(-50%, -50%)
             translate(calc(cos(var(--group-angle)) * (var(--group-radius) + var(--group-pull))),
                       calc(sin(var(--group-angle)) * (var(--group-radius) + var(--group-pull))));
  display: flex;
  align-items: center;
  justify-content: center;
  /* Match the anchor's glass aesthetic so the button reads as a
     control rather than a notification badge. Brand-filled + white
     digit + dark backdrop looked like an unread-count pip; glass +
     brand-colored glyph reads as a sibling of the anchor knob. */
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 26'%3E%3Cdefs%3E%3CradialGradient id='s' cx='40%25' cy='35%25' r='55%25'%3E%3Cstop offset='0%25' stop-color='%23D8E8FF'/%3E%3Cstop offset='40%25' stop-color='%23A0C4F0'/%3E%3Cstop offset='75%25' stop-color='%235882B0'/%3E%3Cstop offset='100%25' stop-color='%231E3550'/%3E%3C/radialGradient%3E%3CradialGradient id='h' cx='38%25' cy='28%25' r='25%25'%3E%3Cstop offset='0%25' stop-color='%23fff' stop-opacity='0.7'/%3E%3Cstop offset='100%25' stop-color='%23fff' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Ccircle cx='13' cy='13' r='13' fill='url(%23s)'/%3E%3Ccircle cx='13' cy='13' r='13' fill='url(%23h)'/%3E%3C/svg%3E") no-repeat center / contain;
  color: white;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
  border: none;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
  cursor: pointer;
  transition: background 0.2s ease, backdrop-filter 0.2s ease,
              border-color 0.2s ease, color 0.2s ease, opacity 0.2s ease;
  pointer-events: auto;
  padding: 0;
  z-index: 5;
}
.star-group:active {
  transform: translate(-50%, -50%)
             translate(calc(cos(var(--group-angle)) * (var(--group-radius) + var(--group-pull))),
                       calc(sin(var(--group-angle)) * (var(--group-radius) + var(--group-pull))))
             scale(0.9);
}
.star-group::before {
  content: "";
  position: absolute;
  inset: -16px;
  border-radius: 9999px;
}
/* Gravitational lensing — Sirius B warps spacetime around it.
   Light bends around the white dwarf rather than passing through,
   concentrating at the limb (Einstein ring). Thin bright rim where
   deflected light paths converge, plus soft outer glow. */
.star-group::after {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: 9999px;
  border: 1.5px solid rgba(200, 220, 255, 0.4);
  box-shadow: 0 0 6px 1px rgba(200, 220, 255, 0.25);
  z-index: -1;
  pointer-events: none;
}
.star-group-inner {
  width: 100%;
  height: 100%;
  border-radius: 9999px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  transform: translateY(-1px);
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.6))
          drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4))
          drop-shadow(0 0 8px rgba(0, 0, 0, 0.25));
}
/* Coin-spin loading state: the paw/group control flips on its vertical axis
   (rotateY) like a spinning coin while we wait for the AI to respond. Toggled
   by show/hideThinkingBubble() in listening_mode.js. perspective() gives the 3D
   coin depth; the static -1px nudge is preserved; default backface stays visible
   so the reverse face shows mid-flip, like a real coin. */
@keyframes star-group-coin-spin {
  from { transform: perspective(240px) translateY(-1px) rotateY(0deg); }
  to   { transform: perspective(240px) translateY(-1px) rotateY(360deg); }
}
.star-group-inner.coin-spin {
  animation: star-group-coin-spin 1s linear infinite;
}
@keyframes group-tap-wave {
  0%   { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.5); }
  100% { box-shadow: 0 0 0 14px rgba(255, 255, 255, 0); }
}
.group-tap-wave {
  animation: group-tap-wave 0.35s ease-out forwards;
}
.star-group .orbit-slot-group-paw {
  font-size: 16px;
  font-variation-settings: "FILL" 1, "wght" 500;
  line-height: 1;
}
.star-group .orbit-slot-group-calendar-paw {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}
.star-group .orbit-slot-group-cal-icon {
  font-size: 18px;
  font-variation-settings: "FILL" 0, "wght" 400;
  line-height: 1;
}
.star-group .orbit-slot-group-cal-paw {
  position: absolute;
  top: calc(55% + 1px);
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 8px;
  font-variation-settings: "FILL" 1, "wght" 500;
  line-height: 1;
}
/* Numbered badge marking which Today group when Today overflows into several. */
.star-group .orbit-slot-group-today-badge {
  position: absolute;
  bottom: -1px;
  right: -1px;
  min-width: 13px;
  height: 13px;
  padding: 0 2px;
  box-sizing: border-box;
  border-radius: 7px;
  background: #4A8BFF;
  color: #fff;
  font-size: 8px;
  font-weight: 700;
  line-height: 13px;
  text-align: center;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.star-group .orbit-slot-group-dots {
  display: flex;
  gap: 2px;
  margin-top: -1px;
}
.star-group .orbit-slot-group-dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.4);
  flex-shrink: 0;
}
.star-group .orbit-slot-group-dot-filled {
  background: white;
}
/* Frown arc — outer dots sit higher than center dot */
.star-group .orbit-slot-group-dot:first-child,
.star-group .orbit-slot-group-dot:last-child {
  transform: translateY(-1.5px);
}
.star-group.group-grip {
  animation: group-grip-wave 0.9s ease-out infinite;
}
@keyframes group-grip-wave {
  0%   { box-shadow: 0 0 0 0 rgba(74, 139, 255, 0.7); }
  100% { box-shadow: 0 0 0 50px rgba(74, 139, 255, 0); }
}
/* Group satellite feedback: white confirmation ring (matches core tap). */
.star-group.group-save-pulse {
  animation: group-save-wave 0.4s ease-out;
}
@keyframes group-save-wave {
  0%   { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.5); }
  100% { box-shadow: 0 0 0 24px rgba(255, 255, 255, 0); }
}
/* Snap-back transition for the group's radial-pull gesture.
   Ease-out-back curve for a tactile rubber-band rebound feel. */
.star-group.group-pull-return {
  transition: --group-pull 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}


/* Idle rest — the CONCEALED star, untouched for a few seconds (JS adds
   body.star-resting via _scheduleStarRest), settles: the corona fades out and
   the core goes NEAR-TRANSPARENT. Any interaction (wakeStar) removes the class
   and they return. Only ever present alongside .star-concealed, so it never
   affects the extended star. The fade-out is gentle (0.6s); the fade-back on
   wake is the element's own quicker transition, so waking feels responsive.
   History: opacity → brightness filter (2026-07-02, the see-through core showed
   content lined up behind it) → back to opacity 0.2 (2026-07-11, Axel: the
   resting star should reveal what's under it; the panel redesigns since made
   the old artifact moot). */
body.star-resting .star-corona-layer {
  opacity: 0;
  transition: opacity 0.6s ease;
}
body.star-resting .star-body-btn {
  opacity: 0.2;
  transition: opacity 0.6s ease;
}

/* Generic long-press confirmation wave, reused for any small circular
   target that wants a post-fire "done" feedback. Applied by
   `_playLongPressWave(el)` to an element that is already circular
   (e.g. `.orbit-slot-img`). Also reused by group button feedback. */
@keyframes long-press-wave {
  0%   { box-shadow: 0 0 0 0 rgba(255, 107, 74, 0.7); }
  100% { box-shadow: 0 0 0 50px rgba(255, 107, 74, 0); }
}
.long-press-wave {
  animation: long-press-wave 0.6s ease-out !important;
}

@keyframes orbit-tap-wave {
  0%   { box-shadow: 0 0 0 0 rgba(255, 107, 74, 0.6); }
  100% { box-shadow: 0 0 0 18px rgba(255, 107, 74, 0); }
}
.orbit-tap-wave {
  animation: orbit-tap-wave 0.35s ease-out !important;
  border-radius: 50%;
}

.orbit-slot {
  position: absolute;
  top: 50%; left: 50%;
  width: 39px;
  margin-left: -19.5px;
  margin-top: -19.5px;
  --orbit-radius: -152px;
  --slot-angle: calc(var(--angle) + var(--orbit-rotation, 0deg));
  transform: rotate(var(--slot-angle)) translateY(var(--orbit-radius)) rotate(calc(-1 * var(--slot-angle)));
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: pointer;
  transition: box-shadow 0.15s;
  pointer-events: auto;
}
/* Invisible expanded hit area for dog planets — 59px tap target
   around the 39px visual. Action planets excluded via [data-quick-action]. */
.orbit-slot:not([data-quick-action])::before {
  content: "";
  position: absolute;
  inset: -10px;
  border-radius: 50%;
}

/* Plan 088: orbit-expand/retract animations removed — orbits are always
   visible when the star is shown. Slots use their final position directly
   via the inline transform set by the renderer. */
.orbit-slot-img {
  width: 39px; height: 39px;
  border-radius: 50%;
  overflow: hidden;
  /* Plan 262 — ACHROMATIC CASING. A dog planet is a photograph sitting on a
     photograph (the strips behind it), and until this the entire separation
     between the two was the dark shadow below: a dark-on-LIGHT device. Over a
     dark region of a ground photo it contributed nothing, and outdoors it
     contributed nothing at all, because sunlight washout crushes the bottom of
     the luminance range into one value. Separation was a lottery whose odds
     were set by whatever the walker photographed that morning.

     Light ring INSIDE, dark ring OUTSIDE: whichever half matches the ground,
     the other one separates. That is a guaranteed contrast floor against ANY
     photo, in either theme, in sunlight — and it is fully achromatic, so it
     cannot fail the "hue is never the only carrier" rule.

     The light ring MUST stay an `outline`, not a fourth box-shadow layer.
     Every selection state (.orbit-pre-selected,
     .orbit-slot-swap-selected) REPLACES this whole box-shadow stack inside its
     keyframes — that is what the drop-shadow-layers copy-paste convention
     below is about — so a box-shadow casing would vanish at exactly the moment
     a dog is selected. `outline` is a separate property and is immune. */
  /* 1.5px, was 2px (founder, 2026-07-30): at 39px the full 2px read as a heavy rim;
     1.5 keeps the guaranteed-contrast floor while the planet reads as a photo again. */
  outline: 1.5px solid rgba(226, 236, 255, 0.94);
  outline-offset: 0;
  /* The dark half of the casing and the lift pair are named so every selection
     keyframe can restate them without re-typing four literals — see the
     drop-shadow-layers note further down. A keyframe that omits --planet-casing
     loses the dark ring for the duration of the animation. */
  --planet-casing: 0 0 0 3.5px rgba(0, 0, 0, 0.55);
  --planet-lift: 0 1px 2px rgba(0, 0, 0, 0.30), 0 4px 12px rgba(0, 0, 0, 0.22);
  box-shadow: var(--planet-casing), var(--planet-lift);
}
/* Ad-hoc 2026-04-11: suppress iOS Safari's native image long-press
   behaviour on orbit thumbnails. Without these, iOS hijacks our 500ms
   long-press timer by starting its own image drag-preview (the raw,
   un-clipped image) and share/save callout, which manifests as a
   "square thumbnail" appearing over the properly-clipped planet and
   being draggable away from it. `-webkit-touch-callout: none` kills
   the share menu; `-webkit-user-drag: none` kills the drag preview;
   `user-select: none` blocks any text-selection fallback. Applied to
   both the `.orbit-slot-img` div and the child `<img>` so iOS has
   nothing to latch onto. */
.orbit-slot-img,
.orbit-slot-img img {
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
}
.orbit-slot-img.orbit-just-added {
  animation: orbit-add-wave 0.5s ease-out both;
}
@keyframes orbit-add-wave {
  0%   { box-shadow: var(--planet-casing), 0 0 0 0 rgba(74, 139, 255, 0.5), var(--planet-lift); }
  100% { box-shadow: var(--planet-casing), 0 0 0 10px rgba(74, 139, 255, 0), var(--planet-lift); }
}
.orbit-slot:has(.orbit-slot-empty) .orbit-slot-img {
  box-shadow: none;
}
.orbit-slot:has(.orbit-slot-empty) {
  cursor: default;
}
body:not(.photo-orbit-edit) .orbit-slot:has(.orbit-slot-empty) {
  visibility: hidden;
  pointer-events: none;
}
.orbit-slot.orbit-dim {
  opacity: 0 !important;
}
/* Subtle de-emphasis on non-gathered dog planets when at least one dog
   is gathered.  Just enough to let the gathered outlines pop without
   making the rest feel hidden. */
.orbit-slot-img.orbit-ungathered {
  opacity: 0.9;
  transition: opacity 0.15s ease-out;
}
/* Grey/cancel dogs already look distinct — keep image at full opacity,
   only dim via the name rule below. */
.orbit-slot-img.orbit-ungathered[data-unsatisfiable] {
  opacity: 1;
}
.orbit-slot-img.orbit-ungathered ~ .orbit-slot-name {
  opacity: 1;
  transition: opacity 0.15s ease-out;
}
/* Steady pulse on Body core in editable mode — uses box-shadow glow
   instead of opacity so the element always receives pointer events. */
@keyframes shutter-edit-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 107, 74, 0.7), 0 8px 30px rgba(0,0,0,0.35); }
  50% { box-shadow: 0 0 0 14px rgba(255, 107, 74, 0), 0 8px 30px rgba(0,0,0,0.35); }
}
.shutter-edit-pulse {
  /* Edit mode indicated by faster ray spin + "Edit" label instead */
}
/* Star core — 3D sphere (Sirius blue-white) */
.star-core-sphere {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 60'%3E%3Cdefs%3E%3CradialGradient id='s' cx='50%25' cy='50%25' r='50%25'%3E%3Cstop offset='0%25' stop-color='%23FFFFFF'/%3E%3Cstop offset='25%25' stop-color='%23E0EEFF'/%3E%3Cstop offset='55%25' stop-color='%23A0CCFF'/%3E%3Cstop offset='80%25' stop-color='%235A9AE8'/%3E%3Cstop offset='100%25' stop-color='%233070C0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Ccircle cx='30' cy='30' r='30' fill='url(%23s)'/%3E%3C/svg%3E") no-repeat center / contain;
}

/* Sphere shading overlay — curves photo thumbnails to match the core shape.
   Hidden when core is empty (sphere background acts as its own light source). */
[data-quick-photo-target="shutterCore"]:not(.star-core-sphere)::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 60'%3E%3Cdefs%3E%3CradialGradient id='s' cx='40%25' cy='35%25' r='50%25'%3E%3Cstop offset='0%25' stop-color='%23fff' stop-opacity='0.12'/%3E%3Cstop offset='15%25' stop-color='%23fff' stop-opacity='0.06'/%3E%3Cstop offset='30%25' stop-color='%23fff' stop-opacity='0'/%3E%3Cstop offset='70%25' stop-color='%23000' stop-opacity='0'/%3E%3Cstop offset='80%25' stop-color='%23000' stop-opacity='0.03'/%3E%3Cstop offset='88%25' stop-color='%23000' stop-opacity='0.08'/%3E%3Cstop offset='94%25' stop-color='%23000' stop-opacity='0.15'/%3E%3Cstop offset='100%25' stop-color='%23000' stop-opacity='0.25'/%3E%3C/radialGradient%3E%3C/defs%3E%3Ccircle cx='30' cy='30' r='30' fill='url(%23s)'/%3E%3C/svg%3E") no-repeat center / contain;
  pointer-events: none;
  z-index: 1;
}

/* Hide starburst rays when star is floating without StarView */
.star-burst-layer {
  opacity: 0;
  transition: opacity 0.25s ease;
}
body[data-quick-photo-state-value="starview"] .star-burst-layer,
body[data-quick-photo-state-value="listening"] .star-burst-layer {
  opacity: 1;
}
/* Listening mode — intense, realistic corona without planets to obscure.
   Both corona and starburst outer edges are heavily feathered to avoid a disc look. */
body.listening-adorned .star-corona-layer {
  transform: translate(-50%, -50%) scale(1.56);
  background: radial-gradient(
    circle at center,
    rgba(220, 235, 255, 0.95) 5%,
    rgba(200, 225, 255, 0.8) 8%,
    rgba(180, 215, 255, 0.65) 11%,
    rgba(170, 200, 245, 0.5) 14%,
    rgba(180, 190, 230, 0.38) 18%,
    rgba(200, 180, 210, 0.28) 23%,
    rgba(220, 170, 180, 0.20) 28%,
    rgba(240, 160, 140, 0.14) 34%,
    rgba(255, 150, 110, 0.09) 40%,
    rgba(255, 140, 90, 0.06) 47%,
    rgba(255, 130, 75, 0.035) 55%,
    rgba(255, 120, 60, 0.02) 63%,
    rgba(255, 110, 50, 0.01) 72%,
    rgba(255, 100, 42, 0.004) 82%,
    rgba(255, 95, 35, 0.001) 92%,
    transparent 100%
  );
  filter: blur(8px);
  transition: width 0.8s ease-out, height 0.8s ease-out, filter 0.8s ease-out;
}
body.listening-adorned .star-burst-layer {
  -webkit-mask-image: radial-gradient(circle,
    transparent 6%,
    rgba(255,255,255,0.02) 10%,
    rgba(255,255,255,0.05) 14%,
    rgba(255,255,255,0.1) 18%,
    rgba(255,255,255,0.18) 22%,
    rgba(255,255,255,0.3) 26%,
    rgba(255,255,255,0.5) 30%,
    rgba(255,255,255,0.7) 34%,
    rgba(255,255,255,0.85) 38%,
    white 42%,
    rgba(255,255,255,0.6) 52%,
    rgba(255,255,255,0.3) 62%,
    rgba(255,255,255,0.12) 72%,
    rgba(255,255,255,0.04) 82%,
    transparent 92%);
  mask-image: radial-gradient(circle,
    transparent 6%,
    rgba(255,255,255,0.02) 10%,
    rgba(255,255,255,0.05) 14%,
    rgba(255,255,255,0.1) 18%,
    rgba(255,255,255,0.18) 22%,
    rgba(255,255,255,0.3) 26%,
    rgba(255,255,255,0.5) 30%,
    rgba(255,255,255,0.7) 34%,
    rgba(255,255,255,0.85) 38%,
    white 42%,
    rgba(255,255,255,0.6) 52%,
    rgba(255,255,255,0.3) 62%,
    rgba(255,255,255,0.12) 72%,
    rgba(255,255,255,0.04) 82%,
    transparent 92%);
}


/* Star corona — inside radial, rendered before orbit so it sits beneath planets */
.star-corona-layer {
  position: absolute;
  width: 320px;
  height: 320px;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  transition: transform 0.6s ease-out, filter 0.6s ease-out;
  background: radial-gradient(
    circle at center,
    rgba(200, 220, 255, 0.7) 9%,
    rgba(180, 210, 255, 0.56) 12%,
    rgba(170, 200, 245, 0.45) 15%,
    rgba(180, 190, 230, 0.36) 18%,
    rgba(200, 180, 210, 0.29) 22%,
    rgba(220, 170, 180, 0.23) 26%,
    rgba(240, 160, 140, 0.18) 30%,
    rgba(255, 150, 110, 0.14) 35%,
    rgba(255, 140, 90, 0.11) 40%,
    rgba(255, 130, 75, 0.08) 45%,
    rgba(255, 120, 60, 0.05) 52%,
    rgba(255, 110, 50, 0.03) 60%,
    rgba(255, 100, 42, 0.01) 70%,
    transparent 85%
  );
  pointer-events: none;
}
/* Starburst rays — five-fold luminous glow, old-world esoteric feel.
   Layered conic gradients create soft light wedges in pentagonal symmetry.
   Sirius blue-white tones bleed outward through the corona. */
.star-burst-layer {
  position: absolute;
  width: 320px;
  height: 320px;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  pointer-events: none;
  animation: star-burst-spin 90s linear infinite;
  /* 5 primary rays — diffuse light beams with heavy feathering.
     Each ray ~22° wide with 6° fade-in, 10° plateau, 6° fade-out.
     Extra sub-threshold steps near the edges dissolve the boundary. */
  background:
    conic-gradient(
      from 0deg,
      transparent 0deg,
      rgba(210, 228, 255, 0.02) 1deg,
      rgba(210, 228, 255, 0.06) 3deg,
      rgba(210, 228, 255, 0.12) 5deg,
      rgba(220, 235, 255, 0.2) 7deg,
      rgba(220, 235, 255, 0.25) 9deg,
      rgba(220, 235, 255, 0.25) 13deg,
      rgba(220, 235, 255, 0.2) 15deg,
      rgba(210, 228, 255, 0.12) 17deg,
      rgba(210, 228, 255, 0.06) 19deg,
      rgba(210, 228, 255, 0.02) 21deg,
      transparent 22deg,
      transparent 72deg,
      rgba(210, 228, 255, 0.02) 73deg,
      rgba(210, 228, 255, 0.06) 75deg,
      rgba(210, 228, 255, 0.12) 77deg,
      rgba(220, 235, 255, 0.2) 79deg,
      rgba(220, 235, 255, 0.25) 81deg,
      rgba(220, 235, 255, 0.25) 85deg,
      rgba(220, 235, 255, 0.2) 87deg,
      rgba(210, 228, 255, 0.12) 89deg,
      rgba(210, 228, 255, 0.06) 91deg,
      rgba(210, 228, 255, 0.02) 93deg,
      transparent 94deg,
      transparent 144deg,
      rgba(210, 228, 255, 0.02) 145deg,
      rgba(210, 228, 255, 0.06) 147deg,
      rgba(210, 228, 255, 0.12) 149deg,
      rgba(220, 235, 255, 0.2) 151deg,
      rgba(220, 235, 255, 0.25) 153deg,
      rgba(220, 235, 255, 0.25) 157deg,
      rgba(220, 235, 255, 0.2) 159deg,
      rgba(210, 228, 255, 0.12) 161deg,
      rgba(210, 228, 255, 0.06) 163deg,
      rgba(210, 228, 255, 0.02) 165deg,
      transparent 166deg,
      transparent 216deg,
      rgba(210, 228, 255, 0.02) 217deg,
      rgba(210, 228, 255, 0.06) 219deg,
      rgba(210, 228, 255, 0.12) 221deg,
      rgba(220, 235, 255, 0.2) 223deg,
      rgba(220, 235, 255, 0.25) 225deg,
      rgba(220, 235, 255, 0.25) 229deg,
      rgba(220, 235, 255, 0.2) 231deg,
      rgba(210, 228, 255, 0.12) 233deg,
      rgba(210, 228, 255, 0.06) 235deg,
      rgba(210, 228, 255, 0.02) 237deg,
      transparent 238deg,
      transparent 288deg,
      rgba(210, 228, 255, 0.02) 289deg,
      rgba(210, 228, 255, 0.06) 291deg,
      rgba(210, 228, 255, 0.12) 293deg,
      rgba(220, 235, 255, 0.2) 295deg,
      rgba(220, 235, 255, 0.25) 297deg,
      rgba(220, 235, 255, 0.25) 301deg,
      rgba(220, 235, 255, 0.2) 303deg,
      rgba(210, 228, 255, 0.12) 305deg,
      rgba(210, 228, 255, 0.06) 307deg,
      rgba(210, 228, 255, 0.02) 309deg,
      transparent 310deg,
      transparent 360deg
    ),
    /* 5 secondary rays — dimmer, offset 36°, same feathering profile */
    conic-gradient(
      from 36deg,
      transparent 0deg,
      rgba(200, 220, 255, 0.02) 1deg,
      rgba(200, 220, 255, 0.05) 3deg,
      rgba(200, 220, 255, 0.1) 5deg,
      rgba(200, 220, 255, 0.14) 7deg,
      rgba(200, 220, 255, 0.14) 11deg,
      rgba(200, 220, 255, 0.1) 13deg,
      rgba(200, 220, 255, 0.05) 15deg,
      rgba(200, 220, 255, 0.02) 17deg,
      transparent 18deg,
      transparent 72deg,
      rgba(200, 220, 255, 0.02) 73deg,
      rgba(200, 220, 255, 0.05) 75deg,
      rgba(200, 220, 255, 0.1) 77deg,
      rgba(200, 220, 255, 0.14) 79deg,
      rgba(200, 220, 255, 0.14) 83deg,
      rgba(200, 220, 255, 0.1) 85deg,
      rgba(200, 220, 255, 0.05) 87deg,
      rgba(200, 220, 255, 0.02) 89deg,
      transparent 90deg,
      transparent 144deg,
      rgba(200, 220, 255, 0.02) 145deg,
      rgba(200, 220, 255, 0.05) 147deg,
      rgba(200, 220, 255, 0.1) 149deg,
      rgba(200, 220, 255, 0.14) 151deg,
      rgba(200, 220, 255, 0.14) 155deg,
      rgba(200, 220, 255, 0.1) 157deg,
      rgba(200, 220, 255, 0.05) 159deg,
      rgba(200, 220, 255, 0.02) 161deg,
      transparent 162deg,
      transparent 216deg,
      rgba(200, 220, 255, 0.02) 217deg,
      rgba(200, 220, 255, 0.05) 219deg,
      rgba(200, 220, 255, 0.1) 221deg,
      rgba(200, 220, 255, 0.14) 223deg,
      rgba(200, 220, 255, 0.14) 227deg,
      rgba(200, 220, 255, 0.1) 229deg,
      rgba(200, 220, 255, 0.05) 231deg,
      rgba(200, 220, 255, 0.02) 233deg,
      transparent 234deg,
      transparent 288deg,
      rgba(200, 220, 255, 0.02) 289deg,
      rgba(200, 220, 255, 0.05) 291deg,
      rgba(200, 220, 255, 0.1) 293deg,
      rgba(200, 220, 255, 0.14) 295deg,
      rgba(200, 220, 255, 0.14) 299deg,
      rgba(200, 220, 255, 0.1) 301deg,
      rgba(200, 220, 255, 0.05) 303deg,
      rgba(200, 220, 255, 0.02) 305deg,
      transparent 306deg,
      transparent 360deg
    );
  /* Radial fade — very gradual inner dissolve so rays emerge softly from the core.
     Many sub-threshold steps at the inner edge prevent hard wedge bases. */
  -webkit-mask-image: radial-gradient(circle,
    transparent 8%,
    rgba(255,255,255,0.02) 12%,
    rgba(255,255,255,0.05) 15%,
    rgba(255,255,255,0.10) 18%,
    rgba(255,255,255,0.18) 21%,
    rgba(255,255,255,0.30) 25%,
    rgba(255,255,255,0.45) 29%,
    rgba(255,255,255,0.65) 33%,
    rgba(255,255,255,0.85) 37%,
    white 42%,
    white 50%,
    transparent 80%);
  mask-image: radial-gradient(circle,
    transparent 8%,
    rgba(255,255,255,0.02) 12%,
    rgba(255,255,255,0.05) 15%,
    rgba(255,255,255,0.10) 18%,
    rgba(255,255,255,0.18) 21%,
    rgba(255,255,255,0.30) 25%,
    rgba(255,255,255,0.45) 29%,
    rgba(255,255,255,0.65) 33%,
    rgba(255,255,255,0.85) 37%,
    white 42%,
    white 50%,
    transparent 80%);
}
@keyframes star-burst-spin {
  from { transform: translate(-50%, -50%) rotate(0deg); }
  to   { transform: translate(-50%, -50%) rotate(360deg); }
}
/* Edit mode: rays spin faster + gentle pulsate */
.photo-orbit-edit .star-burst-layer {
  animation: star-burst-spin 30s linear infinite, star-burst-pulse 1s ease-in-out infinite;
}
@keyframes star-burst-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}
/* Rotation hint arrows — rotate with planets via --orbit-rotation */
.star-rotate-arrows {
  position: absolute;
  width: 200px;
  height: 200px;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) rotate(calc(var(--orbit-rotation, 0deg) - 45deg));
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease;
}
.photo-orbit-edit .star-rotate-arrows {
  opacity: 1;
}
/* Small arrows flanking the white dwarf — arcs on same orbit, rotate with dwarf */
.star-group-arrows {
  position: absolute;
  width: 160px;
  height: 160px;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) rotate(var(--group-angle, 45deg));
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease;
}
.photo-orbit-edit .star-group-arrows {
  opacity: 1;
}

/* Joystick mode — pull offset without scale change */
.core-joystick [data-quick-photo-target="shutterCore"] {
  transform: translate(
    var(--body-pull-x, 0px),
    var(--body-pull-y, 0px)
  );
  transition: transform 0.15s ease-out;
}

/* Single outward pulse from the core on tap / joystick pull — preserves the blue outline */
@keyframes core-tap-wave {
  0%   { box-shadow: 0 0 0 1px rgba(255,255,255,0.7), 0 0 0 3px #4A8BFF, 0 0 0 3px rgba(255, 255, 255, 0.5); }
  100% { box-shadow: 0 0 0 1px rgba(255,255,255,0.7), 0 0 0 3px #4A8BFF, 0 0 0 18px rgba(255, 255, 255, 0); }
}
.core-tap-wave {
  animation: core-tap-wave 0.35s ease-out;
}

/* State class on core when a dog thumbnail is displayed (no visual effect) */
.core-dog-selected {
}

/* Charging wave while the finger holds the Body before edit mode fires.
   Tighter cadence than the edit pulse — feels like building pressure. */
@keyframes shutter-charging {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 107, 74, 0.5), 0 8px 30px rgba(0,0,0,0.35); }
  50% { box-shadow: 0 0 0 8px rgba(255, 107, 74, 0), 0 8px 30px rgba(0,0,0,0.35); }
}
.shutter-charging {
  animation: shutter-charging 0.9s ease-in-out infinite;
}
/* Lift when joystick drag mode activates; drop on release */
.star-dragging {
  scale: 1.04;
  transition: scale 150ms ease-out;
}
.star-drop {
  scale: 1;
  transition: scale 200ms ease-in;
}
/* Single large burst wave when edit mode activates — then hands off to
   the normal edit pulse. */
@keyframes shutter-edit-burst {
  0%   { box-shadow: 0 0 0 0 rgba(255, 107, 74, 0.8), 0 8px 30px rgba(0,0,0,0.35); }
  100% { box-shadow: 0 0 0 40px rgba(255, 107, 74, 0), 0 8px 30px rgba(0,0,0,0.35); }
}
.shutter-edit-burst {
  animation: shutter-edit-burst 0.5s ease-out both;
}

/* Header Dog Star icon press indicator — sustained radiating pulse while
   the user's finger is on the icon. Doubles as tap feedback (since the
   icon is tiny) and as a "you've got hold of the Dog Star" signal when the
   finger-chase drag engages. Uses the same ring pulse as
   `shutter-edit-pulse` but at a tighter cadence to feel more immediate. */
@keyframes header-star-press {
  0%   { box-shadow: 0 0 0 0 rgba(255, 107, 74, 0.75); }
  100% { box-shadow: 0 0 0 14px rgba(255, 107, 74, 0); }
}
.header-star-press {
  animation: header-star-press 0.9s ease-out infinite;
}

/* Lock hint — first-dog onboarding pulse + tooltip */
.lock-hint-pulse {
  animation: lock-hint-wave 1.6s ease-out infinite;
}
@keyframes lock-hint-wave {
  0%   { box-shadow: 0 0 0 0 var(--color-brand-a40, rgba(255, 107, 74, 0.4)); }
  70%  { box-shadow: 0 0 0 10px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.lock-hint-pulse {
  border-radius: 9999px;
}
/* Steady pulse on the lock button while the card is unlocked */
.modal-panel.unlocked [data-dog-modal-target="lockHint"] {
  animation: lock-hint-wave 1.6s ease-out infinite;
  border-radius: 9999px;
}
.lock-hint-tooltip {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  white-space: nowrap;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  color: white;
  background: var(--color-brand);
  padding: 4px 10px;
  border-radius: 6px;
  pointer-events: none;
  animation: lock-hint-tooltip-fade 1.6s ease-in-out infinite;
}
.lock-hint-tooltip::before {
  content: "";
  position: absolute;
  bottom: 100%;
  right: 12px;
  border: 5px solid transparent;
  border-bottom-color: var(--color-brand);
}
@keyframes lock-hint-tooltip-fade {
  0%, 100% { opacity: 0.7; }
  50%      { opacity: 1; }
}

/* The upload tray (Plan 388): one bar per file. The fill is the brand colour; "saving" (the
   app's own POST after the bytes landed) shimmers by background-position, never by opacity
   (ui-conventions); a failed row wears a rose ring. */
.upload-bar {
  height: 5px;
  border-radius: 999px;
  overflow: hidden;
  background: color-mix(in srgb, var(--tint-shade) 10%, transparent);
}
.upload-bar-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--color-brand);
  transition: width 0.2s ease-out;
}
.upload-row[data-state="saving"] .upload-bar-fill {
  background: linear-gradient(90deg, var(--color-brand) 0%, color-mix(in srgb, var(--color-brand) 45%, transparent) 50%, var(--color-brand) 100%);
  background-size: 200% 100%;
  animation: upload-saving 1.1s linear infinite;
}
@keyframes upload-saving {
  from { background-position: 200% 0; }
  to   { background-position: 0 0; }
}
.upload-row[data-state="failed"] {
  box-shadow: 0 0 0 2px var(--color-rose);
}
@media (prefers-reduced-motion: reduce) {
  .upload-row[data-state="saving"] .upload-bar-fill { animation: none; }
}

/* The walk in the header (Plan 383). LIVE is a static pack-coloured ring — box-shadow, never
   opacity, so the pack glyph's dots stay readable (panel H8); the colour rides --hdr-walk-color
   from the control. The FLASH plays three cycles only when the state changed (a start, an
   end), then the ring rests: a blink for the whole walk is "needs you now", and a walk is the
   expected state for an hour. Reduced motion: the ring, no flash. (Replaces .nav-walk-pulse and
   paw-pulse, the old opacity exception, retired with the nav signal.) */
.hdr-walk-live {
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--hdr-walk-color, var(--color-brand)) 55%, transparent);
}
.hdr-walk-flash {
  animation: hdr-walk-flash 0.9s ease-in-out 3;
}
@keyframes hdr-walk-flash {
  0%, 100% { box-shadow: 0 0 0 2px color-mix(in srgb, var(--hdr-walk-color, var(--color-brand)) 55%, transparent); }
  50%      { box-shadow: 0 0 0 7px color-mix(in srgb, var(--hdr-walk-color, var(--color-brand)) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .hdr-walk-flash { animation: none; }
}


/* Swap-selected dog slot in edit mode: white + brand double ring.
   Ad-hoc 2026-04-11: the outline settles in from a larger size
   (swap-select-settle, 0.28s) before entering the infinite idle pulse
   (swap-select-pulse). The settle is a one-shot `cubic-bezier` ease-out
   so the ring visibly contracts onto the planet — reads as "locked on". */
/* ── Planet drop shadow shorthand ──
   Every outline / pulse / fade keyframe must restate `--planet-casing` (the
   dark half of the Plan 262 casing) and `--planet-lift` (the two trailing lift
   layers), because a box-shadow animation REPLACES the whole stack. Omit the
   casing and the planet loses its dark ring exactly while selected; omit the
   lift and it stops reading as lifted. The LIGHT half of the casing is an
   `outline` on .orbit-slot-img and survives all of this untouched — that is
   why it is an outline. Search "drop-shadow-layers" when editing.

   Plan 262 geometry: the selection ring now starts OUTSIDE the casing (5.5px,
   pulsing to 7.5px) rather than at 3px with its own 1px white ring — the
   casing supplies the white ring, so selection reads by thickness + motion +
   hue rather than borrowing a carrier the casing already owns. */
.orbit-slot-swap-selected {
  box-shadow: var(--planet-casing), 0 0 0 5.5px var(--color-brand), var(--planet-lift);
  animation:
    swap-select-settle 0.28s cubic-bezier(0.2, 0.8, 0.2, 1) both,
    swap-select-pulse 1s ease-in-out 0.28s infinite;
}
@keyframes swap-select-settle {
  0%   { box-shadow: var(--planet-casing), 0 0 0 16px rgba(255, 107, 74, 0), var(--planet-lift); }
  60%  { box-shadow: var(--planet-casing), 0 0 0 8.5px var(--color-brand), var(--planet-lift); }
  100% { box-shadow: var(--planet-casing), 0 0 0 5.5px var(--color-brand), var(--planet-lift); }
}
@keyframes swap-select-pulse {
  0%, 100% { box-shadow: var(--planet-casing), 0 0 0 5.5px var(--color-brand), var(--planet-lift); }
  50%      { box-shadow: var(--planet-casing), 0 0 0 7.5px var(--color-brand), var(--planet-lift); }
}
/* The selected dog — settle-in wave, then a continuous pulse, so selected vs
   unselected is unmistakable. The keyframes carried gather-* names until
   Plan 302 deleted the multi-dog machinery; they belong to the live single
   selection and were renamed with it. */
.orbit-pre-selected {
  animation: select-settle 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both,
             select-pulse 0.9s ease-out 0.45s infinite;
  border-radius: 50%;
}
@keyframes select-settle {
  0%   { box-shadow: var(--planet-casing), 0 0 0 5.5px #4A8BFF, var(--planet-lift); }
  50%  { box-shadow: var(--planet-casing), 0 0 0 7.75px #4A8BFF,
                     0 0 4px 0.5px rgba(74, 139, 255, 0.15), var(--planet-lift); }
  100% { box-shadow: var(--planet-casing), 0 0 0 5.5px #4A8BFF, var(--planet-lift); }
}
@keyframes select-pulse {
  0%   { box-shadow: var(--planet-casing), 0 0 0 5.5px #4A8BFF,
                     0 0 0 5.5px rgba(74, 139, 255, 0.5), var(--planet-lift); }
  100% { box-shadow: var(--planet-casing), 0 0 0 5.5px #4A8BFF,
                     0 0 0 11.5px rgba(74, 139, 255, 0), var(--planet-lift); }
}
/* (.orbit-gathered-deselecting below is a LEGACY NAME but live: it animates
   the replaced dog's fade when a tap moves the selection.) */
/* Fade-out for a deselected dog — now the common case, since a tap REPLACES
   the selection (Plan 262: single is the only mode). */
.orbit-gathered-deselecting {
  animation: gather-deselect-fade 0.3s ease-out forwards !important;
  border-radius: 50%;
}
@keyframes gather-deselect-fade {
  from { box-shadow: var(--planet-casing), 0 0 0 5.5px #4A8BFF, var(--planet-lift); }
  to   { box-shadow: var(--planet-casing), 0 0 0 5.5px rgba(74,139,255,0), var(--planet-lift); }
}
/* Selected dog's name on the outer ring, just below the core image.
   LEGACY NAME (the multi-dog gather era, deleted in Plan 302) but live —
   written by renderer.applyCoreState whenever a dog shows in the core. */
.core-gather-name {
  position: absolute;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 9px;
  line-height: 1;
  color: #fff;
  text-align: center;
  pointer-events: none;
  z-index: 10;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
  background: rgba(20, 45, 76, 0.85);
  padding: 2px 4px;
  border-radius: 3px;
}
/* Plan 033: group cycle fade. When the active group cursor changes, the
   dog planets fade out → swap → fade in while the action planets
   (including the group digit itself) stay lit so the walker sees the
   new number immediately. The `:not([data-quick-action])` selector
   narrows the fade to dog planets only.

   The !important on the class rule ensures the opacity override wins
   even if other rules set opacity on the slot. */
.orbit-ring .orbit-slot:not([data-quick-action]) {
  transition: opacity 0.2s ease;
}
.orbit-ring.orbit-cycle-fade .orbit-slot:not([data-quick-action]) {
  opacity: 0 !important;
}

/* Plan 033: centred digit inside the group planet. Font and weight
   match the other display-font numerals in the app; size tuned to sit
   alongside the 16px Material Symbols glyphs without looking out of
   place. */
.orbit-slot-group-digit {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  color: #fff;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 18px;
  line-height: 1;
}

/* Plan 035: paw glyph shown in the Group planet while the walk group
   is the active group. Sized to match the 18px digit it replaces so
   the Ring 3 centreline stays stable when the walker transitions in
   and out of a walk. */
.orbit-slot-group-paw {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-size: 18px;
  font-variation-settings: "FILL" 1, "wght" 500;
  line-height: 1;
}

/* Core group-icon placeholder — shown in the Body when the selected
   dog has no photo. Mirrors the group satellite's icon + dots style. */
.core-group-placeholder {
  position: relative;
  background: linear-gradient(135deg, #C8DBF0, #7BA4CC);
  color: white;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  gap: 0;
}
.core-group-icon {
  font-size: 22px;
  font-variation-settings: "FILL" 1, "wght" 500;
  line-height: 1;
  margin-top: -6px;
}
.core-group-calendar {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: -6px;
}
.core-group-cal-icon {
  font-size: 24px;
  font-variation-settings: "FILL" 0, "wght" 400;
  line-height: 1;
}
.core-group-cal-paw {
  position: absolute;
  top: calc(55% + 1px);
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 10px;
  font-variation-settings: "FILL" 1, "wght" 500;
  line-height: 1;
}
.core-group-dots {
  display: flex;
  gap: 3px;
  margin-top: 2px;
}
.core-group-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.35);
  flex-shrink: 0;
}
.core-group-dot-filled {
  background: white;
}
.core-group-dot:first-child,
.core-group-dot:last-child {
  transform: translateY(-1.5px);
}
.core-group-label {
  font-size: 11px;
  font-weight: 700;
  font-family: var(--font-display);
  letter-spacing: 0.03em;
  line-height: 1;
  text-shadow:
    0 1px 1px rgba(0, 0, 0, 0.6),
    0 2px 4px rgba(0, 0, 0, 0.4),
    0 0 8px rgba(0, 0, 0, 0.25);
}

.orbit-slot-name {
  margin-top: -1px;
  font-family: var(--font-display);
  font-size: 0.5625rem;
  font-weight: 700;
  color: #222;
  text-align: center;
  white-space: nowrap;
  text-shadow: none;
  background: rgba(255, 255, 255, 0.85);
  padding: 0px 3px;
  border-radius: 4px;
  transition: background 0.2s, color 0.2s;
}
body:not([data-quick-photo-state-value="hidden"]) .orbit-slot-name {
  color: #fff;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
  background: rgba(20, 45, 76, 0.85);
}

/* ── Select arrow (shared chevron for all custom selects) ── */
select.edit-input,
.dog-status-select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' viewBox='0 -960 960 960' width='16' fill='%23A0A0B2'%3E%3Cpath d='M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 4px center;
  background-size: 14px;
  padding-right: 20px;
  appearance: none;
  -webkit-appearance: none;
}

/* ── Photo filter pills ── */
.photo-pill {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 12px;
  padding: 3px 8px;
  border-radius: 9999px;
  border: 1px solid rgba(122, 122, 144, 0.3);
  background: transparent;
  color: var(--color-ink-faint);
  cursor: pointer;
  white-space: nowrap;
  touch-action: manipulation;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.photo-pill-active {
  background: var(--color-brand);
  color: white;
  border-color: var(--color-brand);
}

/* Photo tag gallery — selection outline */
.photo-tag-selected {
  outline: 3px solid #4A8BFF;
  outline-offset: -3px;
}

/* ── Inline style replacements (plan 049) ── */

.lightbox-badge {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 11px;
  color: white;
  background: rgba(0, 0, 0, 0.6);
  padding: 3px 10px;
  border-radius: 9999px;
  line-height: 1.3;
  white-space: nowrap;
}

.no-transition {
  transition: none !important;
}

/* Weather-icon swap fade (application.js#_updateWeatherBtns) — a named class per the
   Plan 049 rule: static transition strings never get retyped in JS. */
.fade-icon-swap {
  transition: opacity 0.3s ease-out;
}

/* Thinking spinner — orbiting dot ring on star core while waiting for Gemini */
.core-thinking::before {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  border: 2px solid transparent;
  border-top-color: rgba(99, 102, 241, 0.9);
  border-right-color: rgba(99, 102, 241, 0.4);
  animation: core-think-spin 0.8s linear infinite;
  pointer-events: none;
  z-index: 2;
}
@keyframes core-think-spin {
  to { transform: rotate(360deg); }
}

.core-glass {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 60'%3E%3Cdefs%3E%3CradialGradient id='s' cx='40%25' cy='35%25' r='55%25'%3E%3Cstop offset='0%25' stop-color='%23F0F5FF'/%3E%3Cstop offset='35%25' stop-color='%23D0E2FF'/%3E%3Cstop offset='65%25' stop-color='%23B0D0F0'/%3E%3Cstop offset='100%25' stop-color='%2385ADCC'/%3E%3C/radialGradient%3E%3CradialGradient id='h' cx='38%25' cy='28%25' r='30%25'%3E%3Cstop offset='0%25' stop-color='%23fff' stop-opacity='0.8'/%3E%3Cstop offset='100%25' stop-color='%23fff' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Ccircle cx='30' cy='30' r='30' fill='url(%23s)'/%3E%3Ccircle cx='30' cy='30' r='30' fill='url(%23h)'/%3E%3C/svg%3E") no-repeat center / contain;
}


.action-dim {
  opacity: 0.2;
}

.modal-fling {
  transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}

.modal-snapback {
  transition: transform 0.18s ease-out;
}

.sortable-flip {
  transition: transform 400ms cubic-bezier(0.25, 1, 0.5, 1);
}

/* Drag-to-reorder (2026-07-11 rework): while a row is dragged, the OTHERS wear this
   and glide out of its way — the dragged row itself is under .no-transition, pinned
   to the finger. */
.sortable-shifting {
  transition: transform 0.18s ease;
}
/* A HELD row has come off the page (Plan 368) — it casts further, and it is the most SOLID thing
   on screen. The dragged row used to drop to opacity 0.85, which reads as fading rather than
   rising; a card in your hand should not go translucent. The magnification itself is composed
   into the drag transform in JS, because that property is rewritten on every move. */
.sortable-lifted {
  box-shadow: var(--shadow-depth-lg);
}
/* Sortable rows are grab targets, not documents — starting a drag must never begin
   a text selection (field report: dragging driving rows selected their text), and
   iOS shouldn't offer the long-press callout on them. */
[data-sortable-target~="item"] {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
/* While a drag is live (body class from sortable#dragStart), the WHOLE page refuses
   selection — a fast sweep across non-sortable text would otherwise highlight it. */
body.sortable-drag-active {
  -webkit-user-select: none;
  user-select: none;
}
/* Front-zone drop target (the logistics hero): lit while the dragged row would
   replace it on release. */
.sortable-front-active {
  box-shadow: 0 0 0 2px var(--color-brand), 0 0 18px rgba(255, 107, 74, 0.35);
  border-radius: 1rem;
  transition: box-shadow 0.15s ease;
}

/* Address autocomplete dropdown */
.address-autocomplete-dropdown {
  background: var(--color-inset-strong);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.04);
  overflow: hidden;
  max-height: 240px;
  overflow-y: auto;
}

.address-autocomplete-item {
  padding: 10px 14px;
  font-size: 13px;
  color: var(--color-ink);
  cursor: pointer;
  border-bottom: 1px solid var(--color-line);
}

.address-autocomplete-item:last-child {
  border-bottom: none;
}

.address-autocomplete-item:hover,
.address-autocomplete-item-active {
  background: var(--color-canvas-warm);
}

/* ── Help / Guide content ── */
.help-content h2 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.25rem;
  color: var(--color-ink);
  margin-top: 2rem;
  margin-bottom: 0.75rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--color-line);
  /* Jump-list chips target h2 ids — land clear of the fixed header. */
  scroll-margin-top: calc(4.5rem + env(safe-area-inset-top, 0px));
}

/* Cross-links between guide sections (See also, More:, the typical-day hub). */
.help-content a {
  color: var(--color-brand-text);
  font-weight: 600;
  text-decoration: none;
}

.help-content h2:first-child {
  margin-top: 0;
}

.help-content h3 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  color: var(--color-ink);
  margin-top: 1.5rem;
  margin-bottom: 0.5rem;
}

.help-content p {
  font-family: var(--font-body);
  font-size: 0.9375rem;
  line-height: 1.65;
  color: var(--color-ink-soft);
  margin-bottom: 0.75rem;
}

.help-content ul,
.help-content ol {
  font-family: var(--font-body);
  font-size: 0.9375rem;
  line-height: 1.65;
  color: var(--color-ink-soft);
  padding-left: 1.25rem;
  margin-bottom: 0.75rem;
}

.help-content li {
  margin-bottom: 0.375rem;
}

.help-content strong {
  color: var(--color-ink);
  font-weight: 600;
}

.help-content .guide-card {
  background: var(--glass-strong);
  border-radius: 1rem;
  padding: 1rem 1.125rem;
  margin-bottom: 0.75rem;
}

/* Grid, not flex (founder, 2026-08-13): flex laid the number beside ONE paragraph, so a
   step with several block children rendered them as side-by-side columns — unreadable
   word-per-line strips on a phone, and this is a phone app. Column 1 is the number;
   every other child is forced into column 2, so any number of blocks stack. */
.help-content .guide-step {
  display: grid;
  grid-template-columns: 1.75rem 1fr;
  align-items: start;
  column-gap: 0.75rem;
  row-gap: 0.5rem;
  margin-bottom: 0.75rem;
}

.help-content .guide-step > :not(.guide-step-num) {
  grid-column: 2;
}

.help-content .guide-step-num {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: rgba(255, 107, 74, 0.1);
  color: var(--color-brand-text);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.8125rem;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  margin-top: 0.125rem;
}

.help-content .guide-step p {
  margin-bottom: 0;
}

.help-content .guide-tip {
  padding: 0.875rem 1rem;
  background: rgba(255, 107, 74, 0.04);
  border-radius: 0.875rem;
  border-left: 3px solid var(--color-brand-soft);
  font-family: var(--font-body);
  font-size: 0.875rem;
  line-height: 1.65;
  color: var(--color-ink-soft);
  margin-bottom: 0.75rem;
}

.help-content .guide-tip strong {
  color: var(--color-brand-text);
}

/* Stacked term→meaning entries — the mobile-first replacement for two-column
   guide tables, whose cells wrapped into unreadable multi-row blocks at phone
   width (Plan 187 feedback). The term sits on its own line, meaning beneath. */
.help-content .guide-def {
  margin: 0;
}

.help-content .guide-def > div {
  padding: 0.625rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--tint-shade) 5%, transparent);
}

.help-content .guide-def > div:first-child {
  padding-top: 0;
}

.help-content .guide-def > div:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

.help-content .guide-def dt {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.9375rem;
  color: var(--color-ink);
  margin-bottom: 0.125rem;
}

.help-content .guide-def dt .guide-def-note {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 0.8125rem;
  color: var(--color-ink-faint);
  margin-left: 0.375rem;
}

.help-content .guide-def dd {
  font-family: var(--font-body);
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--color-ink-soft);
  margin: 0;
}

.help-content .guide-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-family: var(--font-body);
  font-size: 0.875rem;
  margin-bottom: 1rem;
  border-radius: 0.75rem;
  overflow: hidden;
}

.help-content .guide-table th {
  text-align: left;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.8125rem;
  color: var(--color-ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.5rem 0.75rem;
  background: var(--color-canvas-warm);
  border-bottom: 1px solid color-mix(in srgb, var(--tint-shade) 6%, transparent);
}

.help-content .guide-table td {
  padding: 0.5rem 0.75rem;
  color: var(--color-ink-soft);
  border-bottom: 1px solid color-mix(in srgb, var(--tint-shade) 4%, transparent);
  line-height: 1.5;
  /* Mobile-first: long unbreakable tokens (Accounts::PaymentsController) must wrap
     mid-word or the table widens past the phone viewport and the whole PAGE scrolls
     sideways (the dev guide's controllers page did exactly that at 390px). */
  overflow-wrap: anywhere;
}

.help-content .guide-table th {
  overflow-wrap: anywhere;
}

.help-content .guide-table tr:last-child td {
  border-bottom: none;
}

.help-content .guide-icon {
  vertical-align: middle;
  font-size: 18px;
  margin-right: 0.25rem;
}

.help-content .guide-screenshot {
  margin: 1.25rem 0;
  text-align: center;
}

.help-content .guide-screenshot img,
.help-content .guide-screenshot video {
  max-width: 280px;
  width: 100%;
  border-radius: 1rem;
  box-shadow: 0 2px 12px rgba(0,0,0,0.10), 0 0.5px 2px rgba(0,0,0,0.06);
  border: 1px solid color-mix(in srgb, var(--tint-shade) 6%, transparent);
}

/* Placeholder when a guide screenshot asset is missing (guide_img fallback) —
   keeps the layout intact instead of 500ing the page. */
.help-content .guide-screenshot-missing {
  max-width: 280px;
  aspect-ratio: 9 / 16;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 1rem;
  background: var(--color-canvas-warm);
  border: 1px dashed color-mix(in srgb, var(--tint-shade) 12%, transparent);
  color: var(--color-ink-faint);
}
.help-content .guide-screenshot-missing .material-symbols-outlined {
  font-size: 40px;
}

.help-content .guide-screenshot figcaption {
  font-family: var(--font-body);
  font-size: 0.8125rem;
  color: var(--color-ink-faint);
  margin-top: 0.5rem;
  line-height: 1.4;
}

.help-content pre {
  background: var(--color-canvas-warm, #f5f3ef);
  border: 1px solid color-mix(in srgb, var(--tint-shade) 6%, transparent);
  border-radius: 0.75rem;
  padding: 1rem 1.25rem;
  overflow-x: auto;
  margin: 1rem 0;
  -webkit-overflow-scrolling: touch;
}

.help-content pre code {
  font-family: ui-monospace, "SF Mono", "Cascadia Code", "Fira Code", Menlo, Consolas, monospace;
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--color-ink);
  white-space: pre;
}

.help-content code {
  font-family: ui-monospace, "SF Mono", "Cascadia Code", "Fira Code", Menlo, Consolas, monospace;
  font-size: 0.875em;
  background: var(--color-canvas-warm, #f5f3ef);
  border-radius: 0.25rem;
  padding: 0.125rem 0.375rem;
  overflow-wrap: anywhere; /* inline route/class names must not widen the phone viewport */
}

/* ── StarPaw panel shared positioning ── */
/* Links/buttons in panels are non-interactive until their element is focused */
.starpaw-context-panel a,
.starpaw-context-panel button {
  -webkit-tap-highlight-color: transparent;
  pointer-events: none;
}
.starpaw-context-panel .starpaw-joystick-focus a,
.starpaw-context-panel .starpaw-joystick-focus button,
.starpaw-context-panel .starpaw-joystick-focus-dog a,
.starpaw-context-panel .starpaw-joystick-focus-dog button {
  pointer-events: auto;
}
/* Star concealed — no joystick, all panel links/buttons are directly tappable */
body.star-concealed .starpaw-context-panel a,
body.star-concealed .starpaw-context-panel button {
  pointer-events: auto;
}
/* Core always has electric blue outline — visible in both extended and retracted states */
body:not([data-quick-photo-state-value="hidden"]) [data-quick-photo-target="shutterCore"] {
  box-shadow: 0 0 0 1px rgba(255,255,255,0.7), 0 0 0 3px #4A8BFF;
}
/* Remove the blue outline in listening/conversation mode */
body.listening-adorned [data-quick-photo-target="shutterCore"] {
  box-shadow: none;
}
.starpaw-context-panel {
  position: absolute;
  top: calc(env(safe-area-inset-top) + 104px);
  left: 12px;
  right: 12px;
  z-index: 10;
  pointer-events: auto;
  opacity: 0;
  transform: translateY(-10px);
  transition: opacity 0.25s ease-out, transform 0.25s ease-out;
}
.starpaw-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 24px 16px;
  color: rgba(255, 255, 255, 0.25);
  font-size: 14px;
  font-style: italic;
  text-align: center;
  transform: none;
}
.starpaw-empty-group {
  font-size: 16px;
  font-weight: 700;
  font-style: normal;
  font-family: var(--font-display);
  letter-spacing: 0.03em;
  color: rgba(255, 255, 255, 0.5);
}


/* ── StarPaw joystick navigation focus ── */
.starpaw-joystick-focus {
  box-shadow: 0 0 0 1px rgba(255,255,255,0.7), 0 0 0 3px #4A8BFF;
  border-radius: 6px;
}
.starpaw-joystick-focus-dog {
  box-shadow: 0 0 0 1px rgba(255,255,255,0.7), 0 0 0 3px #4A8BFF;
  border-radius: 8px;
}

/* Body pull visual feedback — shift the core in the pull direction (non-joystick only) */
[data-quick-photo-target="coreBarrel"]:not(.core-joystick) > [data-quick-photo-target="shutterCore"] {
  translate: var(--body-pull-x, 0) var(--body-pull-y, 0);
}
.body-pull-return {
  transition: translate 0.2s ease-out !important;
}

/* ── StarPaw joystick directional arrows ── */
.starpaw-joystick-arrows {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 1;
}
.photo-orbit-edit .starpaw-joystick-arrows {
  display: none;
}
.starpaw-joystick-chevron {
  position: absolute;
  width: 6px;
  height: 6px;
  border-right: 1.5px solid rgba(255,255,255,0.9);
  border-bottom: 1.5px solid rgba(255,255,255,0.9);
}
.starpaw-joystick-chevron-up {
  top: 4px;
  left: 50%;
  transform: translateX(-50%) rotate(-135deg);
}
.starpaw-joystick-chevron-down {
  bottom: 4px;
  left: 50%;
  transform: translateX(-50%) rotate(45deg);
}
.starpaw-joystick-chevron-left {
  left: 4px;
  top: 50%;
  transform: translateY(-50%) rotate(135deg);
}
.starpaw-joystick-chevron-right {
  right: 4px;
  top: 50%;
  transform: translateY(-50%) rotate(-45deg);
}

/* Grip ring: rotatable tick marks between the directional chevrons.
   Visually turns during rotation gestures to give "dial" feedback. */

/* ── Crop overlay (dog_modal thumbnail crop) ── */
.crop-overlay {
  position: fixed;
  inset: 0;
  z-index: 500;
  background: rgba(0,0,0,0.92);
  display: flex;
  flex-direction: column; /* image wrapper + footer (Plan 235) */
  align-items: center;
  justify-content: center;
  touch-action: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  padding: env(safe-area-inset-top, 0) env(safe-area-inset-right, 0) env(safe-area-inset-bottom, 0) env(safe-area-inset-left, 0);
}
.crop-wrapper {
  position: relative;
  max-width: 100%;
  max-height: 100%;
  overflow: hidden;
}
.crop-image {
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.crop-square {
  position: absolute;
  border: 2px solid white;
  border-radius: 8px;
  box-shadow: 0 0 0 9999px rgba(0,0,0,0.55);
  z-index: 10;
  touch-action: none;
  cursor: grab;
}
/* Photo-adjust lightbox (Plan 235) — the new-dog dialog's framing overlay. */
.pa-tabs {
  display: flex;
  gap: 8px;
  margin-bottom: 14px;
}
.pa-tab {
  padding: 7px 16px;
  border-radius: 999px;
  border: none;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.65);
  background: rgba(255, 255, 255, 0.12);
  cursor: pointer;
}
.pa-tab-active {
  color: #fff;
  background: var(--color-brand);
}
/* Position mode's see-outside window (Plan 236): the crop-square treatment
   (white edge + dimmed outside), brand-colored so it reads as THE hero frame. */
.pa-frame-window {
  border-color: var(--color-brand);
  cursor: grab;
}
.pa-hint {
  margin-top: 12px;
  font-size: 11px;
  color: rgba(255, 255, 255, 0.55);
  text-align: center;
}
.pa-crop-handle {
  position: absolute;
  right: -10px;
  bottom: -10px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--color-brand);
  border: 2px solid #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}

/* Plan 235: crop-overlay footer — Save commits, Cancel reverts. Real hit
   targets (44px), pinned under the image. */
.crop-footer {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-top: 16px;
}
.crop-footer-btn {
  min-width: 118px;
  padding: 11px 20px;
  border-radius: 12px;
  border: none;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
}
.crop-footer-cancel {
  color: rgba(255, 255, 255, 0.85);
  background: rgba(255, 255, 255, 0.14);
}
.crop-footer-save {
  color: #fff;
  background: var(--color-brand);
}

/* Completed-card lock tooltip — flashed when tapping the lock on a completed walk / past
   daycare or boarding card. Appended to <body> (fixed) by completed_lock_controller so the
   card's overflow-hidden can't clip it; positioned in JS. */
.cal-locked-tooltip {
  position: fixed;
  z-index: 60;
  white-space: nowrap;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  /* Match the app's tooltip colour language — brand fill + white text, same as the
     dog-modal lock-hint tooltip — with an upward caret toward the (right-aligned) lock. */
  color: #fff;
  background: var(--color-brand);
  padding: 4px 10px;
  border-radius: 6px;
  pointer-events: none;
  opacity: 0;
  transform: translateY(-2px);
  transition: opacity 0.15s ease, transform 0.15s ease;
  box-shadow: 0 4px 12px color-mix(in srgb, var(--tint-shadow) 18%, transparent);
}
.cal-locked-tooltip::before {
  content: "";
  position: absolute;
  bottom: 100%;
  right: 12px;
  border: 5px solid transparent;
  border-bottom-color: var(--color-brand);
}
.cal-locked-tooltip.is-visible { opacity: 1; transform: translateY(0); }

/* Calendar card lock — gate transfer buttons when card is locked */
[data-controller~="card-lock"]:not(.cal-card-unlocked) .cal-edit-action {
  pointer-events: none;
  cursor: default;
}
[data-controller~="card-lock"]:not(.cal-card-unlocked) .cal-unlock-visible {
  display: none;
}
/* The counterpart (Plan 379): what shows only while LOCKED — the completed card's plain clock,
   which the unlocked state replaces with the same clock as a control. */
[data-controller~="card-lock"].cal-card-unlocked .cal-lock-visible {
  display: none;
}

/* Pencil icon on transfer buttons when card is unlocked */
.cal-card-unlocked .cal-edit-action {
  position: relative;
}
.cal-card-unlocked .cal-edit-action::after {
  content: "edit";
  font-family: "Material Symbols Outlined";
  font-size: 14px;
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-brand-text);
  opacity: 0.5;
}

/* Walk replay slider — a tall, transparent hit box (easy to grab, esp. on touch) with a
   thin visible track drawn by the runnable-track. The 28px box ≫ the 4px line it shows. */
input[type="range"].walk-slider {
  -webkit-appearance: none;
  appearance: none;
  height: 28px;
  background: transparent;
  outline: none;
  cursor: pointer;
  touch-action: none; /* claim the drag so a horizontal swipe scrubs rather than pans */
}
input[type="range"].walk-slider::-webkit-slider-runnable-track {
  height: 4px;
  background: rgba(255,255,255,0.3);
  border-radius: 2px;
}
input[type="range"].walk-slider::-moz-range-track {
  height: 4px;
  background: rgba(255,255,255,0.3);
  border-radius: 2px;
}
input[type="range"].walk-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 22px;
  height: 22px;
  margin-top: -9px; /* center the 22px thumb on the 4px track */
  border-radius: 50%;
  background: var(--color-ink);
  cursor: pointer;
  border: 2px solid white;
  box-shadow: 0 1px 4px rgba(0,0,0,0.3);
}
input[type="range"].walk-slider::-moz-range-thumb {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--color-ink);
  cursor: pointer;
  border: 2px solid white;
  box-shadow: 0 1px 4px rgba(0,0,0,0.3);
}

/* Incident marker pulse */
@keyframes incident-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(244, 63, 94, 0.5); }
  100% { box-shadow: 0 0 0 10px rgba(244, 63, 94, 0); }
}

/* ── Gallery dog filter — search + autocomplete + removable pills (Plan 160).
   Type to find a dog, pick to add a pill, tap a pill to remove. Replaces the
   Plan 158 chip row + drag-to-tag (retired). JS-built rows/pills need real CSS
   (Tailwind won't emit classes set on createElement'd nodes). ── */
.gallery-search-wrap { background: var(--color-glass-strong); border: 1px solid color-mix(in srgb, var(--tint-shade) 8%, transparent); min-height: 2.75rem; }
.gallery-search-input { color: var(--color-ink); }
.gallery-search-input::placeholder { color: var(--color-ink-faint); }

/* Facet toggles (Plan 160 polish) — one bonded segmented control replacing the
   floating circles. Active segment uses the brand accent, matching the pills, so
   "orange = an active filter" reads as a system. */
/* Gallery accent — the light gallery (Plan 169) wears the app's brand coral, so it reads as part
   of the regular light app (calendar/dogs/accounts) rather than a StarView-style dark media surface.
   One var drives every active/selected state so it stays consistent. */
[data-gallery-overlay] { --gallery-accent: var(--color-brand); }

.gallery-facets { background: color-mix(in srgb, var(--tint-shade) 4%, transparent); border: 1px solid color-mix(in srgb, var(--tint-shade) 10%, transparent); }
.gallery-facet { background: transparent; border: none; color: var(--color-ink-soft); cursor: pointer; transition: background 0.15s, color 0.15s; }
.gallery-facet + .gallery-facet { border-left: 1px solid color-mix(in srgb, var(--tint-shade) 10%, transparent); }
.gallery-facet-label { font-family: var(--font-display, inherit); font-weight: 600; font-size: 0.75rem; }
.gallery-facet.is-active { background: var(--gallery-accent); color: #fff; }
.gallery-facet.is-active .material-symbols-outlined { font-variation-settings: "FILL" 1; }

/* Date picker lives at the search level (it's a value input, not a boolean toggle) —
   a square button beside the search box, same height + rectangular geometry. */
.gallery-datebtn { width: 2.75rem; background: var(--color-glass-strong); border: 1px solid color-mix(in srgb, var(--tint-shade) 8%, transparent); color: var(--color-ink-soft); cursor: pointer; transition: background 0.15s, color 0.15s; }
.gallery-datebtn.is-active { background: var(--gallery-accent); color: #fff; }
.gallery-datebtn.is-active .material-symbols-outlined { font-variation-settings: "FILL" 1; }

/* Bottom action bar (Plan 160 polish) — make the selection→action state loud.
   Dimmed + inert-looking with no selection; full white + accent icon once
   ≥1 photo is selected. Still tappable when dimmed so a dead tap can hint. */
.gallery-action { transition: opacity 0.15s, color 0.15s; }
.gallery-action.is-dimmed { opacity: 0.35; }
.gallery-action.is-armed { opacity: 1; color: var(--color-ink); }
.gallery-action.is-armed .material-symbols-outlined { color: var(--gallery-accent); }
/* Photo-selection checkbox (was Tailwind bg-brand) → the gallery accent. */
.gallery-check-on { background: var(--gallery-accent); border-color: var(--gallery-accent); }
.gallery-action-divider { width: 1px; align-self: stretch; background: color-mix(in srgb, var(--tint-shade) 14%, transparent); margin: 0.3rem 0.25rem; }

/* Title-bar Select / Done — a pill so it reads as a button, not a label (Plan 161).
   Subtle outlined chip in view mode; accent-filled while selecting (same "accent = active"
   language as the facets/pills/checkbox). */
.gallery-select-toggle {
  display: flex; align-items: center; gap: 0.3rem;
  margin-right: 0.15rem; padding: 0.34rem 0.7rem 0.34rem 0.55rem;
  min-height: 2.1rem; border-radius: 9999px;
  background: color-mix(in srgb, var(--tint-shade) 5%, transparent);
  border: 1px solid color-mix(in srgb, var(--tint-shade) 15%, transparent);
  color: var(--color-ink-soft);
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.gallery-select-toggle.is-active {
  background: var(--gallery-accent); border-color: var(--gallery-accent); color: #fff;
}
.gallery-select-toggle.is-active .material-symbols-outlined { font-variation-settings: "FILL" 1; }

/* A soft lift around each photo so thumbnails read as matted cards on the warm canvas
   ground (rather than floating), matching the light app's depth-sm card treatment. */
[data-gallery-grid] [data-photo-id] { box-shadow: 0 1px 4px color-mix(in srgb, var(--tint-shadow) 10%, transparent), 0 0 0 1px color-mix(in srgb, var(--tint-shade) 5%, transparent); }

/* Select mode (Plan 161): the per-photo checkboxes + the selection actions only show
   while selecting. View mode is a clean tap-to-open grid (Upload stays). */
[data-gallery-overlay]:not(.gallery-selecting) [data-photo-select] { display: none !important; }
/* The bottom bar is mode-exclusive (Plan 161): view mode = only Upload (add photos);
   select mode = only the selection operators (Share / Publish / Delete). Upload doesn't
   act on a selection, so it's hidden while selecting. The old Upload↔actions divider is
   pointless now (the two groups never coexist) — hidden. */
[data-gallery-overlay]:not(.gallery-selecting) .gallery-action { display: none !important; }
/* Owner expanded view (Plan 193): the footer actions ARE the photo's actions —
   selection = the expanded photo, so they show armed while expanded. */
[data-gallery-overlay].gallery-expanded-mode .gallery-action { display: flex !important; opacity: 1; }
[data-gallery-overlay].gallery-selecting [data-gallery-upload] { display: none !important; }
.gallery-action-divider { display: none !important; }

.gallery-suggestions {
  background: var(--color-glass-strong);
  border: 1px solid color-mix(in srgb, var(--tint-shade) 10%, transparent);
  box-shadow: 0 8px 24px color-mix(in srgb, var(--tint-shadow) 14%, transparent);
  max-height: 16rem;
  overflow-y: auto;
}
.gallery-suggestion {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  width: 100%;
  padding: 0.5rem 0.75rem;
  background: transparent;
  border: none;
  cursor: pointer;
  text-align: left;
}
.gallery-suggestion.active,
.gallery-suggestion:hover { background: color-mix(in srgb, var(--tint-shade) 5%, transparent); }
.gallery-suggestion-thumb {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 9999px;
  object-fit: cover;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.gallery-suggestion-thumb--empty {
  background: color-mix(in srgb, var(--tint-shade) 6%, transparent);
  color: var(--color-ink-faint);
  font-size: 16px;
}
.gallery-suggestion-name {
  font-family: var(--font-display, inherit);
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--color-ink);
}

.gallery-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.2rem 0.4rem 0.2rem 0.6rem;
  border-radius: 9999px;
  border: none;
  cursor: pointer;
  background: var(--gallery-accent);
  color: #fff;
  font-family: var(--font-display, inherit);
  font-weight: 700;
  font-size: 0.75rem;
  touch-action: manipulation;
}
.gallery-pill-x { font-size: 15px; opacity: 0.85; }

/* ── Map glass circle button (plan 148 — was a recipe pasted 8× across the map
   controller). Borderless by default; the few buttons that kept a subtle border
   re-add it inline (preserving the pre-existing look exactly). ── */
.map-glass-btn {
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--color-glass-strong);
  backdrop-filter: blur(24px) saturate(1.5);
  -webkit-backdrop-filter: blur(24px) saturate(1.5);
  display: flex; align-items: center; justify-content: center;
  color: var(--color-ink);
  border: none; cursor: pointer; touch-action: manipulation;
}

/* ── Nearby-parks list rows (plan 148 Phase B — server-rendered partial) ── */
.park-row { display: flex; flex-direction: column; gap: 3px; }
.park-row--bordered { border-top: 1px solid var(--color-line-soft); padding-top: 8px; margin-top: 8px; }
.park-row--expandable { cursor: pointer; }
.park-row-head { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; }
.park-name { font-size: 13px; font-weight: 700; color: var(--color-ink); line-height: 1.3; min-width: 0; }
.park-dist { font-size: 10px; color: var(--color-ink-faint); white-space: nowrap; flex-shrink: 0; }
.park-badges { display: flex; flex-wrap: wrap; gap: 4px; }
.park-badge { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 9999px; line-height: 1.4; }
.park-badge--off_leash { color: #22c55e; background: rgba(34, 197, 94, 0.12); }
.park-badge--on_leash { color: var(--color-amber-text); background: rgba(245, 158, 11, 0.12); }
.park-badge--mixed { color: var(--color-brand-text); background: rgba(255, 107, 74, 0.12); }
.park-badge--fenced { color: var(--color-ink-soft); background: rgba(107, 107, 128, 0.1); }
.park-badge--water { color: #3b82f6; background: rgba(59, 130, 246, 0.1); }
.park-badge--surface { color: var(--color-ink-faint); background: rgba(107, 107, 128, 0.08); }
.park-hours { font-size: 11px; font-weight: 600; line-height: 1.3; color: var(--color-ink-faint); }
.park-hours--open { color: #22c55e; }
.park-addr-row { display: flex; align-items: center; gap: 6px; }
.park-addr { font-size: 11px; color: var(--color-ink-faint); line-height: 1.3; min-width: 0; flex: 1; }
.park-dir-btn { flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; background: var(--color-brand); display: flex; align-items: center; justify-content: center; border: none; cursor: pointer; }
.park-dir-btn .material-symbols-outlined { font-size: 16px; color: #fff; }
.park-detail { display: none; flex-direction: column; gap: 6px; padding-top: 4px; }
.park-row.is-expanded .park-detail { display: flex; }
.park-notes { font-size: 11px; color: var(--color-ink-soft); line-height: 1.4; }
.park-thumbs { display: flex; gap: 4px; overflow-x: auto; }
/* Plan 393 — pins, stars, groups, the filter chips and the property edit. */
.park-group { font-size: 10px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--color-ink-faint); padding: 8px 0 2px; }
.park-row-head .park-pin { flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: transparent; border: none; cursor: pointer; margin: -6px -6px -6px 0; }
.park-pin .material-symbols-outlined { font-size: 18px; color: var(--color-ink-faint); }
.park-pin.is-on .material-symbols-outlined { color: var(--color-brand-text); }
.park-stars { display: flex; gap: 0; margin: -2px 0 0 -4px; }
.park-star { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; background: transparent; border: none; cursor: pointer; padding: 0; }
.park-star .material-symbols-outlined { font-size: 16px; color: var(--color-ink-faint); }
.park-star.is-on .material-symbols-outlined { color: var(--color-amber-text); }
.parks-chips { display: flex; flex-wrap: wrap; gap: 6px; }
/* Unlayered plain CSS beats every Tailwind utility (ui-conventions): the picker folds its
   chips with the `hidden` class, so the fold has to be said here too. */
.parks-chips.hidden { display: none; }
.parks-chip { font-size: 11px; font-weight: 700; padding: 5px 10px; border-radius: 9999px; border: 1px solid var(--color-line); background: transparent; color: var(--color-ink-soft); cursor: pointer; }
.parks-chip.is-on { background: var(--color-brand); border-color: var(--color-brand); color: var(--color-on-accent); }
[data-parks-popover] .parks-chips { padding: 0 0 8px; position: sticky; top: 0; z-index: 1; background: inherit; }
.park-edit { display: flex; flex-direction: column; gap: 6px; padding-top: 6px; border-top: 1px solid var(--color-line-soft); }
.park-edit-row { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--color-ink-soft); }
.park-edit-select { font-size: 11px; padding: 3px 6px; border-radius: 8px; border: 1px solid var(--color-line); background: transparent; color: var(--color-ink); }
.park-edit-surfaces { display: flex; flex-wrap: wrap; gap: 4px; }
.park-edit-chip { font-size: 10px; color: var(--color-ink-soft); display: inline-flex; align-items: center; gap: 3px; padding: 2px 6px; border-radius: 9999px; border: 1px solid var(--color-line-soft); }
.park-edit-save { align-self: flex-start; font-size: 11px; font-weight: 700; padding: 5px 10px; border-radius: 9999px; border: none; background: var(--color-brand); color: var(--color-on-accent); cursor: pointer; }
.park-thumb { width: 56px; height: 56px; border-radius: 8px; object-fit: cover; flex-shrink: 0; cursor: pointer; }

/* Boarding's standard violet (matches .month-dot-boarding). Defined here rather than in the
   Tailwind @theme because no utility class references it, so v4 would tree-shake it away. */
:root { --color-violet: #8B5CF6; --color-violet-light: #EFE9FE; }

/* Account STATUS line — triage words under the dog's name ("240 unpaid · 3 walks · 2 wks" /
   "paid through Jun 14" / "settled"), replacing the signed balance gauge. OWING deliberately gets
   NO tint: performing services in advance of payment is the routine state of a cash business — a
   warm colour read as an alarm (a deepened amber was tried and pulled the same day). Colour only
   marks the notable states: prepaid mint, settled faint. Words + the aging figure carry urgency. */
.account-status-line { display: block; font-size: 12px; line-height: 1.3; margin-top: 1px; font-weight: 600; color: var(--color-ink-soft); font-variant-numeric: tabular-nums; }
.account-status-line.is-owing   { color: var(--color-ink-soft); } /* routine — plain ink, no alarm */
.account-status-line.is-prepaid { color: var(--color-status-prepaid); } /* deepened mint — measured 5.0:1 on glass */
.account-status-line.is-settled { color: var(--color-status-settled); } /* quietest by design, but stepped up from ink-faint's 2.5:1 */
.account-status-line.is-unstarted { color: var(--color-ink-faint); } /* books not started (Plan 329) — quieter than settled: there is nothing to read yet */

/* Owner Account status headline (Plan 194) — the same tint doctrine as the walker's
   .account-status-line hooks, at the status card's headline size: ink when owing (owed is
   navy/ink, a gauge, never a rose alarm), the measured deepened mint for prepaid AND
   settled (both are good news addressed to the owner). */
.owner-acct-status.is-owing { color: var(--color-ink); }
.owner-acct-status.is-prepaid, .owner-acct-status.is-settled { color: var(--color-status-prepaid); }

/* ── Dog accounts — service strip (Plan 139) ──
   Joined service-bucket squares (oldest left), each filling left→right by how paid it is
   (FIFO → the strip reads as the paid frontier advancing). A small day-of-month sits under
   each square and a scroll-synced month label above, mirroring the dog-index schedule tape.
   The strip shows ~7 cells (a week) at a time and paginates by the week via the chevrons. */
/* wide + short → cells read as longitudinal (landscape) bars, not squares; the height also
   matches the chevrons so the strip + its controls share a proportionate band. --zig is the zigzag
   tooth depth at month seams; --stub-r the radius of the ticket-stub pinch notches.
   IMPORTANT: cells carry NO horizontal margins — every cell is exactly --acct-cell-w wide so the
   strip stays uniform-width. The pagination controller (account_strip_controller.js) maps scroll
   position → cell index assuming a constant cell width and pages by a fixed viewport width; any
   per-cell margin (gap or overlap) desyncs snap/paging. All seam treatments are therefore drawn
   with clip-path + pseudo-element overlays, which don't affect layout. */
.account-strip-wrap { --acct-cell-w: 34px; --acct-cell-h: 38px; --zig: 4px; --stub-r: 3px; }
.account-strip {
  display: flex;
  width: 100%;
  min-width: 0;
  overflow-x: auto;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding: 2px 0 0;
  /* Snap each cell's RIGHT edge to the strip's right wall. This is a future-oriented element — the
     right side (today / the paid-through frontier) is what matters, and it reads right-to-left — so
     the right edge is the anchor (a partially-clipped cell, if any, falls on the less-important
     left). Scrolling to the end therefore lands exactly on the true end with "next" disabled. */
  scroll-snap-type: x mandatory;
}
.account-strip::-webkit-scrollbar { display: none; }

.account-cell {
  position: relative; /* positioning context for ticket-detail pseudo-elements (perf dots, stub
                         notches, dashed frontier) drawn over the cell seams */
  flex: 0 0 auto;
  display: flex; flex-direction: column; align-items: center;
  gap: 2px;
  scroll-snap-align: end; /* right edge → right wall (see .account-strip) */
}

.account-segment {
  position: relative;
  width: var(--acct-cell-w); height: var(--acct-cell-h);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0;
  overflow: hidden; /* clip the fill to the rounded / puzzle shape */
  color: #fff; /* value + icon always white — flat (no outline); high contrast on both the saturated
                  paid fills and the near-black unpaid base */
  /* empty base = the UNPAID portion, rendered as a WINDOW INTO ONE CONTINUOUS SPACE: the StarView
     deep-space nebula (purple/teal/pink/blue clouds) over deep navy, laid out as a single wide
     backdrop (500px tile) and offset per cell by its index (--neb-x = -index*cell-width) so the
     clouds flow continuously across a run of owed cells instead of repeating per cell. An owed cell
     is a porthole onto that shared cosmos; white text stays legible. (Grain rides the coloured fill
     only; this nebula shows through wherever a cell is unpaid.) */
  background-color: rgb(12 20 42);
  background-image:
    /* nebula clouds */
    radial-gradient(ellipse 60% 60% at 14% 24%, rgba(120, 60, 160, 0.42), rgba(80, 40, 130, 0.14) 45%, transparent 78%),
    radial-gradient(ellipse 54% 66% at 62% 80%, rgba(40, 140, 160, 0.36), rgba(30, 100, 140, 0.12) 45%, transparent 78%),
    radial-gradient(ellipse 40% 50% at 84% 30%, rgba(170, 60, 105, 0.30), transparent 72%),
    radial-gradient(ellipse 46% 46% at 36% 78%, rgba(50, 80, 185, 0.34), transparent 72%);
  background-repeat: repeat-x;
  background-size: 500px 100%;
  background-position-x: var(--neb-x, 0px);
  /* Ticket texture: a faint top sheen + bottom shade gives each cell a slight stub relief, so the
     strip reads as a run of individual ticket stubs. Inset so it survives the clip-path shapes. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), inset 0 -1px 0 rgba(0, 0, 0, 0.06);
}
.account-segment-fill {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: var(--paid, 0%);
  background: var(--seg-color, var(--color-ticket-mint));
  z-index: 0;
}
/* Faint paper grain (inline SVG fractal noise, no asset / CSP-safe) over the COLOURED fill only —
   sized to var(--paid) so it never touches the StarView-blue unpaid base. Reads as printed ticket
   stock; sits below the text/details (z-index 0). */
.account-segment-grain {
  position: absolute; left: 0; top: 0; bottom: 0; width: var(--paid, 0%);
  z-index: 0; pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 120px 120px;
  opacity: 0.3;
  mix-blend-mode: overlay;
}
/* A strip too short to overflow sticks to the LEFT (no margin trickery). When it DOES overflow, the
   controller still scrolls to the right end on load (most recent), so long histories open at today. */
.account-strip > .account-cell:first-child .account-segment {
  border-top-left-radius: 8px; border-bottom-left-radius: 8px;
}
.account-strip > .account-cell:last-child .account-segment {
  border-top-right-radius: 8px; border-bottom-right-radius: 8px;
}
/* Credit value + icon stack inside the segment; both readable on filled + empty cells. */
.account-segment-credits {
  /* Printed-ticket / fare type: monospace tabular figures read as a stamped coupon amount. */
  font-family: ui-monospace, "SF Mono", "Cascadia Code", "Fira Code", Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 8px; line-height: 1; font-weight: 700; letter-spacing: -0.02em;
  /* DARK edge, not the old white "relief" shadow: white glyphs measured 2.0:1 on the amber fill and
     2.8:1 on brand (a white shadow blurs the edge further). The dark halo defines them on every
     light fill and disappears on the navy unpaid base. Ghost cells override with the blue stroke. */
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45), 0 0 1px rgba(0, 0, 0, 0.35);
  position: relative; z-index: 1;
}
.account-segment-icon {
  font-size: 15px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45), 0 0 1px rgba(0, 0, 0, 0.35);
  position: relative; z-index: 1;
}

/* ── Ticket-stock detailing ── */
/* Perforated tear-off edges: a faint dashed line just inside every cell's top & bottom; adjacent
   cells' lines join into a continuous perforation running the length of the strip (a ticket roll). */
.account-segment::before {
  content: ""; position: absolute; z-index: 1; pointer-events: none;
  left: 0; right: 0; top: 2px; bottom: 2px;
  border-top: 1px dashed rgba(255, 255, 255, 0.45);
  border-bottom: 1px dashed rgba(255, 255, 255, 0.45);
}
/* Punch hole on REDEEMED tickets — a ticket is punched when it's paid in full (settled), like a
   conductor punching a fare. Owed/partial cells stay un-punched (unredeemed), and future ghost
   cells stay un-punched (not yet used). So punched = a completed, paid service. */
.account-cell:not(.is-ghost) .account-segment.is-settled::after {
  content: ""; position: absolute; z-index: 2; pointer-events: none;
  top: 3px; right: 3px; width: 4px; height: 4px; border-radius: 50%;
  background: var(--color-glass-strong);
  box-shadow: inset 0 0.5px 1px rgba(0, 0, 0, 0.35);
}

/* Ghost cells — prepaid credit projected forward into scheduled services (Plan 140). Future cells
   render as a faded, ethereal version of a real cell — the whole segment (nebula + fill + label) is
   dropped to a low opacity, and the fill colour is muted further toward white — so "upcoming" reads
   unmistakably as a ghost at a glance. Unpaid portion stays StarView blue; how far the mint fill
   reaches across the ghosts IS how far prepaid covers (no separate marker — the colour is the gauge). */
.account-cell.is-ghost .account-segment {
  opacity: 0.85;
}
.account-cell.is-ghost .account-segment-fill {
  background: color-mix(in srgb, var(--seg-color, var(--color-ticket-mint)) 55%, white);
}
/* Ghost glyphs use the SAME white-with-dark-halo treatment as real cells (the base rule above) —
   an electric-blue outline was tried here but was one hue too many on a money surface (the palette
   is brand · navy · mint · inks; colour is a financial claim, not decoration). The dark halo
   defines white glyphs on the pale ghost fill exactly as it does on the amber-era fills. */
/* day-of-month + weekday letter below each cell. */
.account-cell-date {
  font-size: 8px; line-height: 1; font-weight: 500;
  color: var(--color-ink-soft);
}
.account-cell-dow {
  margin-top: -1px;
  font-size: 7px; line-height: 1; font-weight: 600;
  color: var(--color-ink-faint); /* was --color-quiet-label; see the ink ladder note */
}

/* Scroll-synced month label above the strip — matches the tape's data-tape-month
   (8px, font-700, uppercase, ink-faint). */
.account-strip-month {
  height: 10px;
  font-size: 8px; line-height: 10px;
  font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--color-ink-faint); /* was --color-quiet-label; see the ink ladder note */
  text-align: center; /* centred over the strip by its flex-col items-center wrapper */
}

/* Zigzag month boundary: the first cell of each new month clips its left edge into a zigzag, so
   consecutive months meet on a torn/perforated line. month-end no longer carries a tab; the seam is
   drawn entirely by the next month's first cell. NO margin/overlap (cells must stay uniform-width —
   see .account-strip-wrap): because flush cells don't sit behind one another, the clipped zigzag
   valleys reveal the strip's background colour directly, giving the torn-paper gap for free even
   when both months are the same colour. A 1px drop-shadow in that colour keeps the toothed edge
   crisp against the prior cell; the perforation/stub overlays below add the rest of the cream. */
.account-cell.is-month-start .account-segment {
  clip-path: polygon(
    var(--zig) 0%, 100% 0%, 100% 100%, var(--zig) 100%,
    0 87.5%, var(--zig) 75%, 0 62.5%, var(--zig) 50%,
    0 37.5%, var(--zig) 25%, 0 12.5%
  );
  filter: drop-shadow(-1px 0 0 var(--color-glass-strong));
}
/* Month-seam detailing — the torn shape shows the BACKGROUND (cream) through it, so a tear reads as
   torn paper, not a painted band. (The week signal is a separate white line — see .acct-week-line.)
   ::before — a cream band BEHIND the segment at the seam; the zigzag clip lets it show through the
   valleys, filling the gap with the background colour even between same-coloured months.
   ::after — the classic ticket-stub pinch: a half-circle bitten out of the top + bottom edge. */
.account-cell.is-month-start::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  left: 0; top: 0; width: var(--zig); height: var(--acct-cell-h);
  background: var(--color-glass-strong);
}
.account-cell.is-month-start::after {
  content: ""; position: absolute; z-index: 2; pointer-events: none;
  left: calc(-1px - var(--stub-r)); top: 0;
  width: calc(var(--stub-r) * 2); height: var(--acct-cell-h);
  background-image:
    radial-gradient(circle at 50% 0, var(--color-glass-strong) var(--stub-r), transparent calc(var(--stub-r) + 0.5px)),
    radial-gradient(circle at 50% 100%, var(--color-glass-strong) var(--stub-r), transparent calc(var(--stub-r) + 0.5px));
}

/* The future run (Plan 142) — ghosts are plain faded cells with no per-cell dividers; the existing
   white WEEK line (.acct-week-line) is the only separator, so the ghosts cluster into week groups. */

/* Week boundary — a white line on the cell's left edge (weeks start Monday), rendered as a child
   <span> so it can coexist with the month-tear pseudos. White ALWAYS means "week boundary": it's
   drawn at every week boundary, including on a month tear, so a tear that's also a week boundary
   shows the torn shape (month) + this line (week), while an ordinary month tear has no white line. */
.account-cell .acct-week-line {
  content: ""; position: absolute; z-index: 3; pointer-events: none;
  left: 0; top: 2px; width: 0; height: calc(var(--acct-cell-h) - 4px);
  border-left: 1px solid rgba(255, 255, 255, 0.85);
}
/* On a month tear the seam is a cream gap, where a near-white line would vanish — so nudge the week
   line just past the torn edge onto the solid fill, keeping it visible. The torn shape (month) sits
   to its left, the white line (week) just inside: both read. */
.account-cell.is-month-start .acct-week-line { left: calc(var(--zig) + 1px); }

/* The strip's archive door (Plan 298) — a slim cell-height chip leading the windowed
   strip; earlier settled cells live in the history sheet it opens. */
.account-strip-earlier {
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px; min-width: 30px; padding: 4px 5px; border-radius: 8px;
  border: 1px dashed var(--color-line); color: var(--color-ink-faint);
  text-decoration: none; cursor: pointer;
}
.account-strip-earlier-count { font-size: 10px; font-weight: 700; font-family: var(--font-display); }

.account-strip-nav {
  flex: 0 0 auto;
  width: 20px; height: var(--acct-cell-h);
  /* Row is items-start; offset down past the month label (10px) + strip top padding (2px) so the
     chevron's vertical centre lines up with the cell segment's centre, not the segment+date+dow
     stack. */
  margin-top: 12px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 7px;
  color: var(--color-ink-soft);
  background: var(--color-glass-subtle);
}
/* Off-state keeps the chevron's footprint (visibility, not display:none — toggled as `.is-off` by
   the controller) so every card's strip shares the same right edge whether or not it overflows. */
.account-strip-nav.is-off { visibility: hidden; pointer-events: none; }
.account-strip-nav:disabled { opacity: 0.3; pointer-events: none; }
.account-strip-nav:active { transform: scale(0.92); }

/* Money controls on a card (Plan 142): the +◎ account button (a COIN — credits are currency-neutral). Quiet mint
   circular buttons (mint = our money colour, matching Pay / Enter); they open glass-window panels /
   pour the reserve. */
.account-pay-btn, .account-faucet-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 9999px; border: none;
  color: var(--color-mint-text); background: color-mix(in srgb, var(--color-mint) 16%, var(--tint-chip));
  cursor: pointer; padding: 0;
  transition: transform 0.1s ease, background 0.15s ease, color 0.15s ease;
}
/* Standard tactile press: scale-down + brightness darken (matches .tactile/.btn-press). */
.account-pay-btn:active, .account-faucet-btn:active { transform: scale(0.92); filter: brightness(0.85); }
.account-pay-mark { font-size: 19px; line-height: 1; }
/* The HISTORY button beside the money button — the dog's full-page ledger statement. Same quiet
   circle, but INK, not mint: it's navigation, not a money action (colour is a claim about money
   state on these surfaces — see docs/accounts_ledger.md, "Colour rule for money surfaces"). */
.account-history-btn, .account-request-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 9999px; border: none;
  color: var(--color-ink-soft); background: color-mix(in srgb, var(--color-ink) 8%, var(--tint-chip));
  cursor: pointer; padding: 0; text-decoration: none;
  transition: transform 0.1s ease, filter 0.15s ease;
}
.account-history-btn:active, .account-request-btn:active { transform: scale(0.92); filter: brightness(0.9); }
/* Dogs-index info hint (Plan 194 review): pinned to the card's top-right corner,
   out of the row's flex flow so the schedule tape never shifts. Smaller than the
   accounts buttons — it's a hint, not a control. */
.dog-card-info { position: absolute; top: 3px; right: 6px; width: 22px; height: 22px; opacity: 0.7; }
.account-history-mark { font-size: 18px; line-height: 1; }
/* The faucet shares the +$ button's exact look — same brand colour + soft background (inherited from
   the shared rule above), so the two money controls match. Always present; greyed when nothing to pour. */
.account-faucet-btn .material-symbols-outlined { transform: scaleX(-1); } /* flip the faucet to pour the other way */
.account-faucet-btn:disabled { opacity: 0.4; cursor: default; }
.account-faucet-btn:disabled:active { transform: none; }

/* ── Account panel (glass window content for the reserve + service ledgers, Plan 142) ── */
.account-panel { display: flex; flex-direction: column; gap: 10px; min-height: 0; }
/* When the viewport-capped drawer squeezes the panel, ONLY the claims block absorbs it —
   figures, title, toggle and hint are fixed-size (the keypad below never shrinks at all). */
.account-panel > * { flex-shrink: 0; }
.account-panel > .acct-claims { flex-shrink: 1; }
.account-panel-head { text-align: center; position: relative; } /* relative anchors the bonus float */
.account-panel-figure {
  font-family: var(--font-display); font-weight: 800; font-size: 30px;
  font-variant-numeric: tabular-nums; color: var(--color-ink); line-height: 1.1;
  transition: color 0.5s ease; /* eases back from the flow tint when the count-down settles */
}
/* While the figure counts down (deposited credits flowing onto services) it tints the credit blue. */
.account-panel-figure.is-flowing { color: #4A8BFF; transition: none; }
.account-panel-figure-label {
  display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--color-ink-soft);
}
/* Credits + Unpaid: two equal figures side by side, with an "apply →" affordance between them
   that pours all held credits onto the bills (Plan 144 reshaped). */
.account-panel-figures { display: flex; justify-content: center; align-items: flex-start; gap: 1rem; }
.account-panel-col { display: inline-flex; flex-direction: column; align-items: center; gap: 2px; }
.acct-apply-arrow {
  margin-top: 5px; /* center the pill on the 30px figure row, above the labels */
  display: inline-flex; align-items: center; gap: 4px;
  padding: 5px 12px; border: none; border-radius: 9999px;
  background: var(--color-brand-light); color: var(--color-brand-text);
  font-family: var(--font-display); font-weight: 700; font-size: 12px;
  text-transform: uppercase; letter-spacing: 0.04em; cursor: pointer;
  transition: transform 0.1s ease;
}
.acct-apply-arrow:active { transform: scale(0.95); }
.acct-apply-caret { font-size: 15px; line-height: 1; }
/* Per-dog "auto-apply credits" toggle (button_to), under the figures. */
.acct-auto-apply { display: flex; justify-content: center; margin-top: 10px; }
.acct-auto-apply-toggle {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer; border: none; background: none;
  padding: 4px 6px; font-family: var(--font-display); font-weight: 700;
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--color-ink-soft);
}
.acct-auto-apply-box {
  width: 16px; height: 16px; border-radius: 5px; border: 1.5px solid var(--color-line-strong);
  display: inline-flex; align-items: center; justify-content: center; transition: all 0.12s ease;
}
.acct-auto-apply-toggle.is-on { color: var(--color-brand-text); }
.acct-auto-apply-toggle.is-on .acct-auto-apply-box { background: var(--color-brand); border-color: var(--color-brand); }
.acct-auto-apply-toggle.is-on .acct-auto-apply-box::after {
  content: ""; width: 4px; height: 8px; border: solid #fff; border-width: 0 2px 2px 0;
  transform: rotate(45deg) translate(-0.5px, -1px);
}
.account-panel-sub { display: block; font-size: 12px; color: var(--color-ink-soft); margin-top: 2px; }
.account-panel-error { color: var(--color-panel-error); font-size: 12px; text-align: center; margin: 0; }
.acct-ref {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-weight: 800;
  font-size: 24px; letter-spacing: 0.12em; color: var(--color-ink);
}
.acct-copy {
  margin-left: 8px; padding: 2px 10px; border: none; border-radius: 9999px;
  background: var(--color-glass-subtle); color: var(--color-ink-soft);
  font-size: 12px; font-weight: 700; cursor: pointer;
}
.acct-display {
  height: 38px; padding: 0 12px; border-radius: 10px; background: var(--color-glass-subtle);
  display: flex; align-items: center; justify-content: flex-end;
  font-family: var(--font-display); font-weight: 800; font-size: 22px;
  font-variant-numeric: tabular-nums; color: var(--color-ink);
}
/* Calculator pad: 4 columns — digits + ⌫ / + / − in the last column, and a full-width 0. */
.acct-pad { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.acct-key {
  height: 44px; border: none; border-radius: 10px;
  background: var(--color-glass-subtle); color: var(--color-ink);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-weight: 700; font-size: 18px;
  cursor: pointer; transition: transform 0.08s ease;
}
.acct-key:active { transform: scale(0.94); }
.acct-key:disabled { opacity: 0.3; cursor: default; } /* e.g. the − operator on the positive-only Pay panel */
.acct-key:disabled:active { transform: none; }
.acct-key-aux { color: var(--color-ink-soft); }
.acct-key-op { /* + / − operators — tinted so they read as the "calculator" keys */
  background: color-mix(in srgb, var(--color-brand) 14%, var(--tint-chip));
  color: var(--color-brand-text); font-weight: 800; font-size: 22px;
}
.acct-key-zero { grid-column: 1 / -1; } /* wide 0 spanning the bottom row (no Enter key) */
.acct-key-half { grid-column: span 2; } /* 0 sharing the bottom row with the Enter key */
.acct-key-enter { /* the calculator's submit key (reserve panel) — solid mint, the action colour */
  grid-column: span 2; background: var(--color-mint); color: #fff; font-weight: 800; font-size: 16px;
}
.acct-sign-hint { margin: 0; font-size: 11px; color: var(--color-ink-faint); text-align: center; }
.acct-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.acct-act {
  height: 44px; border: none; border-radius: 9999px; color: #fff;
  font-family: var(--font-display); font-weight: 800; font-size: 14px; cursor: pointer;
  transition: transform 0.08s ease, opacity 0.15s ease;
}
.acct-act:active { transform: scale(0.96); }
.acct-act:disabled { opacity: 0.4; pointer-events: none; }
.acct-act-in { background: var(--color-mint); }
.acct-act-transfer { background: var(--color-brand); }
.acct-act-out { background: var(--color-ink-soft); }
/* ── Payment claims (Plan 194) ─────────────────────────────────────────────────────────────
   Owner "I sent ◎N" annotations surfacing walker-side: the roster card's "said sent" badge and
   the drawer panel's pending list / post-deposit match offer. Ink, not mint — a claim is money
   the CLIENT says is moving, not money in the books (the colour rule: tint = financial claim). */
.acct-claim-badge {
  display: inline-flex; align-items: center; gap: 3px; margin-top: 2px;
  padding: 1px 8px; border-radius: 9999px;
  background: var(--color-glass-subtle); color: var(--color-ink-soft);
  font-family: var(--font-display); font-weight: 700; font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.acct-claim-badge .material-symbols-outlined { font-size: 13px; }
.acct-claims {
  margin: 10px 0 2px; padding: 8px 10px; border-radius: 12px;
  background: var(--color-glass-subtle);
  display: flex; flex-direction: column; min-height: 0;
}
.acct-claims > :not(.acct-claims-scroll) { flex-shrink: 0; } /* title + the picker's skip row stay put */
.acct-claims-title {
  margin: 0 0 6px; font-family: var(--font-display); font-weight: 800; font-size: 11px;
  color: var(--color-ink-soft); text-transform: uppercase; letter-spacing: 0.04em;
}
.acct-claim-row { display: flex; align-items: center; gap: 8px; min-height: 28px; }
.acct-claim-row + .acct-claim-row { margin-top: 6px; }
.acct-claim + .acct-claim { margin-top: 2px; }
/* The claim ROWS live in the drawer's one FLEXIBLE region (title + skip stay put): the drawer is
   hard-capped at the viewport and everything else is fixed-size, so this box gets exactly the
   leftover height — many claims scroll internally, few claims shrink it to content (no fixed cap).
   touch-action pan-y + the _wireFling exclusion keep an in-list swipe a SCROLL, never the
   drawer's drag-to-dismiss; overscroll containment stops a finished scroll leaking anywhere. */
.acct-claims-scroll {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  overscroll-behavior: contain;
  touch-action: pan-y; /* overrides the tray's touch-action: none inside this zone */
}
.acct-claims-scroll + .acct-claim-row { margin-top: 6px; } /* the picker's skip row below the box */
.acct-claim-row form.button_to { display: contents; } /* keep button_to's form out of the flex layout */
.acct-claim-meta {
  flex: 1; min-width: 0; font-family: var(--font-display); font-weight: 600; font-size: 12px;
  color: var(--color-ink); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; /* ONE line, always — the actions share it */
}
.acct-claim-match {
  border: none; border-radius: 9999px; padding: 5px 12px; cursor: pointer;
  background: var(--color-mint); color: #fff;
  font-family: var(--font-display); font-weight: 800; font-size: 12px;
}
.acct-claim-match.is-primary { flex: 1; min-width: 0; padding: 9px 12px; text-align: center; }
/* Multi-claim picker (post-deposit offer): each pending claim is ONE full-width tappable row —
   owner · ◎amount · method · date · “note” on the left, a quiet "same amount" tag when the
   claim matches the just-recorded entry (those rows list first: the likeliest match is the
   top tap). White pill = the candidate surface, same vocabulary as .acct-claim-candidate. */
.acct-claim-pick {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px;
  border: none; border-radius: 10px; padding: 8px 10px; cursor: pointer;
  background: var(--color-glass-strong); color: var(--color-ink); text-align: left;
  font-family: var(--font-display); font-weight: 600; font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.acct-claim-pick-meta { flex: 1; min-width: 0; }
.acct-claim-same {
  flex: none; padding: 1px 7px; border-radius: 9999px;
  background: var(--color-mint-light); color: var(--color-mint-text);
  font-weight: 800; font-size: 10px; text-transform: uppercase; letter-spacing: 0.03em;
  white-space: nowrap;
}
.acct-claim-skip, .acct-claim-dismiss {
  border: none; background: none; padding: 5px 8px; cursor: pointer;
  font-family: var(--font-display); font-weight: 700; font-size: 12px;
  color: var(--color-ink-faint); white-space: nowrap;
}
/* LATE claim matching: each compact FYI row carries an inline "match" link — a bare <details>
   holding only the summary (so the drawer's summary-tap fling exclusion still covers it); its
   [open] state reveals the SIBLING .acct-claim-picker below the row via :has(), keeping the row
   itself one line. The picker lists candidate receipts (the dog's newest unmatched incoming
   payment entries, same-amount first) as white pills — the owner method-pill surface. */
.acct-claim-late { flex: none; }
.acct-claim-late summary {
  list-style: none; display: inline-flex; align-items: center; cursor: pointer; padding: 5px 8px;
  font-family: var(--font-display); font-weight: 700; font-size: 12px;
  color: var(--color-brand-text); white-space: nowrap;
}
.acct-claim-late summary::-webkit-details-marker { display: none; }
.acct-claim-late[open] summary { color: var(--color-ink-soft); }
.acct-claim-picker { display: none; }
.acct-claim:has(.acct-claim-late[open]) > .acct-claim-picker { display: block; }
.acct-claim-late-list { display: flex; flex-wrap: wrap; gap: 4px; padding: 4px 0 2px; }
.acct-claim-late-list form.button_to { display: contents; }
.acct-claim-candidate {
  border: none; border-radius: 9999px; padding: 6px 12px; cursor: pointer;
  background: var(--color-glass-strong); color: var(--color-ink);
  font-family: var(--font-display); font-weight: 700; font-size: 12px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.acct-claim-late-empty {
  margin: 0; padding: 4px 0 2px;
  font-family: var(--font-display); font-weight: 600; font-size: 11px;
  color: var(--color-ink-faint);
}
/* Owner side: the "I sent a payment" disclosure + its method pills (owner shell — no keypad). */
.claim-details summary { list-style: none; }
.claim-details summary::-webkit-details-marker { display: none; }
.claim-details[open] .claim-details-caret { transform: rotate(180deg); }
.claim-details-caret { transition: transform 0.15s ease; }
.claim-method-pill { position: relative; display: inline-flex; cursor: pointer; }
.claim-method-pill input { position: absolute; opacity: 0; pointer-events: none; }
.claim-method-pill span {
  display: inline-flex; align-items: center; padding: 6px 12px; border-radius: 9999px;
  background: var(--color-glass-strong); color: var(--color-ink-soft);
  font-family: var(--font-display); font-weight: 700; font-size: 12px;
}
.claim-method-pill input:checked + span { background: var(--color-brand); color: #fff; }
/* The account KEYBOARD (Plan 144) — one persistent bottom tray shared by every money surface, summoned
   like an OS keyboard: slides up from below the viewport, slides back down when the surface closes, and
   can be FLUNG down (velocity gesture — but never dragged around). Above the glass overlay (z-300/301)
   so it stays tappable under an open Account window. */
.acct-keyboard {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 320;
  max-width: 480px; margin: 0 auto;
  /* The app's standard frosted glass (matches .glass-strong) — content scrolling under the drawer
     reads through the blur, like the OS keyboard. */
  background: var(--color-glass-strong);
  backdrop-filter: blur(24px) saturate(1.5);
  -webkit-backdrop-filter: blur(24px) saturate(1.5);
  border: 1px solid var(--color-line-soft); border-bottom: none;
  border-radius: 20px 20px 0 0;
  box-shadow: 0 -10px 36px rgba(12, 20, 42, 0.22);
  padding: 6px 12px calc(12px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; gap: 8px;
  /* HARD viewport cap (Plan 194 addendum): the drawer must NEVER extend above the viewport.
     Bottom-anchored, so 100dvh minus the top safe area (+8px breathing room) is the ceiling;
     inside, the keypad/display/actions are fixed flex items and the claims area is the ONLY
     flexible one (min-height 0 chain down to .acct-claims-scroll) — many claims scroll
     internally, few claims shrink. The translateY slide is transform-based and unaffected. */
  max-height: calc(100dvh - env(safe-area-inset-top, 0px) - 8px);
  transform: translateY(110%);
  transition: transform 0.28s cubic-bezier(0, 0, 0.2, 1); /* keyboard-like slide */
  touch-action: none; /* a vertical swipe on the tray is the fling, never page scroll */
}
.acct-keyboard.is-up { transform: none; }
.acct-keyboard-grip { width: 36px; height: 4px; border-radius: 9999px; background: color-mix(in srgb, var(--tint-shade) 15%, transparent); margin: 2px auto 0; flex-shrink: 0; }
/* The drawer's summary area — the tapped surface's panel (title · balance · hints) above the keys.
   It's the drawer's one SHRINKABLE flex item (the keys/display/actions below are fixed): a flex
   column of its own so the squeeze reaches the injected .account-panel and, through it, the
   claims scroller. */
.acct-keyboard-panel { display: flex; flex-direction: column; min-height: 0; }
.acct-keyboard-panel:not(:empty) { padding: 2px 2px 4px; border-bottom: 1px solid var(--color-line-soft); }
/* The keypad is ALWAYS fully visible — nothing below the summary area may shrink. */
.acct-keyboard > .acct-display, .acct-keyboard > .acct-pad, .acct-keyboard > .acct-actions { flex-shrink: 0; }
.acct-panel-title {
  margin: 0 0 2px; font-family: var(--font-display);
  font-weight: 700; font-size: 13px; color: var(--color-ink-soft);
  display: flex; align-items: center; justify-content: center; gap: 8px; /* thumb + "who · what · when" */
}
/* The date deep-links to that calendar day + service tab — brand-tinted with a small calendar glyph. */
.acct-date-link { color: var(--color-brand-text); text-decoration: none; white-space: nowrap; }
.acct-date-link .material-symbols-outlined { font-size: 13px; vertical-align: -2px; margin-right: 1px; }
/* Tap / dismiss confirmation — the ticket FLASHES TWICE (a white overlay blink on the whole segment).
   The tap flash fills the beat before the drawer rises ("you hit this one"); the dismissal flash is
   the parting "this is what you had open" cue. The overlay is a REAL element (.acct-flash-overlay in
   the cell partial) — both the segment's and the cell's pseudo-elements are already taken (settled
   punch hole, month-tear fragments), which kept hijacking the flash's geometry. */
@keyframes acct-cell-flash { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }
.account-cell.is-acct-flash { z-index: 5; }
.acct-flash-overlay {
  position: absolute; top: 0; left: 0; right: 0; height: var(--acct-cell-h);
  pointer-events: none; z-index: 6;
  background: rgba(255, 255, 255, 0.9); opacity: 0;
}
.account-cell.is-acct-flash .acct-flash-overlay { animation: acct-cell-flash 0.15s ease 2; }
.account-panel-figure-of { font-size: 16px; font-weight: 700; color: var(--color-ink-soft); } /* the "/m" after the big paid figure */
/* Overpay bonus — the excess credited to the account EMANATES FROM THE "paid n/m" FRACTION itself,
   popping out of the number and floating up as it fades, video-game style ("+29"). A real element
   spawned by account_panel_controller, anchored to .account-panel-figure; mint = money. */
@keyframes acct-bonus-float {
  0% { opacity: 0; transform: translate(-50%, -50%) scale(0.7); }
  10% { opacity: 1; transform: translate(calc(-50% + 14px), calc(-50% - 28px)) scale(1.05); }
  70% { opacity: 1; transform: translate(calc(-50% + 54px), calc(-50% - 80px)) scale(1); } /* hold readable while drifting */
  100% { opacity: 0; transform: translate(calc(-50% + 76px), calc(-50% - 110px)) scale(1); }
}
.acct-bonus-float {
  position: absolute; left: 50%; top: 50%; pointer-events: none; z-index: 10;
  font-family: var(--font-display); font-weight: 800; font-size: 18px; color: var(--color-mint-text);
  text-shadow: var(--shadow-bonus-float); white-space: nowrap;
  animation: acct-bonus-float 2.2s ease-out forwards;
}

/* The side note ("· n unpaid") hangs OFF the centred figure (absolute → it doesn't shift the big
   number off-centre). The figure gets position:relative to anchor it. */
.account-panel-figure { position: relative; display: inline-block; }
.account-panel-credits {
  position: absolute; left: calc(100% + 8px); top: 50%; transform: translateY(-50%);
  font-size: 14px; font-weight: 600; color: var(--color-ink-soft); white-space: nowrap;
}
/* The ledger reads as a TABLE: a fixed header + grid rows (amount · detail · type · date), amounts and
   dates right-aligned, faint row rules. The header sits above the scroll area so it stays put. */
.acct-history-head, .acct-row {
  display: grid; grid-template-columns: 52px 1fr 62px 52px; gap: 6px; align-items: start; padding: 5px 4px;
}
.acct-history-head {
  font-size: 9px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700;
  color: var(--color-ink-faint); margin-top: 6px; padding-top: 6px;
  border-top: 1px solid var(--color-line-soft);
}
.acct-history-head .h-amt, .acct-history-head .h-date { text-align: right; }
.acct-history { list-style: none; margin: 0; padding: 0; max-height: 180px; overflow-y: auto; }
.acct-row { font-size: 13px; border-bottom: 1px solid color-mix(in srgb, var(--color-ink) 6%, transparent); }
.acct-row:last-child { border-bottom: none; }
.acct-amt { font-variant-numeric: tabular-nums; font-weight: 800; text-align: right; }
.acct-amt.is-in { color: var(--color-amt-in); }
.acct-amt.is-out { color: var(--color-ink-soft); }
.acct-kind { color: var(--color-ink); text-transform: capitalize; display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.acct-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-source { text-transform: none; color: var(--color-ink-faint); font-size: 11px; font-weight: 400; }
.acct-type { color: var(--color-ink-soft); text-transform: capitalize; font-size: 12px; }
.acct-date { color: var(--color-ink-faint); font-size: 12px; text-align: right; }
.acct-empty { color: var(--color-ink-faint); font-size: 12px; text-align: center; padding: 8px; }
/* The Pay panel's statement ending balance — the rows above reconcile to this `available` total. */
.acct-statement-end {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-top: 6px; padding-top: 8px; border-top: 1px solid var(--color-line-soft);
}
.acct-statement-end span { font-size: 12px; color: var(--color-ink-soft); text-transform: uppercase; letter-spacing: 0.04em; }
.acct-statement-end strong { font-variant-numeric: tabular-nums; font-weight: 800; font-size: 16px; color: var(--color-ink); }
/* The account HISTORY SHEET (Plan 194 addendum) — a card's history button summons the dog's full
   ledger as a bottom sheet with the keyboard drawer's slide (same glass, same curve), instead of
   navigating away. Taller than the drawer (a reading surface, 88dvh) and ABOVE it (z 338–340 vs
   the drawer's 320 — opening one closes the other), with a tap-to-close backdrop the drawer never
   needed. The sheet's BODY is the scroller — the shell's lazy month frames chain inside it — so
   only the grip zone is drag chrome (touch-action:none there alone; the body keeps touch scroll). */
.acct-sheet-backdrop {
  position: fixed; inset: 0; z-index: 338;
  background: rgba(12, 20, 42, 0.35);
  opacity: 0; pointer-events: none;
  transition: opacity 0.28s ease;
}
.acct-sheet-backdrop.is-on { opacity: 1; pointer-events: auto; }
.acct-history-sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 340;
  max-width: 480px; margin: 0 auto;
  height: 88dvh;
  display: flex; flex-direction: column;
  background: var(--color-glass-strong);
  backdrop-filter: blur(24px) saturate(1.5);
  -webkit-backdrop-filter: blur(24px) saturate(1.5);
  border: 1px solid var(--color-line-soft); border-bottom: none;
  border-radius: 20px 20px 0 0;
  box-shadow: 0 -10px 36px rgba(12, 20, 42, 0.22);
  transform: translateY(110%);
  transition: transform 0.28s cubic-bezier(0, 0, 0.2, 1); /* the drawer's keyboard-like slide */
}
.acct-history-sheet.is-up { transform: none; }
.acct-sheet-grip { padding: 8px 0 6px; flex-shrink: 0; touch-action: none; cursor: grab; }
.acct-sheet-body {
  flex: 1; min-height: 0;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain; /* the sheet's scroll never chains to the locked page */
  padding: 0 16px calc(16px + env(safe-area-inset-bottom));
}

/* Rendered Terms of Service (kramdown → HTML) — readable defaults. */
.terms-prose h1 { display: none; } /* the page already shows the title */
.terms-prose h2 { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--color-ink); margin: 1.1rem 0 0.4rem; }
.terms-prose h3 { font-weight: 600; font-size: 14px; color: var(--color-ink); margin: 0.8rem 0 0.3rem; }
.terms-prose p, .terms-prose li { font-size: 13px; line-height: 1.55; color: var(--color-ink-soft); margin-bottom: 0.5rem; }
.terms-prose ul { list-style: disc; padding-left: 1.2rem; }
.terms-prose strong { color: var(--color-ink); }
.terms-prose blockquote { border-left: 3px solid var(--color-brand); padding-left: 0.75rem; margin: 0.75rem 0; color: var(--color-ink-faint); }
.terms-prose hr { border: none; border-top: 1px solid color-mix(in srgb, var(--tint-shade) 8%, transparent); margin: 1rem 0; }

/* Plan 199 — "Includes walks" toggles in the Account tab: read-mode shows state,
   edit mode (unlocked) makes the checkbox tappable — the live-input gating pattern. */
.acct-include-walks input[type="checkbox"] { pointer-events: none; }
.modal-panel.unlocked .acct-include-walks input[type="checkbox"] { pointer-events: auto; cursor: pointer; }

/* ── First-login avatar callout (Plan 219) ──────────────────────────────────
   A looping pulse ring on the avatar button while the callout bubble is up.
   Box-shadow alpha fade, NEVER element opacity (looping-pulse rule:
   ui-conventions § CSS physics — opacity loops flash at the loop boundary). */
@keyframes avatar-onboarding-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-brand) 45%, transparent); }
  50%      { box-shadow: 0 0 0 10px color-mix(in srgb, var(--color-brand) 0%, transparent); }
}
.avatar-onboarding-pulse {
  border-radius: 9999px;
  animation: avatar-onboarding-pulse 1.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .avatar-onboarding-pulse { animation: none; box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-brand) 35%, transparent); }
}

/* ── Dictation mic (Plan 232) — the new-dog notes box's record state.
   Box-shadow alpha pulse with equal loop endpoints (ui-conventions § CSS
   physics — opacity loops flash at the loop boundary). NOTE: while recording,
   dictation_controller's live input-level meter normally REPLACES this pulse
   (inline animation:none + a level-driven box-shadow ring); this animation is
   the fallback when AudioContext isn't available. */
.dictation-recording {
  color: #e11d48; /* rose-600 — recording is unmistakably live */
  border-radius: 9999px;
  animation: dictation-pulse 1.6s ease-in-out infinite;
}
@keyframes dictation-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(225, 29, 72, 0.35); }
  50%      { box-shadow: 0 0 0 8px rgba(225, 29, 72, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .dictation-recording { animation: none; box-shadow: 0 0 0 4px rgba(225, 29, 72, 0.3); }
}


/* ── The cosmic brand surface ────────────────────────────────────────────────
   Moved out of layouts/marketing's inline <style> (Plan 257) so the AUTH pages
   can share it. The split it encodes: BRAND surfaces — landing, sign-in, print —
   are the night sky; the WORKING app stays light and warm. A walker arriving from
   a dark cosmic landing page used to hit a cream sign-in screen, which read as two
   different products. Unlayered on purpose: these paint a page background and must
   not lose to a Tailwind utility. */
/* Cosmic brand surface (Plan 248) — the poster night sky, as a page. A pure-CSS star
   field (layered radial-gradients, no JS) + two soft brand nebulae; glass cards ride on
   top. style-src allows inline, so no nonce needed. */
.cosmos {
  background:
    radial-gradient(1200px 700px at 78% -8%, rgba(255,107,74,0.18), transparent 60%),
    radial-gradient(900px 600px at 10% 12%, rgba(52,199,160,0.12), transparent 60%),
    linear-gradient(180deg, #12121F 0%, #171528 45%, #1A1A2E 100%);
}
.cosmos-plain {
  /* The same night, without the two brand nebulae. The landing page needs the haze — it is
     doing the selling, and the nebulae carry the colour. Sign-in is a doorway: the haze sits
     behind a form and just makes the field edges harder to read. Starfield stays. */
  background: linear-gradient(180deg, #12121F 0%, #171528 45%, #1A1A2E 100%);
}
.starfield {
  position: fixed; inset: 0; pointer-events: none; z-index: 0;
  background-image:
    radial-gradient(1.4px 1.4px at 20% 30%, #ffffff, transparent),
    radial-gradient(1.2px 1.2px at 65% 18%, rgba(255,255,255,0.9), transparent),
    radial-gradient(1.6px 1.6px at 40% 62%, #ffffff, transparent),
    radial-gradient(1.1px 1.1px at 85% 48%, rgba(255,255,255,0.85), transparent),
    radial-gradient(1.3px 1.3px at 12% 78%, rgba(255,255,255,0.9), transparent),
    radial-gradient(1.5px 1.5px at 72% 82%, #ffffff, transparent),
    radial-gradient(1.1px 1.1px at 52% 8%, rgba(255,255,255,0.8), transparent),
    radial-gradient(1.2px 1.2px at 92% 72%, rgba(255,255,255,0.85), transparent);
  background-repeat: repeat;
  background-size: 480px 480px;
  opacity: 0.7;
}
/* The starfield scoped inside a positioned container (the pre-walk map backdrop,
   Plan 299) instead of fixed to the viewport — the walk screen's wrapper animates
   a transform (animate-rise), which would trap `fixed` inside it anyway. */
.starfield.starfield-abs { position: absolute; }

/* The dark-ground glass card. Also lifted out of layouts/marketing (Plan 257) so the auth
   pages use the SAME card as the landing page rather than a look-alike. `.glass-strong`
   is its light-ground counterpart and stays for the working app. */
.glass-card {
  background: linear-gradient(180deg, rgba(255,255,255,0.085), rgba(255,255,255,0.035));
  border: 1px solid rgba(255,255,255,0.12);
  border-top-color: rgba(255,255,255,0.22);   /* catch-light on the top edge */
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.07), 0 22px 44px -26px rgba(0,0,0,0.75);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}



/* ── Appearance picker (Plan 260) ── */
.theme-choice {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: 12px 4px; border-radius: 14px;
  background: var(--color-canvas-warm);
  border: 1px solid transparent;
  color: var(--color-ink-faint);
  cursor: pointer; text-align: center;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.theme-choice.is-active {
  background: var(--color-brand-light);
  border-color: var(--color-brand);
  color: var(--color-brand-text);
}
/* "Fix up my bio" (founder, 2026-08-05) — the OPTIONAL rewrite, shown only once there is
   something to rewrite. Quiet by default: it is an offer, not the way to write a bio. */
.bio-polish-button {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 4px 10px; border-radius: 999px;
  border: 1px solid var(--color-line-strong); background: transparent;
  color: var(--color-brand-text);
  font-family: var(--font-display); font-weight: 700; font-size: 12px;
  cursor: pointer; white-space: nowrap;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.bio-polish-button[hidden] { display: none; }
.bio-polish-button:disabled { opacity: 0.6; cursor: default; }
.bio-polish-button.is-working .material-symbols-outlined { animation: spin 1s linear infinite; }

/* Same-every-day ⇄ Different, per pack (founder, 2026-08-05). Two submit buttons rather
   than a switch: each one names the mode it selects, so the control reads the same before
   and after tapping — a bare switch would leave "which side is on?" to the walker. */
.walk-mode-toggle {
  display: inline-flex; gap: 2px; padding: 2px; border-radius: 999px;
  background: var(--color-canvas-warm);
}
.walk-mode-choice {
  padding: 4px 10px; border-radius: 999px; border: 0;
  background: transparent; color: var(--color-ink-faint);
  font-family: var(--font-display); font-weight: 700; font-size: 11px;
  cursor: pointer; white-space: nowrap;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  transition: color 0.15s ease, background 0.15s ease;
}
.walk-mode-choice.is-active {
  background: var(--color-brand-light);
  color: var(--color-brand-text);
}
.walk-mode-choice:focus-visible { outline: 2px solid var(--color-brand); outline-offset: 2px; }

/* Keyboard reachability: the radio itself is sr-only, so the ring has to be drawn
   on the label it lives inside. */
.theme-choice:has(input:focus-visible) {
  outline: 2px solid var(--color-brand); outline-offset: 2px;
}


/* The watch's LIGHT counterpart (founder, 2026-08-04): the panel lifts to the page's
   own ground and the chrome ink darkens; the cosmic FACE and both hands are unchanged
   (a watch face doesn't invert — and the sleeping-dog art is the point). Expressed as
   light overrides so the base rule stays the night sky; both theme signals covered,
   the same discipline as the two dark blocks. */
/* `theme: system` renders NO attribute, so a system walker on a light OS must get the
   light watch too — the page around it already did. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .orbit-dial-panel {
    --dial-ground: var(--color-canvas);
    --dial-ink: var(--color-ink);
    --dial-ink-soft: var(--color-ink-soft);
    --dial-edge: var(--color-line-strong);
    --dial-lift: var(--shadow-depth-lg);
  }
}
:root[data-theme="light"] .orbit-dial-panel {
  --dial-ground: var(--color-canvas);
  --dial-ink: var(--color-ink);
  --dial-ink-soft: var(--color-ink-soft);
  --dial-edge: var(--color-line-strong);
  --dial-lift: var(--shadow-depth-lg);
}
/* ── Settings landmarks (Plan 308) ───────────────────────────────────────────
   Grouped scroll instead of tabs (the settings-IA panel, 2026-08-04): thin group
   labels, a sticky jump-chip row, and per-pack disclosures. The chips scroll via
   settings_nav_controller (scrollIntoView — body is the scroller, anchors no-op);
   scroll-margin-top clears the sticky rows when they land. */
.settings-group-label {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-ink-faint);
  /* NO negative bottom margin (founder screenshot, 2026-08-04): -0.5rem pulled the
     next card up over the label and ate half of it. The cards' own space-y supplies
     the rhythm; the label needs real air above it (founder, 2026-08-16 — group
     boundaries should read at a glance) and a hair below. */
  margin: 1.5rem 0.25rem 0.25rem;
}
.settings-chips {
  position: sticky;
  top: calc(3.5rem + env(safe-area-inset-top));
  z-index: 30;
  display: flex;
  flex-wrap: wrap; /* ALL of them visible (founder, 2026-08-04): a scrolling row hid
                      "App" off the edge, so the walker had to guess what was there.
                      Two short rows beat one row with a secret. */
  gap: 6px;
  padding: 0.5rem 0.25rem;
  margin: 0 -0.25rem 0.5rem;
  background: var(--color-canvas);
}
.settings-chip {
  flex: none;
  height: 30px;
  padding: 0 12px;
  border-radius: 999px;
  background: var(--color-canvas-warm);
  color: var(--color-ink-soft);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 12px;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  cursor: pointer;
}
/* A fallback only — the jump measures the sticky chip row live (it wraps, so its
   height isn't a constant). This covers any native scroll that isn't the chips. */
/* The Settings Save bar's FLOATING state (Plan 351) — applied by
   settings_save#connect, never by the markup, so the position cannot depend on
   how the page was parsed. env() keeps it above the bottom nav on notched
   phones; z sits under the modal/StarView layers like the loading spinner. */
.settings-save-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(4.5rem + env(safe-area-inset-bottom));
  z-index: 40;
  padding-left: 1rem;
  padding-right: 1rem;
}

.settings-section {
  scroll-margin-top: calc(3.5rem + env(safe-area-inset-top) + 3rem);
}
/* Pack disclosures — native <details>; fields stay mounted (the one-form save). */
.settings-pack {
  border: 1px solid var(--color-line-soft);
  border-radius: 12px;
  padding: 8px 10px;
}
.settings-pack-summary {
  display: flex;
  align-items: center;
  gap: 8px;
  list-style: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.settings-pack-summary::-webkit-details-marker { display: none; }
.settings-pack[open] .settings-pack-chevron { transform: rotate(180deg); }
.settings-pack-chevron { transition: transform 0.15s ease; }

/* ── Time-select stepper (2026-08-04) ────────────────────────────────────────
   The ▼/▲ nudge chevrons around every quarter-hour time select (TimeSelectHelper
   owns the shape rationale). The wrapper stays inline so the select keeps the
   layout slot the old <input type="time"> had; the chevrons are deliberately
   quiet — the select itself is the primary control. */
.time-stepper {
  position: relative;
  display: inline-flex; align-items: center;
  /* Takes over the flex-1 slot the bare select used to occupy in the
     start–end rows; inert where the parent isn't a flex row. */
  flex: 1 1 auto; min-width: 0;
}
/* (The ▼/▲ chevrons that once overlaid these fields died with the watch —
   founder, 2026-08-04.) */
.time-stepper > select {
  flex: 1; min-width: 0; width: 100%;
  appearance: none; -webkit-appearance: none;
}
/* The hidden form truth behind the chip (Plan 306): focusable for AT/keyboard and
   drivable by specs, invisible to everyone else. Chip shows the focus ring for it. */
.time-select-native {
  position: absolute;
  width: 1px; height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}
.time-chip {
  flex: 1; min-width: 0; width: 100%;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.time-stepper:has(.time-select-native:focus-visible) .time-chip {
  outline: 2px solid var(--color-brand); outline-offset: 1px;
}

/* ── The Orbit Dial (Plan 306, watch v2) ─────────────────────────────────────
   The celestial time picker as a WATCH: hour + minute hands over the sleeping-dog
   nebula face (founder's pick E — a faint curled dog hidden in the gas,
   bin/generate_nebula's family, committed asset, no runtime network). Both hands
   drag; taps land; quarter ticks mark the minute track; Set commits. Deliberately
   a DARK surface in both themes — night sky, not chrome. Native <dialog> (top
   layer) so it stacks above the add-dog dialog without z-index games. */
.orbit-dial {
  position: fixed;
  inset: 0;
  margin: auto; /* dead-centre on every platform — the UA default drifted on iOS */
  width: fit-content;
  height: fit-content;
  border: none;
  background: transparent;
  padding: 0;
  max-width: none;
}
/* ONE modal at a time (founder, 2026-08-04 — superseding the same-day transparent-
   window experiment): the time fields already live in modals, and two stacked
   floating panels read wrong. The watch presents exactly like the modal it came
   from (the .new-dog-dialog scrim), and the PARENT modal conceals while the watch
   is up (orbit-dial-conceals-parent, applied by the controller), returning on
   close — so the page context shows behind the watch, never a modal sandwich. */
.orbit-dial::backdrop {
  background: var(--color-scrim-deep);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.orbit-dial-conceals-parent {
  visibility: hidden;
}
.orbit-dial-panel {
  /* The watch is a WINDOW over a themed page, so it themes too (founder,
     2026-08-04). The face art and the hands are constant — a watch face does not
     invert — so the light counterpart lifts the PANEL around them and darkens the
     chrome ink; the light overrides live in their own block below. */
  --dial-r: 108px; /* numeral orbit radius; the face is r + 24px */
  --dial-ground: #101422; /* the poster night sky */
  --dial-ink: #fff;
  --dial-ink-soft: rgba(255, 255, 255, 0.6);
  --dial-edge: rgba(255, 255, 255, 0.16);
  --dial-lift: 0 18px 60px rgba(0, 0, 0, 0.65), 0 2px 12px rgba(0, 0, 0, 0.4);
  position: relative;
  overflow: hidden;
  width: min(92vw, 316px);
  padding: 18px 16px 16px;
  border-radius: 24px;
  background: var(--dial-ground);
  border: 1px solid var(--dial-edge);
  box-shadow: var(--dial-lift);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.orbit-dial-x {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 1;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--dial-ink-soft);
  -webkit-tap-highlight-color: transparent;
  cursor: pointer;
}
.orbit-dial-x .material-symbols-outlined { font-size: 20px; }
.orbit-dial-core,
.orbit-dial-core-input {
  position: relative;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 22px;
  color: var(--dial-ink);
  letter-spacing: 0.02em;
}
/* The readout is also the fast path in (founder, 2026-08-04): tap it and type. */
.orbit-dial-core { cursor: text; -webkit-tap-highlight-color: transparent; }
.orbit-dial-core-input {
  width: 5.5ch;
  text-align: center;
  background: color-mix(in srgb, var(--tint-shade) 12%, transparent);
  border-radius: 10px;
  outline: none;
}
.orbit-dial-core-input:focus { box-shadow: 0 0 0 2px var(--color-brand); }
.orbit-watch-face {
  position: relative;
  width: calc(var(--dial-r) * 2 + 48px);
  height: calc(var(--dial-r) * 2 + 48px);
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.14);
  overflow: hidden;
  flex: none;
  background-image: url("/assets/orbit_dial_face-3f7086bd.webp");
  background-size: cover;
  background-position: center;
  box-shadow: inset 0 0 34px rgba(0, 0, 0, 0.65), 0 0 22px rgba(90, 120, 255, 0.10);
  touch-action: none; /* the hands are a drag surface */
  cursor: pointer;
}
/* The minute track: a tick every quarter-hour angle, hour positions stronger. */
.orbit-watch-tick {
  position: absolute;
  left: calc(50% - 0.5px);
  top: 6px;
  width: 1px;
  height: 7px;
  transform-origin: 0.5px calc(var(--dial-r) + 18px);
  background: rgba(255, 255, 255, 0.28);
  pointer-events: none;
}
.orbit-watch-tick-hour {
  left: calc(50% - 1px);
  width: 2px;
  height: 11px;
  transform-origin: 1px calc(var(--dial-r) + 18px);
  background: rgba(255, 255, 255, 0.5);
}
.orbit-watch-numeral {
  position: absolute;
  left: calc(50% - 15px);
  top: calc(50% - 15px);
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 15px;
  color: rgba(255, 255, 255, 0.8);
  -webkit-tap-highlight-color: transparent;
  cursor: pointer;
}
.orbit-watch-numeral.is-selected {
  color: #fff;
  text-shadow: 0 0 12px color-mix(in srgb, var(--color-brand) 90%, transparent);
}
/* The hands. The hour hand rides the minutes like a real watch (renderer);
   the minute hand carries the brand coral so the two never read as twins. */
.orbit-watch-hand {
  position: absolute;
  bottom: 50%;
  transform-origin: bottom center;
  border-radius: 3px;
  pointer-events: none;
}
.orbit-watch-hand-hour {
  left: calc(50% - 2px);
  width: 4px;
  height: calc(var(--dial-r) - 42px);
  background: linear-gradient(to top, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.65));
  box-shadow: 0 0 8px rgba(255, 255, 255, 0.35);
}
.orbit-watch-hand-minute {
  left: calc(50% - 1.25px);
  width: 2.5px;
  height: calc(var(--dial-r) - 10px);
  background: linear-gradient(to top, color-mix(in srgb, var(--color-brand) 95%, transparent), color-mix(in srgb, var(--color-brand) 55%, #fff));
  box-shadow: 0 0 10px color-mix(in srgb, var(--color-brand) 50%, transparent);
}
.orbit-watch-cap {
  position: absolute;
  left: calc(50% - 5px);
  top: calc(50% - 5px);
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
  pointer-events: none;
}
.orbit-dial-ampm {
  position: relative;
  display: flex;
  gap: 8px;
}
.orbit-dial-ampm button {
  width: 44px;
  height: 32px;
  border-radius: 999px;
  color: var(--dial-ink-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  cursor: pointer;
}
.orbit-dial-ampm button .material-symbols-outlined { font-size: 18px; }
.orbit-dial-ampm button.is-selected {
  background: color-mix(in srgb, var(--tint-shade) 14%, transparent);
  color: var(--dial-ink);
}
.orbit-dial-done {
  position: relative;
  width: 100%;
  height: 40px;
  border-radius: 12px;
  background: var(--color-brand);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 14px;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  cursor: pointer;
}

/* ── THE DARK THEME (Plan 260) ────────────────────────────────────────────────
   Everything in the working app addresses colour through a token, so the theme is
   a re-pointing of those tokens rather than a second stylesheet — there is not one
   duplicated RULE below, only duplicated declarations. Unlayered on purpose:
   Tailwind emits its theme inside `@layer theme`, and an unlayered rule beats any
   layered one regardless of specificity (ui-conventions, "CSS & browser physics").
   That is also why nothing here needs `!important`.

   WHAT IS DELIBERATELY NOT HERE: StarView, the landing page, the auth pages and the
   print surfaces. Those are the night sky in BOTH themes — the Dog Star's corona,
   nebulae and planets assume a black ground and have no light-mode design. In dark
   mode the seam Plan 257 drew between "brand surface" and "working app" simply
   stops being visible, which is the whole point.

   THE TWO BLOCKS BELOW MUST STAY IDENTICAL. One serves an explicit choice; the other
   serves `theme: system` via the OS setting, where `:not([data-theme="light"])` is
   what lets an explicit LIGHT choice beat a dark phone. CSS cannot spell "this
   selector OR that media query" as one rule, so `bin/check-css` compares them and
   fails if they drift. Edit both, or edit neither.

   Note the media-query half MUST stay inside the query: unqualified,
   `:root:not([data-theme="light"])` also matches the no-attribute default, which is
   LIGHT — it would put dark elevation and dark-tuned text on a cream page. */

:root[data-theme="dark"] {
  color-scheme: dark;

  /* Ink inverts. `body { color: var(--color-ink) }` is unlayered, so every heading
     that owns no colour of its own follows this one line — the same cascade fact
     that made Plan 248's headlines invisible now does the work for us. */
  --color-ink:            #F2F0F7;
  --color-ink-soft:       #B0AEC4;
  --color-ink-faint:      #918EA8;

  /* The night ramp, straight off the landing page: #12121F → #171528 → #1A1A2E.
     `--color-ink` was ALREADY the bottom of that ramp, which is why the working app
     and the brand surfaces were never really two palettes. */
  --color-canvas:         #12121F;
  --color-canvas-warm:    #0D0D18;   /* recessed — DARKER here, lighter on cream */
  --color-canvas-cool:    #171528;

  --color-glass:          #1E1C30;
  --color-glass-strong:   #26243C;
  --color-glass-subtle:   #191727;

  /* Lines go the other way from fills: lighter than what they sit on — and by MORE
     than the naive inversion suggests. Contrast RATIO lies here: measured against
     `--color-glass-strong`, the first pass scored a higher ratio than the light theme
     while reading as fainter, because a ratio is a luminance quotient and near-black
     differences compress under it. The perceptually uniform measure (CIELAB L*) told
     the true story: the card divider had half the light theme's separation (3.6 vs
     7.1) and `line-soft` — the edge on every .glass card — had 0.27, i.e. none.
     These are solved to match each light counterpart's dL* on the ground it actually
     sits on. Re-derive them the same way if the glass tokens ever move. */
  --color-line-soft:      #312F46;   /* dL* 5.1 — light: 4.9 */
  --color-line:           #36334C;   /* dL* 7.1 — light: 7.1 */
  --color-line-strong:    #726E97;   /* dL* 32.7 — light: 32.7 */

  /* Brand holds. The coral CTA was chosen against the night sky in the first place,
     so the landing page and the app now agree by construction. `brand-deep` inverts
     direction — it is the DARKER press state on cream and must be LIGHTER than brand
     on ink, or "pressed" reads as "disabled". */
  --color-brand-light:    #2A1620;
  --color-brand-soft:     #4A2418;
  --color-brand-deep:     #FF8566;

  /* Accents brighten; their -light tints become deep washes to sit under text. */
  --color-mint:           #4FDCB4;
  --color-mint-light:     #10291F;
  --color-amber:          #F7B547;
  --color-amber-light:    #2A1F0C;
  --color-solar:          #FFD65C;   /* day care — bright here, where the ground is ink */
  --color-rose:           #FF6B85;
  --color-rose-light:     #2C121A;

  /* On ink the ordinary accent IS the readable one — the -text variants collapse onto it,
     so a `text-mint-text` call site needs no per-theme branching. */
  --color-brand-text:     #FF6B4A;
  --color-mint-text:      #4FDCB4;
  --color-amber-text:     #F7B547;
  --color-solar-text:     #FFD65C;
  --color-rose-text:      #FF6B85;
  --color-indigo-text:    #818CF8;
  --color-violet:         #A78BFA;
  --color-violet-light:   #1E1733;

  /* The notice band inverts like the other *-light tints: a deep wash carrying light
     text, rather than a pale band carrying dark text. */
  --color-notice:         #3A2D10;
  --color-notice-line:    #6B5416;
  --color-notice-ink:     #F7C566;

  /* "Darken this surface" becomes "lighten this surface". Shadows do NOT flip —
     they deepen, because a shadow needs more alpha to read on ink than on cream. */
  --tint-shade:           #FFFFFF;
  --tint-shadow:          #000000;
  --tint-chip:            #1E1C30;

  --color-inset:          rgba(255, 255, 255, 0.06);
  --color-inset-strong:   rgba(38, 36, 60, 0.96);
  --color-inset-bar:      rgba(18, 18, 31, 0.86);
  --color-inset-handle:   rgba(38, 36, 60, 0.90);
  --color-scrim:          rgba(0, 0, 0, 0.55);
  --color-scrim-deep:     rgba(0, 0, 0, 0.70);
  --color-overlay-canvas: rgba(18, 18, 31, 0.98);
  --color-canvas-veil:    rgba(18, 18, 31, 0.75);

  /* Elevation is not a re-colour. Shadow does not read on a dark ground — the card
     and its shadow are the same colour — so dark signals depth the way .glass-card
     already does: a catch-light on the top edge, a faint ring, and a shadow deep
     enough to matter. */
  --shadow-depth-sm: inset 0 1px 0 rgba(255,255,255,0.05), 0 2px 8px rgba(0,0,0,0.45),
                     0 0 0 1px rgba(255,255,255,0.04);
  --shadow-depth-md: inset 0 1px 0 rgba(255,255,255,0.06), 0 4px 20px rgba(0,0,0,0.55),
                     0 0 0 1px rgba(255,255,255,0.05);
  --shadow-depth-lg: inset 0 1px 0 rgba(255,255,255,0.07), 0 8px 40px rgba(0,0,0,0.65),
                     0 0 0 1px rgba(255,255,255,0.06);

  /* Restated, not derived — the light values were measured against cream. Ratios
     below are against --color-glass (#1E1C30). */
  --color-status-prepaid: #5FE0BC;
  --color-status-settled: #8E8BA6;
  --color-panel-error:    #FF8095;
  --color-amt-in:         color-mix(in srgb, var(--color-mint) 70%, #FFF);
  --shadow-bonus-float:   0 1px 0 rgba(0, 0, 0, 0.6);
}


/* ── Mapbox popups follow the app's theme (2026-08-07) ─────────────────────────────────────────
   Mapbox hard-codes the popup bubble to WHITE while its text simply inherits — so on the dark
   theme an incident tooltip rendered near-white ink on white and was completely unreadable
   (founder, on Luna's walk map). It has been that way since incidents gained map markers; nothing
   caught it because the light theme happens to look fine.

   Specificity is deliberate, not decorative: `mapbox-gl.css` is linked AFTER this file, so an
   equal-specificity override loses on source order. `.mapboxgl-popup .mapboxgl-popup-content`
   (0,2,0) beats their (0,1,0), and the tip rules add the popup's own class to beat their (0,2,0).
   Sizing and radius are left to Mapbox — only colour is ours. */
.mapboxgl-popup .mapboxgl-popup-content {
  background: var(--color-canvas);
  color: var(--color-ink);
  border-radius: 12px;
  box-shadow: var(--shadow-depth-md);
}

.mapboxgl-popup.mapboxgl-popup-anchor-top .mapboxgl-popup-tip,
.mapboxgl-popup.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip,
.mapboxgl-popup.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip {
  border-bottom-color: var(--color-canvas);
}
.mapboxgl-popup.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip,
.mapboxgl-popup.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip,
.mapboxgl-popup.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip {
  border-top-color: var(--color-canvas);
}
.mapboxgl-popup.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
  border-right-color: var(--color-canvas);
}
.mapboxgl-popup.mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
  border-left-color: var(--color-canvas);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    /* Ink inverts. `body { color: var(--color-ink) }` is unlayered, so every heading
       that owns no colour of its own follows this one line — the same cascade fact
       that made Plan 248's headlines invisible now does the work for us. */
    --color-ink:            #F2F0F7;
    --color-ink-soft:       #B0AEC4;
    --color-ink-faint:      #918EA8;

    /* The night ramp, straight off the landing page: #12121F → #171528 → #1A1A2E.
       `--color-ink` was ALREADY the bottom of that ramp, which is why the working app
       and the brand surfaces were never really two palettes. */
    --color-canvas:         #12121F;
    --color-canvas-warm:    #0D0D18;   /* recessed — DARKER here, lighter on cream */
    --color-canvas-cool:    #171528;

    --color-glass:          #1E1C30;
    --color-glass-strong:   #26243C;
    --color-glass-subtle:   #191727;

    /* Lines go the other way from fills: lighter than what they sit on — and by MORE
       than the naive inversion suggests. Contrast RATIO lies here: measured against
       `--color-glass-strong`, the first pass scored a higher ratio than the light theme
       while reading as fainter, because a ratio is a luminance quotient and near-black
       differences compress under it. The perceptually uniform measure (CIELAB L*) told
       the true story: the card divider had half the light theme's separation (3.6 vs
       7.1) and `line-soft` — the edge on every .glass card — had 0.27, i.e. none.
       These are solved to match each light counterpart's dL* on the ground it actually
       sits on. Re-derive them the same way if the glass tokens ever move. */
    --color-line-soft:      #312F46;   /* dL* 5.1 — light: 4.9 */
    --color-line:           #36334C;   /* dL* 7.1 — light: 7.1 */
    --color-line-strong:    #726E97;   /* dL* 32.7 — light: 32.7 */

    /* Brand holds. The coral CTA was chosen against the night sky in the first place,
       so the landing page and the app now agree by construction. `brand-deep` inverts
       direction — it is the DARKER press state on cream and must be LIGHTER than brand
       on ink, or "pressed" reads as "disabled". */
    --color-brand-light:    #2A1620;
    --color-brand-soft:     #4A2418;
    --color-brand-deep:     #FF8566;

    /* Accents brighten; their -light tints become deep washes to sit under text. */
    --color-mint:           #4FDCB4;
    --color-mint-light:     #10291F;
    --color-amber:          #F7B547;
    --color-amber-light:    #2A1F0C;
    --color-solar:          #FFD65C;   /* day care — bright here, where the ground is ink */
    --color-rose:           #FF6B85;
    --color-rose-light:     #2C121A;

    /* On ink the ordinary accent IS the readable one — the -text variants collapse onto it,
       so a `text-mint-text` call site needs no per-theme branching. */
    --color-brand-text:     #FF6B4A;
    --color-mint-text:      #4FDCB4;
    --color-amber-text:     #F7B547;
    --color-solar-text:     #FFD65C;
    --color-rose-text:      #FF6B85;
    --color-indigo-text:    #818CF8;
    --color-violet:         #A78BFA;
    --color-violet-light:   #1E1733;

    /* The notice band inverts like the other *-light tints: a deep wash carrying light
       text, rather than a pale band carrying dark text. */
    --color-notice:         #3A2D10;
    --color-notice-line:    #6B5416;
    --color-notice-ink:     #F7C566;

    /* "Darken this surface" becomes "lighten this surface". Shadows do NOT flip —
       they deepen, because a shadow needs more alpha to read on ink than on cream. */
    --tint-shade:           #FFFFFF;
    --tint-shadow:          #000000;
    --tint-chip:            #1E1C30;

    --color-inset:          rgba(255, 255, 255, 0.06);
    --color-inset-strong:   rgba(38, 36, 60, 0.96);
    --color-inset-bar:      rgba(18, 18, 31, 0.86);
    --color-inset-handle:   rgba(38, 36, 60, 0.90);
    --color-scrim:          rgba(0, 0, 0, 0.55);
    --color-scrim-deep:     rgba(0, 0, 0, 0.70);
    --color-overlay-canvas: rgba(18, 18, 31, 0.98);
    --color-canvas-veil:    rgba(18, 18, 31, 0.75);

    /* Elevation is not a re-colour. Shadow does not read on a dark ground — the card
       and its shadow are the same colour — so dark signals depth the way .glass-card
       already does: a catch-light on the top edge, a faint ring, and a shadow deep
       enough to matter. */
    --shadow-depth-sm: inset 0 1px 0 rgba(255,255,255,0.05), 0 2px 8px rgba(0,0,0,0.45),
                       0 0 0 1px rgba(255,255,255,0.04);
    --shadow-depth-md: inset 0 1px 0 rgba(255,255,255,0.06), 0 4px 20px rgba(0,0,0,0.55),
                       0 0 0 1px rgba(255,255,255,0.05);
    --shadow-depth-lg: inset 0 1px 0 rgba(255,255,255,0.07), 0 8px 40px rgba(0,0,0,0.65),
                       0 0 0 1px rgba(255,255,255,0.06);

    /* Restated, not derived — the light values were measured against cream. Ratios
       below are against --color-glass (#1E1C30). */
    --color-status-prepaid: #5FE0BC;
    --color-status-settled: #8E8BA6;
    --color-panel-error:    #FF8095;
    --color-amt-in:         color-mix(in srgb, var(--color-mint) 70%, #FFF);
    --shadow-bonus-float:   0 1px 0 rgba(0, 0, 0, 0.6);
  }
}

/* THE ETA WORDING TABS (Plan 367). Which of a slot's three wordings is on screen — a pure view
   switcher: all three always exist, so these never appear or disappear. Tokens only, and the
   ACTIVE one takes the brand fill with text-on-accent, per the theming rule that a vivid accent
   is a FILL and text on it is on-accent (--color-ink would read near-white on dark). */
.variation-pill {
  background: color-mix(in srgb, var(--tint-shade) 6%, transparent);
  color: var(--color-ink-soft);
  border: none;
  transition: background 0.15s, color 0.15s;
}
.variation-pill.is-active {
  background: var(--color-brand);
  color: var(--color-on-accent);
}
