/* foundation-ui shell.css — shared chrome (topbar, drawer, bottom bar, modals,
 * toast, buttons, segmented controls, login + settings layout).
 *
 * Components reference ROLE tokens, not brand tokens. Each app maps its palette
 * to these in a tiny alias block, e.g.:
 *
 *   :root{
 *     --acc:var(--sea); --acc-deep:var(--sea-deep); --acc-soft:var(--sea-soft);
 *     --a2:var(--coral); --a2-soft:var(--coral-soft); --tb:var(--topbar-bg);
 *   }
 *
 * Base tokens (--paper,--ink,--line,--card,--r-*,--shadow*) are already defined
 * by every app stylesheet; the :root below only supplies safe fallbacks so the
 * shell renders standalone (e.g. in tests / the design gallery).
 * ------------------------------------------------------------------------- */

:root {
  --paper: #fbf6ec; --paper-2: #f2e8d5; --card: #fffdf8;
  --ink: #243b40; --ink-soft: #5d747a; --ink-faint: #65797e; --line: #e7dcc6;
  --acc: #1f7a8c; --acc-deep: #0f5a69; --acc-soft: #d7ecef;
  --a2: #e07a5f; --a2-soft: #fbe2d8;
  --tb: linear-gradient(180deg, rgba(255,253,248,.94), rgba(251,246,236,.86));
  --r-lg: 18px; --r-md: 13px; --r-sm: 9px; --r-pill: 999px;
  /* ---- Shape scale (Material 3) ----------------------------------------
   * Semantic corner tokens; components migrate to these incrementally (the
   * --r-* values above stay as the app-facing legacy aliases). The scale also
   * powers the M3 Expressive press shape-morph below (pill → rounded rect
   * while pressed, springing back on release). */
  --shape-xs: 4px; --shape-sm: 8px; --shape-md: 12px;
  --shape-lg: 16px; --shape-xl: 28px; --shape-full: 999px;
  --shadow-sm: 0 1px 2px rgba(15,60,70,.06);
  --shadow: 0 6px 18px -8px rgba(15,60,70,.22), 0 2px 5px rgba(15,60,70,.06);
  --shadow-lg: 0 18px 38px -14px rgba(15,60,70,.32);
  --space-2: .5rem; --space-3: .75rem; --space-4: 1rem; --space-5: 1.5rem;
  /* Brand type (wolf-labs family): Bricolage Grotesque display + Inter UI.
   * Variable woff2s ship with the shell bundle (fonts/, ~150KB total, latin);
   * the shell owns the brand pair — apps must NOT re-declare --display/--body
   * (their palettes stay per-app; the type is what makes them one family). */
  --display: "Bricolage Grotesque", "Fraunces", Georgia, serif;
  --body: "Inter", "Nunito", "Segoe UI", system-ui, sans-serif;
  --z-topbar: 20; --z-bottomnav: 30; --z-scrim: 90; --z-drawer: 100; --z-modal: 110;

  /* ---- Motion language (Material 3) ----------------------------------- *
   * Easing is intentionally non-linear; springs add a little overshoot so
   * things feel alive, never machine-like. Every motion in the shell + apps
   * draws from these tokens. Honour prefers-reduced-motion (see below).     */
  --ease-emphasized: cubic-bezier(.2, 0, 0, 1);
  --ease-emphasized-decel: cubic-bezier(.05, .7, .1, 1);
  --ease-emphasized-accel: cubic-bezier(.3, 0, .8, .15);
  --ease-standard: cubic-bezier(.2, 0, 0, 1);
  /* Springs. Default to a cubic-bezier overshoot so EVERY browser gets the
     bouncy character (Safari < 17.4 / Firefox < 112 lack linear()); the precise
     CSS linear() springs are applied via @supports below where available. */
  --spring-snappy: cubic-bezier(.22, 1.2, .36, 1);
  --spring-bouncy: cubic-bezier(.34, 1.56, .64, 1);
  --dur-short: 150ms; --dur-medium: 300ms; --dur-long: 450ms;

  /* ---- Type scale (Material 3) -----------------------------------------
   * The subset of the M3 type scale this compact mobile shell actually uses
   * (no display/headline roles — those are for marketing-scale layouts).
   * Combined size/line-height pairs compose into the `font` shorthand
   * (`font: 700 var(--type-title-large) var(--display)`); the -size-only
   * variants let a component swap just its font-size onto the scale without
   * touching a line-height it doesn't currently set — the incremental,
   * never-silently-breaks-a-hand-tuned-value migration path. */
  --type-title-large:   1.375rem/1.75rem;   /* 22sp/28sp — app-bar title */
  --type-title-medium:  1rem/1.5rem;        /*   16/24  — drawer/login headings */
  --type-title-small:   .875rem/1.25rem;    /*   14/20  — card/section headers */
  --type-body-large:    1rem/1.5rem;        /*   16/24 */
  --type-body-medium:   .875rem/1.25rem;    /*   14/20 — default UI body text */
  --type-body-small:    .75rem/1rem;        /*   12/16 */
  --type-label-large:   .875rem/1.25rem;    /*   14/20 — buttons, tabs */
  --type-label-medium:  .75rem/1rem;        /*   12/16 — chips, captions */
  --type-label-small:   .6875rem/1rem;      /*   11/16 — smallest legible label */
  --type-title-large-size:  1.375rem;
  --type-title-medium-size: 1rem;
  --type-title-small-size:  .875rem;
  --type-body-large-size:   1rem;
  --type-body-medium-size:  .875rem;
  --type-body-small-size:   .75rem;
  --type-label-large-size:  .875rem;
  --type-label-medium-size: .75rem;
  --type-label-small-size:  .6875rem;
}

/* Brand fonts — variable axes (Bricolage: opsz+wght, Inter: opsz+wght), served
 * from the shell bundle so every app carries them without per-app font files. */
@font-face {
  font-family: "Bricolage Grotesque"; font-style: normal; font-weight: 200 800;
  font-display: swap; src: url("fonts/bricolage-grotesque-var.woff2") format("woff2-variations");
}
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 100 900;
  font-display: swap; src: url("fonts/inter-var.woff2") format("woff2-variations");
}

*,*::before,*::after { box-sizing: border-box; }
body.modal-open, body.drawer-open { overflow: hidden; }

/* Precise CSS linear() springs where supported (Chromium 113+, FF 112+,
 * Safari 17.4+); elsewhere the cubic-bezier fallbacks above still bounce. */
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    --spring-snappy: linear(0,.063,.247,.515,.785,.96,1.06,1.094,1.085,1.058,1.029,1.008,.999,.997,1);
    --spring-bouncy: linear(0,.043,.17 4.5%,.385,.679 13.5%,.818,.935,1.03,1.105,1.16,1.193 28.4%,1.205,1.206,1.196,1.176 37.6%,1.124,1.062 47.6%,.996 53%,.967,.954,.957,.97 68%,1.003 80%,1.012,1.013,1.007,1);
  }
}

/* Motion preference. The OS prefers-reduced-motion is the accessibility floor;
 * an in-app setting (data-motion on <html>, FOUC-applied) lets the user choose:
 *   full       — everything (default)
 *   essential  — keep interaction feedback, drop decorative/ambient motion
 *                (anything tagged [data-decorative]/.motion-decorative)
 *   off        — collapse all motion, same outcome without movement.            */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    animation-delay: 0s !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
}
html[data-motion="off"] *, html[data-motion="off"] *::before, html[data-motion="off"] *::after {
  animation-duration: .01ms !important; animation-iteration-count: 1 !important;
  animation-delay: 0s !important;
  transition-duration: .01ms !important; scroll-behavior: auto !important;
}
html[data-motion="essential"] [data-decorative],
html[data-motion="essential"] .motion-decorative { animation: none !important; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* Cross-document view transitions (Chromium 126+): a soft cross-fade between
 * routes. No-op on browsers without support (normal navigation). Decorative, so
 * collapsed under Off / Essential / reduced-motion.
 * The app chrome (top bar, bottom bar) gets its own named transition groups so
 * it reads as PERSISTENT across navigations — only the content cross-fades,
 * the navigation bar stays put (M3: the nav bar is fixed chrome, not page
 * content). */
@view-transition { navigation: auto; }
.topbar { view-transition-name: shell-topbar; }
.bottomnav { view-transition-name: shell-bottomnav; }
::view-transition-old(root), ::view-transition-new(root) {
  animation-duration: var(--dur-medium); animation-timing-function: var(--ease-emphasized);
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation-duration: .01ms !important; }
}
html[data-motion="off"] ::view-transition-group(*), html[data-motion="off"] ::view-transition-old(*), html[data-motion="off"] ::view-transition-new(*),
html[data-motion="essential"] ::view-transition-group(*), html[data-motion="essential"] ::view-transition-old(*), html[data-motion="essential"] ::view-transition-new(*) {
  animation-duration: .01ms !important;
}

