/* MooMoo — app shell, layout and components. */

*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
}

body {
  font-family: var(--font-pixel);
  font-size: var(--fs-md);
  color: var(--ink);
  background: var(--cream);
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
}

/* Every piece of text in the app renders in the pixel font, per explicit
   request — body sets it as the default so paragraphs, chat and long
   explanations all inherit it without a per-selector list. No carve-outs:
   an earlier readable-sans exception for .chart text (SVG axis labels at
   10px) was removed on request for total font consistency — see tokens.css
   for the current --font-pixel stack (Pixel Code). */
h1, h2, h3, h4 {
  margin: 0; line-height: 1.3;
  font-family: var(--font-pixel);
  font-weight: var(--style-heading-weight);
  letter-spacing: var(--style-heading-tracking);
  text-transform: var(--style-heading-transform);
}
h1 { font-size: var(--fs-xxl); }
h2 { font-size: var(--fs-xl); }
h3 { font-size: var(--fs-lg); }
p  { margin: 0 0 var(--s3); line-height: 1.6; }
p:last-child { margin-bottom: 0; }
a { color: var(--accent-deep); }

/* This list is now mostly redundant with the body default above — kept so
   these specific selectors keep working even if something downstream ever
   sets a different inherited font on an ancestor. */
.pixel-label, .card-title, .tag, .rar, .stat .n, .stat .l, .timer-num, .timer-label,
.day-col .dh, .screen-head .eyebrow, .wallet, .brand, .accordion summary,
.task-title, label.field > span:first-child, .toast, .empty .bold,
.cow-bubble, .msg.cow .bub, .banner, .btn, .chip, .mini-task, .nav-link {
  font-family: var(--font-pixel);
  letter-spacing: 0;
}

/* Focus is always visible and always the same shape, everywhere. Uses
   --focus-ring, not --accent-deep: on a dark theme, "deep" accent shades
   can sit almost as dark as the surface behind them (measured 1.06:1 in
   an earlier version of this palette) — an invisible focus ring for
   keyboard users is a real bug, so this gets its own always-legible token
   instead of reusing a shade tuned for a different job. See tokens.css. */
:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
:focus:not(:focus-visible) { outline: none; }

/* router.js gives the new screen's heading tabindex="-1" and focuses it on
   every navigation, purely so screen readers announce the page change — a
   real, deliberate a11y feature, not a mistake. Sighted users were seeing
   its side effect though: the global focus ring above drawing a big box
   around the title every time a page opened. Headings are never reachable
   by an actual Tab key press (tabindex="-1" removes them from the tab
   order — the only way they're ever focused is this one script-driven
   call), so suppressing the visual ring here can't ever hide a real
   keyboard-navigation focus indicator. */
