/* shell.css — the reScribe 2.0 shell and shared components
 * (docs/redesign-2.0/02-PATTERNS.md). Loaded AFTER app.css.
 *
 * Rules for this file (pinned by functions/test/shell.mjs):
 *   • reads ONLY the 2.0 token names from tokens.css (--s-*, --t-*, --accent*,
 *     --r2-*, --sh-*, …) — never a legacy name (--pink, --bg, --card, --text…);
 *   • no prefers-color-scheme (themes are the data-theme attribute);
 *   • layout flexibility (OPEN-QUESTIONS #24): no fixed phone-width px widths
 *     — %, flex, grid, rem, min()/clamp(). Fixed px stays for hairlines,
 *     radii, shadows, glyph boxes, touch targets and exact token values;
 *   • every class is v2- prefixed, so nothing here can restyle a legacy view.
 * The legacy tab bar is hidden on 2.0 routes; everything else legacy is
 * untouched until Phase 9.
 */

/* ---------- body on a 2.0 route ---------- */
body.v2 {
  background: var(--s-page);
  color: var(--t-primary);
  /* Batch 18 §5 (Randy 2026-10-16): no colour transition on a 2.0 route. The
   * legacy body rule (app.css) fades background / colour over 200 ms on a
   * theme change; the header, strip and panel switch instantly, so the body
   * — which paints the strip row and the panel's rounded-corner cut-outs —
   * lagged behind them as a light (or dark) flash on every switch and on a
   * cold start (the fade started from the pre-theme first paint). */
  transition: none;
  font-family: -apple-system, "SF Pro Text", "SF Pro Display", system-ui, "Helvetica Neue", sans-serif;
  /* The legacy has-tabbar padding rules read --tabbar-h: give them the
   * floating nav's real clearance so any legacy sheet/toast keeps clearing it. */
  --tabbar-h: calc(var(--nav-h) + 14px);
}
body.v2 #app {
  display: flex;
  flex-direction: column;
}
.v2-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- the screen: header + (strip) + panel ---------- */
.v2-screen {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 100%;
  width: 100%;
}

/* ---------- headers (§1) ---------- */
/* Sticky on every page (Randy, 2026-09-29): the bar stays put while the
 * page scrolls under it. It reaches up under the status bar (#app's
 * safe-area padding is cancelled by the negative margin and re-added as
 * padding) so nothing scrolls visibly above it. z 20: over the strip (2)
 * and the Transcript player (5), under the create scrim (45) and nav (50). */
.v2-head {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 12px;
  box-sizing: border-box;
  min-height: calc(env(safe-area-inset-top, 0px) + var(--head-h));
  margin-top: calc(-1 * env(safe-area-inset-top, 0px));
  padding: calc(env(safe-area-inset-top, 0px) + 13px) var(--pad-x) 14px;
  background: var(--s-page);
}
/* What a rubber-band bounce reveals is the ROOT background (the native
 * scroll view's colour is cleared, ViewController.swift). The panel colour:
 * a bottom bounce — every screen has one — continues the panel instead of
 * showing a dark band. The body paints the page colour itself, so nothing
 * else changes. (Measured on the simulator: WebKit reads this colour from
 * the stylesheet but ignores a class toggled on <html> after load, so it
 * can't follow the scroll position; a top bounce, only on screens without
 * pull-to-refresh, shows the panel colour above the header.) In the app
 * build this is only the fallback: on iOS 26 WKWebView still showed
 * near-black, so js/native-nav.js sends each edge's colour to the native
 * rubber band (GlassNavPlugin.setEdgeColors, Randy 2026-10-04). */
html:has(> body.v2) {
  background: var(--s-panel);
}
/* Anchor jumps and scrollIntoView land below the sticky bar. */
html:has(> body.v2) {
  scroll-padding-top: calc(env(safe-area-inset-top, 0px) + var(--head-h));
}
.v2-head--pushed {
  padding-left: 16px;
  padding-right: 16px;
  gap: 10px;
}
.v2-head__right {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
}
.v2-head__spacer {
  flex: 1 1 auto;
}
.v2-wordmark {
  display: flex;
  -webkit-tap-highlight-color: transparent;
  align-items: baseline;
  font-size: 25px;
  font-weight: 700;
  letter-spacing: -0.55px;
  line-height: 1.1;
  text-decoration: none;
}
.v2-wordmark__slash {
  color: var(--t-slash);
}
.v2-wordmark__word {
  /* WORDMARK RULE (Randy 2026-10-16, batch 17B): the /reScribe wordmark is
   * the Meetings token for the current theme on EVERY screen and section —
   * the vivid Meetings pink in light (#FF2D6F), the soft one in dark
   * (#FF6B9A); never Journal blue, Cookbook amber or the deeper crimson
   * --pink-text (batch 17 §6's one-day detour). */
  color: var(--meetings-ink);
}
.v2-head__title {
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.4px;
  color: var(--t-nav);
}
.v2-head__gear {
  width: var(--hit-min);
  height: var(--hit-min);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: -11px;
  color: var(--t-label);
  flex: none;
}
.v2-back {
  display: flex;
  align-items: center;
  gap: 5px;
  height: 44px;
  padding: 0 16px 0 12px;
  border-radius: 22px;
  background: var(--s-card);
  border: 1px solid var(--raised-line);
  box-shadow: var(--sh-raised);
  color: var(--t-nav);
  font-size: 14.5px;
  font-weight: 620;
  text-decoration: none;
  flex: none;
  max-width: 62%; /* the label names the origin — a long project / meeting name ellipsizes (item 12) */
}
/* 6b (Randy 2026-10-08): the pushed header's back pill (the one naming the
 * origin) SHRINKS when the header runs out of room — its label ellipsizes
 * and the right-hand action pill (flex: none) always fits. As flex: none,
 * 62% + a three-button pill ran off a 320 / 390 screen with a long recipe
 * or meeting name. Cancel / Top pills keep flex: none. The chevron never
 * shrinks. */
.v2-back[data-back] {
  flex: 0 1 auto;
  min-width: 0;
}
.v2-back > svg {
  flex: none;
}
.v2-back > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.v2-actpill {
  display: flex;
  align-items: center;
  height: 44px;
  border-radius: 22px;
  background: var(--s-card);
  border: 1px solid var(--raised-line);
  box-shadow: var(--sh-raised);
  overflow: hidden;
  flex: none;
}
.v2-actpill__btn {
  width: 46px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--t-nav);
  background: none;
  border: 0;
  padding: 0;
}
.v2-actpill__rule,
.v2-tool__rule {
  width: 1px;
  height: 22px;
  background: var(--strip-line);
  flex: none;
}

/* ---------- tab strip (§2) — geometry from boards/Main.dc.html ---------- */
.v2-strip {
  position: relative;
  z-index: 2;
  margin-bottom: -1px;
  overflow: hidden;
}
.v2-strip__row {
  display: flex;
  align-items: flex-end;
}
/* The meeting's section strip (the only scrollable strip) is pinned directly
 * under the sticky top bar (Randy, 2026-09-29): switching Gist → Photos →
 * Transcript never needs a scroll back to the top first. A page-coloured
 * band hides what scrolls under it; the inset hairline stands in for the
 * panel's top border once that has scrolled away (the active tab, flush with
 * the bottom, paints over it). z 15: over the panel and the Transcript
 * player (5), under the top bar (20). */
.v2-strip.is-scrollable {
  position: sticky;
  top: calc(env(safe-area-inset-top, 0px) + var(--head-h));
  z-index: 15;
  background: var(--s-page);
  box-shadow: inset 0 -1px var(--strip-line), var(--rb-guard); /* guard: see below */
}
/* Anchor jumps on a meeting land below the bar AND the strip. */
html:has(> body.v2 > #app .v2-strip.is-scrollable) {
  scroll-padding-top: calc(env(safe-area-inset-top, 0px) + var(--head-h) + var(--strip-h));
}
.v2-strip.is-scrollable .v2-strip__row {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  /* the flares of the last tab hang 14px past it */
  padding-right: 14px;
}
.v2-strip.is-scrollable .v2-strip__row::-webkit-scrollbar {
  display: none;
}
.v2-strip__tab {
  position: relative;
  flex: none;
  margin-left: -1px;
  margin-bottom: 1px;
  height: 44px;
  padding: 0 20px;
  border-radius: var(--r2-tab) var(--r2-tab) 0 0;
  background: var(--s-strip);
  border: 1px solid var(--strip-line);
  border-bottom: 0;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  font-size: 15px;
  font-weight: 570;
  letter-spacing: -0.2px;
  color: var(--t-nav-off);
  text-decoration: none;
  white-space: nowrap;
}
.v2-strip__tab.is-first {
  margin-left: 0;
  border-left: 0;
  padding-left: var(--pad-x);
}
.v2-strip__tab.is-active {
  margin-bottom: 0;
  background: var(--s-panel);
  box-shadow: var(--sh-tab);
  font-weight: 680;
  color: var(--accent-ink);
}
/* Batch 10 §6 (Randy 2026-10-13): in the LIGHT theme the selected tab casts
 * no shadow at all — every strip (the skill tabs, the Meetings section tabs,
 * Tasks, Schedule, Transcript). Dark keeps --sh-tab exactly as it was. */