.shell-icon { width: 1.4em; height: 1.4em; stroke: currentColor; stroke-width: 2;
  fill: none; stroke-linecap: round; stroke-linejoin: round; flex: 0 0 auto; }

/* Skip link uses --ink/--paper (the body text contrast pair) so it clears AA in
   every app + theme — white-on-accent failed where an app's accent is light
   (e.g. sage/sea in dark, 3.08:1). */
.skip-link { position: absolute; left: -999px; top: 0; z-index: 200; background: var(--ink);
  color: var(--paper); padding: 10px 16px; border-radius: 0 0 var(--r-sm) 0; font-weight: 800; }
.skip-link:focus { left: 0; }

/* =================== Topbar =================== */
.topbar { position: sticky; top: 0; z-index: var(--z-topbar); background: var(--tb);
  backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); }
/* M3 small top app bar: 64dp container, 8dp grid, 48dp touch targets. */
.topbar-inner { display: flex; align-items: center; gap: 4px; padding: 8px;
  min-height: 64px; padding-top: calc(8px + env(safe-area-inset-top)); }
.icon-btn { width: 48px; height: 48px; display: grid; place-items: center; flex: 0 0 auto;
  border: 1px solid transparent; background: transparent; border-radius: 999px;
  color: var(--ink); cursor: pointer; }
/* M3 state layers: translucent on-surface overlays (hover 8% / pressed 10%) that
   read correctly over any surface in either theme. */
.icon-btn:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }
.icon-btn:active { background: color-mix(in srgb, var(--ink) 11%, transparent); }
.icon-btn:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
.icon-btn .shell-icon { width: 24px; height: 24px; }
/* Title = the current screen (title-large ≈ 22sp), left-aligned after the menu. */
.appbar-title { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: .3rem;
  margin-left: 4px; font: 700 var(--type-title-large) var(--display);
  letter-spacing: -.01em; color: var(--ink);
  /* overflow lives on the child span (below), not here — clipping the box
     itself at line-height 1.1 cut descenders (e.g. the "g" in "Einstellungen"). */
  white-space: nowrap; }
/* Action buttons live in the bar's trailing slot — never full-width (the shell's
   own .btn is width:100% for stacked dialog/settings buttons; reset it here). */
.topbar-inner .btn { width: auto; flex: 0 0 auto; }
.appbar-title > span, .appbar-title #active-list-name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.brand, .brand-link { display: flex; align-items: center; gap: 7px; text-decoration: none;
  color: var(--ink); }
.brand-emoji { font-size: 1.3rem; line-height: 1; }
.topbar-below { padding: 0 16px 12px; }

.context-chip { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-pill);
  padding: 7px 12px; font: inherit; font-weight: 700; font-size: .85rem; color: var(--ink);
  box-shadow: var(--shadow-sm); max-width: 56vw; }
.context-chip > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.context-chip .chev { font-size: .7rem; color: var(--ink-faint); }

/* =================== Main =================== */
.shell-main { padding: 16px 16px 24px; }
.shell-main.has-bottomnav { padding-bottom: calc(65px + env(safe-area-inset-bottom)); }

/* =================== Bottom tab bar =================== */
/* M3 Expressive flexible navigation bar (owner-picked "variant B"): 64dp tall
   with vertical icon+label — ~20% more content space than the 80dp baseline,
   labels stay always-visible. Pill active-indicator behind the icon. */
.bottomnav { position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-bottomnav);
  display: flex; background: var(--tb); backdrop-filter: blur(12px);
  border-top: 1px solid var(--line); padding-bottom: env(safe-area-inset-bottom); }
.tab { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 8px 0 10px; background: transparent; border: 0; cursor: pointer;
  color: var(--ink-faint); font: inherit; text-decoration: none; }
.tab-indicator { position: relative; width: 56px; height: 28px; border-radius: 999px;
  display: grid; place-items: center; }
/* Pill background lives on ::before so the M3 active-indicator can scale in on
   selection without distorting the icon (which sits above it, z-index 1). */
.tab-indicator::before { content: ""; position: absolute; inset: 0; border-radius: 999px;
  background: transparent; z-index: 0; transition: background-color var(--dur-short) var(--ease-standard); }
.tab .shell-icon { width: 24px; height: 24px; position: relative; z-index: 1;
  transition: stroke-width var(--dur-short) var(--ease-standard); }
/* M3 selected-icon emphasis: spec swaps outlined → filled on selection; these
   are stroke sprites, so the equivalent is a heavier stroke on the active tab
   (and the active drawer destination below). */
.tab[aria-current="page"] .shell-icon { stroke-width: 2.5; }
.tab-label { font-size: var(--type-label-medium-size); line-height: 16px; font-weight: 600; letter-spacing: .01em; }
/* Nav tabs are <a>; kill any app-level a:hover/:focus underline in every state
   (the flicker you saw was the underline toggling with hover/focus). */
.bottomnav .tab, .bottomnav .tab:hover, .bottomnav .tab:focus, .bottomnav .tab:active,
.bottomnav .tab:hover .tab-label, .bottomnav .tab:focus .tab-label { text-decoration: none; }
/* Drawer menu button: rail-only (compact keeps the top app bar hamburger). */
.rail-menu { display: none; }
.tab[aria-current="page"] { color: var(--acc-deep); }
.tab[aria-current="page"] .tab-indicator::before { background: var(--acc-soft); }
.tab[aria-current="page"] .tab-label { font-weight: 800; }
.tab:hover:not([aria-current="page"]) .tab-indicator::before { background: color-mix(in srgb, var(--ink) 8%, transparent); }
.tab:active .tab-indicator::before { background: color-mix(in srgb, var(--ink) 11%, transparent); }
.tab[aria-current="page"]:hover .tab-indicator::before { background: color-mix(in srgb, var(--acc) 24%, var(--acc-soft)); }
.tab:focus-visible { outline: 2px solid var(--acc); outline-offset: -3px; border-radius: 8px; }

/* =================== Adaptive navigation (M3 window size classes) ===========
 * compact  (<600px): bottom navigation bar (above) + modal drawer.
 * medium+ (≥600px):  the same bar becomes a vertical navigation rail (80dp) on
 *                    the left; the top app bar + content shift beside it.
 * expanded (≥840px): content gets a comfortable max-width so it never stretches.
 * Pure CSS — the same markup restyles, no JS branching. Only bottom-bar apps
 * carry `body.has-rail`; drawer-only apps (e.g. single-view) are unaffected. */
@media (min-width: 600px) {
  body.has-rail .bottomnav {
    flex-direction: column; justify-content: flex-start;
    top: 0; right: auto; bottom: 0; width: 80px; gap: 4px;
    border-top: 0; border-right: 1px solid var(--line);
    padding: calc(12px + env(safe-area-inset-top)) 0 12px;
  }
  body.has-rail .bottomnav .tab { flex: 0 0 auto; padding: 8px 0; }
  /* Rail keeps the M3 rail indicator geometry (56×32) — the compact bar's
     28dp indicator is a bottom-bar-only economy. */
  body.has-rail .bottomnav .tab-indicator { width: 56px; height: 32px; }
  body.has-rail .topbar { margin-left: 80px; }
  body.has-rail .shell-main { margin-left: 80px; padding-bottom: 24px; }
  /* The rail carries primary nav + a menu button that opens the drawer, so the
     app bar's menu button is redundant at this size — hide it. */
  body.has-rail #shell-hamburger { display: none; }
  /* M3 nav rail: menu button above the destinations — the only route to the
     drawer sections (settings/legal/session) once the hamburger is hidden. */
  body.has-rail .bottomnav .rail-menu { display: inline-flex; margin-bottom: 8px; }
}
@media (min-width: 840px) {
  /* Expanded: content gets a comfortable max-width so it never stretches. */
  /* Drawer-only apps centre their content; rail apps keep the 80dp rail offset
     (margin-inline:auto would cancel it) and pad inside the capped width. */
  body:not(.has-rail) .shell-main { margin-inline: auto; max-width: 1100px; }
  body.has-rail .shell-main { max-width: 1180px; padding-left: 32px; padding-right: 32px; }
}