h1:focus-visible, h2:focus-visible { outline: none; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip-link {
  position: absolute; left: var(--s3); top: -100px; z-index: 200;
  background: var(--surface); color: var(--ink); padding: var(--s3) var(--s4);
  border-radius: var(--r-md); box-shadow: var(--shadow-md); font-weight: 700;
}
.skip-link:focus { top: var(--s3); }

/* ============================ layout ============================ */
.app {
  display: grid;
  grid-template-columns: var(--nav-w) 1fr;
  min-height: 100vh;
  min-height: 100dvh;
}

.main {
  /* Fill the available viewport rather than sitting in a narrow centred column.
     Wider side padding on big screens keeps text from running edge to edge, and
     the .grid helpers below fan single columns into multiple on wide layouts.
     Bottom padding also folds in --dock-clearance: Moo's cow-dock is fixed,
     bottom-right, with a clickable cow button + SOS button (pointer-events:
     auto) — without this, a list that happens to end near the bottom of the
     page has no way to scroll its last row's controls out from under them. */
  padding: var(--s5) var(--s6) calc(var(--s8) + var(--dock-clearance));
  max-width: 100%;
  width: 100%;
  position: relative;
}
/* The theme's painted scene, confined to the main content column — the
   sidebar keeps its own solid --mm-bg-dark untouched. This is a ::before,
   not a background on .main itself, because of a real sizing bug that
   shipped in the first version: `background-attachment: fixed` computes
   `cover` against the VIEWPORT, not the element's own box — and the
   viewport here is wider than what .main actually shows, by exactly the
   sidebar's width. That made `cover` scale the picture to fill width it's
   never displayed in, so only an off-centre slice of an over-covered image
   ever showed through .main's (narrower) box — cropped tighter than it
   needed to be, cutting off real content. Pinning this ::before's own edges
   to the actual visible rect (`left: var(--nav-w)`) makes `cover`'s sizing
   area match reality exactly: the least-cropped scale that still fills the
   box with no gaps, correctly centred this time. `position: fixed` on the
   pseudo (not the parent) keeps the earlier benefit too — sizing area is
   this fixed box's own rect, which never grows with a long scrolling page,
   so `cover` can't balloon the image up chasing .main's scroll height. A
   tinted-cream wash layered over the picture keeps every card/heading that
   sits on top of it readable without hiding the art entirely. */
.main::before {
  content: '';
  position: fixed;
  top: 0; right: 0; bottom: 0; left: var(--nav-w);
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(color-mix(in srgb, var(--cream) 55%, transparent), color-mix(in srgb, var(--cream) 55%, transparent)),
    var(--bg-image, none);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
/* Ultra-wide desktops: grow the side gutters instead of the text measure. */
@media (min-width: 1600px) {
  .main { padding-left: var(--s8); padding-right: var(--s8); }
}
@media (min-width: 2000px) {
  .main { padding-left: 96px; padding-right: 96px; }
}

.screen { animation: rise var(--med) var(--ease); }
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

.screen-head { margin-bottom: var(--s5); }
.screen-head .eyebrow {
  font-size: var(--fs-sm); color: var(--ink-soft); font-weight: 600; margin-bottom: var(--s1);
}

.grid { display: grid; gap: var(--s4); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
.row { display: flex; gap: var(--s3); align-items: center; }
.row-wrap { flex-wrap: wrap; }
.between { justify-content: space-between; }
.spacer { flex: 1; }
.stack { display: flex; flex-direction: column; gap: var(--s3); }
.stack-lg { display: flex; flex-direction: column; gap: var(--s5); }

/* ============================ navigation ============================ */
/* The nav is the "environment" that frames the world — deep plum/brown rather
   than a floating lavender panel, so lavender panels (.card etc.) read as
   objects sitting inside that world instead of everything being one texture. */
.sidebar {
  position: sticky; top: 0; height: 100vh; height: 100dvh;
  display: flex; flex-direction: column; gap: var(--s2);
  padding: var(--s5) var(--s3);
  background: var(--mm-bg-dark);
  border-right: 2px solid var(--mm-border-dark);
  overflow-y: auto;
}
.brand {
  display: flex; align-items: center; gap: var(--s3);
  padding: 0 var(--s3) var(--s4);
  font-weight: 600; font-size: 1.2rem; letter-spacing: 0;
  color: var(--mm-text-primary);
}
.brand svg { width: 40px; height: 40px; flex: none; }

.nav-link {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s3);
  border-radius: var(--r-md);
  color: var(--mm-text-secondary); text-decoration: none; font-weight: 600;
  border: 1.5px solid transparent; background: none; cursor: pointer; width: 100%;
  font-size: var(--fs-md); text-align: left;
  transition: background var(--fast), color var(--fast), border-color var(--fast), transform var(--fast);
}
.nav-link:hover { background: color-mix(in srgb, var(--mm-lavender) 18%, transparent); color: var(--mm-text-primary); }
.nav-link[aria-current="page"] {
  background: color-mix(in srgb, var(--mm-lavender) 30%, transparent);
  color: var(--mm-lavender-pale);
  border-color: var(--mm-lavender);
  box-shadow: inset 3px 0 0 var(--mm-lavender-light);
}
.nav-link .ico { width: 24px; text-align: center; font-size: 1.15rem; flex: none; }
img.ico { width: 24px; height: 24px; flex: none; }
.nav-link svg.ico { height: 24px; }

/* ---- nav/tab icons ----
   These are vector transcriptions of the original PNG pixel art
   (js/art/navicons.js): every colour the artist drew is kept, and only the
   lavender/plum accent shades follow the theme, re-tinted to the live
   --accent's hue by retintNavIcon(). Nothing here sets their colour — the
   fills are per-path — so all this rule does is match the box the <img> had.
   `shape-rendering: crispEdges` on the svg keeps the pixel grid hard. */
svg.ico-svg { flex: none; }
.nav-badge {
  margin-left: auto; background: var(--mm-reward-gold); color: var(--mm-text-dark);
  border-radius: var(--r-pill); font-size: var(--fs-xs); font-weight: 700;
  min-width: 20px; padding: 1px 6px; text-align: center;
  font-family: var(--font-pixel);
}

.wallet {
  margin-top: auto; padding: var(--s3);
  background: var(--mm-bg-deep); border: 1.5px solid var(--mm-border-dark);
  border-radius: var(--r-md);
  box-shadow: var(--pixel-bevel-sm);
  display: flex; flex-direction: column; gap: var(--s2); font-weight: 600;
  color: var(--mm-text-secondary);
}
.wallet .w-row { display: flex; align-items: center; gap: var(--s2); font-size: var(--fs-sm); }
.wallet .w-row .val { margin-left: auto; color: var(--mm-text-primary); }

/* Account / sync status. Sits directly under the wallet and is deliberately
   quiet: it reports, it never nags. The `tone` attribute is the only thing that
   changes as state moves, so the sync engine can extend it later without
   touching this layout. Matches .wallet's box rather than .chip's clip-path
   recipe on purpose — the two read as one stacked unit at the sidebar foot. */
.account-chip {
  margin-top: var(--s2); padding: var(--s2) var(--s3);
  display: flex; align-items: center; gap: var(--s2);
  width: 100%; text-align: left; cursor: pointer;
  font: inherit; font-size: var(--fs-sm); font-weight: 600;
  color: var(--mm-text-secondary);
  background: var(--mm-bg-deep);
  border: 1.5px solid var(--mm-border-dark);
  border-radius: var(--r-md);
  box-shadow: var(--pixel-bevel-sm);
}
.account-chip:hover { background: var(--surface-2); }
/* The plain mid-tones, NOT --green-on-wash / --yellow-on-wash. Those two are
   tuned for a light `-wash` fill and resolve to dark colours, which on this
   chip's dark `--mm-bg-deep` sidebar background measured 2.4:1 and 2.5:1 in the
   two light themes — the same "text on the wrong background family" bug this
   codebase has shipped before. Measured live in all four themes: --green
   5.0-8.2:1, --yellow 6.0-11.6:1. */
.account-chip[data-tone="on"] { color: var(--green); }
.account-chip[data-tone="warn"] { color: var(--yellow); }
.account-chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.bottom-nav { display: none; }

@media (max-width: 860px) {
  .app { grid-template-columns: 1fr; }
  .sidebar { display: none; }
  .main { padding: var(--s4) var(--s4) calc(var(--bottom-nav-h) + var(--s8) + var(--dock-clearance)); }
  /* No sidebar to leave clear at this width — .main's ::before background
     needs to start at the real left edge instead of var(--nav-w). */
  .main::before { left: 0; }
  .bottom-nav {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
    background: var(--mm-bg-dark);
    border-top: 2px solid var(--mm-border-dark);
    padding: var(--s2) var(--s2) calc(var(--s2) + env(safe-area-inset-bottom));
    justify-content: space-around;
  }
  .bottom-nav .nav-link {
    flex-direction: column; gap: 2px; padding: var(--s2) var(--s1);
    font-size: .68rem; font-weight: 600; border-radius: var(--r-md);
    min-height: 52px; justify-content: center;
  }
  .bottom-nav .nav-link[aria-current="page"] { box-shadow: inset 0 3px 0 var(--mm-lavender-light); }
  .bottom-nav .nav-link .ico { font-size: 1.35rem; width: auto; }
  .bottom-nav .nav-link img.ico,
  .bottom-nav .nav-link svg.ico { width: 22px; height: 22px; }
  .bottom-nav .nav-badge { position: absolute; top: 2px; right: 12px; margin: 0; }
  .bottom-nav .nav-link { position: relative; }
  h1 { font-size: 1.6rem; }
}

/* ============================ pixel panels ============================ */
/* Every card-like surface in the app (card, hero, modal, cow-bubble, toast, day
   cell, matrix cell, dot-list row, accordion, farm stage…) shares this recipe:
   the notch silhouette, --pixel-ring for the drawn contour and --pixel-bevel
   for the raised light-top-left/dark-bottom-right edge (both inset shadows,
   so the clip-path can't eat them), plus filter:drop-shadow for outer depth.
   That reads as a cosy game-UI window instead of a blurred SaaS card, from
   one shared recipe rather than bespoke styling per page. */
.card {
  position: relative;
  background: var(--surface);
  clip-path: var(--panel-clip);
  border-radius: var(--panel-radius);
  padding: var(--panel-pad);
  filter: var(--drop-sm);
  box-shadow: var(--pixel-bevel), var(--pixel-ring);
}
.card-lg { padding: var(--s5); }
.card-title {
  font-weight: var(--style-heading-weight); font-size: var(--fs-md); color: var(--ink);
  letter-spacing: var(--style-heading-tracking); text-transform: var(--style-heading-transform);
  display: flex; align-items: center; gap: var(--s2); margin-bottom: var(--s3);
}
.card-title .sub { font-weight: 600; color: var(--ink-faint); font-size: var(--fs-sm); }

/* Deliberately the SAME fill and border as .card — every panel in the app
   is one consistent colour. This used to be a gradient (and, briefly, a
   tinted "spotlight" surface); both made it read as a different kind of
   box from the cards around it, which is the opposite of what's wanted.
   It keeps its roomier padding, so it still leads the page through
   SPACING and content hierarchy rather than through colour. */
.hero {
  position: relative;
  background: var(--surface);
  clip-path: var(--panel-clip);
  border-radius: var(--panel-radius);
  padding: var(--s5);
  filter: var(--drop-sm);
  box-shadow: var(--pixel-bevel), var(--pixel-ring);
}

.muted { color: var(--ink-soft); }
.faint { color: var(--ink-faint); }
.tiny { font-size: var(--fs-xs); }
.small { font-size: var(--fs-sm); }
.center { text-align: center; }
.bold { font-weight: 800; }

/* ============================ pixel buttons ============================ */
/* Genuinely notched, not just recoloured: clip-path cuts a staircase corner
   (--notch-sm) instead of rounding it. Since clip-path also crops away a
   plain `border`, the border comes from --pixel-ring (an inset box-shadow,
   see tokens.css) which is clipped to the notch silhouette rather than
   fighting it; variants recolour it by setting --ring-color alone.
   OUTER depth comes from filter:drop-shadow (an outset box-shadow would get
   clipped away), which hugs the notched silhouette instead of a plain
   rectangle — inset shadows like the ring and bevel are unaffected by that.
   Tactile feel: a hard bottom edge at rest, lift + brightness on hover,
   press-down flattening on click — kept subtle (140ms, no bounce). */
.btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  font-family: var(--font-pixel); font-size: var(--fs-md); font-weight: 600;
  padding: var(--control-pad-y) var(--control-pad-x);
  clip-path: var(--control-clip);
  border-radius: var(--control-radius);
  border: none; background: var(--surface-sunk); color: var(--ink);
  cursor: pointer; text-decoration: none;
  min-height: 44px;
  filter: var(--drop-sm);
  box-shadow: var(--pixel-bevel-sm), var(--pixel-ring);
  transition: transform .14s var(--ease), filter .14s var(--ease), background var(--fast);
}
.btn:hover:not(:disabled) { transform: translateY(-2px); filter: var(--drop-md) brightness(1.12); }
.btn:active:not(:disabled) {
  transform: translateY(2px); filter: drop-shadow(0 1px 0 var(--mm-border-dark)) brightness(.94);
}
/* Disabled reads as genuinely INERT, not just a fainter version of active —
   the bevel/ring depth that makes every other button feel pressable is
   deliberately flattened away here (grayscale removes the accent colour
   that draws the eye, the dropped shadow/bevel removes the "this is a
   raised, tappable object" cue), so a disabled control visually recedes
   instead of sitting at half-strength but still looking inviting to tap. */
.btn:disabled {
  opacity: .55; cursor: not-allowed;
  filter: grayscale(.6);
  box-shadow: none;
  transform: none;
}
/* Loading is its own state, distinct from disabled: the button stays at
   full colour/depth (still clearly "this is happening because of me", not
   inert) and gains a small chunky pixel-square spinner before its existing
   label — deliberately stepped rotation (steps(8), not a smooth CSS
   rotate) so it reads as a pixel-art spinner rather than a generic
   Bootstrap-style loading ring. */
.btn.loading { pointer-events: none; }
/* Deliberately NOT wired to --control-clip/--control-radius like other
   controls: a solid circle rotating via steps() shows no visible motion at
   all (no corner to reveal the steps), so the spinner keeps its own fixed
   small-notch square in every style — a functional motion cue, not a
   themeable panel. */
.btn-spinner {
  display: inline-block; width: 12px; height: 12px; flex: none;
  background: currentColor;
  clip-path: var(--notch-sm);
  animation: pixel-spin .9s steps(8) infinite;
}
@keyframes pixel-spin { to { transform: rotate(360deg); } }

.btn-primary { background: var(--accent); color: var(--on-accent-1); --ring-color: var(--accent-deep); }
/* The secondary accent — a genuinely different hue from --accent, not just a
   dimmer version of it, for actions that deserve real colour (notifications,
   a second CTA on the same screen) without competing with the one primary
   action. Ghost stays the "quiet" option for everything else. --on-accent-1/
   -2 (not the shared --on-accent) because in a light theme like Orchard the
   two accents are bright enough to need dark text while other filled
   surfaces elsewhere (tick, SOS) use darker colours that still want white —
   one shared token couldn't serve both. */
.btn-secondary { background: var(--accent-2); color: var(--on-accent-2); --ring-color: var(--accent-2-deep); }
/* Ghost/danger buttons stay deliberately flat until interacted with — that's
   the point of "ghost" — so they opt out of the raised bevel the base .btn
   gets, same as they already opt out of filter. The ring is all the edge
   they have, so they keep that. */
.btn-ghost { background: transparent; color: var(--ink-soft); filter: none; box-shadow: var(--pixel-ring); }
.btn-ghost:hover:not(:disabled) { background: var(--surface-sunk); color: var(--ink); filter: var(--drop-sm); }
.btn-ghost:active:not(:disabled) { filter: none; }
.btn-sm { padding: var(--s2) var(--s3); font-size: var(--fs-sm); min-height: 36px; }
.btn-lg { padding: var(--s4) var(--s6); font-size: var(--fs-lg); min-height: 56px; }
.btn-block { width: 100%; }
.btn-danger { background: transparent; color: var(--pink-deep); filter: none; box-shadow: var(--pixel-ring); --ring-color: var(--pink); }

.chip {
  position: relative;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px var(--s3);
  clip-path: var(--control-clip);
  border-radius: var(--control-radius);
  background: var(--surface-sunk); border: none;
  font-size: var(--fs-sm); font-weight: 600; color: var(--ink-soft);
  font-family: var(--font-pixel);
  cursor: pointer; min-height: 36px;
  box-shadow: var(--pixel-bevel-sm), var(--pixel-ring);
  --ring-color: var(--line);
  transition: background var(--fast), color var(--fast), transform .14s var(--ease);
}
/* Secondary Accent, not Primary — a chip's "selected" state is one of many
   places in the app that used to all borrow the one Primary Accent colour
   (buttons, the calendar's "today", selected chips…), leaving everything
   that wasn't literally the main CTA looking the same. Primary now means
   "the one main action" (.btn-primary, active nav, progress); Secondary
   covers this whole family of toggled/selected/highlighted states instead. */
.chip[aria-pressed="true"], .chip.on {
  background: var(--accent-2-wash); color: var(--accent-2-on-wash);
  --ring-color: var(--accent-2-deep);
}
.chip:hover { --ring-color: var(--line-strong); background: var(--surface-2); }
.chip[aria-pressed="true"]:hover, .chip.on:hover { --ring-color: var(--accent-2-deep); background: var(--accent-2-wash); }
.chip:active { transform: translateY(1px); }
.chip-static { cursor: default; }
.chip:disabled { opacity: .5; filter: grayscale(.6); box-shadow: none; cursor: not-allowed; }

/* ============================ theme picker ============================ */
.theme-picker { display: flex; gap: var(--s3); flex-wrap: wrap; }
.theme-option {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: var(--s2);
  min-width: 132px;
  padding: var(--s3) var(--s4);
  clip-path: var(--control-clip);
  border-radius: var(--control-radius);
  border: none; background: var(--surface-sunk); color: var(--ink-soft);
  cursor: pointer;
  box-shadow: var(--pixel-bevel-sm), var(--pixel-ring);
  --ring-color: var(--line);
  transition: background var(--fast), color var(--fast), transform var(--fast) var(--ease);
}
.theme-option:hover { --ring-color: var(--line-strong); transform: translateY(-1px); }
.theme-option[aria-pressed="true"] {
  background: var(--accent-2-wash); color: var(--accent-2-on-wash);
  --ring-color: var(--accent-2-deep);
}
.theme-swatch { display: flex; gap: 3px; }
.theme-swatch span {
  width: 20px; height: 20px; border-radius: var(--r-sm);
  /* was a hardcoded rgba(0,0,0,.18): invisible against a dark theme's own
     swatch chips. --bevel-dark is the theme's own "shade" overlay, so the
     outline stays visible whichever theme is currently painting the page. */
  border: 1.5px solid var(--bevel-dark);
}
.theme-name { font-size: var(--fs-sm); font-weight: 700; }

/* ---- custom-theme background picker ----
   Same button recipe as .theme-option (bevel/ring/pressed state), just with
   a picture thumbnail instead of 3 colour dots — reused via a second class
   rather than duplicated, so any future tweak to the base button chrome
   can't drift between the two. */
.bg-thumb {
  width: 84px; aspect-ratio: 3 / 2; border-radius: var(--r-sm);
  background-size: cover; background-position: center;
  background-color: var(--surface-2);
  border: 1.5px solid var(--bevel-dark);
}

/* ---- custom colour picker ---- */
.custom-picker { margin-top: var(--s3); padding-top: var(--s3); border-top: 1.5px dashed var(--line); }

/* Live readability notice under the six pickers. Uses the status washes, so
   it reads as part of whichever theme is currently painting — including the
   half-finished custom theme it is warning about. */
.custom-contrast { margin-top: var(--s3); padding: var(--s2) var(--s3); border-radius: var(--r-sm); }
.custom-contrast.ok { color: var(--green-on-wash); background: var(--green-wash); margin-bottom: 0; }
.custom-contrast.bad { color: var(--pink-on-wash); background: var(--pink-wash); }
.custom-contrast p { margin: 0 0 var(--s1); }
.custom-contrast p:last-child { margin-bottom: 0; }
.custom-contrast ul { margin: 0 0 var(--s2); padding-left: var(--s4); }
.custom-picker-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--s3); margin-top: var(--s2);
}
.custom-swatch-field {
  display: flex; flex-direction: column; align-items: center; gap: var(--s2);
  font-size: var(--fs-xs); font-weight: 700; color: var(--ink-soft); text-align: center;
}
/* Native colour inputs render their own OS swatch button — strip the
   default chrome (padding/border varies wildly per browser) and give it
   the same notch+bevel+ring frame every other control here has, so it
   still reads as a MooMoo control and not a bare OS widget. */