:root:not([data-theme="dark"]) .v2-strip__tab.is-active {
  box-shadow: none;
}
.v2-flare {
  position: absolute;
  bottom: 0;
  width: 14px;
  height: 14px;
  box-sizing: border-box;
  pointer-events: none;
}
.v2-flare--r-base,
.v2-flare--r-curve {
  right: -14px;
}
.v2-flare--l-base,
.v2-flare--l-curve {
  left: -14px;
}
.v2-flare--r-base,
.v2-flare--l-base {
  background: var(--s-strip);
}
.v2-strip__tab.is-active .v2-flare--r-base,
.v2-strip__tab.is-active .v2-flare--l-base {
  background: var(--s-panel);
}
.v2-flare--r-curve {
  background: var(--s-strip);
  border-bottom-left-radius: 14px;
  border-left: 1px solid var(--strip-line);
  border-bottom: 1px solid var(--strip-line);
}
.v2-flare--r-curve.to-page {
  background: var(--s-page);
}
.v2-flare--l-curve {
  background: var(--s-strip);
  border-bottom-right-radius: 14px;
  border-right: 1px solid var(--strip-line);
  border-bottom: 1px solid var(--strip-line);
}
.v2-strip__fade,
.v2-strip__chev {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  transition: opacity 150ms ease;
}
.v2-strip__fade {
  top: 0;
  bottom: 0;
  width: 40px;
  z-index: 200;
}
.v2-strip__fade--r {
  right: 0;
  background: linear-gradient(90deg, var(--page-clear) 0%, var(--s-page) 76%);
}
.v2-strip__fade--l {
  left: 0;
  background: linear-gradient(270deg, var(--page-clear) 0%, var(--s-page) 76%);
}
.v2-strip__chev {
  top: 14px;
  z-index: 201;
  color: var(--t-placeholder);
  display: flex;
}
.v2-strip__chev--r {
  right: 7px;
}
.v2-strip__chev--l {
  left: 5px;
}
.v2-strip.has-left .v2-strip__fade--l,
.v2-strip.has-left .v2-strip__chev--l,
.v2-strip.has-right .v2-strip__fade--r,
.v2-strip.has-right .v2-strip__chev--r {
  opacity: 1;
}

/* ---------- collapsing header over the skill strip (Randy 2026-09-30) ----
 * On a page whose /reScribe header sits directly over the SKILL strip
 * (Meetings, Journal, Journal calendar, Collections, All Recipes, and —
 * Randy 2026-10-01, one pattern everywhere — the Tasks and Schedule tabs:
 * any .is-pin strip), the
 * header collapses like a native large title: it slides away 1:1 with the
 * finger (a sticky box carried by a `translate`, batch 8 §1), up behind a
 * fixed page-coloured band over the status bar / Dynamic Island; the strip
 * pins directly under that band and takes its place. Scrolling back brings
 * the header down the same way — and any scroll UP brings it back by the
 * distance scrolled, from wherever it is ("Header follows the scroll" below,
 * batch 6 §1; js/shell/head-reveal.js). The wordmark row also fades and
 * lifts over the first stretch, run by the compositor from the top — no
 * scroll handler (JS scroll events lag the native scroll on iOS). */
.v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top {
  /* ONE mechanism (batch 8 §1): the header is ALWAYS sticky at the top, z 15
   * (under the status band's 16); its offset is a `translate` only — written
   * by the compositor's scroll-driven animation (below) until the first
   * scroll up, by js/shell/head-reveal.js from then on. The box itself never
   * fades or scales (an opacity on the box let the content scrolling under it
   * show through part-way — the batch 8 glitch), only the wordmark row does. */
  position: sticky;
  top: 0;
  z-index: 15;
}
.v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-strip.is-pin {
  position: sticky;
  top: calc(env(safe-area-inset-top, 0px) + var(--v2-head-shift));
  z-index: 15;
  /* the page colour stops 1px short: the strip overlaps the panel edge by
   * 1px (margin-bottom −1px) and that edge's line must show through */
  background: linear-gradient(var(--s-page), var(--s-page)) top / 100% calc(100% - 1px) no-repeat;
}
/* The search bar (+ sort / view / filter) pins directly under the strip
 * (Randy 2026-09-30): once the strip pinned, the panel's top edge — the
 * 1px line the tabs rest on and its rounded corner — scrolled away, leaving
 * a page-coloured hairline under the tabs. The pinned band IS that edge:
 * it overlays the panel's top border and padding exactly (identical at
 * rest), carries the same line and corner, and sticks under the strip, so
 * tabs + search read as one continuous pinned surface and only the content
 * scrolls beneath. */
.v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-panel > .v2-toolbar:first-child {
  position: sticky;
  top: calc(env(safe-area-inset-top, 0px) + var(--strip-h) + var(--v2-head-shift));
  z-index: 14;
  margin: -21px calc(-1 * var(--pad-x)) -12px;
  padding: 20px var(--pad-x) 12px;
  background: var(--s-panel);
  border-top: 1px solid var(--strip-line);
  border-radius: 0 var(--r2-panel) 0 0;
}
/* The strip reaches the right edge of the screen (skill-strip.js
 * updateStripCorners — overflowing, or its last tab runs into the curve):
 * the panel's top-right corner goes square so it meets the tab instead of
 * curving away from it (Randy 2026-10-06, item 10). Otherwise it keeps the
 * curve. Covers the pinned toolbar band's copy of the corner too. */
.v2-screen:has(> .v2-strip.is-at-edge) > .v2-panel--strip,
.v2-screen:has(> .v2-strip.is-at-edge) > .v2-panel > .v2-toolbar:first-child {
  border-top-right-radius: 0;
}
/* Bottom rubber band guard (Randy 2026-10-03: bounce back at the bottom,
 * without the pinned chrome vanishing). Measured on the simulator: in a
 * bottom overscroll WKWebView keeps every sticky box in place but stops
 * DRAWING one whose bottom edge sits within the overscroll distance of the
 * viewport top (tops 0/40/80px vanished in a 130px bounce, 120px+ stayed) —
 * its layer is culled as if it had scrolled off with the content. A box
 * whose visual overflow reaches further down is kept: a transparent shadow
 * offset 40vh (past the deepest rubber band) does it, paints nothing and
 * takes no touches. (A ::after can't — .v2-strip clips its overflow.) This
 * is what let ViewController drop its bottom-edge clamp. (--rb-guard is in
 * tokens.css.) */
.v2-head,
.v2-tplayer,
.v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-strip.is-pin,
.v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-panel > .v2-toolbar:first-child {
  box-shadow: var(--rb-guard);
}
/* The band the header slides under: exactly the status-bar inset. On BODY,
 * not the screen (Randy 2026-09-30, device): pull-to-refresh translates the
 * screen section, and a transformed ancestor turns position:fixed into
 * "move with the ancestor" — the band slid down with every pull / hard top
 * bounce, uncovering the status bar (the flicker) and painting over the
 * /reScribe header (the header vanishing, the strip seeming to jump). Nothing
 * ever transforms body. Keyed on the LIVE screen in #app (batch 3 §4): a
 * swipe's snapshot clone (body > .v2-swipe-snap) also holds a library, and
 * this band then lay over the sliding page; the snapshot draws its own. */
body.v2:has(> #app .v2-screen > .v2-head--top + .v2-strip.is-pin)::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-top, 0px);
  background: var(--s-page);
  z-index: 16;
  pointer-events: none;
}
/* Anchor jumps clear the pinned strip, not the (gone) header. */
html:has(> body.v2 > #app .v2-screen > .v2-head--top + .v2-strip.is-pin) {
  scroll-padding-top: calc(env(safe-area-inset-top, 0px) + var(--strip-h));
}
/* The "away" motion from the top (batch 8 §1, ONE source of truth): every
 * sticky mover — header, strip, search band, Schedule's Today pill — carries
 * `translate: 0 −min(scrollY, H)` from a scroll-driven animation the
 * COMPOSITOR runs (no JS scroll event, which lag the native scroll on iOS).
 * Geometrically identical to the old in-flow header scrolling away 1:1, but
 * the header's position / layer never changes (the relative → sticky switch
 * at the first scroll up made a new layer in the very frame of its first
 * paint — the same WebKit flash as batch 6 §6). Over the first H − 15 px
 * the wordmark ROW fades and lifts; the header box stays opaque. The
 * keyframe is a literal: var() inside @keyframes takes the animation off
 * the compositor in WebKit, so it is pinned to --head-h (= --v2-head-shift)
 * by functions/test/head-reveal.mjs. Follow mode (.is-head-follow, from the
 * first scroll up until the page is back at the top; also wherever scroll
 * timelines are unsupported): the animations are off and
 * js/shell/head-reveal.js writes the same `translate` and the same fade,
 * continuing from the current offset — never a reset. */