/* =================== Drawer + scrim =================== */
.shell-scrim { position: fixed; inset: 0; z-index: var(--z-scrim); background: rgba(8,16,20,.46);
  opacity: 0; visibility: hidden; transition: opacity var(--dur-medium) var(--ease-standard); }
body.drawer-open .shell-scrim { opacity: 1; visibility: visible; }
.shell-drawer { position: fixed; top: 0; bottom: 0; left: 0; z-index: var(--z-drawer);
  width: 80%; max-width: 320px; background: var(--card); border-right: 1px solid var(--line);
  box-shadow: var(--shadow-lg); display: flex; flex-direction: column; visibility: hidden;
  transform: translateX(-104%); outline: none;
  /* visibility must not flip until the slide-out transform finishes, or the
     panel vanishes instantly on close instead of animating away. */
  transition: transform var(--dur-long) var(--ease-emphasized-decel), visibility 0s linear var(--dur-long); }
body.drawer-open .shell-drawer { transform: none; visibility: visible;
  transition: transform var(--dur-long) var(--ease-emphasized-decel), visibility 0s linear; }
/* M3 drawer header: the brand headline lives here (not in the app bar). */
.drawer-brand { display: flex; align-items: center; gap: .55rem; text-decoration: none;
  padding: calc(18px + env(safe-area-inset-top)) 18px 14px; color: var(--ink);
  font-family: var(--display); font-weight: 700; font-size: 1.3rem; letter-spacing: -.01em;
  border-bottom: 1px solid var(--line); }
/* wolf-labs paw mark: accent-coloured next to the app name. On drawer open the
   print appears the way a track is left: the four toes tap in one after the
   other, then the pad presses down and settles. Decorative, so it is dropped
   under Essential alongside the nav landing animations. */
.drawer-brand .brand-paw { width: 26px; height: 26px; color: var(--acc); }
/* STALK v4-A (brand/stalk-animation.css, owner-approved): toes tap in one
   after another → claws extend from their bases → the W-pad presses in as the
   closing beat. Base delay lets the drawer slide most of the way in first. */
body.drawer-open .drawer-brand .brand-paw .np-toe {
  animation: np-toe-tap 280ms var(--ease-emphasized-decel) both; transform-origin: center; }
body.drawer-open .drawer-brand .brand-paw .np-t1 { animation-delay: 140ms; }
body.drawer-open .drawer-brand .brand-paw .np-t2 { animation-delay: 210ms; }
body.drawer-open .drawer-brand .brand-paw .np-t3 { animation-delay: 280ms; }
body.drawer-open .drawer-brand .brand-paw .np-t4 { animation-delay: 350ms; }
@keyframes np-toe-tap {
  0%   { transform: scale(0); opacity: 0; }
  60%  { transform: scale(1.3); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
body.drawer-open .drawer-brand .brand-paw .np-claw {
  animation: np-claw-dig 300ms var(--ease-emphasized-accel) both; transform-origin: 50% 100%; }
body.drawer-open .drawer-brand .brand-paw .np-c1 { animation-delay: 520ms; }
body.drawer-open .drawer-brand .brand-paw .np-c2 { animation-delay: 590ms; }
body.drawer-open .drawer-brand .brand-paw .np-c3 { animation-delay: 660ms; }
body.drawer-open .drawer-brand .brand-paw .np-c4 { animation-delay: 730ms; }
@keyframes np-claw-dig {
  0%   { transform: scaleY(0); opacity: 0; }
  45%  { opacity: 1; }
  70%  { transform: scaleY(1.15); }
  100% { transform: scaleY(1); opacity: 1; }
}
body.drawer-open .drawer-brand .brand-paw .np-wpad {
  animation: np-wpad-press 460ms var(--ease-emphasized-decel) 1080ms both; transform-origin: 50% 40%; }
@keyframes np-wpad-press {
  0%   { transform: translateY(-2.5px) scale(.55); opacity: 0; }
  55%  { transform: translateY(.4px) scale(1.07); opacity: 1; }
  78%  { transform: translateY(0) scale(.98); }
  100% { transform: none; opacity: 1; }
}
html[data-motion="essential"] body.drawer-open .drawer-brand .brand-paw * { animation: none; }
/* Family signature at the drawer's foot: same mark, whisper-quiet. */
.drawer-family { display: flex; align-items: center; justify-content: center; gap: 6px;
  padding: 12px 0 calc(14px + env(safe-area-inset-bottom)); border-top: 1px solid var(--line);
  color: var(--ink-faint); font: 700 var(--type-label-small) var(--body);
  letter-spacing: .14em; }
.drawer-family .shell-icon { width: 14px; height: 14px; }
.drawer-profile { margin-top: 10px; padding: 10px 18px 16px; border-bottom: 1px solid var(--line);
  display: flex; align-items: center; gap: 12px; }
.drawer-nav { flex: 1; overflow-y: auto; padding: 8px 12px 16px; }
/* Section subheader (M3 title-small) */
.drawer-section { font-size: .7rem; text-transform: uppercase; letter-spacing: .1em;
  color: var(--ink-faint); font-weight: 800; margin: 16px 16px 6px; }
/* Heading-less group spacing (settings: a label would duplicate the link). */
.drawer-sep { margin-top: 14px; }
/* M3 destination: 56dp tall, full-pill active indicator (secondaryContainer). */
.drawer-link { display: flex; align-items: center; gap: 14px; min-height: 56px; padding: 0 16px;
  border-radius: 999px; color: var(--ink); font: inherit; font-weight: 600; font-size: .95rem;
  cursor: pointer; text-decoration: none; border: 0; background: transparent; width: 100%; text-align: left; }
.drawer-link:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }
.drawer-link:active { background: color-mix(in srgb, var(--ink) 11%, transparent); }
.drawer-link.active:hover { background: color-mix(in srgb, var(--acc) 22%, var(--acc-soft)); }
.drawer-link:focus-visible { outline: 2px solid var(--acc); outline-offset: -2px; }
.drawer-link.active { background: var(--acc-soft); color: var(--acc-deep); }
.drawer-link.active .shell-icon { color: var(--acc-deep); stroke-width: 2.5; }
.drawer-link.danger { color: var(--a2); }
.drawer-link .shell-icon { width: 24px; height: 24px; }
.drawer-link form { margin: 0; display: contents; }

/* =================== Avatar =================== */
.avatar { width: 46px; height: 46px; border-radius: 50%; background: var(--acc); color: #fff;
  display: grid; place-items: center; font-family: var(--display); font-weight: 700;
  font-size: 1.1rem; flex: 0 0 auto; box-shadow: var(--shadow-sm); overflow: hidden; }
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.identity b { font-family: var(--body); font-weight: 800; font-size: 1.02rem; color: var(--ink);
  display: block; line-height: 1.2; letter-spacing: -.005em; }
.identity span { font-size: var(--type-label-medium-size); color: var(--ink-soft); font-weight: 700; }

/* =========== Drawer appearance/language control (stacked) =========== */
/* A label row (icon + text) with the segmented control on its own full-width
   row beneath — so a 3-option theme seg never overflows the drawer. */
.drawer-control { padding: 9px 10px 6px; }
.drawer-control-head { display: flex; align-items: center; gap: 12px; color: var(--ink);
  font-weight: 700; font-size: .92rem; margin-bottom: 9px; }
.drawer-control-head .shell-icon { width: 21px; height: 21px; }
.mini-seg { display: flex; gap: 3px; width: 100%; background: var(--paper-2);
  border-radius: 10px; padding: 3px; }
.mini-seg button { flex: 1; font: inherit; font-size: .76rem; font-weight: 800; padding: 7px 6px;
  border-radius: 7px; border: 0; background: transparent; color: var(--ink-soft); cursor: pointer; }
.mini-seg button[aria-pressed="true"] { background: var(--card); color: var(--ink); box-shadow: var(--shadow-sm); }
.mini-seg button:focus-visible { outline: 2px solid var(--acc); outline-offset: 1px; }

/* =================== Segmented control (settings) =================== */
.seg-control { display: flex; background: var(--paper-2); border: 1px solid var(--line);
  border-radius: 12px; padding: 4px; gap: 3px; }
.seg-option { flex: 1; font: inherit; font-weight: 800; font-size: .85rem; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; min-height: 44px;  /* WCAG/M3 tap target */
  border: 0; background: transparent; color: var(--ink-soft); padding: 9px 6px; border-radius: 9px;
  /* Spring press feedback — segmented buttons are interactive, so they get motion
     (the motion-coverage gate expects a spring transform here). */
  transition: color .15s, background .15s, box-shadow .15s, transform var(--dur-medium) var(--spring-bouncy); }
.seg-option:active { transform: scale(.94); transition: transform 90ms var(--ease-emphasized-accel); }
.seg-option:hover:not([aria-pressed="true"]) { color: var(--ink); background: color-mix(in srgb, var(--ink) 6%, transparent); }
.seg-option[aria-pressed="true"] { background: var(--card); color: var(--ink); box-shadow: var(--shadow-sm); }
.seg-option:focus-visible { outline: 2px solid var(--acc); outline-offset: 1px; }

/* =================== Cards / settings rows =================== */
.shell-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow); padding: 16px; margin-bottom: 12px; }
.shell-card-h { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
/* Section label — matches the apps' established Nunito uppercase section titles
   (the app family reserves the Fraunces display font for page titles + brand,
   not in-card section headers). Keeps one heading treatment per page. */
.shell-card-h h3 { font-family: var(--body); font-weight: 800; font-size: .82rem;
  text-transform: uppercase; letter-spacing: .06em; margin: 0; color: var(--ink-soft); }
.settings-hero { display: flex; align-items: center; gap: 13px; margin-bottom: 16px; }
.settings-hero .avatar { width: 54px; height: 54px; font-size: 1.3rem; box-shadow: var(--shadow); }
.field { margin-bottom: 13px; }
.field.last { margin-bottom: 0; }
.field > label { display: block; font-size: .78rem; font-weight: 800; color: var(--ink-soft);
  margin: 0 2px 7px; text-transform: uppercase; letter-spacing: .05em; }
.link-row { display: flex; align-items: center; gap: 10px; padding: 12px 2px;
  border-top: 1px solid var(--line); font: inherit; font-weight: 700; color: var(--ink);
  cursor: pointer; text-decoration: none; background: transparent; border-left: 0; border-right: 0; width: 100%; }
.link-row:hover { background: color-mix(in srgb, var(--ink) 6%, transparent); }
.link-row:first-of-type { border-top: 0; }
.link-row .arr { margin-left: auto; color: var(--ink-faint); }
.link-row.danger { color: var(--a2); }

/* =================== Inputs / buttons =================== */
.input { width: 100%; font: inherit; font-weight: 600; padding: 11px 13px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--paper); color: var(--ink); }
.input::placeholder { color: var(--ink-faint); }
.input:focus-visible { outline: 2px solid var(--acc); outline-offset: 1px; border-color: var(--acc); }
.btn { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%;
  font: inherit; font-weight: 800; font-size: .95rem; padding: 13px; border-radius: 13px;
  min-height: 44px;  /* WCAG/M3 tap target */
  border: 0; cursor: pointer; }