.custom-swatch-field input[type="color"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; min-height: 44px; height: 44px; padding: 3px;
  border: none; clip-path: var(--control-clip); border-radius: var(--control-radius);
  background: var(--surface-sunk);
  box-shadow: var(--pixel-bevel-sm), var(--pixel-ring);
  cursor: pointer;
}
.custom-swatch-field input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.custom-swatch-field input[type="color"]::-webkit-color-swatch { border: none; clip-path: var(--control-clip); }
.custom-swatch-field input[type="color"]::-moz-color-swatch { border: none; }

/* ============================ forms ============================ */
label.field { display: flex; flex-direction: column; gap: 6px; font-weight: 700; font-size: var(--fs-sm); }
label.field > .hint { font-weight: 600; color: var(--ink-faint); font-size: var(--fs-xs); }
.custom-swatch-field .hint { font-weight: 500; color: var(--ink-faint); line-height: 1.4; }

/* Inputs used to be the one surface in the app NOT built from the shared
   pixel-panel recipe (a plain border-radius rectangle sitting next to
   notched buttons/chips/cards) — brought into the same notch + inset-bevel
   + ring family everything else uses, so a form genuinely reads as part of
   the same game UI instead of a stock HTML control. Kept in the "dense
   tier" (bevel + ring, no filter:drop-shadow) rather than the full card
   treatment — a form with several stacked fields would look heavy with a
   drop-shadow under every one of them, the same reasoning already applied
   to .task/.day-col/.matrix-cell. */