@supports (animation-timeline: scroll()) {
  .v2-screen:not(.is-head-follow):has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top,
  .v2-screen:not(.is-head-follow):has(> .v2-head--top + .v2-strip.is-pin) > .v2-strip.is-pin,
  .v2-screen:not(.is-head-follow):has(> .v2-head--top + .v2-strip.is-pin) > .v2-panel > .v2-toolbar:first-child,
  .v2-screen:not(.is-head-follow):has(> .v2-head--top + .v2-strip.is-pin) ~ .v2-fl-todaypill {
    animation: v2-head-away linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 var(--v2-head-shift);
  }
  .v2-screen:not(.is-head-follow):has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top > .v2-wordmark,
  .v2-screen:not(.is-head-follow):has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top > .v2-head__right {
    animation: v2-head-collapse linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 calc(var(--v2-head-shift) - 15px);
  }
}
@keyframes v2-head-away {
  to {
    translate: 0 -71px; /* = −var(--head-h); literal on purpose, see above */
  }
}
@keyframes v2-head-collapse {
  to {
    opacity: 0;
    transform: translateY(-8px) scale(0.97);
  }
}
@media (prefers-reduced-motion: reduce) {
  .v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top > .v2-wordmark,
  .v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top > .v2-head__right {
    animation: none;
  }
}
/* Header follows the scroll (Randy 2026-10-08 item 7; rewritten 2026-10-09
 * batch 6 §1, and again 2026-10-12 batch 8 §1; js/shell/head-reveal.js).
 * Follow mode: the sticky geometry is the same as above — the strip, the
 * search band and Schedule's Today pill stick one header-height
 * (--v2-head-shift = --head-h, tokens.css) under the inset in BOTH modes —
 * only the writer of `translate` changes: the animations stop and the JS
 * moves all four together with `translate: 0 <offset>` (−H…0) every frame,
 * by the distance scrolled, and fades the wordmark row from the offset. Only
 * sticky offsets, so no in-flow box moves (no layout or scroll jump). */
.v2-screen.is-head-follow:has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top,
.v2-screen.is-head-follow:has(> .v2-head--top + .v2-strip.is-pin) > .v2-strip.is-pin,
.v2-screen.is-head-follow:has(> .v2-head--top + .v2-strip.is-pin) > .v2-panel > .v2-toolbar:first-child,
.v2-screen.is-head-follow:has(> .v2-head--top + .v2-strip.is-pin) ~ .v2-fl-todaypill,
.v2-screen.is-head-follow:has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top > .v2-wordmark,
.v2-screen.is-head-follow:has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top > .v2-head__right {
  animation: none;
}
.v2-screen:has(> .v2-head--top + .v2-strip.is-pin) ~ .v2-fl-todaypill {
  top: calc(env(safe-area-inset-top, 0px) + var(--strip-h) + 84px + var(--v2-head-shift));
}

/* ---------- thumbnail loading (Journal + Cookbook lists) ----------
 * A frame whose photo is on its way (.is-loading, js/shell/thumbs.js) shows a
 * soft sweep instead of the "no photo" glyph, so a tile never reads as empty
 * before its picture lands (Randy 2026-09-30). */
[data-thumb].is-loading {
  background-color: var(--shimmer-base);
}
[data-thumb].is-loading > [aria-hidden="true"] {
  visibility: hidden;
}
[data-thumb].is-loading::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 20%, var(--shimmer-hi) 50%, transparent 80%);
  background-size: 250% 100%;
  animation: v2-thumb-sweep 1.3s ease-in-out infinite;
  pointer-events: none;
}
@keyframes v2-thumb-sweep {
  0% { background-position: 120% 0; }
  100% { background-position: -120% 0; }
}
@media (prefers-reduced-motion: reduce) {
  [data-thumb].is-loading::after {
    animation: none;
    opacity: 0.5;
  }
}

/* ---------- panel + sections (§4) ---------- */
.v2-panel {
  flex: 1 1 auto;
  background: var(--s-panel);
  border-top: 1px solid var(--strip-line);
  border-radius: var(--r2-panel) var(--r2-panel) 0 0;
  padding: 20px var(--pad-x) 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}
.v2-panel--strip {
  border-radius: 0 var(--r2-panel) 0 0;
}
/* Clear the floating nav: its height + its offset + breathing room. */
body.v2.has-tabbar .v2-panel {
  padding-bottom: calc(var(--nav-h) + 40px + env(safe-area-inset-bottom, 0px));
}
body.v2:not(.has-tabbar) .v2-panel {
  padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px));
}
.v2-label {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--t-label);
}
.v2-label__count {
  color: var(--t-dim);
  font-variant-numeric: tabular-nums;
}
.v2-card {
  background: var(--s-card);
  border: 1px solid var(--raised-line);
  box-shadow: var(--sh-raised);
  border-radius: var(--r2-card);
  min-width: 0;
}
.v2-card--big {
  border-radius: var(--r2-bigcard);
  padding: 14px;
}
.v2-caption {
  margin: -8px 4px 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--t-muted);
}
.v2-rule {
  border: 0;
  height: 1px;
  margin: 0 0 0 13px;
  background: var(--strip-line);
}

/* ---------- rows (§5) ---------- */
.v2-row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 46px;
  padding: 0 13px;
  box-sizing: border-box;
  background: none;
  border: 0;
  color: var(--t-primary);
  text-align: left;
  text-decoration: none;
  font: inherit;
}
.v2-row.has-sub {
  min-height: 52px;
}
.v2-row__tile {
  width: 28px;
  height: 28px;
  border-radius: var(--r2-tile);
  background: var(--s-chip);
  color: var(--t-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
.v2-row__tile.is-skill {
  background: var(--accent-tint);
  color: var(--accent-ink);
}
.v2-row__text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  flex: 1 1 auto;
}
.v2-row__label {
  font-size: 15px;
  font-weight: 560;
  letter-spacing: -0.2px;
}
.v2-row__sub,
.v2-row__value {
  font-size: 12.5px;
  color: var(--t-muted);
}
.v2-row__value {
  font-size: 14px;
  flex: none;
}
.v2-row__chev {
  display: flex;
  color: var(--t-chevron);
  flex: none;
}

/* ---------- tags, chips, dashed affordances (§13, §14) ---------- */
.v2-tag {
  display: inline-flex;
  align-items: center;
  height: 28px;
  padding: 0 12px;
  border-radius: var(--r2-chip);
  background: var(--accent-tint);
  border: 1px solid var(--accent-tint-line);
  color: var(--accent-ink);
  font-size: 13px;
  font-weight: 570;
  white-space: nowrap;
}
.v2-dash {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 28px;
  padding: 0 12px;
  border-radius: var(--r2-chip);
  background: transparent;
  border: 1.5px dashed var(--accent-tint-line);
  color: var(--accent-ink);
  font-size: 13px;
  font-weight: 600;
}
.v2-dash.is-neutral {
  border-color: var(--dash-line);
  color: var(--t-muted);
}

/* ---------- notices ---------- */
.v2-notice {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 12px 14px;
  border-radius: var(--r2-card);
  background: var(--accent-tint);
  border: 1px solid var(--accent-tint-line);
  color: var(--accent-ink);
}
.v2-notice.is-glass {
  background: var(--accent-glass-tint);
  border-color: var(--accent-glass-line);
  box-shadow: var(--sh-raised);
}
.v2-notice svg {
  flex: none;
  margin-top: 1px;
}
.v2-notice p {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.42;
}

/* ---------- wells (recessed: things you type into) ---------- */
.v2-well,
.v2-elist__well {
  background: var(--s-well);
  border: 1px solid var(--well-line);
  box-shadow: var(--sh-well);
  border-radius: var(--r2-well);
  color: var(--t-primary);
  font: inherit;
  font-size: 14.5px;
}
.v2-well::placeholder,
.v2-elist__well::placeholder,
.v2-search__input::placeholder {
  color: var(--t-placeholder);
}

/* ---------- toolbar (§9) ---------- */
.v2-toolbar {
  display: flex;
  gap: 10px;
  min-width: 0;
}
.v2-search {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 9px;
  height: 42px;
  padding: 0 14px;
  border-radius: 21px;
  background: var(--s-card);
  border: 1px solid var(--raised-line);
  box-shadow: var(--sh-raised);
  color: var(--t-nav-off);
  position: relative;
}
.v2-search__input {
  flex: 1 1 auto;
  min-width: 0;
  height: 40px;
  background: none;
  border: 0;
  outline: none;
  color: var(--t-primary);
  font-size: 14.5px;
  -webkit-appearance: none;
  appearance: none;
}
.v2-tool {
  display: flex;
  align-items: center;
  height: 42px;
  border-radius: 21px;
  background: var(--s-card);
  border: 1px solid var(--raised-line);
  box-shadow: var(--sh-raised);
  overflow: hidden;
  flex: none;
}
.v2-tool .v2-tool__rule {
  height: 20px;
}
.v2-tool__btn {
  width: 42px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0;
  color: var(--t-nav);
}
.v2-tool__btn--round {
  width: 34px;
  height: 34px;
  border-radius: 17px;
  background: var(--s-card);
  border: 1px solid var(--raised-line);
  box-shadow: var(--sh-raised);
  position: relative;
}
/* 34px visual, 44px hit (01-TOKENS "touch slop in the build") */
.v2-tool__btn--round::after {
  content: "";
  position: absolute;
  inset: -5px;
}

/* ---------- toggle (§7) ---------- */
.v2-toggle {
  position: relative;
  display: inline-flex;
  flex: none;
  width: 46px;
  height: 28px;
}
.v2-toggle__input {
  position: absolute;
  inset: -8px;
  opacity: 0;
  margin: 0;
  z-index: 1;
}
.v2-toggle__track {
  width: 100%;
  height: 100%;
  border-radius: 14px;
  background: var(--s-track);
  transition: background-color 160ms ease;
  position: relative;
}
.v2-toggle__knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 24px;
  height: 24px;
  border-radius: 12px;
  background: var(--on-accent);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
  transition: transform 160ms ease;
}
.v2-toggle__input:checked + .v2-toggle__track {
  background: var(--accent);
}
.v2-toggle__input:checked + .v2-toggle__track .v2-toggle__knob {
  transform: translateX(18px);
}
.v2-toggle__input:focus-visible + .v2-toggle__track {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}
.v2-toggle.is-disabled {
  opacity: 0.5;
}