.btn-primary { background: var(--acc); color: #fff; box-shadow: var(--shadow); }
.btn-ghost { background: var(--card); color: var(--ink); border: 1px solid var(--line); }
.btn-danger { background: var(--a2); color: #fff; }
/* Buttons draw from the motion tokens like everything else: spring release,
   crisp accelerated press (M3 pressed state responds immediately). */
.btn { transition: filter var(--dur-short) var(--ease-standard),
  background-color var(--dur-short) var(--ease-standard),
  border-radius var(--dur-medium) var(--spring-snappy),
  transform var(--dur-medium) var(--spring-bouncy); }
.btn-primary:hover, .btn-danger:hover { filter: brightness(1.07); }
.btn-ghost:hover { background: color-mix(in srgb, var(--ink) 6%, var(--card)); }
.btn:active { transform: scale(.985); filter: brightness(.96); border-radius: var(--shape-sm);
  transition: transform 90ms var(--ease-emphasized-accel), border-radius 90ms var(--ease-emphasized-accel),
    filter 90ms var(--ease-standard); }
.btn:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }

/* =================== Login =================== */
.login-wrap { min-height: 70vh; display: flex; flex-direction: column; justify-content: center; padding: 24px 18px; }
.login-card { background: var(--card); border: 1px solid var(--line); border-radius: 22px;
  box-shadow: var(--shadow-lg); padding: 26px 22px; width: 100%; max-width: 420px; margin: 0 auto; }
.login-brand { text-align: center; margin-bottom: 18px; }
.login-brand .em { font-size: 2.6rem; }
.login-brand .login-logo { display: inline-flex; }
.login-brand .login-logo .shell-icon { width: 44px; height: 44px; color: var(--acc-deep); }
.login-brand img.login-logo { width: 56px; height: 56px; }
/* ---- Product-logo entrance HELPERS (generic motion library) --------------
 * The logo itself is APP-OWNED (templates/_app_logo.html) — the shell stays
 * product-agnostic and only ships reusable one-shot keyframes so app logos
 * share the family's motion language. App CSS applies them to its own pl-*
 * parts, e.g.
 *   .login-brand .product-logo .pl-bowl { animation: pl-pop 420ms
 *     var(--ease-emphasized-decel) both; transform-origin: 50% 100%; }
 * All keyframes end at identity (no held composited transforms — see the
 * .nav-settled rationale); collapse under Off/Essential globally. */
.login-brand .product-logo [class*="pl-"] { transform-box: fill-box; transform-origin: center; }
@keyframes pl-pop {
  0%   { transform: scale(.7); opacity: 0; }
  60%  { transform: scale(1.06); opacity: 1; }
  100% { transform: none; opacity: 1; }
}
@keyframes pl-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes pl-rise {
  0%   { transform: translateY(3px); opacity: 0; }
  55%  { opacity: 1; }
  100% { transform: none; opacity: 1; }
}
.login-brand h2 { font-family: var(--display); font-weight: 700; font-size: 1.5rem; margin: 6px 0 2px; color: var(--ink); }
.login-brand p { margin: 0; color: var(--ink-soft); font-size: .85rem; font-weight: 700; }
.tabs { display: flex; gap: 4px; background: var(--paper-2); border-radius: 11px; padding: 4px; margin-bottom: 16px; }
.tabs button { flex: 1; font: inherit; font-weight: 800; font-size: var(--type-label-large-size); border: 0;
  background: transparent; color: var(--ink-soft); padding: 9px; border-radius: 8px; cursor: pointer; }
.tabs button[aria-pressed="true"] { background: var(--card); color: var(--ink); box-shadow: var(--shadow-sm); }
.divider { display: flex; align-items: center; gap: 10px; margin: 15px 0; color: var(--ink-faint);
  font-size: var(--type-label-medium-size); font-weight: 700; }
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.muted-link { display: block; text-align: center; margin-top: 14px; color: var(--acc-deep);
  font-weight: 700; font-size: .82rem; text-decoration: none; }

/* =================== Modals =================== */
/* Namespaced .shell-modal* so the shared kit never collides with a host app's
   own .modal vocabulary (packliste uses .modal for the inner card, not the
   overlay — an un-namespaced .modal here leaked display:flex into its cards). */
.shell-modal { position: fixed; inset: 0; z-index: var(--z-modal); display: flex; align-items: center;
  justify-content: center; padding: 20px; background: rgba(8,16,20,.5); }
.shell-modal[hidden] { display: none; }
.shell-modal-box { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg); padding: 22px; width: 100%; max-width: 380px; }
.shell-modal-box h3 { font-family: var(--display); font-weight: 600; font-size: 1.2rem; margin: 0 0 8px; color: var(--ink); }
.shell-modal-box p { margin: 0 0 18px; color: var(--ink-soft); font-size: var(--type-body-medium-size); line-height: 1.5; }
.shell-modal-actions { display: flex; gap: 10px; }
.shell-modal-actions .btn { width: auto; flex: 1; }