input[type="text"], input[type="time"], input[type="date"], input[type="number"],
input[type="password"], input[type="search"], textarea {
  font-family: inherit; font-size: var(--fs-md); color: var(--ink);
  background: var(--surface-2);
  border: none;
  clip-path: var(--control-clip);
  border-radius: var(--control-radius);
  box-shadow: var(--pixel-bevel-sm), var(--pixel-ring);
  padding: var(--s3);
  width: 100%;
  min-height: 44px;
  transition: background var(--fast);
}
/* Plain `border-color` can't trace a clip-path shape (the recurring gotcha
   documented throughout this file) — focus recolours --ring-color instead,
   same mechanism every other notched control already uses. */
input:focus, textarea:focus { --ring-color: var(--accent-deep); background: var(--surface); }
/* Chrome/Edge paint autofilled fields with their own baked-in pale-yellow
   fill (visible right through the clip-path as an "unwanted yellow border"
   around the field) that normal background/border rules can't override — it
   needs this specific pseudo-class and the huge inset box-shadow trick to
   beat the browser's internal styling. A long transition delay stops it
   flashing yellow for a frame before the override applies. Text colour is
   forced too: autofill also ignores plain `color`. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill {
  -webkit-text-fill-color: var(--ink);
  -webkit-box-shadow: 0 0 0 1000px var(--surface-2) inset;
  box-shadow: 0 0 0 1000px var(--surface-2) inset, var(--pixel-bevel-sm), var(--pixel-ring);
  transition: background-color 600000s 0s, color 600000s 0s;
}
textarea { resize: vertical; min-height: 90px; line-height: 1.55; }
input::placeholder, textarea::placeholder { color: var(--ink-faint); }

/* Custom dropdown (components/dropdown.js) — replaces <select> wherever the
   OS-rendered native option popup would break the notched pixel-panel look.
   Same bordered-notch recipe as .btn/.chip: clip-path cuts the staircase
   corner, the border is a separate ::before offset outward by --px-border
   (a plain border can't trace a clip-path shape), and depth comes from
   filter:drop-shadow since box-shadow gets clipped away on a clip-path'd
   element. Compact by default (matches the old inline-sized selects); add
   .dd-block for a full-width field like the other inputs in a form. */
.dd { position: relative; display: inline-block; }
.dd-block { display: block; width: 100%; }
.dd-trigger {
  position: relative;
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  width: 100%;
  font-family: var(--font-pixel); font-size: var(--fs-sm); font-weight: 600;
  padding: 6px var(--s3);
  clip-path: var(--control-clip);
  border-radius: var(--control-radius);
  border: none; background: var(--surface-sunk); color: var(--ink);
  cursor: pointer; min-height: 36px; min-width: 110px;
  filter: var(--drop-sm);
  box-shadow: var(--pixel-bevel-sm), var(--pixel-ring);
  transition: transform .14s var(--ease), filter .14s var(--ease), background var(--fast);
}
.dd-block .dd-trigger { min-height: 44px; padding: var(--s3) var(--s4); font-size: var(--fs-md); }
.dd-trigger:hover { filter: var(--drop-md) brightness(1.05); }
.dd-trigger:active { transform: translateY(1px); }
.dd-arrow { font-size: .7em; color: var(--ink-faint); transition: transform var(--fast) var(--ease); flex: none; }
.dd.open .dd-arrow { transform: rotate(180deg); }
.dd-list {
  /* overflow-y:auto (for long option lists) would clip the usual outward
     ::before border ring before it could render — same tradeoff .modal and
     .farm-stage make — so this gets a plain border instead.
     position:fixed + left/top set inline by dropdown.js's positionList(): this
     is a portal appended straight to <body> (see the comment in dropdown.js),
     not a normal descendant of `.dd`, so there's no percentage-of-parent to
     anchor off any more. z-index sits just above .modal/.overlay (300) —
     several dropdowns (e.g. task recurrence) open from inside a modal, and
     without this they'd portal to <body> only to render underneath it. */
  position: fixed; z-index: 310;
  list-style: none; margin: 0; padding: 4px;
  background: var(--surface);
  clip-path: var(--control-clip);
  border-radius: var(--control-radius);
  border: 2px solid var(--line-strong);
  filter: var(--drop-md);
  box-shadow: var(--pixel-bevel-sm);
  max-height: 220px; overflow-y: auto;
}
.dd-option {
  padding: 7px var(--s3); font-family: var(--font-pixel); font-size: var(--fs-sm); font-weight: 600;
  color: var(--ink-soft); cursor: pointer; white-space: nowrap;
}
.dd-option.active { background: var(--accent-2-wash); color: var(--accent-2-on-wash); }
.dd-option[aria-selected="true"] { color: var(--ink); font-weight: 800; }

.switch { display: flex; align-items: center; gap: var(--s3); cursor: pointer; padding: var(--s2) 0; }
.switch input { position: absolute; opacity: 0; width: 44px; height: 26px; margin: 0; cursor: pointer; }
.switch .track {
  width: 46px; height: 27px; clip-path: var(--control-clip); border-radius: var(--control-radius);
  background: var(--line-strong);
  position: relative; flex: none; transition: background var(--fast);
}
.switch .track::after {
  /* A small square pixel thumb by default — becomes a round thumb sliding
     in a pill track for free in Soft Meadow, since both just inherit
     --control-clip/--control-radius like every other control. */
  content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 21px;
  clip-path: var(--control-clip); border-radius: var(--control-radius); background: var(--surface);
  filter: drop-shadow(0 1.5px 0 var(--mm-border-dark));
  transition: transform var(--med) var(--ease);
}
.switch input:checked + .track { background: var(--accent-2-deep); }
.switch input:checked + .track::after { transform: translateX(19px); }
.switch input:focus-visible + .track { outline: 3px solid var(--focus-ring); outline-offset: 2px; }
.switch .label { font-weight: 700; }

/* ============================ tasks ============================ */
.task {
  display: flex; align-items: flex-start; gap: var(--s3);
  padding: var(--s3); border-radius: var(--r-md);
  /* Matches .dot-list li's border weight/colour — this is the single
     most-seen row type in the app (every task, everywhere), it shouldn't be
     the most weakly-bordered of the "dense row" family. */
  background: var(--surface-2); border: 1.5px solid var(--line-strong);
  transition: background var(--fast), opacity var(--med);
}
.task:hover { background: var(--surface-sunk); }
.task.done { opacity: .55; }
.task.done .task-title { text-decoration: line-through; text-decoration-color: var(--ink-faint); }
.task-body { flex: 1; min-width: 0; }
.task-title { font-weight: 700; }
.task-meta { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: 4px; }
.task-actions { display: flex; gap: 4px; flex: none; }

.tick {
  position: relative;
  /* 44x44 touch target (was 28px) — this is the primary "mark done" control
     for every task and to-do row, tapped constantly, so it gets the full
     WCAG 2.5.5 minimum rather than shrinking to fit the glyph. */
  flex: none; width: 44px; height: 44px;
  clip-path: var(--control-clip);
  border-radius: var(--control-radius);
  border: none; background: var(--surface);
  cursor: pointer; display: grid; place-items: center; padding: 0;
  box-shadow: var(--pixel-bevel-sm), var(--pixel-ring);
  /* Named properties, not `all` — matches .chip's exact pattern (the ring
     colour deliberately isn't transitioned here either, same as .chip). */
  transition: background var(--fast), color var(--fast), transform var(--fast) var(--ease);
  color: transparent;
  font-size: 1.15rem; font-weight: 900;
}
.tick:hover { transform: scale(1.08); --ring-color: var(--green-deep); }
.tick[aria-pressed="true"] {
  background: var(--green-deep); color: var(--on-accent);
  --ring-color: var(--green-deep);
}
.icon-btn {
  /* 42px (was 34px) — close to the 44px touch-target guideline for the
     delete/menu/close buttons used throughout, without the bulk of a full
     44px box in the tighter rows (chat avatars, task-action clusters) it
     sits in alongside other controls. */
  width: 42px; height: 42px; border-radius: var(--r-sm); border: none;
  background: transparent; color: var(--ink-faint); cursor: pointer;
  display: grid; place-items: center; font-size: 1rem;
  transition: background var(--fast), color var(--fast);
}
.icon-btn:hover { background: var(--surface-sunk); color: var(--ink); }