/* ---------- inset slider (Projects, Randy 2026-10-04) ----------
 * A recessed track; the selected option is a raised card in the skill
 * accent that slides between equal slots. js/shell/inset-slider.js. */
.v2-inset {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--inset-n, 2), minmax(0, 1fr));
  padding: 3px;
  border-radius: var(--r2-well);
  background: var(--s-well);
  box-shadow: var(--sh-well);
  isolation: isolate;
}
.v2-inset__thumb {
  position: absolute;
  z-index: 0;
  top: 3px;
  bottom: 3px;
  left: 3px;
  width: calc((100% - 6px) / var(--inset-n, 2));
  transform: translateX(calc(100% * var(--inset-i, 0)));
  border-radius: calc(var(--r2-well) - 3px);
  background: var(--accent-tint);
  border: 1px solid var(--accent-tint-line);
  box-shadow: var(--sh-raised);
  box-sizing: border-box;
  transition: transform 220ms cubic-bezier(0.3, 0.7, 0.3, 1);
  pointer-events: none;
}
.v2-inset__opt {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 2.25rem;
  padding: 0 0.5rem;
  color: var(--t-muted);
  font-size: 0.875rem;
  font-weight: 580;
  letter-spacing: -0.15px;
  text-align: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 160ms ease;
}
/* Three or more options: a smaller label so each fits a phone-width slot. */
.v2-inset.is-many .v2-inset__opt {
  padding: 0 0.25rem;
  font-size: 0.8rem;
  letter-spacing: -0.2px;
}
.v2-inset__opt.is-on {
  color: var(--accent-ink);
  font-weight: 660;
}
.v2-inset__label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.v2-inset__input {
  position: absolute;
  opacity: 0;
  inset: 0;
  margin: 0;
}
.v2-inset__opt:has(.v2-inset__input:focus-visible) {
  outline: 2px solid var(--accent-ink);
  outline-offset: -2px;
  border-radius: calc(var(--r2-well) - 3px);
}
@media (prefers-reduced-motion: reduce) {
  .v2-inset__thumb {
    transition: none;
  }
}

/* ---------- segmented (§8) ---------- */
.v2-seg {
  display: flex;
  gap: 8px;
  min-width: 0;
}
.v2-seg__opt {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  height: 34px;
  border-radius: 17px;
  background: var(--s-chip);
  border: 1px solid var(--raised-line);
  box-shadow: var(--sh-raised);
  color: var(--t-muted);
  font-size: 13.5px;
  font-weight: 560;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  cursor: pointer;
}
.v2-seg__opt.is-on {
  background: var(--accent-tint);
  border-color: var(--accent-tint-line);
  color: var(--accent-ink);
  font-weight: 660;
}
.v2-seg__input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
}
.v2-seg__opt:has(.v2-seg__input:focus-visible) {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}
.v2-seg--tall {
  padding: 5px;
  gap: 4px;
  border-radius: var(--r2-card);
  background: var(--s-card);
  border: 1px solid var(--raised-line);
  box-shadow: var(--sh-raised);
}
.v2-seg--tall .v2-seg__opt {
  height: 74px;
  border-radius: 12px;
  flex-direction: column;
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  font-size: 13px;
}
.v2-seg--tall .v2-seg__opt.is-on {
  background: var(--s-chip);
  border-color: var(--raised-line);
  box-shadow: var(--sh-raised);
  color: var(--accent-ink);
}
.v2-seg__icon {
  display: flex;
  margin-bottom: 3px;
}
.v2-seg__icon svg {
  width: 17px;
  height: 17px;
}
.v2-seg__sub {
  font-size: 11.5px;
  font-weight: 500;
  color: var(--t-muted);
}

/* ---------- bottom nav (§3) ---------- */
.v2-nav {
  position: fixed;
  left: var(--nav-inset-x);
  right: var(--nav-inset-x);
  bottom: max(14px, calc(env(safe-area-inset-bottom, 0px) - 4px));
  height: var(--nav-h);
  z-index: 50;
  display: flex;
  align-items: center;
  padding: 0 7px 0 4px;
  gap: 1px;
  border-radius: var(--r2-nav);
  /* Liquid Glass (tokens --lg-*): a light tint that fades downward over a
   * short, saturated blur, so the page reads THROUGH the bar; the rim and
   * sheen are the two pseudo-elements below. */
  background: linear-gradient(180deg, var(--lg-tint-top), var(--lg-tint-bottom));
  -webkit-backdrop-filter: var(--lg-filter);
  backdrop-filter: var(--lg-filter);
  border: 1px solid var(--lg-edge);
  box-shadow: var(--sh-lg);
  box-sizing: border-box;
  isolation: isolate;
}
/* Specular rim: a 1px gradient ring inside the edge — brightest top-left
 * and bottom-right, where light catches a curved lens. */
.v2-nav::before,
.v2-nav::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: -1;
}
.v2-nav::before {
  padding: 1px;
  background: linear-gradient(135deg, var(--lg-rim-hi) 0%, var(--lg-rim-lo) 32%, var(--lg-rim-lo) 68%, var(--lg-rim-hi) 100%);
  -webkit-mask: linear-gradient(black, black) content-box, linear-gradient(black, black);
  -webkit-mask-composite: xor;
  mask: linear-gradient(black, black) content-box, linear-gradient(black, black);
  mask-composite: exclude;
}
/* Sheen: a soft highlight across the upper half. */
.v2-nav::after {
  background: radial-gradient(120% 90% at 30% -30%, var(--lg-sheen), transparent 60%);
}
.v2-nav__items,
.v2-nav__create {
  position: relative;
  z-index: 1;
}
.v2-nav.is-hidden,
body.kb-open .v2-nav {
  display: none;
}
/* Swipe-back (batch 8 §2, js/shell/swipe-bar.js via native-nav.js
 * setSwipeHidden): on builds where the WEB nav draws itself (iOS < 26, the
 * PWA) it leaves the same way the native glass bar does — slides below the
 * bottom edge, fading, while the page slides out from under it (180 ms), and
 * rises into the destination once that is in place (250 ms). Its own
 * `bottom` is at most the safe-area inset, so 100% + 60px clears the edge
 * and the shadow. Under the native bar the class is never set. */
.v2-nav {
  transition:
    transform 250ms cubic-bezier(0.2, 0.9, 0.3, 1),
    opacity 250ms cubic-bezier(0.2, 0.9, 0.3, 1);
}
.v2-nav.is-swipe-away {
  transform: translateY(calc(100% + 60px));
  opacity: 0;
  pointer-events: none;
  transition:
    transform 180ms ease-in,
    opacity 180ms ease-in;
}
@media (prefers-reduced-motion: reduce) {
  .v2-nav,
  .v2-nav.is-swipe-away {
    transition: none;
  }
}
/* iOS 26+ app: the native Liquid Glass bar (GlassNavPlugin, driven by
 * js/native-nav.js) draws the nav. The web nav stays laid out and
 * hit-testable — that is how native-nav.js knows nothing covers it — but
 * draws nothing. */