/* =================== Toast =================== */
/* Centre with left/right:0 + margin auto, NOT left:50%: a fixed element with
   only left:50% and no width gets a shrink-to-fit available width of just 50vw,
   so a longer message collapses tall + narrow instead of using max-width. */
#toast { position: fixed; left: 0; right: 0; margin-inline: auto; width: fit-content;
  bottom: calc(20px + env(safe-area-inset-bottom));
  z-index: 200; background: var(--ink); color: var(--paper);
  padding: 12px 18px; border-radius: 16px; box-shadow: var(--shadow-lg);
  font-weight: 700; font-size: var(--type-label-large-size); display: flex; align-items: center; gap: 12px;
  max-width: min(520px, calc(100vw - 32px)); }
#toast[hidden] { display: none; }
/* slides up from below with a little overshoot when shown (centering stays on
 * margin-inline:auto, so we animate translateY only). */
#toast:not([hidden]) { animation: shell-toast-in var(--dur-long) var(--spring-bouncy); }
@keyframes shell-toast-in { from { transform: translateY(150%); } to { transform: translateY(0); } }
#toast.error { background: var(--a2); color: #fff; }
.toast-action-btn { font: inherit; font-weight: 800; background: transparent; border: 0;
  color: var(--acc-deep); cursor: pointer; }

/* =================== Auth-page language switcher =================== */
/* Compact DE/EN pill group shown in the top app bar on anonymous auth pages
   (login / forgot / reset / join). Wired by shell.js via [data-lang-control]. */
.lang-switch { display: flex; gap: 2px; background: var(--paper-2); border-radius: var(--r-pill); padding: 3px; flex: none; }
.lang-pill { font: inherit; font-weight: 800; font-size: .72rem; border: 0; background: transparent;
  color: var(--ink-soft); padding: 5px 10px; border-radius: var(--r-pill); cursor: pointer; }
.lang-pill[aria-pressed="true"] { background: var(--card); color: var(--ink); box-shadow: var(--shadow-sm); }

/* Anonymous auth screens: centre the card within the shell main. */
.auth-page .shell-main { display: flex; flex-direction: column; align-items: center; padding-top: 2rem; }
.auth-page .login-card, .auth-page .join-card { width: 100%; max-width: 420px; margin: 0; }

/* =================== Motion utilities (shared) =================== *
 * Reusable joyful micro-interactions. Apps opt in via these classes; the
 * tokens at the top of this file drive every timing/curve. All collapse to
 * instant under the global prefers-reduced-motion rule above.               */

/* Press feedback: shell chrome controls spring back a touch when tapped, and
   round shapes morph toward a rounded rect while pressed (M3 Expressive shape
   morph) — the radius springs back with the scale on release. */
.icon-btn, .drawer-link, .context-chip { transition: transform var(--dur-medium) var(--spring-bouncy),
  border-radius var(--dur-medium) var(--spring-snappy),
  background-color var(--dur-short) var(--ease-standard); }
.icon-btn:active, .drawer-link:active, .context-chip:active { transform: scale(.92);
  transition: transform 90ms var(--ease-emphasized-accel), border-radius 90ms var(--ease-emphasized-accel); }
.icon-btn:active { border-radius: var(--shape-lg); }
.drawer-link:active { border-radius: var(--shape-lg); }
.context-chip:active { border-radius: var(--shape-md); }
.tab-indicator { transition: transform var(--dur-medium) var(--spring-bouncy); }
.tab:active .tab-indicator { transform: scale(.86); transition: transform 90ms var(--ease-emphasized-accel); }
/* M3 Expressive selection: on the landed page (nav is server-rendered, so this
   fires once per navigation) the active-indicator pill squashes-and-stretches in
   behind the icon, and the icon does a joyful multi-bounce with a rotation wobble
   (spatial motion with personality, not a flat zoom). The bounce lives in the
   keyframes, so the easing is a smooth emphasized-decel (a spring easing on top
   would double up). Collapses under Off/reduced-motion globally; the decorative
   entrance is also dropped in Essential (press feedback is kept). */
/* The pill does a confident, crisp expand (no jelly). */
.tab[aria-current="page"] .tab-indicator::before { animation: nav-ind-in 420ms var(--ease-emphasized-decel) both; }
@keyframes nav-ind-in {
  0%   { transform: scaleX(.35) scaleY(.8); opacity: .2; }
  55%  { transform: scaleX(1.05) scaleY(1); opacity: 1; }
  100% { transform: scaleX(1) scaleY(1); opacity: 1; }
}

/* DEFAULT icon motion = confident pop with a small overshoot. Restrained, not
   childish — the right baseline for a technical app (see the memory
   "animation-character-fits-content"). Icons opt into a personality via
   [data-icon]; each override reinforces what the icon *means*. Two families:
   scuba (dive/tank/cert/pin/stats) reads atmospheric + measured; beikost
   (book/doc) reads playful; gs (wrench) reads mechanical. Amplitudes bumped from
   the first pass — the earlier motion was too subtle to notice (owner feedback:
   "I only see the pin drop").

   The same personalities fire in the DRAWER: when it opens, the active
   destination's icon plays its animation once (drawer-open toggles the selector
   match, so it restarts per open). --nav-delay lets the drawer variant wait for
   the panel to slide most of the way in; tabs play immediately (delay 0). */
body.drawer-open .drawer-link.active .shell-icon { --nav-delay: 220ms; }
.tab[aria-current="page"] .shell-icon,
body.drawer-open .drawer-link.active .shell-icon { animation: nav-pop 400ms var(--ease-emphasized-decel) var(--nav-delay, 0ms) both; }
/* Once shell.js has seen every animation in the host finish, it marks the host
   settled: dropping the animations releases the fill-mode hold so the icon
   leaves its composited layer and re-rasterizes sharp at device DPI. All
   entrances end at identity, so removal is visually seamless. */
.nav-settled .shell-icon, .nav-settled .shell-icon * { animation: none !important; }
@keyframes nav-pop {
  0%   { transform: scale(.72); }
  50%  { transform: scale(1.14); }
  78%  { transform: scale(.97); }
  100% { transform: scale(1); }
}
/* Location pin — drops in and settles with a bounce, like a map marker landing. */
.tab[aria-current="page"][data-icon="pin"] .shell-icon,
body.drawer-open .drawer-link.active[data-icon="pin"] .shell-icon { animation: nav-drop 560ms var(--ease-emphasized-decel) var(--nav-delay, 0ms) both; transform-origin: 50% 100%; }
@keyframes nav-drop {
  0%   { transform: translateY(-13px) scale(.9); opacity: 0; }
  45%  { transform: translateY(0) scale(1); opacity: 1; }
  62%  { transform: translateY(-4px) scale(1.02); }
  80%  { transform: translateY(0) scaleY(.92) scaleX(1.06); }  /* squash on impact */
  100% { transform: translateY(0) scale(1); }
}
/* Stats — bars shoot up from the baseline, overshoot, then settle: growing data.
   (Sprite fallback only — the inline icon grows each bar individually below.) */
.tab[aria-current="page"][data-icon="stats"] .shell-icon:not(.np-icon),
body.drawer-open .drawer-link.active[data-icon="stats"] .shell-icon:not(.np-icon) { animation: nav-rise 540ms var(--ease-emphasized-decel) var(--nav-delay, 0ms) both; transform-origin: 50% 100%; }
@keyframes nav-rise {
  0%   { transform: translateY(9px) scaleY(.45); opacity: .15; }
  55%  { transform: translateY(-3px) scaleY(1.18); opacity: 1; }
  78%  { transform: translateY(1px) scaleY(.94); }
  100% { transform: translateY(0) scaleY(1); }
}
/* Certification — a stamp pressing down onto the card. Crisp, technical.
   (Sprite fallback — inline: the box pops and the check draws itself below.) */