.tag {
  font-size: var(--fs-xs); font-weight: 700; padding: 2px 8px;
  border-radius: var(--r-pill); background: var(--surface-sunk); color: var(--ink-soft);
}
.tag.e-very_low { background: var(--blue-wash); color: var(--blue-on-wash); }
.tag.e-low      { background: var(--blue-wash); color: var(--blue-on-wash); }
.tag.e-medium   { background: var(--green-wash); color: var(--green-on-wash); }
.tag.e-high     { background: var(--yellow-wash); color: var(--yellow-on-wash); }
.tag.e-very_high{ background: var(--pink-wash); color: var(--pink-on-wash); }
.tag.p-high     { background: var(--pink-wash); color: var(--pink-on-wash); }

.empty {
  text-align: center; padding: var(--s6) var(--s4); color: var(--ink-soft);
}
.empty .big { font-size: 2.5rem; margin-bottom: var(--s2); }

/* ============================ progress ============================ */
/* Track is a PixelLab 9-slice (border-image corners never stretch, so the
   rounded end-caps stay crisp at any bar width). Fill is a plain stretched
   image — a flat gradient band tolerates non-uniform stretch fine, and a
   second nested border-image here would fight the track's for space. */
.bar {
  height: 12px; overflow: hidden;
  border-style: solid; border-width: 2px 6px;
  border-image-source: url(/assets/ui/bar-track.png);
  border-image-slice: 2 6 fill;
  border-image-width: 2px 6px;
  border-image-repeat: stretch;
  image-rendering: pixelated;
}
.bar > i {
  display: block; height: 100%;
  background-image: url(/assets/ui/bar-fill.png);
  background-size: 100% 100%;
  image-rendering: pixelated;
  transition: width var(--slow) var(--ease);
}
.ring { transform: rotate(-90deg); }
.ring circle { fill: none; stroke-linecap: round; }

.stat { text-align: center; padding: var(--s3); }
.stat .n { font-size: 1.7rem; font-weight: 800; line-height: 1.1; }
.stat .l { font-size: var(--fs-xs); color: var(--ink-soft); font-weight: 700; margin-top: 2px; }

/* ============================ modal & sheet ============================ */
.overlay {
  position: fixed; inset: 0; z-index: 120;
  background: color-mix(in srgb, var(--mm-bg-deep) 55%, transparent);
  backdrop-filter: blur(3px);
  display: grid; place-items: center;
  padding: var(--s4);
  animation: fade var(--fast) ease;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.modal {
  /* overflow-y:auto here implicitly sets overflow-x:auto too (the CSS spec's
     "if only one axis is set to something other than visible, the other
     computes to auto" rule), so — same as the farm stage — this clips away
     the usual outward-offset ::before border ring before it renders. A
     wrapper element would fix that properly, but a plain border here is a
     much smaller change for an imperceptible corner-pixel imperfection. */
  position: relative;
  background: var(--surface); clip-path: var(--panel-clip); border-radius: var(--panel-radius);
  border: 2px solid var(--line-strong);
  filter: var(--drop-lg); width: min(560px, 100%);
  box-shadow: var(--pixel-bevel);
  max-height: 88dvh; overflow-y: auto; padding: var(--s5);
  animation: pop var(--med) var(--ease);
}
.modal-wide { width: min(880px, 100%); }
@keyframes pop { from { opacity: 0; transform: scale(.94) translateY(14px); } to { opacity: 1; transform: none; } }
.modal-head { display: flex; align-items: center; gap: var(--s3); margin-bottom: var(--s4); }
.modal-head h3 { flex: 1; }

@media (max-width: 620px) {
  .overlay { align-items: flex-end; padding: 0; }
  .modal {
    width: 100%; max-height: 92dvh; border-radius: var(--r-xl) var(--r-xl) 0 0;
    padding: var(--s5) var(--s4) calc(var(--s5) + env(safe-area-inset-bottom));
    animation: sheet var(--med) var(--ease);
  }
  @keyframes sheet { from { transform: translateY(100%); } to { transform: none; } }
}

/* ============================ toasts ============================ */
.toasts {
  position: fixed; z-index: 200; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--bottom-nav-h) + var(--s4));
  display: flex; flex-direction: column; gap: var(--s2); align-items: center;
  pointer-events: none; width: min(420px, calc(100% - 32px));
}
@media (min-width: 861px) { .toasts { bottom: var(--s5); } }
.toast {
  position: relative;
  background: var(--surface);
  clip-path: var(--control-clip); border-radius: var(--control-radius); padding: var(--s3) var(--s4);
  filter: var(--drop-md); font-weight: 600; font-size: var(--fs-sm);
  box-shadow: var(--pixel-bevel-sm), var(--pixel-ring);
  display: flex; align-items: center; gap: var(--s2);
  animation: toastin var(--med) var(--ease);
  max-width: 100%;
}
.toast.good { background: var(--green-wash); color: var(--green-on-wash); --ring-color: var(--green); }
.toast.warn { background: var(--yellow-wash); color: var(--yellow-on-wash); --ring-color: var(--yellow); }
.toast.bad  { background: var(--pink-wash); color: var(--pink-on-wash); --ring-color: var(--pink); }
@keyframes toastin { from { opacity: 0; transform: translateY(16px) scale(.96); } to { opacity: 1; transform: none; } }

/* ============================ cow mascot ============================ */
/* Home-screen-only sticky note (components/stickyNote.js) — fixed to the
   top-right corner, opposite Moo's dock, so the two never compete for the
   same space. Sits above the bottom-nav but below toasts/modals. */
.sticky-note {
  position: fixed; top: var(--s4); right: var(--s4); z-index: 80;
  width: 190px; max-width: calc(100vw - 32px);
  background: var(--surface); color: var(--ink);
  clip-path: var(--panel-clip); border-radius: var(--panel-radius);
  padding: var(--s3);
  box-shadow: var(--pixel-bevel-sm), var(--pixel-ring);
  filter: var(--drop-sm);
  --ring-color: var(--line);
  display: flex; flex-direction: column; gap: var(--s2);
}
.sticky-note-head { font-size: var(--fs-xs); font-weight: 800; color: var(--ink-soft); }
.sticky-note-line { display: flex; align-items: flex-start; gap: var(--s1); }
.sticky-note-text { flex: 1; font-size: var(--fs-sm); font-weight: 600; line-height: 1.3; word-break: break-word; }
.sticky-note-line .icon-btn, .sticky-note-form .icon-btn {
  width: 26px; height: 26px; min-height: 0; font-size: .8rem; flex: none;
}
.sticky-note-form { display: flex; align-items: center; gap: var(--s1); }
.sticky-note-form input {
  flex: 1; min-height: 32px; padding: 4px var(--s2); font-size: var(--fs-xs);
}

/* Moo floats above everything except modals — fixed, so she never occupies
   layout space or pushes content around, and she clears the mobile bottom nav
   plus the device safe-area inset so she's never hidden behind a home bar. */