body.v2-native-nav .v2-nav {
  opacity: 0;
}
.v2-nav__items {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: 1px;
  min-width: 0;
}
.v2-nav__item {
  flex: 1 1 0;
  min-width: 0;
  height: 52px;
  border-radius: 26px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  color: var(--t-nav-off);
  text-decoration: none;
}
.v2-nav__item.is-active {
  background: var(--accent-pill);
  color: var(--accent-ink);
  /* a small lens inside the bar: its own top highlight */
  box-shadow: inset 0 1px 1px var(--lg-pill-hi), inset 0 0 0 0.5px var(--lg-pill-hi);
}
.v2-nav__label {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: -0.1px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.v2-nav__item.is-active .v2-nav__label {
  font-weight: 650;
}
.v2-nav__gap {
  width: 4px;
  flex: none;
}
.v2-nav__create {
  width: 50px;
  height: 50px;
  border-radius: 25px;
  border: 0;
  padding: 0;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: 0 0 12px var(--accent-glow), inset 0 1px 0 var(--glow-inset);
}
.v2-nav__create svg {
  transition: transform 180ms ease;
}
.v2-nav__create.is-open {
  box-shadow: 0 0 0 3px var(--close-ring), 0 0 12px var(--accent-glow), inset 0 1px 0 var(--glow-inset);
}
.v2-nav__create.is-open svg {
  transform: rotate(45deg);
}
/* Busy while a guest's trial entry spins up (app.js runCreateChoice):
 * the pulse the legacy record button gave, so a cold start never reads
 * as a dead tap. */
.v2-nav__create[aria-busy="true"] {
  animation: v2-create-busy 1.1s ease-in-out infinite;
  cursor: default;
}
@keyframes v2-create-busy {
  0%, 100% { opacity: 0.45; }
  50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .v2-nav__create[aria-busy="true"] {
    animation: none;
    opacity: 0.6;
  }
}

/* ---------- page shield while an overlay is up (overlay-guard.js §3) ----------
 * Randy 2026-10-07, item 1: a tap around an open overlay must only close it —
 * no pressed look, no tap highlight, no action on the page behind. WebKit
 * paints :active / the highlight from the hit test at touch-down, so the page
 * must take no hits at all: everything outside a live zone ([data-tapguard] —
 * the create menu, bubbles, every view dropdown — and the nav under the create
 * menu) gets pointer-events:none, and the touch lands on the bare body. The
 * html.v2-tapshield class holds it for the rest of a touch that closed one.
 * The document still scrolls (iOS scrolls the root whatever is hit). */
html:has([data-tapguard]:not([hidden])) body *:not([data-tapguard], [data-tapguard] *, .v2-create-open .v2-nav, .v2-create-open .v2-nav *),
html.v2-tapshield body * {
  pointer-events: none !important;
}
html:has([data-tapguard]:not([hidden])) :is([data-tapguard], .v2-create-open .v2-nav) {
  pointer-events: auto;
}
html:has([data-tapguard]:not([hidden])),
html.v2-tapshield {
  -webkit-tap-highlight-color: transparent;
}
/* Batch 9 §8 (Randy 2026-10-13): the shared toast's action link (Undo)
 * follows the SECTION that raised it — Journal blue, Cookbook amber, Meetings
 * pink — through the accent the body carries for the route (bottom-nav.js
 * sets data-skill on <body> for every 2.0 screen). The legacy rule in app.css
 * painted it the brand pink everywhere. Same type, same place: the colour
 * only. */
body.v2[data-skill] .v2-toast__action {
  color: var(--accent);
}
/* A modal's own scrim takes the outside tap (nothing behind it is hit) — and
 * flashes nothing either (iOS's default grey tap highlight). */
/* (The legacy .confirm__backdrop / .sharesheet__backdrop carry the same line
 * in app.css.) */
:is(.v2-ask__scrim, .v2-rc__scrim, .v2-ck-dialog__scrim, .v2-fl-confirm__scrim, .v2-create__scrim, .v2-cdlg__scrim) {
  -webkit-tap-highlight-color: transparent;
}

/* ---------- create menu (§11) ---------- */
/* A tap outside the option stack only closes the menu (create-menu.js,
 * overlay-guard.js — it never reaches the page: 2026-10-06 item 8, and the
 * page shield above stops even its pressed look: 2026-10-07 item 1). The
 * menu root is the live zone, so it (not the page) takes the outside touch;
 * the scrim stays pointer-events:none. Scrolling the page behind is blocked
 * in create-menu.js (Randy 2026-10-03). */
.v2-create {
  position: fixed;
  inset: 0;
  z-index: 45; /* under the nav (50), whose button becomes the ✕ */
  pointer-events: none;
}
.v2-create__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: var(--scrim-create);
  -webkit-backdrop-filter: blur(var(--scrim-create-blur));
  backdrop-filter: blur(var(--scrim-create-blur));
}
.v2-create__stack {
  pointer-events: auto;
  position: absolute;
  left: 20px;
  right: 20px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 32rem;
  margin: 0 auto;
}
.v2-create__label {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 1.1px;
  color: var(--t-nav-off);
  padding-left: 4px;
  margin-bottom: 2px;
}
.v2-create__opt {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 80px;
  padding: 10px 16px;
  width: 100%;
  box-sizing: border-box;
  border-radius: 20px;
  background: var(--glass-card);
  -webkit-backdrop-filter: blur(30px) saturate(190%);
  backdrop-filter: blur(30px) saturate(190%);
  border: 1px solid var(--glass-card-line);
  box-shadow: var(--sh-raised);
  text-align: left;
  color: var(--t-primary);
  font: inherit;
}
.v2-create__orb {
  width: 44px;
  height: 44px;
  border-radius: 22px;
  background: var(--accent);
  color: var(--on-accent);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
.v2-create__text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.v2-create__title {
  font-size: 16px;
  font-weight: 660;
  letter-spacing: -0.3px;
}
.v2-create__sub {
  font-size: 12.5px;
  color: var(--t-muted);
  line-height: 1.3;
}
.v2-create__fine {
  font-size: 11px;
  color: var(--t-placeholder);
  line-height: 1.3;
}

/* ---------- audio player (§10) ---------- */
.v2-player {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 10px 11px;
  border-radius: var(--r2-card);
  background: var(--s-card);
  border: 1px solid var(--raised-line);
  box-shadow: var(--sh-raised);
  min-width: 0;
}
.v2-player button {
  border: 0;
  padding: 0;
  background: none;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
}
.v2-player__skip {
  width: 36px;
  height: 36px;
  color: var(--t-muted);
}
.v2-player__play {
  width: 44px;
  height: 44px;
  border-radius: 22px;
  background: var(--accent) !important;
  color: var(--on-accent);
}
/* Centred glyphs (Randy 2026-10-01: off-centre, worst on pause). Pause is
 * symmetric in its box, so it sits dead centre. The play triangle's path is
 * already drawn right of its box centre; +0.5px puts its centroid — where
 * the eye reads a triangle's middle — on the button's centre. (Both used to
 * take a blanket 2px shove, which pushed pause 2px right.) */
.v2-player__tri {
  margin-left: 0.5px;
}
/* Waiting for audio (Randy 2026-09-30: a timestamp jump could take ~20 s
 * with only a faint fade — easy to read as broken). A spinning ring around
 * the play button, the processing screen's orb ring in miniature. */
.v2-player__play {
  position: relative;
}
.v2-player.is-loading .v2-player__play::after {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  border: 2.5px solid var(--accent-tint);
  border-top-color: var(--accent);
  animation: v2PlayerSpin 0.9s linear infinite;
  pointer-events: none;
}
@keyframes v2PlayerSpin {
  to {
    transform: rotate(360deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .v2-player.is-loading .v2-player__play::after {
    animation: none;
    border-color: var(--accent);
    border-style: dashed;
  }
}
.v2-player__scrub {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 0 2px;
}
.v2-player__track {
  position: relative;
  height: 4px;
  border-radius: 2px;
  background: var(--s-track);
  touch-action: none;
  cursor: pointer;
}
/* the 4px bar is drawn; the hit area is taller */
.v2-player__track::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -14px;
  bottom: -14px;
}
.v2-player__fill {
  height: 4px;
  border-radius: 2px;
  background: var(--accent);
}
.v2-player__handle {
  position: absolute;
  top: -4px;
  width: 12px;
  height: 12px;
  margin-left: -6px;
  border-radius: 6px;
  background: var(--accent);
}
.v2-player__times {
  display: flex;
  justify-content: space-between;
  /* 320 px (Randy 2026-10-08, "0:001:07"): the scrub column shrinks to ~45 px,
   * the two labels' own width — a guaranteed gap between them, and the
   * narrow-phone rule below gives the column back the room. */
  gap: 0.5em;
  white-space: nowrap;
  font-size: 11px;
  font-weight: 600;
  color: var(--t-muted);
  font-variant-numeric: tabular-nums;
}
@media (max-width: 359px) {
  .v2-player {
    gap: 4px;
    padding-inline: 8px;
  }
  .v2-player__skip {
    width: 32px;
  }
}
.v2-player__rate {
  height: 28px;
  min-width: 32px;
  padding: 0 7px !important;
  border-radius: 14px;
  background: var(--s-chip) !important;
  color: var(--t-nav);
  font-size: 12px;
  font-weight: 680;
  font-variant-numeric: tabular-nums;
}
.v2-player__dl {
  width: 32px;
  height: 32px;
  border-radius: 16px;
  background: var(--s-chip) !important;
  color: var(--accent-ink);
}

/* ---------- editable list rows (§12) ---------- */
.v2-elist__rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.v2-elist__row,
.v2-elist__addrow {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.v2-elist__addrow {
  margin-top: 8px;
}
/* Drag to reorder (shell/drag-reorder.js, item 12 2026-10-06) — shared by
 * the recipe editor's lists and a project's suggested order:
 *   the GHOST — a full-width copy of the whole row riding the finger
 *     (fixed; its card width + padding and ground are set inline), raised;
 *   the GAP — the row itself while lifted: same size, content hidden, a
 *     dashed outline in the skill's accent reaching its card's edges;
 *   nothing selectable anywhere while a row is lifted, and never a
 *     selection / callout / magnifier from a grip. */
.v2-drag-ghost {
  z-index: 60;
  pointer-events: none;
  border-radius: var(--r2-tile);
  box-shadow: var(--sh-lg);
  will-change: transform;
}
.is-drag-gap {
  position: relative;
}
.is-drag-gap > * {
  visibility: hidden;
}
.is-drag-gap::after {
  content: "";
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: var(--drag-gap-l, 0px);
  right: var(--drag-gap-r, 0px);
  border: 2px dashed var(--accent);
  border-radius: var(--r2-tile);
  pointer-events: none;
}
.is-drag-reordering,
.is-drag-reordering * {
  -webkit-user-select: none !important;
  user-select: none !important;
  -webkit-touch-callout: none !important;
}
[data-grip] {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}
/* Drag a TILE in a grid (shell/tile-reorder.js, batch 6 §5e 2026-10-09 —
 * the Collections grid in reorder mode): the grid carries .is-tile-wiggle
 * while reordering and its tiles ([data-tile], each carrying its [data-grip]
 * handle — batch 8 §3) wiggle gently,
 * iPhone-home-screen style (neighbours out of phase; `rotate`, so a tile
 * can still be translated into its slot by the drag); not under Reduce
 * Motion. The lifted tile is the dashed gap (.is-tile-gap, the list drag's
 * look); its copy (.v2-tile-ghost) rides the finger. */
.is-tile-wiggle > [data-tile] {
  animation: v2-tile-wiggle 0.42s ease-in-out infinite alternate;
  transform-origin: 50% 50%;
}
.is-tile-wiggle > [data-tile]:nth-child(even) {
  animation-delay: -0.21s;
  animation-direction: alternate-reverse;
}
.is-tile-wiggle > [data-tile]:nth-child(3n) {
  animation-delay: -0.1s;
}
.is-tile-wiggle > [data-tile].is-tile-gap {
  animation: none;
}
@keyframes v2-tile-wiggle {
  from {
    rotate: -1.2deg;
  }
  to {
    rotate: 1.2deg;
  }
}
@media (prefers-reduced-motion: reduce) {
  .is-tile-wiggle > [data-tile] {
    animation: none;
  }
}
.is-tile-gap {
  position: relative;
}
.is-tile-gap > * {
  visibility: hidden;
}
.is-tile-gap::after {
  content: "";
  position: absolute;
  inset: 2px;
  border: 2px dashed var(--accent);
  border-radius: var(--r2-tile);
  pointer-events: none;
}
.v2-tile-ghost {
  z-index: 60;
  pointer-events: none;
  border-radius: var(--r2-tile);
  will-change: transform;
}
.v2-tile-ghost [data-cover] {
  box-shadow: var(--sh-lg);
}
/* The Reorder / Done toggle (item 10b, 2026-10-06): ONE look for every
 * list that reorders in a mode — the recipe editor's ingredients and steps,
 * a project's suggested order. Accent text on the list's label row; Done is
 * a touch bolder; the tap target reaches past the small text. */
.v2-reorder-toggle {
  position: relative;
  flex: none;
  border: 0;
  padding: 0;
  background: none;
  color: var(--accent-ink);
  font: inherit;
  font-size: 13px;
  font-weight: 640;
  cursor: pointer;
}
.v2-reorder-toggle.is-on {
  font-weight: 720;
}
.v2-reorder-toggle::after {
  content: "";
  position: absolute;
  inset: -12px -8px;
}
/* Reorder mode: the read-only wells are not text to select either. */
.v2-elist.is-reordering .v2-elist__well {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
/* Numbers only (instructions); ingredients show no marker while editing. */
.v2-elist__mark {
  flex: none;
  width: 1.5rem;
  color: var(--accent-ink);
  font-weight: 700;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
/* The well is a textarea that grows with its text (editable-list.js
 * growWell) inside a FIELD box that also holds the type switch at its far
 * right: the well reserves that corner (padding-inline-end), so a long line
 * wraps instead of running under the icon. */
.v2-elist__field {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
}
.v2-elist__well {
  flex: 1 1 auto;
  display: block;
  width: 100%;
  min-width: 0;
  min-height: 42px;
  padding: 10px 12px;
  line-height: 1.4;
  box-sizing: border-box;
  resize: none;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.v2-elist__field.has-kind .v2-elist__well {
  padding-inline-end: calc(12px + 28px + 6px);
}
/* A section heading row ("Dough"): the same well, bold. */
.v2-elist__row.is-heading .v2-elist__well {
  font-weight: 720;
  font-size: 15.5px;
  letter-spacing: -0.2px;
}
.v2-elist__grip,
.v2-elist__del {
  flex: none;
  width: 32px;
  height: 32px;
  border: 0;
  padding: 0;
  background: none;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--t-chevron);
  position: relative;
}
.v2-elist__grip {
  touch-action: none;
  cursor: grab;
}
.v2-elist__del {
  color: var(--accent-ink);
}
.v2-elist__grip::after,
.v2-elist__del::after {
  content: "";
  position: absolute;
  inset: -6px;
}
/* Row-type SWITCH (Randy 2026-10-01; icon-only circle 2026-10-03; INSIDE
 * the well at its far right 2026-10-05, item 24): the swap arrows in a small
 * raised circle pinned to the field's top-right corner (level with the first
 * line however tall the well grows) — the app's soft raised look that
 * presses in on tap; it opens the Ingredient / Heading bubble. A heading
 * row's switch wears the accent. */
.v2-elist__kind {
  position: absolute;
  top: 7px;
  inset-inline-end: 7px;
  z-index: 1;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--raised-line);
  border-radius: 50%;
  background: linear-gradient(180deg, var(--s-card), var(--s-chip));
  box-shadow: var(--sh-raised), 0 1px 1.5px rgba(0, 0, 0, 0.1);
  color: var(--t-muted);
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.08s ease, box-shadow 0.08s ease;
}
.v2-elist__kind svg {
  width: 14px;
  height: 14px;
}
.v2-elist__kind::after {
  content: "";
  position: absolute;
  inset: -7px;
}
.v2-elist__kind:active {
  transform: translateY(1px);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12);
}
.v2-elist__row.is-heading .v2-elist__kind {
  color: var(--accent-ink);
  border-color: var(--accent-tint-line);
  background: linear-gradient(180deg, var(--s-card), var(--accent-tint));
}
/* Reorder mode: read-only rows, a handle on each. */
.v2-elist.is-reordering .v2-elist__well {
  background: var(--s-card);
  box-shadow: var(--sh-raised);
  border-color: var(--raised-line);
  user-select: none;
  -webkit-user-select: none;
}
.v2-elist__adds {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  gap: 8px;
}
.v2-elist__adds .v2-elist__add {
  flex: 1 1 0;
  min-width: 0;
}
.v2-elist__ctl {
  flex: none;
  width: 32px;
}
.v2-elist__add {
  height: 40px;
  border-radius: var(--r2-well);
  background: transparent;
  border: 1.5px dashed var(--accent-tint-line);
  color: var(--accent-ink);
  font: inherit;
  font-size: 14px;
  font-weight: 620;
}

/* ---------- edge swipe-back (js/shell/edge-swipe.js; Randy 2026-10-07, item 7) ----------
 * While the page (#app) follows the finger it sits over an opaque underlay
 * in the page's own colour (edge-swipe.js sets it from the body's) — never
 * the web view's near-black — and casts a soft shadow on it from its left
 * edge, fading out as it leaves (--swipe-shade). */
.v2-swipe-under {
  position: fixed;
  inset: 0;
  z-index: 0;
  background: var(--s-page);
  pointer-events: none;
}
/* Armed at touch-down on the left edge (batch 6 §6, edge-swipe.js prepare):
 * the page's compositing layer exists and is painted before the first
 * follow frame, so no frame ever shows the layers behind it unpainted. */
.v2-swipe-arm {
  will-change: transform;
}
.v2-swiping {
  position: relative;
  z-index: 1;
  will-change: transform;
  /* Opaque top to bottom (batch 3 §4): #app has no background of its own, so
   * a title, a nav row or a gap drawn straight on the page let the snapshot
   * behind show through (Randy: the previous page's tabs over a project's or
   * an entry's title). edge-swipe.js sets the page's own colour. */
  background: var(--swipe-page);
}
/* Item 14c (2026-10-08): the frozen snapshot of the previous screen — an
 * inert clone the router handed edge-swipe.js on the way in. It replaces the
 * plain underlay behind the sliding page (z 0, above the underlay by DOM
 * order) and is its own scroller, so its scroll is the one the screen was
 * left at and its sticky header sticks as it did. When Back lands it covers
 * the real screen (z 2 over #app's z 1, still under the nav's 50) until that
 * has rendered at its restored scroll. Nothing in it animates (a replayed
 * entrance or a running shimmer would read as a live page). */
.v2-swipe-snap {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  /* body.v2's look, in the theme of the moment: every Back target is a 2.0
   * screen, but the screen sliding over it may not be (Terms, Privacy). */
  background: var(--s-page);
  color: var(--t-primary);
  font-family: -apple-system, "SF Pro Text", "SF Pro Display", system-ui, "Helvetica Neue", sans-serif;
}
/* A library's status-bar band (see "The band the header slides under"),
 * drawn INSIDE the snapshot for its own clone — so it sits under the sliding
 * page with the rest of the snapshot, never over it. */
.v2-swipe-snap:has(.v2-screen > .v2-head--top + .v2-strip.is-pin)::after {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-top, 0px);
  background: var(--s-page);
  z-index: 16;
}
.v2-swipe-snap.is-cover {
  z-index: 2;
}
/* The clone of a collapsing-header screen (batch 8 §1): a snapshot is a
 * frozen copy scrolled by its wrap (edge-swipe.js wrap.scrollTop = y), with
 * no scroll timeline of its own, so a header the compositor was driving goes
 * back in flow here — the wrap's scroll places it at −y exactly as the
 * animation had — and a header the JS was driving keeps its inline offset on
 * the sticky geometry. No animation either way. */
.v2-swipe-snap .v2-screen:not(.is-head-follow):has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top {
  position: relative;
  z-index: 1;
}
.v2-swipe-snap .v2-screen:not(.is-head-follow):has(> .v2-head--top + .v2-strip.is-pin) > .v2-strip.is-pin {
  top: env(safe-area-inset-top, 0px);
}
.v2-swipe-snap .v2-screen:not(.is-head-follow):has(> .v2-head--top + .v2-strip.is-pin) > .v2-panel > .v2-toolbar:first-child {
  top: calc(env(safe-area-inset-top, 0px) + var(--strip-h));
}
.v2-swipe-snap .v2-screen:not(.is-head-follow):has(> .v2-head--top + .v2-strip.is-pin) ~ .v2-fl-todaypill {
  top: calc(env(safe-area-inset-top, 0px) + var(--strip-h) + 84px);
}
.v2-swipe-snap .v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top,
.v2-swipe-snap .v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-strip.is-pin,
.v2-swipe-snap .v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-panel > .v2-toolbar:first-child,
.v2-swipe-snap .v2-screen:has(> .v2-head--top + .v2-strip.is-pin) ~ .v2-fl-todaypill,
.v2-swipe-snap .v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top > .v2-wordmark,
.v2-swipe-snap .v2-screen:has(> .v2-head--top + .v2-strip.is-pin) > .v2-head--top > .v2-head__right {
  animation: none;
}
.v2-swipe-snap *,
.v2-swipe-snap *::before,
.v2-swipe-snap *::after {
  animation-delay: -60s !important;
  animation-play-state: paused !important;
  transition: none !important;
}
/* While the cover is up the page is at rest: no compositing hint, so a
 * position: fixed bar inside it is placed by the viewport again. */
.v2-swiping.is-landing {
  will-change: auto;
}
.v2-swiping::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -1.5rem;
  width: 1.5rem;
  background: linear-gradient(to left, var(--swipe-edge), transparent);
  opacity: var(--swipe-shade);
  pointer-events: none;
}

/* ---------- popover surface (Randy 2026-10-07, item 3) ----------
 * ONE look for every dropdown, popover and bubble in the app: the bubbles
 * (project pill, "+", the editor's row-kind, collections, tips), the More ⋯
 * menus, the meeting-type and journal view dropdowns, the tag popovers, the
 * Cookbook / Journal / Tasks / Settings / Ask menus. Light = the project-pill
 * bubble's look, dark = the meeting-type dropdown's (the --pop-* tokens,
 * tokens.css). A popover's own CSS only positions and pads it — it never sets
 * the surface or the row text (pinned by test/shell.mjs). Shapes: bubbles and
 * the tag popovers carry the small V pointer; dropdowns have none.
 * (The Cookbook servings tip keeps its amber sparkle-badge look on purpose.) */
.v2-pop {
  background: var(--pop-bg);
  border: 1px solid var(--pop-line);
  border-radius: var(--pop-radius);
  box-shadow: var(--pop-shadow);
  color: var(--pop-ink);
  font-family: -apple-system, "SF Pro Text", system-ui, "Helvetica Neue", sans-serif;
}
.v2-pop [role^="menuitem"] {
  font-size: var(--pop-size);
  font-weight: var(--pop-weight);
  letter-spacing: var(--pop-track);
}
/* The V pointer of an in-flow popover (the tag popovers: js/shell/pop-tip.js
 * aims it at the control that opened it) — the bubble's pointer, below its
 * anchor. */
.v2-pop__tip {
  position: absolute;
  z-index: 2;
  top: -6px;
  left: var(--pop-tip-x);
  width: 12px;
  height: 12px;
  margin-left: -6px;
  box-sizing: border-box;
  background: var(--pop-bg);
  border: 1px solid var(--pop-line);
  border-right-color: transparent;
  border-bottom-color: transparent;
  border-top-left-radius: 3px;
  transform: rotate(45deg);
  pointer-events: none;
}

/* ---------- bubble (js/shell/bubble.js — Randy 2026-10-05) ----------
 * A raised card (the 2.0 card surface, a hairline, the floating shadow) with a short pointer aimed at its anchor; the maths
 * (placeBubble) sets left/top and --bubble-tip-x, data-placement says which
 * side the pointer is on. The geometry here matches BUBBLE_GEOMETRY:
 * a 12px square turned 45° = a ~17px-wide V (tipHalf 9), sitting in the
 * 12px gap; the body's corners are --r2-card (radius 16). */
.v2-bubble {
  position: fixed;
  z-index: 60; /* over the nav (50) and the create menu (45) */
  max-width: calc(100vw - 16px);
}
.v2-bubble__body {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  width: max-content;
  max-width: min(20rem, calc(100vw - 16px));
  min-width: 11rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* surface: .v2-pop (bubble.js puts the class on the body) */
}
.v2-bubble.is-content .v2-bubble__body {
  max-width: min(18rem, calc(100vw - 16px));
  padding: 14px 16px 12px;
}
/* The pointer: the body's own surface and hairline, on the side facing the
 * anchor, drawn OVER the body's border so the two read as one shape. */
.v2-bubble__tip {
  position: absolute;
  z-index: 2;
  left: var(--bubble-tip-x);
  width: 12px;
  height: 12px;
  margin-left: -6px;
  background: var(--pop-bg);
  border: 1px solid var(--pop-line);
  box-sizing: border-box;
  transform: rotate(45deg);
  pointer-events: none;
}
.v2-bubble[data-placement="below"] .v2-bubble__tip {
  top: -6px;
  border-right-color: transparent;
  border-bottom-color: transparent;
  border-top-left-radius: 3px;
}
.v2-bubble[data-placement="above"] .v2-bubble__tip {
  bottom: -6px;
  border-left-color: transparent;
  border-top-color: transparent;
  border-bottom-right-radius: 3px;
}
/* Rows (menu mode): icon + label + trailing ✓, thin dividers. */
.v2-bubble__row {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  min-height: var(--hit-min);
  padding: 0 14px;
  box-sizing: border-box;
  background: none;
  border: 0;
  border-top: 1px solid var(--strip-line);
  color: var(--t-primary);
  font: inherit; /* size / weight / tracking: .v2-pop [role^="menuitem"] */
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.v2-bubble__row:first-child {
  border-top: 0;
}
.v2-bubble__row:active {
  background: var(--s-chip);
}
.v2-bubble__row:focus-visible {
  outline: none;
  background: var(--s-chip);
}
.v2-bubble__icon {
  display: flex;
  flex: none;
  color: var(--t-muted);
}
.v2-bubble__label {
  flex: 1 1 auto;
  min-width: 0;
}
.v2-bubble__check {
  display: flex;
  flex: none;
  width: 18px;
  color: var(--accent-ink);
}
.v2-bubble__row.is-checked .v2-bubble__icon {
  color: var(--accent-ink);
}
/* Content mode: a line of text, then the actions (last = primary). */
.v2-bubble__text {
  font-size: 14px;
  line-height: 1.42;
  color: var(--t-body);
}
.v2-bubble__text strong {
  color: var(--t-primary);
  font-weight: 680;
}
.v2-bubble__actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 12px;
}
.v2-bubble__action {
  position: relative;
  height: 32px;
  padding: 0 14px;
  border-radius: var(--r2-round);
  border: 1px solid var(--raised-line);
  background: var(--s-chip);
  color: var(--t-primary);
  font: inherit;
  font-size: 13.5px;
  font-weight: 640;
}
.v2-bubble__action.is-primary {
  background: var(--accent);
  border-color: transparent;
  color: var(--on-accent);
  box-shadow: var(--sh-raised);
}
.v2-bubble__action::after {
  content: "";
  position: absolute;
  inset: -6px -2px;
}
/* Sparkle badge (item 27, Cookbook's one-time "Suggested servings" tip —
 * js/views/cookbook/servings-tip-bubble.js): the bubble as a rounded
 * rectangle in the skill accent — tint fill, 1px accent border, accent ink
 * (dark: dark amber / amber / bright amber; light: pale amber / amber /
 * deep amber), a sparkle on the left, two lines, no buttons. */
.v2-bubble.v2-bubble--sparkle .v2-bubble__body {
  min-width: 0;
  padding: 10px 14px 11px 12px;
  background: var(--accent-tint);
  border: 1px solid var(--accent);
  border-radius: var(--r2-well);
  box-shadow: var(--sh-raised);
}
.v2-bubble.v2-bubble--sparkle .v2-bubble__tip {
  background: var(--accent-tint);
  border-color: var(--accent);
}
.v2-bubble.v2-bubble--sparkle[data-placement="below"] .v2-bubble__tip {
  border-right-color: transparent;
  border-bottom-color: transparent;
}
.v2-bubble.v2-bubble--sparkle[data-placement="above"] .v2-bubble__tip {
  border-left-color: transparent;
  border-top-color: transparent;
}
.v2-bubble--sparkle .v2-bubble__actions {
  display: none;
}
.v2-bubble--sparkle .v2-bubble__text,
.v2-bubble--sparkle .v2-bubble__text strong {
  color: var(--accent-ink);
}
.v2-spk {
  display: flex;
  align-items: flex-start;
  gap: 9px;
}
.v2-spk__icon {
  flex: none;
  margin-top: 1px;
}
.v2-spk__lines {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.v2-bubble--sparkle .v2-spk__title {
  font-size: 14px;
  font-weight: 700;
}
.v2-spk__body {
  font-size: 13px;
  line-height: 1.35;
}
/* Batch 8 §5a: the bubble's one text action — "Clear amount" (batch 9 §6,
 * Randy 2026-10-13): a link in the Cookbook amber (the bubble's accent ink),
 * underlined, the body's type; padding + negative margins give it a tall
 * touch target without moving the two lines. */
.v2-spk__clear {
  align-self: flex-start;
  margin: 2px 0 -6px -8px;
  padding: 6px 8px;
  border: 0;
  background: none;
  font: inherit;
  font-size: 13px;
  line-height: 1.35;
  color: var(--accent-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  touch-action: manipulation;
}
.v2-spk__clear:active {
  opacity: 0.6;
}
/* Batch 9 §6: the small ✕ at the bubble's top right — closes the tip for
 * this editing session and keeps the suggested number. The body makes room
 * for it on the right; the hit area is 32 px. */
.v2-bubble.v2-bubble--sparkle .v2-bubble__body {
  position: relative;
  padding-right: 34px;
}
.v2-spk__x {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--accent-ink);
  cursor: pointer;
  touch-action: manipulation;
}
.v2-spk__x:active {
  opacity: 0.6;
}
/* The servings tip lives IN the servings row (batch 4 §1 — the fixed bubble
 * jerked while scrolling): absolutely positioned above the row by CSS alone,
 * so it scrolls with the page natively. Right-aligned to the row (the
 * stepper is the row's last item), its bottom BUBBLE_GEOMETRY.gap (12px)
 * above the stepper's top, the pointer over the stepper's centre. The two
 * custom properties are measured only on layout changes
 * (servings-tip-bubble.js servingsTipAnchorVars); their tokens.css defaults
 * fit a stepper flush with the row's top and ~11rem wide. */
.v2-bubble.v2-bubble--inrow {
  position: absolute;
  z-index: 6;
  top: auto;
  left: auto;
  right: 0;
  bottom: calc(100% - var(--spk-anchor-top) + 12px);
  width: max-content;
  max-width: min(18rem, 100%);
}
.v2-bubble.v2-bubble--inrow .v2-bubble__body {
  width: auto;
  max-width: none;
  /* A non-modal hint in the page (batch 7 §2): not a scroller of its own —
   * the shared bubble's overflow-y:auto + overscroll-behavior:contain would
   * make a drag that starts on the tip scroll nothing on iOS, and a nested
   * scroll container is one more layer to keep in step while the page
   * scrolls. The light raised shadow (the sparkle variant) stays. */
  overflow: visible;
  overscroll-behavior: auto;
  touch-action: manipulation; /* a tap closes it without the double-tap wait */
}
.v2-bubble.v2-bubble--inrow .v2-bubble__tip {
  left: auto;
  right: var(--spk-tip-r);
  margin-left: 0;
  margin-right: -6px;
}
/* Reads env(safe-area-inset-*) for the placement maths. */
.v2-bubble-probe {
  position: fixed;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  visibility: hidden;
  pointer-events: none;
  padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
}

/* ---------- Centered card dialog (shell/card-dialog.js, Randy 2026-10-05)
 * The Create menu's blurred, dimmed page with one raised card on it. The
 * card's top is pinned where it opened centered; it grows
 * downward to a cap and scrolls inside past it. Rides the sheet's inner
 * classes, so it resets the bottom-sheet panel's edge-to-edge shape. */
.v2-cdlg {
  position: fixed;
  inset: 0;
  z-index: 100;
}
.v2-cdlg__scrim {
  position: absolute;
  inset: 0;
  background: var(--scrim-create);
  -webkit-backdrop-filter: blur(var(--scrim-create-blur));
  backdrop-filter: blur(var(--scrim-create-blur));
}
/* top + max-height are set inline by card-dialog.js (the pinned top, and
 * the cap below it minus the keyboard inset). */
.v2-cdlg .v2-cdlg__card {
  position: absolute;
  left: 1rem;
  right: 1rem;
  top: 20%;
  width: auto;
  max-width: 32rem;
  margin: 0 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  box-sizing: border-box;
  padding: 1.25rem 1.1rem 1.1rem;
  border-radius: 20px; /* the Create menu cards' radius */
  border: 1px solid var(--raised-line);
  box-shadow: var(--sh-raised);
  animation: v2-cdlg-in 200ms cubic-bezier(0.2, 0.9, 0.3, 1);
}
@keyframes v2-cdlg-in {
  from {
    opacity: 0;
    transform: scale(0.97);
  }
}
@media (prefers-reduced-motion: reduce) {
  .v2-cdlg .v2-cdlg__card {
    animation: none;
  }
}

/* ---------- the rubber band on the legacy full-page scrollers ----------
 * Top and bottom, like the 2.0 screens. app.css keeps every non-2.0 screen
 * bounce-free (html:not(:has(> body.v2))); this lifts it for #/auth
 * (2026-10-06 item 21), Terms, Privacy, the AI-consent screen, the paywall
 * and the growth dashboard (2026-10-07 item 6: each root carries .v2-bounce;
 * the #app id outranks app.css's rule) and the guest trial's Ask (follow-up
 * item 2: js/views/ask.js is a protected legacy file, so it opts in by ROUTE
 * — routes.js "legacy/ask" bounce: true → body.v2-bounce-route, set by
 * bottom-nav.js). Still bounce-free: the recorder and the processing screens,
 * fixed full-screen layouts, not scrollers. */
html:has(> body > #app > :is(.v2-auth, .v2-bounce)),
html:has(> body.v2-bounce-route) {
  overscroll-behavior-y: auto;
}
/* Log in | Sign up is the 2.0 inset slider (the Meetings / Projects switch)
 * in the auth screen's forced-dark tokens; same 18px gap the old pill had. */
.v2-authswitch {
  width: 100%;
  margin-top: 18px;
}