.tab[aria-current="page"][data-icon="cert"] .shell-icon:not(.np-icon),
body.drawer-open .drawer-link.active[data-icon="cert"] .shell-icon:not(.np-icon) { animation: nav-stamp 440ms var(--ease-emphasized-accel) var(--nav-delay, 0ms) both; }
@keyframes nav-stamp {
  0%   { transform: scale(1.4) rotate(-6deg); opacity: 0; }
  60%  { transform: scale(.9) rotate(0); opacity: 1; }
  78%  { transform: scale(1.05); }
  100% { transform: scale(1); }
}
/* Scuba tank — the icon gives a slow "pressure" bob while it exhales a stream of
   air bubbles that drift up and fade above it. Atmospheric, not a bounce. Three
   bubbles = one ::after + two box-shadows. Purely decorative. */
.tab[aria-current="page"][data-icon="tank"] .shell-icon,
body.drawer-open .drawer-link.active[data-icon="tank"] .shell-icon { animation: nav-tank 620ms var(--ease-emphasized-decel) var(--nav-delay, 0ms) both; transform-origin: 50% 100%; }
@keyframes nav-tank {
  0%   { transform: scaleY(.9) scaleX(1.04); }
  45%  { transform: scaleY(1.04) scaleX(.98); }
  100% { transform: scaleY(1) scaleX(1); }
}
/* (The old ::after bubble hack is gone — the inline tank icon exhales real
   bubbles from its own valve; see the part-animation layer below.) */
/* Beikost book (recipes) — the cover swings open on its spine. Playful, warmer
   character to match a baby-food app (the counterpoint to scuba's restraint). */
.tab[aria-current="page"][data-icon="book"] .shell-icon,
body.drawer-open .drawer-link.active[data-icon="book"] .shell-icon { animation: nav-book 620ms var(--ease-emphasized-decel) var(--nav-delay, 0ms) both; transform-origin: 15% 50%; }
@keyframes nav-book {
  0%   { transform: perspective(240px) rotateY(-82deg) scale(.94); opacity: .3; }
  55%  { transform: perspective(240px) rotateY(10deg) scale(1.02); opacity: 1; }
  78%  { transform: perspective(240px) rotateY(-4deg); }
  /* End at none, not perspective(...) rotateY(0): a held 3D transform keeps the
     icon on its own composited layer and it stays soft after the motion. */
  100% { transform: none; }
}
/* Settings gear — a full mechanical 360° spin with a hair of overshoot, then it
   locks back to true. Literal + satisfying; the gear IS a gear. */
.tab[aria-current="page"][data-icon="gear"] .shell-icon:not(.np-icon),
body.drawer-open .drawer-link.active[data-icon="gear"] .shell-icon:not(.np-icon) { animation: nav-spin 760ms var(--ease-emphasized) var(--nav-delay, 0ms) both; transform-origin: 50% 50%; }
@keyframes nav-spin {
  0%   { transform: rotate(0deg); }
  82%  { transform: rotate(372deg); }
  100% { transform: rotate(360deg); }
}
/* Dive — the three wave humps rock side-to-side like a gentle current/swell,
   a couple of decaying cycles that settle. Atmospheric, on-water. */
.tab[aria-current="page"][data-icon="dive"] .shell-icon:not(.np-icon),
body.drawer-open .drawer-link.active[data-icon="dive"] .shell-icon:not(.np-icon) { animation: nav-wave 900ms var(--ease-standard) var(--nav-delay, 0ms) both; transform-origin: 50% 60%; }
@keyframes nav-wave {
  0%   { transform: translateX(0) rotate(0); }
  22%  { transform: translateX(-1.8px) rotate(-3deg); }
  48%  { transform: translateX(1.6px) rotate(2.6deg); }
  72%  { transform: translateX(-.9px) rotate(-1.3deg); }
  100% { transform: translateX(0) rotate(0); }
}
/* Home — anchored at the base, the house squashes so the ROOF dips down then
   springs up past rest and settles ("roof up/down"). */
.tab[aria-current="page"][data-icon="home"] .shell-icon:not(.np-icon),
body.drawer-open .drawer-link.active[data-icon="home"] .shell-icon:not(.np-icon) { animation: nav-roof 560ms var(--ease-emphasized-decel) var(--nav-delay, 0ms) both; transform-origin: 50% 100%; }
@keyframes nav-roof {
  0%   { transform: scaleY(1); }
  30%  { transform: scaleY(.8); }
  60%  { transform: scaleY(1.09); }
  80%  { transform: scaleY(.96); }
  100% { transform: scaleY(1); }
}
/* Document — the top sheet lifts off the stack with a small tilt and settles
   flat. Light and papery; playful family (beikost sources). */
.tab[aria-current="page"][data-icon="doc"] .shell-icon,
body.drawer-open .drawer-link.active[data-icon="doc"] .shell-icon { animation: nav-doc 520ms var(--ease-emphasized-decel) var(--nav-delay, 0ms) both; transform-origin: 50% 85%; }
@keyframes nav-doc {
  0%   { transform: translateY(8px) rotate(-8deg); opacity: .2; }
  55%  { transform: translateY(-2px) rotate(3deg); opacity: 1; }
  78%  { transform: translateY(.5px) rotate(-1.5deg); }
  100% { transform: translateY(0) rotate(0); }
}
/* Wrench — a torque twist: wind up, tighten past centre, ratchet back to true.
   Mechanical + restrained, the GS garage character (no bounce, just torque). */
.tab[aria-current="page"][data-icon="wrench"] .shell-icon,
body.drawer-open .drawer-link.active[data-icon="wrench"] .shell-icon { animation: nav-wrench 620ms var(--ease-emphasized-decel) var(--nav-delay, 0ms) both; transform-origin: 50% 50%; }
@keyframes nav-wrench {
  0%   { transform: rotate(0); }
  28%  { transform: rotate(-34deg); }
  60%  { transform: rotate(12deg); }
  82%  { transform: rotate(-5deg); }
  100% { transform: rotate(0); }
}

/* ---- Part-level icon motion (inline icons only) ------------------------- *
 * Icons rendered inline (.np-icon, see shell/nav_icons.py) expose sub-parts,
 * so the motion can come from WITHIN the icon: the roof dips, each stats bar
 * grows, the check draws itself, bubbles leave the tank valve. Same contexts
 * as the whole-icon layer (landed tab / drawer open), same --nav-delay slot;
 * per-part stagger rides on top via calc(). SVG sub-part transforms need
 * transform-box: fill-box or the origin is the icon's 0,0.                  */
.np-icon { overflow: visible; }
.np-icon [class*="np-"] { transform-box: fill-box; transform-origin: center; }
.np-icon .np-bub { opacity: 0; }             /* tank bubbles exist only mid-animation */

/* Where the parts carry the story, the whole icon stays still (the generic
   nav-pop would double the motion). Book/doc/tank/pin/wrench keep their
   whole-icon personality; parts play on top. */
.tab[aria-current="page"][data-icon="home"] .np-icon,
.tab[aria-current="page"][data-icon="dive"] .np-icon,
.tab[aria-current="page"][data-icon="stats"] .np-icon,
.tab[aria-current="page"][data-icon="cert"] .np-icon,
.tab[aria-current="page"][data-icon="gear"] .np-icon,
body.drawer-open .drawer-link.active[data-icon="home"] .np-icon,
body.drawer-open .drawer-link.active[data-icon="dive"] .np-icon,
body.drawer-open .drawer-link.active[data-icon="stats"] .np-icon,
body.drawer-open .drawer-link.active[data-icon="cert"] .np-icon,
body.drawer-open .drawer-link.active[data-icon="gear"] .np-icon { animation: none; }

/* Food (apple) — the fruit keeps the joyful default pop while the leaf gives a
   happy little flutter, like the apple just landed fresh. Beikost-playful. */
.tab[aria-current="page"][data-icon="food"] .np-leaf,
body.drawer-open .drawer-link.active[data-icon="food"] .np-leaf {
  transform-origin: 0% 100%;
  animation: np-leaf-flutter 640ms var(--ease-emphasized-decel) calc(var(--nav-delay, 0ms) + 160ms) both; }
@keyframes np-leaf-flutter {
  0%   { transform: rotate(0); }
  35%  { transform: rotate(-14deg); }
  62%  { transform: rotate(8deg); }
  82%  { transform: rotate(-3deg); }
  100% { transform: none; }
}