.cow-dock {
  position: fixed; right: var(--s4); z-index: 100;
  bottom: calc(var(--bottom-nav-h) + var(--s3) + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; align-items: flex-end; gap: var(--s2);
  pointer-events: none;                 /* let taps fall through the empty dock area */
}
.cow-dock > * { pointer-events: auto; } /* …but the cow and her bubble stay tappable */
@media (min-width: 861px) { .cow-dock { bottom: var(--s4); right: var(--s5); } }
.cow-btn {
  position: relative;
  width: 74px; height: 74px;
  clip-path: var(--panel-clip); border-radius: var(--panel-radius);
  border: none; padding: 0;
  background: var(--surface); filter: var(--drop-md); cursor: pointer;
  box-shadow: var(--pixel-bevel), var(--pixel-ring);
  display: grid; place-items: center;
  transition: transform var(--med) var(--ease), filter var(--fast);
}
.cow-btn:hover { transform: scale(1.06) rotate(-3deg); filter: var(--drop-lg); }
.cow-btn svg { width: 58px; height: 58px; }
.cow-btn.tiny-mode { width: 44px; height: 44px; opacity: .75; }
.cow-btn.tiny-mode svg { width: 34px; height: 34px; }
/* A proper pixel dialogue box: the panel recipe (notch corners, layered
   border, drop-shadow depth) plus a small stepped tail — not a triangle, a
   2-step staircase, so it reads as cut from the same pixel-art cloth as the
   rest of the panel — pointing down-right toward the cow button beneath it. */
/* PixelLab-generated 96x96 panel (public/assets/ui/bubble-panel.png, mirrored
   so its tail points down-RIGHT toward the cow button beneath it) with the
   tail baked into its bottom-right 32x24 corner. Border-image corners never
   stretch, so slicing exactly on that corner keeps the tail crisp at any
   bubble size instead of needing a separate ::after shape.
   Text colour is pinned to a fixed dark ink rather than the theme-varying
   var(--ink): the panel's own fill colour is a fixed light lavender baked
   into the image (not a CSS variable), so in night theme the normally-light
   var(--ink) text was going near-invisible against it. */
.cow-bubble {
  position: relative;
  border-style: solid;
  border-width: 16px 32px 24px 16px;
  border-image-source: url(/assets/ui/bubble-panel.png);
  border-image-slice: 16 32 24 16 fill;
  border-image-width: 16px 32px 24px 16px;
  border-image-repeat: stretch;
  image-rendering: pixelated;
  padding: var(--s2) var(--s3); filter: var(--drop-md);
  max-width: min(300px, calc(100vw - 48px)); font-size: var(--fs-sm); font-weight: 600;
  line-height: 1.5; animation: pop var(--med) var(--ease);
  color: var(--mm-text-dark);
}
.cow-bubble .close {
  float: right; margin: -4px -6px 0 6px; border: none; background: none;
  color: var(--mm-text-dark); opacity: .55; cursor: pointer; font-size: 1rem; padding: 2px 4px;
}
.cow-idle { animation: bob 4.5s ease-in-out infinite; }
@keyframes bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }

/* ============================ pixel-art sprites ============================ */
/* The official animal sheet is pixel art, so it must scale by nearest-neighbour
   rather than being smoothed into mush. Every sprite shares one canvas anchored
   to a common ground line, so `contain` inside a square box lines the whole cast
   up without any per-item tweaking. */
.pixel-art {
  image-rendering: -moz-crisp-edges;
  image-rendering: crisp-edges;
  image-rendering: pixelated;
  object-fit: contain;
  display: block;
  user-select: none;
}

/* ============================ collection & farm ============================ */
.collect-grid {
  display: grid; gap: var(--s3);
  grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
}
.collect-card {
  position: relative;
  background: var(--surface);
  clip-path: var(--panel-clip); border-radius: var(--panel-radius); padding: var(--s3); text-align: center;
  cursor: pointer; font-family: inherit; color: inherit;
  filter: var(--drop-sm);
  box-shadow: var(--pixel-bevel), var(--pixel-ring);
  transition: transform var(--fast) var(--ease), filter var(--fast);
}
.collect-card:hover { transform: translateY(-3px); filter: var(--drop-md); }
.collect-card:active { transform: translateY(0); filter: drop-shadow(0 1px 0 var(--mm-border-dark)); }
.collect-card svg,
.collect-card .pixel-art { width: 100%; height: auto; aspect-ratio: 1; }
.collect-card .nm { font-weight: 800; font-size: var(--fs-sm); margin-top: var(--s1); }
.collect-card.locked { background: var(--surface-sunk); }
/* A true silhouette, not a dimmed copy of the real art — brightness(0) drops
   every pixel to black regardless of the item's own colours (rarity, species)
   while keeping its alpha shape, so what's visible is "something is here and
   collectible" without leaking which one. Grayscale+low-opacity used to still
   show enough of the shape/shading to half-guess the item; a flat, slightly
   more opaque shadow keeps the mystery while staying clearly visible. */
.collect-card.locked svg,
.collect-card.locked .pixel-art { filter: brightness(0) opacity(.32); }
.collect-card.locked .nm { color: var(--ink-faint); }
.collect-card .dupe {
  position: absolute; top: 6px; right: 8px; font-size: var(--fs-xs);
  font-weight: 800; color: var(--ink-faint);
}
.rar { font-size: var(--fs-xs); font-weight: 800; }
/* Rarity LABELS use the per-theme text-safe variant, not the raw hue: these
   render on --surface-sunk, where a single fixed hue can't clear AA on both a
   cream card and a near-black one (Epic measured 2.0:1 on the dark themes).
   Same hue, lightness nudged per theme — see RARITY_BASE in make_themes.py.
   Rings and badges below keep the raw --r-* so the identity colour is intact. */
.rar-common { color: var(--r-common-text); }
.rar-uncommon { color: var(--r-uncommon-text); }
.rar-rare { color: var(--r-rare-text); }
.rar-epic { color: var(--r-epic-text); }
.rar-legendary { color: var(--r-legendary-text); }
.collect-card[data-rarity="epic"] { --ring-color: var(--r-epic); }
.collect-card[data-rarity="legendary"] { --ring-color: var(--r-legendary); }
/* Same fill as every other collectible card — rarity is already signalled
   by the gold ring (above) and the gold rarity label, which is enough
   without also tinting the whole panel a different colour from its
   neighbours in the grid. */
.collect-card[data-rarity="legendary"] {
  background: var(--surface);
}

.farm-stage {
  /* overflow:hidden (needed to contain the draggable farm items) would clip
     away the usual outward-offset ::before border ring before it could
     render, so this gets a plain border instead — the one spot in the notch
     system where the corner pixel isn't perfectly traced, invisible at this
     element's size. */
  position: relative; width: 100%; aspect-ratio: 14 / 9;
  clip-path: var(--panel-clip); border-radius: var(--panel-radius); overflow: hidden;
  border: 2.5px solid var(--mm-border-dark);
  filter: var(--drop-md);
  box-shadow: var(--pixel-bevel);
  touch-action: none; user-select: none;
}
.farm-ambience {
  position: absolute; pointer-events: none; opacity: .8;
  filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--mm-border-dark) 60%, transparent));
}
.farm-cell {
  position: absolute; display: grid; place-items: center;
  transition: outline-color var(--fast);
  outline: 2px dashed transparent; outline-offset: -3px; border-radius: var(--r-sm);
}
.farm-cell.editing { outline-color: var(--bevel-light); }
.farm-item { position: absolute; display: grid; place-items: end center; cursor: grab; }
.farm-item svg,
.farm-item .pixel-art { width: 100%; height: 100%; pointer-events: none; filter: drop-shadow(0 3px 3px color-mix(in srgb, var(--shadow-tint) var(--shadow-alpha-sm), transparent)); }
.farm-item.dragging { cursor: grabbing; z-index: 20; filter: brightness(1.05); }
.farm-item.selected { outline: 3px solid var(--on-accent); outline-offset: 2px; border-radius: var(--r-sm); }

/* ============================ pack opening ============================ */
/* Pack reveal: the animal and the decoration side by side, identical markup
   and identical size, because one pack now grants both and neither is the
   consolation prize. Wraps to two rows on a narrow screen. */
.reward-pair {
  display: flex; flex-wrap: wrap; gap: var(--s4);
  align-items: flex-start; justify-content: center;
}
.reward {
  display: grid; justify-items: center; gap: var(--s1);
  flex: 0 1 150px; min-width: 0;
}
.reward .pixel-art { width: 150px; height: 150px; object-fit: contain; }
.reward-name { font-weight: 800; font-size: var(--fs-md); text-wrap: balance; }
.reward .rar { font-size: var(--fs-xs); }
@media (max-width: 420px) {
  .reward { flex-basis: 120px; }
  .reward .pixel-art { width: 120px; height: 120px; }
}
.pack-stage { display: grid; place-items: center; gap: var(--s4); padding: var(--s4) 0; text-align: center; }
.pack-art { width: 190px; height: 190px; }
.pack-shake { animation: shake 1.1s var(--ease) infinite; }

/* Sprite-sheet player (js/components/spritesheet.js). One frame is shown at a
   time by moving the sheet behind a frame-sized window; nothing here knows
   which animation it is. The 1.5x enlargement is done by sizing the element
   and its background (see spritesheet.js), not with a transform, so the layout
   box matches what is drawn and overlays can be positioned against it.

   Note the deliberate absence of any colour: these sheets are fixed art and do
   NOT follow the theme, unlike the inline SVG pack this replaced. */