/* Home — the ROOF itself dips and springs back onto the walls. */
.tab[aria-current="page"][data-icon="home"] .np-roof,
body.drawer-open .drawer-link.active[data-icon="home"] .np-roof {
  animation: np-roof-dip 560ms var(--ease-emphasized-decel) var(--nav-delay, 0ms) both; }
@keyframes np-roof-dip {
  0%   { transform: translateY(0); }
  32%  { transform: translateY(2.6px) scaleY(.68); }
  62%  { transform: translateY(-1.4px) scaleY(1.12); }
  82%  { transform: translateY(.4px) scaleY(.97); }
  100% { transform: none; }
}

/* Stats — each bar grows out of the baseline, tallest last. The baseline
   draws in first so the bars have ground to stand on. */
.tab[aria-current="page"][data-icon="stats"] .np-base,
body.drawer-open .drawer-link.active[data-icon="stats"] .np-base {
  --np-len: 21; stroke-dasharray: 21;
  animation: np-draw 240ms var(--ease-emphasized) var(--nav-delay, 0ms) both; }
.tab[aria-current="page"][data-icon="stats"] .np-bar,
body.drawer-open .drawer-link.active[data-icon="stats"] .np-bar {
  transform-origin: 50% 100%;
  animation: np-bar-grow 520ms var(--ease-emphasized-decel) both; }
.tab[aria-current="page"][data-icon="stats"] .np-b1,
body.drawer-open .drawer-link.active[data-icon="stats"] .np-b1 { animation-delay: calc(var(--nav-delay, 0ms) + 100ms); }
.tab[aria-current="page"][data-icon="stats"] .np-b3,
body.drawer-open .drawer-link.active[data-icon="stats"] .np-b3 { animation-delay: calc(var(--nav-delay, 0ms) + 190ms); }
.tab[aria-current="page"][data-icon="stats"] .np-b2,
body.drawer-open .drawer-link.active[data-icon="stats"] .np-b2 { animation-delay: calc(var(--nav-delay, 0ms) + 280ms); }
@keyframes np-bar-grow {
  0%   { transform: scaleY(0); }
  62%  { transform: scaleY(1.18); }
  100% { transform: scaleY(1); }
}

/* Cert — the card pops onto the surface, then the check draws itself. */
.tab[aria-current="page"][data-icon="cert"] .np-box,
body.drawer-open .drawer-link.active[data-icon="cert"] .np-box {
  animation: np-box-pop 380ms var(--ease-emphasized-decel) var(--nav-delay, 0ms) both; }
@keyframes np-box-pop {
  0%   { transform: scale(.82); }
  60%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}
.tab[aria-current="page"][data-icon="cert"] .np-check,
body.drawer-open .drawer-link.active[data-icon="cert"] .np-check {
  --np-len: 9; stroke-dasharray: 9;
  animation: np-draw 340ms var(--ease-emphasized) calc(var(--nav-delay, 0ms) + 220ms) both; }

/* Dive — the swell rolls through the three humps (phase-offset bob) while the
   diver line dips into it. Atmospheric, no bounce. */
.tab[aria-current="page"][data-icon="dive"] .np-w1,
.tab[aria-current="page"][data-icon="dive"] .np-w2,
.tab[aria-current="page"][data-icon="dive"] .np-w3,
body.drawer-open .drawer-link.active[data-icon="dive"] .np-w1,
body.drawer-open .drawer-link.active[data-icon="dive"] .np-w2,
body.drawer-open .drawer-link.active[data-icon="dive"] .np-w3 {
  animation: np-wave-bob 760ms var(--ease-standard) var(--nav-delay, 0ms) both; }
.tab[aria-current="page"][data-icon="dive"] .np-w2,
body.drawer-open .drawer-link.active[data-icon="dive"] .np-w2 { animation-delay: calc(var(--nav-delay, 0ms) + 130ms); }
.tab[aria-current="page"][data-icon="dive"] .np-w3,
body.drawer-open .drawer-link.active[data-icon="dive"] .np-w3 { animation-delay: calc(var(--nav-delay, 0ms) + 260ms); }
@keyframes np-wave-bob {
  0%   { transform: translateY(0); }
  30%  { transform: translateY(-1.8px); }
  60%  { transform: translateY(1.1px); }
  100% { transform: translateY(0); }
}
.tab[aria-current="page"][data-icon="dive"] .np-diver,
body.drawer-open .drawer-link.active[data-icon="dive"] .np-diver {
  animation: np-diver-dip 900ms var(--ease-standard) var(--nav-delay, 0ms) both; }
@keyframes np-diver-dip {
  0%   { transform: rotate(0) translateY(0); }
  40%  { transform: rotate(-5deg) translateY(1px); }
  75%  { transform: rotate(2deg); }
  100% { transform: rotate(0) translateY(0); }
}

/* Tank — keeps its whole-icon pressure bob; on top, three bubbles leave the
   valve, rise past the icon's bounds (overflow: visible) and pop. */
.tab[aria-current="page"][data-icon="tank"] .np-bub,
body.drawer-open .drawer-link.active[data-icon="tank"] .np-bub {
  animation: np-bubble-rise 1100ms var(--ease-emphasized-decel) both; }
.tab[aria-current="page"][data-icon="tank"] .np-bub1,
body.drawer-open .drawer-link.active[data-icon="tank"] .np-bub1 { animation-delay: calc(var(--nav-delay, 0ms) + 120ms); }
.tab[aria-current="page"][data-icon="tank"] .np-bub2,
body.drawer-open .drawer-link.active[data-icon="tank"] .np-bub2 { animation-delay: calc(var(--nav-delay, 0ms) + 380ms); }
.tab[aria-current="page"][data-icon="tank"] .np-bub3,
body.drawer-open .drawer-link.active[data-icon="tank"] .np-bub3 { animation-delay: calc(var(--nav-delay, 0ms) + 640ms); }
@keyframes np-bubble-rise {
  0%   { transform: translateY(3px) scale(.4); opacity: 0; }
  25%  { opacity: 1; }
  75%  { opacity: .7; }
  100% { transform: translateY(-6px) scale(1.15); opacity: 0; }
}

/* Pin — the marker still drops (whole-icon nav-drop); the inner dot pops in
   once it has landed, like the location locking on. */
.tab[aria-current="page"][data-icon="pin"] .np-dot,
body.drawer-open .drawer-link.active[data-icon="pin"] .np-dot {
  animation: np-dot-pop 300ms var(--ease-emphasized-decel) calc(var(--nav-delay, 0ms) + 360ms) both; }
@keyframes np-dot-pop {
  0%   { transform: scale(0); opacity: 0; }
  60%  { transform: scale(1.5); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}

/* Gear — the teeth ring does the full mechanical revolution; the hub holds
   still, like a real gear on its axle. */
.tab[aria-current="page"][data-icon="gear"] .np-teeth,
body.drawer-open .drawer-link.active[data-icon="gear"] .np-teeth {
  animation: np-teeth-spin 760ms var(--ease-emphasized) var(--nav-delay, 0ms) both; }
@keyframes np-teeth-spin { to { transform: rotate(360deg); } }

/* Book — the cover still swings open (whole-icon nav-book); the title band
   then writes itself across the cover. */
.tab[aria-current="page"][data-icon="book"] .np-line,
body.drawer-open .drawer-link.active[data-icon="book"] .np-line {
  --np-len: 16; stroke-dasharray: 16;
  animation: np-draw 320ms var(--ease-emphasized) calc(var(--nav-delay, 0ms) + 300ms) both; }

/* Doc — the sheet still lifts (whole-icon nav-doc); its text lines then write
   in, top to bottom. */
.tab[aria-current="page"][data-icon="doc"] .np-l1,
body.drawer-open .drawer-link.active[data-icon="doc"] .np-l1 {
  --np-len: 6; stroke-dasharray: 6;
  animation: np-draw 260ms var(--ease-emphasized) calc(var(--nav-delay, 0ms) + 240ms) both; }
.tab[aria-current="page"][data-icon="doc"] .np-l2,
body.drawer-open .drawer-link.active[data-icon="doc"] .np-l2 {
  --np-len: 4; stroke-dasharray: 4;
  animation: np-draw 220ms var(--ease-emphasized) calc(var(--nav-delay, 0ms) + 400ms) both; }

/* Shared draw-in: dasharray/--np-len are set per part above. */
@keyframes np-draw { from { stroke-dashoffset: var(--np-len); } to { stroke-dashoffset: 0; } }

html[data-motion="essential"] .tab[aria-current="page"] .tab-indicator::before,
html[data-motion="essential"] .tab[aria-current="page"] .tab-indicator::after,
html[data-motion="essential"] .tab[aria-current="page"] .shell-icon,
html[data-motion="essential"] .tab[aria-current="page"] .shell-icon *,
html[data-motion="essential"] body.drawer-open .drawer-link.active .shell-icon,
html[data-motion="essential"] body.drawer-open .drawer-link.active .shell-icon * { animation: none; }

/* .u-press — opt-in press scale for app buttons */
.u-press { transition: transform var(--dur-medium) var(--spring-bouncy); }
.u-press:active { transform: scale(.92); transition: transform 90ms var(--ease-emphasized-accel); }

/* .u-pop — a one-shot celebratory bump (e.g. a count/badge changing) */
.u-pop { animation: shell-pop var(--dur-medium) var(--spring-bouncy); }
@keyframes shell-pop { 0% { transform: scale(1); } 40% { transform: scale(1.18); } 100% { transform: scale(1); } }

/* Staggered reveal: children of [data-stagger] cascade in on load. shell.js
 * sets --i on each child; here we turn it into an animation-delay.           */
[data-stagger] > * { animation: shell-enter var(--dur-long) var(--spring-snappy) both; animation-delay: calc(var(--i, 0) * 55ms); }
@keyframes shell-enter { from { opacity: 0; transform: translateY(-10px) scale(.97); } to { opacity: 1; transform: none; } }

/* Ripple: shell.js spawns a .shell-ripple span inside a .ripple element. */
.ripple { position: relative; overflow: hidden; }
.shell-ripple { position: absolute; border-radius: 50%; background: currentColor; opacity: .28;
  transform: scale(0); pointer-events: none; animation: shell-ripple var(--dur-long) var(--ease-emphasized-decel); }
@keyframes shell-ripple { to { transform: scale(2.4); opacity: 0; } }

/* Skeleton loading — bridge load times with a calm shimmer, then reveal. */
.skeleton { position: relative; overflow: hidden; background: var(--paper-2); border-radius: var(--r-sm); }
.skeleton::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 30%, rgba(255,255,255,.55) 50%, transparent 70%);
  background-size: 200% 100%; animation: shell-shimmer 1.2s linear infinite; }
.skeleton-line { height: .8rem; border-radius: var(--r-pill); }
.skeleton-circle { border-radius: 50%; }
@keyframes shell-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .skeleton::after { display: none; } }
/* content that replaces a skeleton fades up into place */
.skeleton-reveal { animation: shell-reveal var(--dur-long) var(--ease-emphasized-decel); }
@keyframes shell-reveal { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* =================== Expressive loading indicator =================== *
 * M3 Expressive replaces the indeterminate spinner with a rotating shape that
 * morphs through the shape library. Approximated here as an accent blob whose
 * uneven border-radius cycles while it rotates — CSS only, no SVG required.
 *   <span class="shell-loader" role="progressbar" aria-label="…"></span>
 * Division of labour: skeletons bridge content-shaped waits (lists, cards);
 * the loader signals action-shaped waits (submit, sync, reconnect). It is
 * FEEDBACK, not decoration — it stays under data-motion="essential" and
 * collapses to a static shape under off/reduced-motion (durations → ~0). */
.shell-loader { display: inline-block; width: 28px; height: 28px; flex: none;
  background: var(--acc); border-radius: 38% 62% 55% 45% / 45% 42% 58% 55%;
  animation: shell-loader-morph 2.4s var(--ease-emphasized) infinite,
    shell-loader-spin 1.6s linear infinite; }
.shell-loader.sm { width: 18px; height: 18px; }
.shell-loader.lg { width: 44px; height: 44px; }
@keyframes shell-loader-morph {
  0%, 100% { border-radius: 38% 62% 55% 45% / 45% 42% 58% 55%; }
  25%      { border-radius: 60% 40% 42% 58% / 55% 60% 40% 45%; }
  50%      { border-radius: 45% 55% 60% 40% / 40% 50% 50% 60%; }
  75%      { border-radius: 55% 45% 38% 62% / 60% 42% 58% 40%; }
}
@keyframes shell-loader-spin { to { transform: rotate(360deg); } }

/* =================== Floating-label field (M3 outlined) =================== *
 * Markup: <div class="float-field"><input placeholder=" " ...><label>…</label></div>
 * The placeholder=" " makes :placeholder-shown reflect emptiness. At rest the
 * label sits inside the field as the placeholder; on focus or when filled it
 * floats up to notch the top border, accent-coloured.
 *
 * KEY: the input fill is TRANSPARENT, so the field shows whatever surface it sits
 * on with no shading of its own — and the floated label masks the border by
 * painting that same surface (--float-surface, default --card). Because the input
 * has no fill, ANY per-input tint (e.g. the global soft ink-tint) can't create a
 * mismatched notch patch — the seam is seamless by construction. The doubled
 * .float-field.float-field raises specificity so transparent wins over the global
 * `input[type=…]` tint rule (equal specificity would otherwise lose on source
 * order). To place a float-field on a non-card surface, set --float-surface on it.
 * The float transition collapses under the motion preference like everything else. */
/* =================== Search field (shared) ===================
 * <div class="search-field"><input type="search" ...>
 *   <button class="search-clear" hidden aria-label="...">✕</button></div>
 * M3 search-bar pill with ONE clear affordance: the UA's native cancel button
 * is suppressed (Chromium renders its own ✕ on type=search — apps that added
 * their own ended up with two). shell.js toggles/wires .search-clear. */
.search-field { position: relative; }
.search-field > input { border-radius: 999px; padding-right: 48px; }
.search-field > input::-webkit-search-cancel-button,
.search-field > input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
.search-field > .search-clear {
  position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  min-width: 44px; min-height: 44px; border: none; background: transparent;
  color: var(--ink-soft); font-size: 1rem; line-height: 1; cursor: pointer;
  border-radius: 999px;
}
.search-field > .search-clear:hover { color: var(--ink); }

.float-field { position: relative; --float-surface: var(--card); }
.float-field.float-field > input,
.float-field.float-field > textarea,
.float-field.float-field > select {
  /* Symmetric vertical padding: the text is vertically CENTRED so the resting
     label (also centred, top:50%) sits exactly over where the text lands — no
     baseline offset. The floated label notches the TOP BORDER (top:0), so no
     reserved top gap is needed inside the field. */
  width: 100%; box-sizing: border-box; min-height: 52px;
  padding: .85rem .8rem; font: 600 1rem var(--body);
  background: transparent;
}
/* A textarea's text starts at the top, not centred — rest the label on the first
   line and give the floated label clearance above it. */
.float-field.float-field > textarea { min-height: 88px; padding-top: .85rem; }
.float-field > textarea ~ label { top: calc(.85rem + .6em); }
.float-field > label {
  position: absolute; left: .55rem; top: 50%; transform: translateY(-50%);
  transform-origin: left center; margin: 0; padding: 0 .3rem;
  max-width: calc(100% - 1.2rem);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  pointer-events: none; background: var(--float-surface);
  color: var(--ink-soft); font: 600 1rem var(--body);
  text-transform: none; letter-spacing: normal;
  transition: top var(--dur-medium) var(--ease-emphasized),
    transform var(--dur-medium) var(--ease-emphasized),
    color var(--dur-short) var(--ease-standard);
}
.float-field > input:focus ~ label,
.float-field > input:not(:placeholder-shown) ~ label,
.float-field > textarea:focus ~ label,
.float-field > textarea:not(:placeholder-shown) ~ label,
.float-field > select:focus ~ label,
.float-field > select:valid ~ label {
  top: 0; transform: translateY(-50%) scale(.78);
  /* Floated label is small accent text on the field surface — it must clear AA in
     both themes. Defaults to --acc-deep (bright-in-dark in most apps); an app whose
     --acc-deep is button-oriented (dark in dark mode) sets --float-label-accent to
     a surface-legible accent instead. */
  color: var(--float-label-accent, var(--acc-deep)); font-weight: 700;
}