.sheet-anim {
  border: none; background-color: transparent; padding: 0; cursor: pointer;
  background-repeat: no-repeat;
  image-rendering: -moz-crisp-edges;
  image-rendering: crisp-edges;
  image-rendering: pixelated;
  transition: filter var(--fast) var(--ease);
}
.sheet-anim:hover { filter: brightness(1.06); }
.sheet-anim:active { filter: brightness(.96); }
.sheet-anim[hidden] { display: none; }

/* The fortune sits IN the cookie: the halves swing out to the frame edges and
   this lands in the gap between them. Absolutely positioned over the untrans-
   formed box, so it stays centred no matter what scale .sheet-anim is drawn at.

   Its colours come from the cookie's own art palette rather than from theme
   tokens — same rule as the sprite itself. That also makes it legible on all
   eight themes without a single conditional: the panel is opaque, so whatever
   it overlaps (a cookie half, the modal behind) is irrelevant. */
/* width:100% is load-bearing. As a grid item inside .pack-stage's
   `place-items: center` this box was sized to min-content, and .sheet-anim's
   `max-width: 100%` makes its min-content contribution zero — so the wrapper
   collapsed to ~125px and the overlay inherited that wrong width. */
.cookie-stage { position: relative; display: block; width: 100%; }
.cookie-stage .sheet-anim { display: block; margin-inline: auto; }
.cookie-fortune {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  pointer-events: none;
}
.cookie-fortune[hidden] { display: none; }
.cookie-fortune span {
  /* No entrance animation, deliberately. Both `reveal` and a plain opacity
     fade sat frozen on their 0% keyframe here instead of playing, which left
     the panel either shrunk to 40% or fully transparent — a decorative effect
     that can hide the content is worse than no effect. The cookie underneath
     is already the moving part. */
  /* display matters: a bare inline <span> ignores width outright, which is
     why this collapsed to one word per line even with a width set. */
  display: block;
  width: 15ch;
  max-width: 100%;
  padding: var(--s2) var(--s3);
  background: #f7f0dc;
  color: #6b4a1f;
  border: 2px solid #8a6a3c;
  border-radius: var(--r-sm);
  font-size: var(--fs-sm); font-weight: 700; line-height: 1.35;
  text-wrap: balance;
}
@keyframes shake {
  0%, 100% { transform: rotate(0) scale(1); }
  25% { transform: rotate(-6deg) scale(1.03); }
  75% { transform: rotate(6deg) scale(1.03); }
}
.reveal { animation: reveal .6s var(--ease); }
@keyframes reveal {
  0% { opacity: 0; transform: scale(.4) rotate(-14deg); }
  70% { transform: scale(1.08) rotate(4deg); }
  100% { opacity: 1; transform: none; }
}
.sparkle-field { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.sparkle {
  position: absolute; width: 8px; height: 8px; border-radius: 50%;
  background: var(--yellow); animation: sparkle 1.4s ease-out forwards;
}
.sparkle-img { width: 16px; height: 16px; border-radius: 0; background: none; }
@keyframes sparkle {
  0% { opacity: 0; transform: scale(0) translateY(0); }
  30% { opacity: 1; transform: scale(1.2) translateY(-14px); }
  100% { opacity: 0; transform: scale(.3) translateY(-52px); }
}

/* A small burst for finishing a task — short and quiet on purpose (see
   ui.js confettiBurst): a dozen pixel-square pieces popping out and settling,
   done inside a second. Positioned over whatever anchor called it (usually
   the tick button), not the whole screen. */
/* position: fixed at a single viewport point (left/top set inline by
   confettiBurst) rather than absolute inside whatever triggered it — see the
   comment in ui.js: the triggering element is usually gone by the next
   render, but this stays in <body> and plays out on its own regardless. */
.confetti-field { position: fixed; left: 0; top: 0; pointer-events: none; z-index: 150; }
.confetti-piece {
  position: absolute; top: 0; left: 0; width: 6px; height: 9px;
  animation: confetti-pop .7s cubic-bezier(.2, .7, .3, 1) forwards;
}
@keyframes confetti-pop {
  0% { transform: translate(-50%, -50%) translate(0, 0) rotate(0deg); opacity: 1; }
  100% { transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) rotate(var(--rot)); opacity: 0; }
}

/* ============================ focus ============================ */
.timer-wrap { display: grid; place-items: center; gap: var(--s4); }
.timer-ring { width: min(300px, 74vw); height: min(300px, 74vw); position: relative; }
.timer-ring svg { width: 100%; height: 100%; }
.timer-face {
  position: absolute; inset: 0; display: grid; place-items: center; align-content: center; gap: 2px;
}
.timer-num { font-size: clamp(2.6rem, 12vw, 3.6rem); font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: -.03em; }
.timer-label { font-size: var(--fs-sm); color: var(--ink-soft); font-weight: 700; }

.matrix { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); }
.matrix-cell {
  /* --r-md, not --r-lg — matches .day-col/.task/.dot-list li's corner radius,
     the other "rounded content cell" shapes in the app. */
  border-radius: var(--r-md); padding: var(--s4); min-height: 128px;
  border: 1.5px solid var(--line-strong); display: flex; flex-direction: column; gap: var(--s2);
  box-shadow: var(--pixel-bevel-sm);
}
.matrix-cell h4 { font-size: var(--fs-sm); }
.matrix-cell .mini { font-size: var(--fs-xs); color: var(--ink-soft); }
.m-a { background: var(--blue-wash); }
.m-b { background: var(--green-wash); }
.m-c { background: var(--yellow-wash); }
.m-d { background: var(--pink-wash); }
.matrix-cell.drop { outline: 2px dashed var(--accent-2-deep); outline-offset: -3px; }

/* ============================ week calendar ============================ */
.week {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--s2);
}
.day-col {
  background: var(--surface); border: 1.5px solid var(--line-strong);
  border-radius: var(--r-md); padding: var(--s2); min-height: 150px;
  display: flex; flex-direction: column; gap: 4px;
  box-shadow: var(--shadow-sm), var(--pixel-bevel-sm);
}
.day-col.today { border-color: var(--accent-2-deep); background: var(--accent-2-wash); }
.day-col .dh { font-size: var(--fs-xs); font-weight: 800; color: var(--ink-soft); text-align: center; }
.day-col .dh .dn { font-size: var(--fs-lg); color: var(--ink); display: block; }
.mini-task {
  font-size: var(--fs-xs); font-weight: 700; padding: 4px 6px;
  border-radius: var(--r-sm); background: var(--surface-sunk);
  cursor: pointer; border: none; font-family: inherit; text-align: left;
  color: var(--ink); width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mini-task.done { opacity: .5; text-decoration: line-through; }
.mini-task:hover { background: var(--accent-2-wash); }
.day-col.drop { outline: 2px dashed var(--accent-2-deep); outline-offset: -3px; }
/* The week-nav arrows double as drop targets (drag a task onto one to bump
   it a week) — same dashed-outline drop cue as a day column. */
.btn.drop { outline: 2px dashed var(--accent-2-deep); outline-offset: 2px; }

@media (max-width: 760px) {
  .week { grid-template-columns: 1fr; }
  .day-col { min-height: auto; flex-direction: row; align-items: flex-start; flex-wrap: wrap; }
  .day-col .dh { text-align: left; min-width: 74px; }
  .day-col .dh .dn { display: inline; margin-left: 4px; font-size: var(--fs-md); }
  .mini-task { width: auto; flex: 1 1 140px; }
}

/* ============================ chat ============================ */
/* A conversation thread shows many bubbles at once, so these get the lighter
   single-notch cut rather than the full dialogue-box treatment (staircase +
   tail) reserved for Moo's one-off floating comment — a full ornate frame on
   every line would be exactly the visual clutter the brief asks to avoid. */
.chat { display: flex; flex-direction: column; gap: var(--s3); }
.msg { display: flex; gap: var(--s2); align-items: flex-end; }
.msg .bub {
  position: relative;
  padding: var(--s3) var(--s4); clip-path: var(--control-clip); border-radius: var(--control-radius);
  max-width: 78%; line-height: 1.55; font-size: var(--fs-md);
}
.msg.you { flex-direction: row-reverse; }
.msg.you .bub { background: var(--accent-wash); }
.msg.cow .bub { background: var(--surface-sunk); }
.msg .avatar { width: 34px; height: 34px; flex: none; }
.typing span {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--ink-faint); margin-right: 4px; animation: blink 1.2s infinite;
}
.typing span:nth-child(2) { animation-delay: .2s; }
.typing span:nth-child(3) { animation-delay: .4s; }
@keyframes blink { 0%, 60%, 100% { opacity: .3; } 30% { opacity: 1; } }

/* ============================ SOS ============================ */
.sos-overlay {
  position: fixed; inset: 0; z-index: 300;
  background: var(--lav-wash);
  display: grid; place-items: center; padding: var(--s5);
  animation: fade var(--med) ease; overflow-y: auto;
}
.sos-inner { width: min(520px, 100%); text-align: center; }
.breather {
  width: 180px; height: 180px; border-radius: 50%; margin: var(--s5) auto;
  background: var(--lav);
  display: grid; place-items: center; font-weight: 800; color: var(--lav-deep);
  animation: breathe 11s ease-in-out infinite;
}
@keyframes breathe {
  0%, 100% { transform: scale(.72); }
  36% { transform: scale(1.12); }
  64% { transform: scale(1.12); }
}
/* Lives inside .cow-dock now (appended after the cow button in cow.js), so
   it sits directly under Moo on the right rather than floating on its own in
   the bottom-left — no more fixed positioning here, the dock's flex column
   places it. Same notched-panel recipe as .btn/.chip instead of a pill. */
.sos-btn-float {
  position: relative;
  clip-path: var(--control-clip); border-radius: var(--control-radius);
  padding: var(--s2) var(--s4);
  /* Fixed --danger tokens (tokens.css), not --pink — this button must read as
     "urgent" the same way in every theme, including a custom one where
     --pink-deep can legitimately drift toward the user's own text colour. */
  background: var(--danger-deep); border: none;
  color: var(--on-danger); font-weight: 800; filter: var(--drop-sm);
  box-shadow: var(--pixel-bevel-sm), var(--pixel-ring);
  --ring-color: var(--danger);
  cursor: pointer; font-family: inherit; min-height: 44px;
}

/* ============================ mini-game ============================ */
.game-canvas {
  width: 100%; border-radius: var(--r-lg); background: var(--blue-wash);
  border: 1px solid var(--line); display: block; touch-action: none;
}

/* ============================ misc ============================ */
.dot-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--s2); }
.dot-list li {
  display: flex; gap: var(--s3); align-items: flex-start;
  padding: var(--s3); background: var(--surface-2); border-radius: var(--r-md);
  border: 1.5px solid var(--line-strong);
}
.accordion { border: 1.5px solid var(--line-strong); border-radius: var(--r-md); overflow: hidden; background: var(--surface); }
.accordion + .accordion { margin-top: var(--s2); }
.accordion summary {
  padding: var(--s3) var(--s4); font-weight: 800; cursor: pointer; list-style: none;
  display: flex; align-items: center; gap: var(--s2); min-height: 48px;
}
.accordion summary::-webkit-details-marker { display: none; }
.accordion summary::after { content: "＋"; margin-left: auto; color: var(--ink-faint); }
.accordion[open] summary::after { content: "－"; }
.accordion .body { padding: 0 var(--s4) var(--s4); color: var(--ink-soft); }

.chart { width: 100%; height: 150px; display: block; }
.chart .grid-line { stroke: var(--line); stroke-width: 1; }
.chart .bar-fill { fill: var(--accent); transition: height var(--slow) var(--ease); }
.chart text { fill: var(--ink-faint); font-size: 10px; font-weight: 700; font-family: var(--font-pixel); }

.banner {
  border-radius: var(--r-md); padding: var(--s3) var(--s4);
  background: var(--yellow-wash); border: 1px solid var(--yellow);
  color: var(--yellow-on-wash); font-weight: 700; font-size: var(--fs-sm);
  display: flex; gap: var(--s2); align-items: flex-start;
  /* Matches .day-col/.matrix-cell's "dense tier" — the one bit of shared
     depth every other surface in the app has, banners included, without
     going as far as the full notch treatment reserved for cards/buttons. */
  box-shadow: var(--pixel-bevel-sm);
}
.banner.info { background: var(--blue-wash); border-color: var(--blue); color: var(--blue-on-wash); }

.cookie-btn {
  width: 150px; height: 150px; border: none; background: none; cursor: pointer;
  transition: transform var(--fast) var(--ease);
}
.cookie-btn:hover { transform: scale(1.07) rotate(-4deg); }
.cookie-btn:active { transform: scale(.96); }

/* ============================ touch ============================ */
/* Everything below is gated on `pointer: coarse` — a finger, not a mouse — so
   the desktop layout keeps its compact controls unchanged. Width is the wrong
   test here: a small window on a laptop is still a mouse, and a tablet in
   landscape is still a finger.

   The target is WCAG 2.5.5's 44px. The app already got .tick there (it is the
   control tapped most), but chips, small buttons and the planner's week-view
   task rows were left between 24px and 42px, which is where mis-taps come from
   — and a mis-tap in the week view moves a task to the wrong day.

   Padding rather than height alone where the control holds text, so a long
   label still wraps instead of being clipped. */
@media (pointer: coarse) {
  .chip { min-height: 44px; padding: var(--s2) var(--s4); }
  .btn-sm { min-height: 44px; padding: var(--s2) var(--s4); }
  .icon-btn { width: 44px; height: 44px; }
  .dd-trigger { min-height: 44px; }

  /* The week view's task rows. These are both the tap target for opening a
     task and the drag handle for moving it to another day, so they need the
     room twice over. */
  .mini-task { min-height: 44px; padding: var(--s2) var(--s3); font-size: var(--fs-sm); }

  /* A switch's real target is the whole label row, not the 26px track. */
  .switch { min-height: 44px; display: flex; align-items: center; }

  /* Hover tints are meaningless without a pointer and, worse, they stick after
     a tap on touch — the last-tapped chip keeps the hover background until
     something else is tapped, which reads as a selection that is not there. */
  .chip:hover { background: var(--surface-sunk); }
  .chip[aria-pressed="true"]:hover, .chip.on:hover { background: var(--accent-2-wash); }
  .mini-task:hover { background: var(--surface-sunk); }
  .task:hover { background: var(--surface); }
  .tick:hover { transform: none; }
  .collect-card:hover, .cookie-btn:hover, .sheet-anim:hover { transform: none; filter: none; }
  .nav-link:hover { background: none; }
  .btn:hover:not(:disabled) { transform: none; filter: none; }

  /* Pressed states do the work instead, so a tap still feels answered. */
  .chip:active { transform: scale(.97); }
  .mini-task:active { background: var(--accent-2-wash); }
}

/* ---- dragging (components/drag.js) ----
   The ghost is a clone that follows the pointer, so on touch the thing being
   dragged is visible above her finger rather than under it. pointer-events are
   off so elementsFromPoint sees the drop zone beneath, not the ghost. */
.drag-ghost {
  position: fixed; z-index: 400; pointer-events: none;
  opacity: .9; transform: rotate(-2deg) scale(1.04);
  filter: var(--drop-lg);
  box-shadow: var(--pixel-bevel-sm), var(--pixel-ring);
  --ring-color: var(--accent-2-deep);
}
/* The original stays in place, dimmed, so the row does not reflow mid-drag and
   she can see where it came from. */
.drag-source { opacity: .35; }

/* ============================ installed on a phone ============================ */
/* Standalone PWA and the Capacitor webview both run edge to edge: the manifest
   asks for `standalone`, the viewport asks for `viewport-fit=cover`, and iOS is
   told `black-translucent`. All three together mean the page owns the whole
   screen INCLUDING the notch and the home indicator, so the app has to inset
   itself or content sits underneath them. */
@media (max-width: 860px) {
  .main {
    /* Top for the notch/status bar; left and right because in landscape the
       notch eats one side and the rounded corners eat both. The bottom already
       accounts for the nav and the home indicator. */
    padding-top: calc(var(--s4) + env(safe-area-inset-top));
    padding-left: calc(var(--s4) + env(safe-area-inset-left));
    padding-right: calc(var(--s4) + env(safe-area-inset-right));
  }
  .bottom-nav {
    padding-left: calc(var(--s2) + env(safe-area-inset-left));
    padding-right: calc(var(--s2) + env(safe-area-inset-right));
  }
  .cow-dock { right: calc(var(--s4) + env(safe-area-inset-right)); }
}

/* The grey flash iOS and Android paint over a tapped control. The app has its
   own :active states now, and the two together read as a double press. */
* { -webkit-tap-highlight-color: transparent; }
