/* Bryn — base styles. Typography-led, no mascot. */

:root {
  /* Page background: warm cream. Lowered a notch from the original
     #faf7f2 because the user flagged the chat area felt too bright next
     to the white bubbles. The drop is small enough not to read as a
     theme change on other pages. */
  --bg: #f3eee5;
  --surface: #ffffff;
  --border: #e6ddd2;
  --ink: #2a2a2a;
  --ink-soft: #5a5a5a;
  --accent: #2c3e35;
  --accent-soft: #5a7a6c;
  --accent-deep: #1d2a23;
  /* Actual rendered height of header.site: padding 0.8rem top + 0.8rem
     bottom + tallest child 2.25rem (site-menu-toggle, site-brand h1) =
     3.85rem. Anything pinned beneath the header (convo-header-bar, .convo
     padding-top) reads this var to clear the header cleanly. Undersized
     values let the convo-header-bar overlap the bottom of the site header
     (dev-queue #343 — the hamburger went missing because the fixed
     header-bar above it covered the lower portion). */
  --site-header-h: 3.85rem;
  --warn: #8a4b1a;
  --error: #a13a2a;
  --radius: 8px;
  --shadow: 0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.04);

  /* T10 / #428 dark-mode polish. Text on accent/warn surfaces gets its own
     token because the right value isn't "white" — it's "the readable
     foreground colour for THIS theme on a coloured pill/button". In light
     mode the accent is dark sage, so white reads cleanly; in dark mode
     the accent is light sage, so white drops below WCAG AA and we want
     near-black text instead. Same logic for warn (dark amber → bright
     amber) and the convo mic and session dot. */
  --ink-on-accent:        #fff;
  --flash-error-bg:       #fdecea;
  --flash-error-border:   #f5c0bb;
  --flash-notice-bg:      #fdf6e3;
  --flash-notice-border:  #f0e0bb;
  --flash-ok-bg:          #ecf5ee;
  --flash-ok-border:      #c8dccd;
  --session-dot:          #c64a14;
  --convo-mic-bg:         #3d8a6f;
  --convo-mic-bg-hover:   #469b7e;
  --convo-mic-muted:      #b9b3a7;
  --wx-sun:               #d68a1e;
  --wx-partly:            #b58a3a;
  --wx-cloud:             #738292;
  --wx-rain:              #4977b3;
  --wx-snow:              #6b8db8;
  --wx-fog:               #8a8a8a;
  --wx-thunder:           #7a3aa8;

  /* Chat bubble tunables (T3 / #412). Exposed as vars so they can be
     dialled in a collaborative tweak session without hunting through the
     rules below. Last values agreed on:
       pad-y           top/bottom padding inside the bubble surface
       pad-x           left/right padding inside the bubble surface
       max-width       % of the chat column the bubble can fill on desktop
       max-width-mob   % of the chat column on mobile (still hard-clamped
                       by 100svw - 1rem so iOS keyboard reflow can't shift
                       the visible edge — dev-queue #380)
       stagger-y       distance the bubble translates from on the open
                       fade-in (used by .bubble-stagger-in keyframe) */
  --bubble-pad-y:         0.5rem;
  --bubble-pad-x:         0.85rem;
  --bubble-max-width:     85%;
  --bubble-max-width-mob: 88%;
  --bubble-stagger-y:     4px;
}

/* Dark theme (#425). Overrides the colour tokens defined above; everything
   else (typography, layout, animations) is shared. Two ways in:
     [data-theme="dark"]                      — user explicitly chose dark
     [data-theme="auto"] @ prefers dark scheme — user is 'auto' and OS prefers
   Light is the default — no rule fires when user is on 'light' or 'auto'
   while the OS is in light mode.

   Palette mirrors the warm-cream-and-green light theme: dark warm
   charcoal background, near-cream ink, slightly-lifted green accent for
   contrast on the darker surface. Bubble/chat surfaces continue to read
   off these vars so they flip automatically. A follow-up pass (still in
   the queue) walks every remaining hard-coded hex in the file and
   converts the visible ones to semantic vars so the dark theme is
   contrast-clean across every surface. */
[data-theme="dark"] {
  --bg:           #1c1a17;
  --surface:      #2a2723;
  --border:       #3a3530;
  --ink:          #ece8e0;
  --ink-soft:     #a39d92;
  --accent:       #7eb098;
  --accent-soft:  #98c1ad;
  --accent-deep:  #6a9c84;
  --warn:         #c47840;
  --error:        #d96d5b;
  --shadow:       0 1px 2px rgba(0,0,0,0.20), 0 4px 12px rgba(0,0,0,0.30);

  /* T10 / #428: dark mode flips text-on-accent because the accent itself
     is now a light sage. White text would drop below WCAG AA on the
     lighter accent. Dark charcoal sits comfortably above it.
     Flash pills get dark-tinted backgrounds with light-tinted text. */
  --ink-on-accent:        #1c1a17;
  --flash-error-bg:       #3b1c1c;
  --flash-error-border:   #6a2e2e;
  --flash-notice-bg:      #2e2618;
  --flash-notice-border:  #4a3a1c;
  --flash-ok-bg:          #1f3328;
  --flash-ok-border:      #2c4f3a;
  --session-dot:          #e0925a;
  --convo-mic-bg:         #6a9c84;
  --convo-mic-bg-hover:   #7eb098;
  --convo-mic-muted:      #5a5550;
  --wx-sun:               #f0b860;
  --wx-partly:            #d0b070;
  --wx-cloud:             #a0b0c0;
  --wx-rain:              #7a9fd0;
  --wx-snow:              #9ab2d4;
  --wx-fog:               #b0b0b0;
  --wx-thunder:           #b08adf;
}
@media (prefers-color-scheme: dark) {
  [data-theme="auto"] {
    --bg:           #1c1a17;
    --surface:      #2a2723;
    --border:       #3a3530;
    --ink:          #ece8e0;
    --ink-soft:     #a39d92;
    --accent:       #7eb098;
    --accent-soft:  #98c1ad;
    --accent-deep:  #6a9c84;
    --warn:         #c47840;
    --error:        #d96d5b;
    --shadow:       0 1px 2px rgba(0,0,0,0.20), 0 4px 12px rgba(0,0,0,0.30);

    --ink-on-accent:        #1c1a17;
    --flash-error-bg:       #3b1c1c;
    --flash-error-border:   #6a2e2e;
    --flash-notice-bg:      #2e2618;
    --flash-notice-border:  #4a3a1c;
    --flash-ok-bg:          #1f3328;
    --flash-ok-border:      #2c4f3a;
    --session-dot:          #e0925a;
    --convo-mic-bg:         #6a9c84;
    --convo-mic-bg-hover:   #7eb098;
    --convo-mic-muted:      #5a5550;
    --wx-sun:               #f0b860;
    --wx-partly:            #d0b070;
    --wx-cloud:             #a0b0c0;
    --wx-rain:              #7a9fd0;
    --wx-snow:              #9ab2d4;
    --wx-fog:               #b0b0b0;
    --wx-thunder:           #b08adf;
  }
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }
/* `overflow-x: clip` cuts off horizontal-escaping children without creating
   a scroll container — that distinction matters because the older
   `overflow-x: hidden` does establish a scroll container, which captures
   `position: sticky` descendants and breaks the sticky site header on
   mobile. We rely on `overflow-wrap: break-word` (below on body) for the
   original long-string protection in browsers that lack clip. */
html, body { overflow-x: clip; }

body {
  margin: 0;
  font-family: 'Manrope', system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 17px;
  font-weight: 500;
  line-height: 1.55;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Long unbroken strings (URLs, single-word strain names, manufacturer
     identifiers) used to push cards wider than the viewport on /shelf and
     similar surfaces. Break them at the last possible moment. */
  overflow-wrap: break-word;
}

p { font-weight: 500; }
strong, b { font-weight: 700; }

a { color: var(--accent); }
a:hover { color: var(--accent-soft); }

main {
  max-width: 36rem;
  margin: 0 auto;
  padding: 1.5rem;
}
/* Conversation surface escapes main's horizontal padding so bubbles can
   pin to the viewport edges, the way iOS Messages and Sesame.ai do. The
   .convo-stream inside still caps at 36rem and centres itself, so the
   bubble column is unchanged on tablet/desktop — only the mobile case
   where the bubble's right edge used to land ~24px short of the screen
   edge is affected. */
main:has(.convo) {
  padding-left: 0;
  padding-right: 0;
  max-width: none;
}
/* T5 #433: on the session page the site header is hidden and the session
   bar takes its place at top:0 as the sole sticky top chrome. The brand
   "Bryn." drops because the surface itself is the orientation marker.
   Site-menu-panel stays in the DOM (it's a sibling of <header>, not a
   child) and opens from the hamburger that now lives inside the session
   bar. All other pages render the site header normally. */
body[data-route="session"] > header.site { display: none; }
/* T5 #433 (rev 135): session bar is now two rows (brand-row on top,
   session-row below). `--convo-bar-h` consumed by `.convo-select-bar` for
   its top offset; size it to the rendered two-row height so the floating
   select bar clears the chrome cleanly. ~3.85rem Row 1 + ~3.25rem Row 2
   = ~7.1rem. Scoped to the session route so other pages keep the prior
   value (only the session page renders the bar). */
body[data-route="session"] { --convo-bar-h: 7.1rem; }
header.site {
  position: sticky;
  top: 0;
  z-index: 50;
  background: transparent;
  border-bottom: none;
  margin-bottom: 1rem;
  transition:
    background 220ms ease,
    backdrop-filter 220ms ease,
    -webkit-backdrop-filter 220ms ease;
}
/* Fade-tail below the header so the blur edge feathers into the page bg
   instead of cutting hard. The tail itself has its own backdrop-filter,
   masked by a vertical gradient so the blur fades from full at the top to
   none at the bottom — gives a proper iOS-style gradient blur instead of
   a hard cutoff. Hidden at scroll-top; fades in once the user has scrolled. */
header.site::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 3rem;
  pointer-events: none;
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  background: linear-gradient(
    to bottom,
    rgba(250, 247, 242, 0.55),
    rgba(250, 247, 242, 0)
  );
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 25%, transparent 100%);
          mask-image: linear-gradient(to bottom, black 0%, black 25%, transparent 100%);
  opacity: 0;
  transition: opacity 220ms ease;
}
body.scrolled header.site {
  background: linear-gradient(
    to bottom,
    rgba(250, 247, 242, 0.92),
    rgba(250, 247, 242, 0.6)
  );
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
}
body.scrolled header.site::after {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  header.site,
  header.site::after { transition: none; }
}
header.site .inner {
  max-width: 36rem;
  margin: 0 auto;
  padding: 0.8rem 1.5rem;
  display: flex;
  align-items: center;
  position: relative;
  min-height: 2.25rem;
}
/* Rev 135 fix: brand styling lifted out of the `header.site` scope so the
   same `.site-brand` in `.convo-header-brand` Row 1 of the session bar
   picks up identical typography + absolute-centring + accent colour. Other
   site-header chrome classes (.site-menu-toggle, .site-header-end,
   .header-bell, .dev-badge) were already class-scoped; brand was the only
   holdout. Absolute centring requires the parent (`.inner` or
   `.convo-header-brand`) to have `position: relative` — both do. */
.site-brand {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-size: 1.5rem;
  line-height: 1.5;
  margin: 0;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 0.01em;
}
.site-brand a {
  color: inherit;
  text-decoration: none;
}

/* Hamburger toggle (left of brand). Glyph only at rest — the circle fills
   in only on hover. When active (menu open), the bars morph to an X but
   the circle is suppressed so the X reads as the active affordance.
   Shape is locked square via aspect-ratio + flex-shrink so the parent
   flex row can't squash it into an ellipse. */
.site-menu-toggle {
  justify-self: start;
  background: transparent;
  border: none;
  width: 2.25rem;
  height: 2.25rem;
  min-width: 2.25rem;
  min-height: 2.25rem;
  flex: 0 0 2.25rem;
  aspect-ratio: 1 / 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: var(--ink);
  cursor: pointer;
  margin-left: -0.4rem;
  box-shadow: none;
  /* Kill iOS Safari's default tap-highlight: with border-radius 999px the
     system overlay reads as a hard dark filled circle when the user taps
     the X to close the menu. -webkit-tap-highlight-color: transparent
     suppresses it; the :active rule below keeps the toggle visibly
     transparent through the touch frame too. */
  -webkit-tap-highlight-color: transparent;
  transition: background 160ms ease, box-shadow 160ms ease, color 160ms ease;
}
.site-menu-toggle:active {
  background: transparent;
  box-shadow: none;
}
/* Mobile Safari sometimes shows a dark filled circle where the icon should
   be after the menu close — the tap-highlight rectangle interacting with
   border-radius:999px, plus a lingering `:focus` ring on the button after
   tap. Hard-suppress both: belt on `-webkit-tap-highlight-color` already
   set above, braces on the focus state here. The keyboard-accessibility
   ring is preserved via `:focus-visible` below. */
.site-menu-toggle:focus {
  outline: none;
  box-shadow: none;
  background: transparent;
}
@media (hover: hover) {
  /* No circular hover background. The earlier white-circle + drop-shadow
     hover state was the desktop analogue of the iOS tap-highlight problem
     documented above: after a click to close the menu the cursor lingers
     over the toggle and the hover circle would fade in, sitting where the
     hamburger icon should read cleanly — Justin called it "a dark circle
     where the menu icon should be". A simple colour shift is enough of an
     affordance; the icon itself doesn't need a frame. */
  .site-menu-toggle:hover {
    color: var(--accent);
    background: transparent;
    box-shadow: none;
  }
}
.site-menu-toggle[aria-expanded="true"] {
  color: var(--accent);
  background: transparent;
  box-shadow: none;
}
.site-menu-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.hamburger-glyph {
  display: block;
  color: var(--ink);
  overflow: visible;
}
.hamburger-glyph .bar {
  /* Rotate around the bar's own midpoint (transform-box: fill-box scopes
     transform-origin to the element's own bounding box on SVG). The
     translateY then carries the rotated bar to the SVG centre, so bar-1
     and bar-3 cross cleanly to form an X. */
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 220ms ease, opacity 180ms ease;
}
.site-menu-toggle[aria-expanded="true"] .hamburger-glyph .bar-1 {
  transform: translateY(6px) rotate(45deg);
}
.site-menu-toggle[aria-expanded="true"] .hamburger-glyph .bar-2 {
  opacity: 0;
}
.site-menu-toggle[aria-expanded="true"] .hamburger-glyph .bar-3 {
  transform: translateY(-6px) rotate(-45deg);
}
.site-menu-spacer {
  display: block;
  justify-self: end;
  /* Mirror the toggle's footprint so the brand stays optically centered. */
  width: 2.5rem;
  height: 1px;
}

/* Full-viewport menu panel below the header. Always position:fixed; toggled
   via opacity + visibility so the paperfold cascade on items below can do
   its own motion without the panel collapsing under it on close. */
.site-menu-panel {
  position: fixed;
  top: var(--site-header-h);
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 45;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* Close timing held to cover the full per-item paperfold cascade
     below. Items 1-10 finish their opacity transitions at 0 + 220,
     55 + 220, … 495 + 220 = up to 715ms (495ms last delay + 220ms
     opacity transition). Delay the panel opacity fade so it starts at
     495ms and finishes at 715ms, then snap visibility to hidden. Open
     stays snappy at 220ms. Previously this stopped at 550ms which
     matched a 7-item cascade and clipped items 8-10 (Admin + signout
     for owners) mid-fold. */
  transition: opacity 220ms ease 495ms, visibility 0s linear 715ms;
  background: rgba(250, 247, 242, 0.45);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
}
/* Desktop: the translucent backdrop reads as a "popup over the page" on
   wider viewports because the items sit in a 36rem column with content
   bleeding through on either side. Justin wanted a proper full-screen
   takeover on desktop, so swap the translucent surface for a near-opaque
   cream below the header (matches the rest of the page background so the
   transition reads as "menu replaces page" rather than "menu floats on
   top"). Mobile keeps the translucent blur — the viewport is narrow enough
   that the items cover most of the width naturally. */
@media (min-width: 641px) {
  .site-menu-panel {
    background: rgba(243, 238, 229, 0.98);
  }
}
/* When the menu is open, force the header into its scrolled-look state
   so the header bg matches the menu panel's near-opaque cream below.
   Uses `body:has(...)` rather than `header.site:has(.site-menu-panel…)`
   because the menu panel is now a sibling of the header (it lives at
   body level so no header backdrop-filter can ever trap it — see
   layout.php). Also dropped the backdrop-filter on the header for the
   menu-open state since that filter on a now-irrelevant ancestor still
   creates a stacking-context surprise (and the scrolled-state rule below
   already handles the blur when the user has scrolled). */
@media (min-width: 641px) {
  body:has(.site-menu-panel[data-open="true"]) header.site {
    background: linear-gradient(
      to bottom,
      rgba(250, 247, 242, 0.92),
      rgba(250, 247, 242, 0.6)
    );
  }
  body:has(.site-menu-panel[data-open="true"]) header.site::after {
    opacity: 1;
  }
}
.site-menu-panel[data-open="true"] {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity 220ms ease, visibility 0s linear 0s;
}
.site-menu-panel > .inner {
  max-width: 36rem;
  margin: 0 auto;
  padding: 0 1.5rem;
  /* Force-stack each item even if a stray cascade tries to make them inline.
     Flex column with stretched cross-axis is the clearest way to express
     "one per row, full width" that no single property can override.
     min-height: 100% lets us push the signout button to the foot via
     margin-top: auto on the form. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  min-height: 100%;
  box-sizing: border-box;
  /* Paperfold runway: items fold out from their top edge, cascaded by
     nth-child below. perspective gives the rotation real depth instead of
     a flat orthographic flip. Inspired by felixniklas.com/paperfold. */
  perspective: 720px;
  perspective-origin: top center;
}
/* Desktop takeover: drop the 36rem column cap so menu items stretch to
   the viewport with a 3rem gutter — Justin: "menu still squashed on
   desktop". Source-ordered AFTER the base .inner block above so the
   equal-specificity max-width override actually wins. Mobile keeps the
   36rem cap; the viewport is already that narrow. */
@media (min-width: 641px) {
  .site-menu-panel > .inner {
    max-width: none;
    padding: 0 3rem;
  }
}
.site-menu-panel > .inner > a,
.site-menu-panel > .inner > form,
.site-menu-panel > .inner > .menu-row {
  display: block !important;
  width: 100% !important;
  flex: 0 0 auto;
  transform: rotateX(-92deg);
  transform-origin: top center;
  opacity: 0;
  transition:
    transform 360ms cubic-bezier(0.32, 0.72, 0.24, 1),
    opacity 220ms ease;
  will-change: transform, opacity;
  backface-visibility: hidden;
}
.site-menu-panel[data-open="true"] > .inner > a,
.site-menu-panel[data-open="true"] > .inner > form,
.site-menu-panel[data-open="true"] > .inner > .menu-row {
  transform: rotateX(0deg);
  opacity: 1;
}
/* Cascade: each panel unfolds slightly after the one above it. Reverses on
   close because the delay applies to BOTH directions of the transition,
   which means the bottom item still has the longest delay when folding
   shut — looks like the menu collapsing top-down too. */
.site-menu-panel > .inner > *:nth-child(1)  { transition-delay:   0ms,   0ms; }
.site-menu-panel > .inner > *:nth-child(2)  { transition-delay:  55ms,  55ms; }
.site-menu-panel > .inner > *:nth-child(3)  { transition-delay: 110ms, 110ms; }
.site-menu-panel > .inner > *:nth-child(4)  { transition-delay: 165ms, 165ms; }
.site-menu-panel > .inner > *:nth-child(5)  { transition-delay: 220ms, 220ms; }
.site-menu-panel > .inner > *:nth-child(6)  { transition-delay: 275ms, 275ms; }
.site-menu-panel > .inner > *:nth-child(7)  { transition-delay: 330ms, 330ms; }
/* With dev + owner modes on (Justin) the menu can grow to 9 items —
   Sessions block, Reminders row, Shelf·Kit row, Bookmarks, Memory,
   Account, Admin, plus the signout form. Without delays past 7 the
   trailing items (Admin in particular) fold/unfold instantly instead
   of joining the cascade. Padding the cascade out to 10 covers any
   realistic permutation. */
.site-menu-panel > .inner > *:nth-child(8)  { transition-delay: 385ms, 385ms; }
.site-menu-panel > .inner > *:nth-child(9)  { transition-delay: 440ms, 440ms; }
.site-menu-panel > .inner > *:nth-child(10) { transition-delay: 495ms, 495ms; }
@media (prefers-reduced-motion: reduce) {
  .site-menu-panel > .inner > a,
  .site-menu-panel > .inner > form,
  .site-menu-panel > .inner > .menu-row {
    transform: none;
    transition: opacity 180ms ease;
  }
  .site-menu-panel > .inner > * { transition-delay: 0ms !important; }
  /* Reduced-motion users get an instant close — no paperfold cascade to
     wait for, so the held-open delay on the panel transitions away too. */
  .site-menu-panel {
    transition: opacity 180ms ease, visibility 0s linear 180ms;
  }
}
/* One label rhythm from Sessions at the top to Admin at the bottom: same
   font-family/size/weight/line-height. Applies to standalone <a> children
   of .inner, label-bearing children of .menu-row blocks, the separator
   dots, and the count badges — all four share the same typographic shape
   so the menu reads as one consistent column rather than a hierarchy. */
.site-menu-panel a,
.site-menu-panel .menu-link-button,
.site-menu-panel .menu-sep,
.site-menu-panel .menu-count {
  font-family: inherit;
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.4;
  color: var(--ink-soft);
  text-decoration: none;
}
.site-menu-panel > .inner > a {
  display: block;
  text-align: center;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--border);
  box-sizing: border-box;
}
/* The link immediately before .signout-form would otherwise leave an
   orphan divider hovering above the bottom-pinned sign-out button. */
.site-menu-panel > .inner > a:has(+ .signout-form),
.site-menu-panel > .inner > .menu-row:has(+ .signout-form) {
  border-bottom: none;
}
.site-menu-panel a:hover,
.site-menu-panel .menu-link-button:hover {
  color: var(--accent);
}
.site-menu-panel .signout-form {
  /* Push Sign out to the foot of the menu — works because .inner is a flex
     column with min-height:100% and signout-form is its last direct child. */
  margin: auto 0 1.25rem;
  padding: 0;
}
.site-menu-panel .signout-form button.signout-button {
  display: block;
  width: 100%;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--ink-soft);
  font-size: 1rem;
  font-family: inherit;
  font-weight: 500;
  text-align: center;
  padding: 0.75rem 0;
  cursor: pointer;
  box-sizing: border-box;
}
.site-menu-panel .signout-form button.signout-button:hover {
  color: var(--accent);
  border-color: var(--accent);
}

/* Combined inline rows in the hamburger menu — Sessions/Start/Continue and
   Reminders/Dev each pack multiple affordances onto one paperfold panel
   item. The row's own bottom border replaces the per-link border each
   child would have. Selector is scoped to the panel-inner level to
   outweigh the ".site-menu-panel > .inner > .menu-row { display: block
   !important }" rule that applies the paperfold to all menu-rows. */
.site-menu-panel > .inner > .menu-row.menu-row-sessions,
.site-menu-panel > .inner > .menu-row.menu-row-reminders,
.site-menu-panel > .inner > .menu-row.menu-row-shelf-kit {
  display: flex !important;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  /* Row carries the height (0.75rem default, bumped to 0.85rem on mobile
     via the rule lower in this file). Inner anchors are stripped of the
     anchor-default padding so the row's padding is the single source of
     vertical rhythm — same shape as a standalone <a> direct child of
     .inner. */
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--border);
  box-sizing: border-box;
}
/* Inline links inside a combined row drop their per-link padding/border so
   the row's own padding + divider does the work. Font/colour come from the
   unified rule above. */
.site-menu-panel .menu-row a,
.site-menu-panel .menu-row .menu-link-button {
  padding: 0;
  border-bottom: none;
}
.site-menu-panel .menu-sep {
  user-select: none;
}
/* Inline form inside the combined row — needs to drop the block/full-width
   forced on direct children of .inner. */
.site-menu-panel .menu-row-sessions .menu-inline-form {
  display: inline-flex !important;
  width: auto !important;
  margin: 0;
  padding: 0;
  transform: none;
  opacity: 1;
  transition: none;
}
.site-menu-panel .menu-link-button {
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0;
}
.site-menu-panel .menu-count {
  /* Same shape as the labels per the unified rule above. */
}

/* Full-height open panel scrolls its own .inner so the header stays fixed
   above. Applies on both desktop and mobile now that the panel itself is
   fixed-positioned in the base rule. */
.site-menu-panel[data-open="true"] > .inner {
  padding: 0.75rem 1.5rem;
  height: 100%;
  overflow-y: auto;
}
@media (max-width: 640px) {
  /* Bump the whole menu's typography one step on mobile. All four kinds
     of label-text (anchors, the Start-session button, the separator
     dots, and the count badges) share the size so the menu reads as one
     consistent column. */
  .site-menu-panel[data-open="true"] a,
  .site-menu-panel[data-open="true"] .menu-link-button,
  .site-menu-panel[data-open="true"] .menu-sep,
  .site-menu-panel[data-open="true"] .menu-count {
    font-size: 1.125rem;
  }
  /* Outer-container padding lives on whichever element is the direct
     child of .inner — standalone <a>'s, .menu-row blocks, and forms.
     One value across all of them keeps row heights uniform. The
     stronger-specificity override below strips inner-anchor padding
     inside menu-rows so the row's padding is the single source. */
  .site-menu-panel[data-open="true"] > .inner > a,
  .site-menu-panel[data-open="true"] > .inner > .menu-row {
    padding: 0.85rem 0;
  }
  .site-menu-panel[data-open="true"] .menu-row a,
  .site-menu-panel[data-open="true"] .menu-row .menu-link-button {
    padding: 0;
  }
}

h1, h2, h3 { color: var(--accent); }
h1 { font-size: 1.6rem; line-height: 1.3; margin-top: 0; }
h2 { font-size: 1.25rem; margin-top: 2rem; }

p, ul, ol { color: var(--ink); }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.25rem 1.25rem 1.1rem;
  box-shadow: var(--shadow);
  margin-bottom: 1.5rem;
}
.card > :first-child { margin-top: 0; }
.card > :last-child  { margin-bottom: 0; }

label {
  display: block;
  font-weight: 500;
  margin-top: 1rem;
  margin-bottom: 0.25rem;
}
label .hint {
  font-weight: 400;
  color: var(--ink-soft);
  font-size: 0.9rem;
  margin-left: 0.25rem;
}

input,
select,
textarea {
  /* 16px floor prevents iOS Safari from auto-zooming on focus. Applied to
     EVERY input/select/textarea including ones with inline style overrides
     elsewhere — !important ensures no later rule drops below the floor. */
  font-size: 16px !important;
}

/* Branded toggle switch for every checkbox. Uses appearance:none + a
   radial-gradient "thumb" baked into the background — works reliably on
   Safari, Chrome, and Firefox without needing pseudo-elements (which
   don't render on replaced form controls in WebKit). */
input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  width: 2.6rem;
  height: 1.5rem;
  flex-shrink: 0;
  cursor: pointer;
  border: 1px solid var(--border);
  border-radius: 999px;
  background-color: var(--border);
  background-image:
    radial-gradient(circle 0.55rem at 0.75rem 50%, #fff 99%, transparent 100%);
  background-repeat: no-repeat;
  transition:
    background-color 180ms ease,
    background-image 180ms cubic-bezier(0.32, 0.72, 0.24, 1),
    border-color 180ms ease,
    box-shadow 180ms ease;
  vertical-align: middle;
  margin: 0;
  padding: 0;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
}
input[type="checkbox"]:checked {
  background-color: var(--accent);
  border-color: var(--accent);
  background-image:
    radial-gradient(circle 0.55rem at 1.85rem 50%, #fff 99%, transparent 100%);
  box-shadow: none;
}
input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
input[type="checkbox"]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
input[type="text"],
input[type="email"],
input[type="number"],
input[type="tel"],
input[type="search"],
input[type="url"],
input[type="password"],
select,
textarea {
  width: 100%;
  padding: 0.65rem 0.75rem;
  font-family: inherit;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  -webkit-appearance: none;
  appearance: none;
}
select {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'><path fill='%235a5a5a' d='M6 8 0 0h12z'/></svg>");
  background-repeat: no-repeat;
  background-position: right 0.75rem center;
  background-size: 0.75rem;
  padding-right: 2.25rem;
}
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent-soft);
  outline-offset: 1px;
  background: var(--surface);
}

/* Full-width primary action — apply to standalone form submits and any
   button/anchor that is the primary CTA on a surface. Small inline
   buttons (action-item dismiss, hamburger toggle, etc) keep .secondary
   with explicit padding overrides and don't get this. */
button.btn-block, .btn-block, button.btn-full, .btn-full,
form > button[type="submit"]:only-child:not(.secondary):not([style*="font-size"]),
form > p > button[type="submit"]:only-child:not(.secondary):not([style*="font-size"]),
form.full-submit > button[type="submit"] {
  display: block;
  width: 100%;
  text-align: center;
  font-size: 1.05rem;
  padding: 0.9rem 1.2rem;
  box-sizing: border-box;
}

button, .btn {
  display: inline-block;
  background: var(--accent);
  color: var(--ink-on-accent);
  padding: 0.7rem 1.2rem;
  border: none;
  border-radius: var(--radius);
  font-size: 1rem;
  font-family: inherit;
  font-weight: 500;
  cursor: pointer;
  text-decoration: none;
}
/* All button hover rules gated on `@media (hover: hover)` so touch
   devices (iOS Safari especially) don't treat the first tap as a hover
   activation and the second tap as the click — a classic two-tap-to-fire
   bug Justin hit on the mute button. With this gate, touch devices skip
   `:hover` entirely; mouse devices keep the visual affordance. Anchor
   `a.btn` hover lock to #fff is still scoped here so it only overrides
   the inherited `a:hover` colour on hover-capable devices. */
@media (hover: hover) {
  button:hover, .btn:hover { background: var(--accent-deep); }
  a.btn:hover { color: var(--ink-on-accent); }
  button.secondary:hover, .btn.secondary:hover {
    background: var(--bg);
    border-color: var(--accent-soft);
  }
}

button.secondary, .btn.secondary {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--border);
}

.flash {
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  margin-bottom: 1rem;
  font-size: 0.95rem;
}
.flash.error { background: var(--flash-error-bg); color: var(--error); border: 1px solid var(--flash-error-border); }
.flash.notice { background: var(--flash-notice-bg); color: var(--warn); border: 1px solid var(--flash-notice-border); }
.flash.ok    { background: var(--flash-ok-bg); color: var(--accent); border: 1px solid var(--flash-ok-border); }

.muted { color: var(--ink-soft); }
.tiny  { font-size: 0.85rem; }

/* Conversation UI */

.convo {
  display: flex;
  flex-direction: column;
  /* dvh (dynamic viewport height) shrinks when the iOS keyboard opens,
     unlike vh which stays at the visual viewport's initial size. With vh
     the chat container stayed at full screen height while the keyboard
     ate the bottom, causing position:fixed .convo-voice to overlap the
     last message and the body to scroll-jump as iOS tried to keep the
     focused field in view. dvh lets layout track the visible area. */
  /* T5 #433: site header is hidden on the session page; the session bar
     is sticky at top:0 and IN FLOW (was fixed before this rev), so its
     own height naturally pushes the rest of the convo down. The earlier
     `padding-top: var(--convo-bar-h, 4rem)` would double-count and leave
     a 4rem dead zone above the first bubble — removed. dvh covers the
     full viewport; the bar occupies the first ~3.85rem and the stream
     fills the remainder. */
  min-height: 100dvh;
}
/* T5 #433 (rev 134): full-width sticky in-session bar sitting at top:0
   as the sole top chrome on the session page. Visual treatment mirrors
   `header.site` exactly — transparent at scroll-top, frosted gradient
   + backdrop-filter when the user scrolls, paired ::after fade-tail
   feathering the blur edge into the page bg. The site header is hidden
   via `body[data-route="session"] > header.site { display: none }`. */
.convo-header-bar {
  position: sticky;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  background: transparent;
  border-bottom: none;
  margin-bottom: 1rem;
  transition:
    background 220ms ease,
    backdrop-filter 220ms ease,
    -webkit-backdrop-filter 220ms ease;
}
/* Fade-tail under the session bar, same pattern as `header.site::after`.
   Hidden at scroll-top; fades in once `body.scrolled` is on. */
.convo-header-bar::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 3rem;
  pointer-events: none;
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  background: linear-gradient(
    to bottom,
    rgba(250, 247, 242, 0.55),
    rgba(250, 247, 242, 0)
  );
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 25%, transparent 100%);
          mask-image: linear-gradient(to bottom, black 0%, black 25%, transparent 100%);
  opacity: 0;
  transition: opacity 220ms ease;
}
body.scrolled .convo-header-bar {
  background: linear-gradient(
    to bottom,
    rgba(250, 247, 242, 0.92),
    rgba(250, 247, 242, 0.6)
  );
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
}
body.scrolled .convo-header-bar::after {
  opacity: 1;
}
/* Same menu-open courtesy state the site header uses: when the slide-out
   menu panel is open, force the bar into its scrolled-look so the bar bg
   matches the near-opaque menu cream below. */
@media (min-width: 641px) {
  body:has(.site-menu-panel[data-open="true"]) .convo-header-bar {
    background: linear-gradient(
      to bottom,
      rgba(250, 247, 242, 0.92),
      rgba(250, 247, 242, 0.6)
    );
  }
  body:has(.site-menu-panel[data-open="true"]) .convo-header-bar::after {
    opacity: 1;
  }
}
@media (prefers-reduced-motion: reduce) {
  .convo-header-bar,
  .convo-header-bar::after { transition: none; }
}
/* T5 #433 (rev 135): Row 1 of the session bar — mirrors `header.site .inner`
   verbatim so the brand row reads identically to every other page (hamburger
   on the left, "Bryn." absolute-centered, reminders bell on the right).
   Same `.site-brand` / `.site-header-end` children that the site header uses
   so the class-scoped styles in lines ~276–290 etc apply without duplication. */
.convo-header-brand {
  max-width: 36rem;
  margin: 0 auto;
  padding: 0.8rem 1.5rem;
  min-height: 2.25rem;
  display: flex;
  align-items: center;
  position: relative;
}
/* T5 #433 (rev 135): Row 2 of the session bar — Session./Started X on the
   left, action icons on the right. Same column / padding shape as Row 1 so
   the two rows feel visually aligned within the same bar. */
.convo-header {
  max-width: 36rem;
  margin: 0 auto;
  padding: 0.4rem 1.5rem 0.6rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
/* T5 #433: Row-2 right cluster — voice-mode + mute + phone? + end. Bell
   moved to Row 1 with the rest of the standard chrome. Flex-wrap defends
   against any future button pushing the row past 36rem. */
.convo-header-actions {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  align-items: center;
}
/* T5 #433 / 5b: drop the mode-toggle in the bar on narrow phones. The FAB
   mic icon already provides voice-mode access, so the bar saves a slot. */
@media (max-width: 480px) {
  .convo-header-actions #convo-mode-toggle { display: none; }
}
.convo-stream {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  flex: 1;
  padding-bottom: 1rem;
  scroll-padding-bottom: 9rem;
  /* Cap matches the in-session header bar (.convo-header) at 33rem so the
     stream and the composer below sit inside the same vertical column as
     the in-session header — Justin: "the header is the max width". Site
     header stays at its own 36rem in `header.site .inner`. */
  width: 100%;
  max-width: 33rem;
  margin-inline: auto;
}
/* Quiet timestamp break between bubbles (dev-queue #348). Server-rendered
   on session-start, large idle gaps, and cross-day boundaries; the JS
   appendBubble() emits one when a fresh turn lands after a long pause or
   crosses midnight. Centred, muted, low-emphasis so it doesn't compete
   with the conversational read. */
.convo-timestamp-break {
  align-self: center;
  font-size: 0.75rem;
  color: var(--ink-soft);
  opacity: 0.65;
  letter-spacing: 0.04em;
  /* Dev-queue #361: breathing room above and below the label so a day/time
     stamp lands as its own beat between bubbles rather than crowding the
     bubble above it. Roughly twice the previous 0.6rem head margin, plus
     a matching tail so the next bubble doesn't slap into the label
     either. */
  margin: 1.4rem 0 0.6rem;
  padding: 0.15rem 0.5rem;
  user-select: none;
}
/* First label in the stream is the session-start marker — there's
   nothing above it, so the 1.4rem head margin reads as a gap at the
   top of a fresh session (Justin's conv 34: "I don't think we need to
   have so much of a space at the top"). Zero it for the first child;
   subsequent labels (cross-day, idle gap) keep the breathing room. */
.convo-stream > .convo-timestamp-break:first-child {
  margin-top: 0;
}
.bubble {
  /* Extra space below when scrolled into view so a freshly-arrived bubble
     doesn't snap right against the fixed input/voice composer. ~10rem
     covers the composer height plus a comfortable reading gap. */
  scroll-margin-bottom: 10rem;
}
.bubble {
  max-width: var(--bubble-max-width);
  display: flex;
  /* Stack children vertically: when a bubble contains the prose text plus
     a weather-cards block or a chip row, default flex-row would put them
     side by side (text left, cards right). Single-child bubbles look
     identical either way, so column is safe across the board. */
  flex-direction: column;
}
@media (max-width: 640px) {
  /* Mobile cap. The min() with 100svw guards against iOS keyboard reflow
     shifting the visible edge — dev-queue #380. */
  .bubble { max-width: min(var(--bubble-max-width-mob), calc(100svw - 1rem)); }
}
/* Belt-and-braces alignment: align-self handles the cross-axis pin, but
   short content can read as "floating in the column" rather than clearly
   sided. margin-{left,right}: auto pushes the bubble hard against the
   opposite edge of the stream so even a one-line opener visibly hugs the
   correct side. */
.bubble-user      { align-self: flex-end;   margin-left:  auto; margin-right: 0; }
.bubble-assistant { align-self: flex-start; margin-right: auto; margin-left:  0; }
/* When Bryn's reply is purely an action token (e.g. [[weather:cards]] on
   its own paragraph), the bubble has no text to show. Drop the chrome and
   let the action UI stand alone in the conversation flow rather than
   leaving a hollow speech bubble above the cards/chips. */
.bubble.bubble-actions-only {
  max-width: 100%;
  align-self: stretch;
  display: block;
}
/* Pre-collapse during streaming: if the live bubble is empty because the
   stream so far is just an action token, hide the bubble-text chrome so
   the user never sees a hollow speech bubble flash before processBubbleLinks
   replaces it with the canonical cards/chips render on stream end. */
.bubble.bubble-actions-only-pending .bubble-text {
  padding: 0;
  border: 0;
  background: transparent;
}
.bubble-text {
  padding: var(--bubble-pad-y) var(--bubble-pad-x);
  border-radius: 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  white-space: pre-wrap;
  word-wrap: break-word;
  line-height: 1.4;
  /* Suppress iOS Safari's long-press callout (text-selection / share
     sheet) so our long-press-to-report gesture has a clean window. Tap
     selection still works via the native double-tap path. */
  -webkit-touch-callout: none;
}
.bubble-user .bubble-text {
  background: var(--accent);
  color: var(--ink-on-accent);
  border-color: var(--accent);
  border-bottom-right-radius: 4px;
}
/* Aside-flagged user turns ("Toby come here", "good boy") — Tidy spotted
   the user wasn't talking to Bryn. Render greyed + italic so the user can
   see the system noticed; these turns are also filtered out of the
   message history fed to subsequent Claude calls. */
.bubble-user[data-aside="true"] .bubble-text {
  background: transparent;
  color: var(--ink-soft);
  border: 1px dashed var(--border);
  font-style: italic;
}
.bubble-user[data-aside="true"] .bubble-text::after {
  content: " · aside";
  font-size: 0.75rem;
  opacity: 0.7;
  font-style: normal;
}
.bubble-assistant .bubble-text {
  border-bottom-left-radius: 4px;
}

/* T9 / item 301: tidy-pass animation on user bubbles. Removed words get a
   strike-through that fades the text to transparent, then the JS prunes the
   span from the DOM so the bubble re-flows around the gap. Added words pulse
   in once. Both timings are matched to the renderTidyDiff REMOVE_MS so the
   class can be cleared cleanly. */
.bubble-text .tidy-removing {
  display: inline;
  animation: tidy-remove 0.65s ease-out forwards;
  text-decoration: line-through;
  text-decoration-thickness: 0.06em;
  text-decoration-color: currentColor;
}
.bubble-text .tidy-added {
  display: inline;
  animation: tidy-add 0.45s ease-out forwards;
}
@keyframes tidy-remove {
  0%   { opacity: 1; }
  50%  { opacity: 0.7; }
  100% { opacity: 0; }
}
/* T6 / item 362: on app open, fresh assistant bubbles that arrived after
   the user's last turn fade in one at a time instead of all-at-once. JS
   applies .bubble-stagger-in + an animation-delay to each. The animation
   names mirror the tidy-* helpers above to keep the reduced-motion rule
   in one place. */
.bubble.bubble-stagger-in {
  opacity: 0;
  animation: bubble-stagger-in 0.45s ease-out forwards;
}
@keyframes bubble-stagger-in {
  0%   { opacity: 0; transform: translateY(var(--bubble-stagger-y)); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes tidy-add {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .bubble-text .tidy-removing,
  .bubble-text .tidy-added,
  .bubble.bubble-stagger-in {
    animation: none;
  }
  .bubble-text .tidy-removing { opacity: 0; }
  .bubble.bubble-stagger-in { opacity: 1; }
}

/* Typing indicator — three animated dots shown after user submits and
   before Claude's first delta arrives. Replaced by the streamed bubble. */
.bubble-thinking .bubble-text {
  display: inline-flex;
  /* #441: dots about a quarter smaller than the previous size. Gap + padding
     scaled together so the placeholder doesn't read oversized next to a
     real bubble. */
  gap: 0.26rem;
  align-items: center;
  padding: 0.7rem 0.85rem;
}
/* T5 #434/#435: hide a held-open bubble while its opening delay is armed
   so the standalone typing-dots placeholder reads as the only "in
   progress" element. Without this, a visibly-empty bubble sits below
   the dots for the duration of the pause. */
.bubble:has(.bubble-text[data-opening-delayed="1"]) {
  display: none;
}
.bubble-thinking .dot {
  /* #441: 0.5rem → 0.375rem (a quarter smaller, matching Justin's spec). */
  width: 0.375rem;
  height: 0.375rem;
  border-radius: 50%;
  background: var(--ink-soft);
  opacity: 0.4;
  animation: bryn-typing 1.2s infinite ease-in-out;
}
.bubble-thinking .dot:nth-child(2) { animation-delay: 0.15s; }
.bubble-thinking .dot:nth-child(3) { animation-delay: 0.30s; }
@keyframes bryn-typing {
  0%, 80%, 100% { opacity: 0.3; transform: translateY(0); }
  40%           { opacity: 1;   transform: translateY(-3px); }
}

/* Privacy blur. Text remains in DOM at full readability for screen readers. */
.bryn-blurable {
  filter: blur(var(--bryn-blur-px, 6px));
  transition: filter 120ms ease-out;
  cursor: pointer;
}
.bryn-blurable.is-revealed { filter: blur(0); cursor: default; }
.bryn-blur-light  { --bryn-blur-px: 3px; }
.bryn-blur-medium { --bryn-blur-px: 6px; }
.bryn-blur-heavy  { --bryn-blur-px: 10px; }
.bryn-blur-max    { --bryn-blur-px: 16px; }

.convo-status {
  font-size: 0.9rem;
  color: var(--ink-soft);
  min-height: 1.5rem;
  padding: 0.25rem 0;
}
.convo-status[data-kind="thinking"] { color: var(--accent-soft); font-style: italic; }
.convo-status[data-kind="error"]    { color: var(--error); }

/* Text composer — fixed to the viewport bottom (like .convo-voice) so it
   stays in a consistent place while the conversation scrolls. The inner
   row is capped to the 36rem column so it lines up with the header bar
   above and the bubbles between. */
.convo-input {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 30;
  /* Frosted glass: translucent bg + blur so the bubble column underneath
     fades through, instead of a hard solid bar cutting the conversation
     in two. Kept on a single rule (no @supports) because every browser
     we target has had backdrop-filter for years. */
  background: rgba(243, 238, 229, 0.78);
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  padding: 0.75rem 1.5rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--border);
  /* #429: iOS Safari leaves a dead-space gap between a fixed-bottom
     composer and the on-screen keyboard because the layout viewport stays
     full-height while the visual viewport shrinks. The conversation.js
     visualViewport listener writes the live keyboard height into
     `--keyboard-inset-h` on the root, and we translate the bar up by
     that amount so it sits against the keyboard top — no gap, no
     keyboard-covering-the-input either. Defaults to 0px when no keyboard
     is up (or on browsers without visualViewport, which fall through to
     the previous fixed-bottom behaviour). */
  transform: translateY(calc(-1 * var(--keyboard-inset-h, 0px)));
  transition: transform 80ms ease-out;
}
.convo-input .convo-text-row {
  /* #442: tracks the chat column width set on .convo-header / .convo-stream. */
  max-width: 33rem;
  margin-inline: auto;
}
.convo.mode-text .convo-stream {
  /* Match the voice-mode padding so the last bubble isn't hidden under
     the fixed composer. */
  padding-bottom: 9rem;
}
.convo-input textarea {
  resize: vertical;
  min-height: 48px;
  /* Translucent so the .convo-input frosted glass behind it shows
     through. White textareas in the bar look like pasted-on rectangles
     against the blurred page; this lets the bubble column tint the
     input area subtly while staying readable. */
  background: rgba(255, 255, 255, 0.55);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.convo-text-row {
  display: flex;
  align-items: stretch;
  gap: 0.5rem;
  width: 100%;
}
/* Override the global `textarea { width: 100% }` here so flex sizing wins
   inside the row — width: 100% + flex: 1 fights itself in some browsers,
   leaving the textarea narrower than the available track. */
.convo-text-row textarea {
  /* flex-basis: 0 so the textarea is a pure grow share of the row width.
     With basis: auto the basis tracks content size, and every keystroke
     nudged the column wider (dev-queue #377). */
  flex: 1 1 0;
  min-width: 0;
}
/* Send button sits in a column-flex actions wrapper that matches the
   textarea height; the button itself fills it. align-items:stretch on
   the row makes both children share the row's height, so a 2-row
   textarea and a multi-row dragged textarea both have a button that
   matches. */
.convo-text-row .convo-actions {
  margin-top: 0;
  display: flex;
  align-items: stretch;
}
.convo-text-row .convo-actions > button {
  flex-shrink: 0;
  height: 100%;
}
.convo-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.5rem;
  align-items: center;
}

/* Stick footer to bottom of viewport even when page content is short. */
body {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
main { flex: 1; }

/* Dev-mode badge — only renders when users.is_dev is true. Sits to the
   right of the hamburger toggle in the header so it's visible alongside
   the menu affordance without crowding the bell on the right. */
.dev-badge {
  padding: 0.2rem 0.5rem;
  border-radius: 999px;
  background: var(--warn);
  color: var(--ink-on-accent);
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  user-select: none;
  white-space: nowrap;
  text-decoration: none;
}
a.dev-badge:hover,
a.dev-badge:focus-visible {
  filter: brightness(1.1);
}
.site-menu-toggle + .dev-badge {
  margin-left: 0.25rem;
  align-self: center;
}
.site-header-end {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.header-bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-soft);
  text-decoration: none;
  width: 32px;
  height: 32px;
  padding: 0;
  line-height: 0;
}
.header-bell svg { display: block; }
.header-bell:hover { color: var(--accent); }
/* Empty-state bell: rendered when there are zero open reminders. The icon
   stays so the affordance is consistent across pages, but it dims back so
   it doesn't read as "you have something to deal with". Hovers brighten. */
.header-bell.is-empty { color: var(--ink-soft); opacity: 0.55; }
.header-bell.is-empty:hover { opacity: 1; color: var(--accent); }
.header-bell-count {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--ink-on-accent);
  font-size: 0.65rem;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Header Session button (top-right). When an unfinished session exists,
   the link form renders with `.is-active` and a small dark-orange dot
   appears top-right; when no session is in progress, the form variant
   submits a POST to /session/new. The form-wrapper styling is reset so
   it doesn't add its own spacing. */
.header-session-form {
  margin: 0;
  padding: 0;
  display: inline-flex;
}
.header-session {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  color: var(--ink-soft);
  width: 32px;
  height: 32px;
  padding: 0;
  line-height: 0;
  cursor: pointer;
  text-decoration: none;
  border-radius: var(--radius);
}
.header-session svg { display: block; }
.header-session:hover { color: var(--accent); }
.header-session.is-active::after {
  content: "";
  position: absolute;
  top: 2px;
  right: 4px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--session-dot);
  box-shadow: 0 0 0 1px var(--bg);
}

/* PWA offline banner: bottom bar, full-bleed background, content
   constrained to 36rem inner column. The update banner uses a different
   layout (centred modal card) — see further down. */
.pwa-banner-offline {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 60;
  padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
  font-size: 0.95rem;
  line-height: 1.3;
  box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.06);
  background: var(--warn);
  color: var(--ink-on-accent);
}
.pwa-banner-offline > .inner {
  max-width: 36rem;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
}

/* Update banner: centred modal-style card on a dimmed, blurred backdrop.
   Reads as a focused interrupt rather than a passive bottom strip. */
.pwa-banner-update {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  pointer-events: none;
}
.pwa-banner-update > .inner {
  pointer-events: auto;
  background: var(--accent-deep, var(--accent));
  color: var(--ink-on-accent);
  border-radius: 18px;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
  padding: 1.5rem 1.5rem 1.4rem;
  width: 100%;
  max-width: 26rem;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.75rem;
  text-align: center;
}
.pwa-banner-heading {
  margin: 0;
  font-size: 1.6rem;
  line-height: 1.2;
  font-weight: 700;
  color: var(--ink-on-accent);
}
.pwa-banner-message {
  margin: 0;
  font-size: 1rem;
  line-height: 1.45;
  color: var(--ink-on-accent);
}
.pwa-banner-action {
  display: block;
  width: 100%;
  margin-top: 0.5rem;
  background: var(--surface);
  color: var(--accent-deep, var(--accent));
  border: none;
  padding: 0.85rem 1rem;
  border-radius: var(--radius);
  font-family: inherit;
  font-weight: 700;
  font-size: 1.05rem;
  cursor: pointer;
  box-sizing: border-box;
}
.pwa-banner-action:hover { background: var(--bg); }

/* Full-viewport backdrop dim + blur while the update card is showing.
   Sits at z-index 55, under the card itself (60), so the card stays crisp
   above a defocused page. */
body.pwa-update-active::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 55;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(6px) saturate(120%);
  -webkit-backdrop-filter: blur(6px) saturate(120%);
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .pwa-banner-offline,
  .pwa-banner-update { transition: none; }
}

/* Account-page-only footer — small print + legal nav. Sits at the bottom of
   /account as the last element on the page, then nowhere else in the app. */
.account-footer {
  margin-top: 2rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.account-footer .legal-nav a {
  margin-left: 1rem;
  color: var(--ink-soft);
  text-decoration: none;
  font-size: 0.85rem;
}
.account-footer .legal-nav a:first-child { margin-left: 0; }
.account-footer .legal-nav a:hover { color: var(--accent); }

/* Home-page primary CTAs — full width of the contained area, large touch
   target. Applied to both the anchor and the button so they render
   identically. Prefixed with element selectors so this beats the bare
   `button, .btn { display: inline-block }` rule on specificity, defending
   against any cascade surprises. */
a.home-cta,
button.home-cta,
.home-cta {
  display: block;
  width: 100%;
  text-align: center;
  font-size: 1.1rem;
  padding: 1rem 1.5rem;
  margin: 0.75rem 0;
  box-sizing: border-box;
}
.home-cta-form {
  margin: 0;
  display: block;
  width: 100%;
}
.home-cta-form .home-cta {
  margin: 0; /* the form wrapper handles vertical rhythm */
}

/* Home page greeting: slow fade + slight slide-up. H1 leads, sub-text
   lines stagger in behind it. Initial state is opacity:0 + translateY:12px
   so the elements aren't visible before the animation fires. */
.home-greeting,
.home-greeting-sub {
  text-align: center;
  opacity: 0;
  transform: translateY(12px);
  animation: home-greeting-in 0.9s ease-out forwards;
}
.home-greeting     { animation-delay: 0.75s; }
.home-greeting-sub { animation-delay: 2.05s; }
@keyframes home-greeting-in {
  0%   { opacity: 0; transform: translateY(12px); }
  100% { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .home-greeting,
  .home-greeting-sub {
    animation: none;
    opacity: 1;
    transform: none;
  }
}


/* Pointer-aware verb swap: "Tap" on touch, "Click" on mouse. */
.action-touch, .action-mouse { /* default: show neither until media query picks one */ }
@media (pointer: coarse) {
  .action-mouse { display: none; }
}
@media (pointer: fine) {
  .action-touch { display: none; }
}
@media (pointer: none) {
  /* No pointer at all — show "Tap" as the safer default. */
  .action-mouse { display: none; }
}

/* Full-bleed background for the login screen — turns the whole viewport
   into the sign-in canvas instead of letting the page bg show through. */
body:has(.login-screen) {
  background:
    radial-gradient(circle at 20% -10%, rgba(44, 62, 53, 0.10), transparent 55%),
    radial-gradient(circle at 110% 110%, rgba(58, 138, 111, 0.08), transparent 55%),
    var(--surface);
}

/* Login / signed-out hero copy. */
.hero-headline {
  font-size: 2rem;
  margin-bottom: 0.25rem;
}
/* Login disclosure ("What happens with my data?") — keep it light and
   visually quiet so the sign-in form is the focus. */
.login-disclosure {
  margin-top: 1rem;
}
.login-disclosure summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.login-disclosure summary::-webkit-details-marker { display: none; }
.login-disclosure summary::before {
  content: "›";
  display: inline-block;
  transition: transform 180ms ease;
  color: var(--ink-soft);
}
.login-disclosure[open] summary::before {
  transform: rotate(90deg);
}
.login-disclosure p {
  margin-top: 0.5rem;
}

/* Privacy hint sits directly under the email input as a quiet link to
   /privacy. The footer hosts only the submit button now (form="login-form"
   association lets the button submit from outside the form). */
.login-data-link {
  margin: 0.4rem 0 0;
}

/* Login screen card: no visible box, no rounded corners, no shadow.
   Just text and form fields aligned with the rest of the page. */
.login-screen .card {
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
  padding: 0.5rem 0 0.75rem;
  margin-bottom: 1rem;
}
.login-screen .card h2 { margin-top: 0; }
.hero-subhead {
  font-size: 1.6rem;
  line-height: 1.3;
  color: var(--accent);
  font-weight: 700;
  margin-top: 0.75rem;
  margin-bottom: 1rem;
}

/* Login screen lays out as a tall flex column: hero up top, form held
   down towards the footer. .login-form-wrap is the hinge — its
   margin-top:auto pushes the sign-in card to the bottom of the
   available space, sitting just above the fixed .login-footer. The
   surrounding body:has(.home-footer) rule (further down) takes care
   of bottom padding so the form-wrap never slides under the footer. */
body:has(.login-screen) main {
  display: flex;
  flex-direction: column;
  /* Tighter footer reservation than the signed-in home page: on login,
     the form-wrap is the last thing above the footer, so the default
     8rem buffer leaves a visible gap between the privacy link and the
     button. ~3rem is enough breathing room without floating the form. */
  padding-bottom: 3rem;
  min-height: calc(100dvh - 3rem);
}
.login-screen {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
}
.login-screen .hero-headline,
.login-screen .hero-subhead {
  flex: 0 0 auto;
}
.login-form-wrap {
  margin-top: auto;
}
@media (min-width: 768px) {
  body:has(.login-screen) main {
    padding-top: 3.5rem;
  }
}

/* Home-page weather cards. Horizontal strip below the H1 greeting; each
   card shows an icon + temperature + short label for a single window
   (Now, This evening, Tomorrow, Weekend). Wraps on narrow viewports
   rather than scrolling so the user always sees all of them.
   Per-icon colour theming gives each weather kind a distinct feel
   without overwhelming the rest of the home page. */
.weather-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 0.5rem 0 1.25rem;
}
.weather-card {
  flex: 1 1 6.5rem;
  min-width: 6rem;
  /* Make button-styled cards look identical to the old div version. */
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  padding: 0.65rem 0.8rem 0.7rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.15rem;
  position: relative;
}
.weather-card-title {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
}
.weather-card-icon {
  display: flex;
  align-items: center;
  height: 28px;
  margin-top: 0.1rem;
  color: var(--accent);
}
.weather-card-temp {
  font-size: 1.1rem;
  font-weight: 700;
  line-height: 1.1;
  color: var(--ink);
}
.weather-card-label {
  font-size: 0.78rem;
  color: var(--ink-soft);
  line-height: 1.25;
}
.weather-card.weather-icon-sun     .weather-card-icon { color: var(--wx-sun); }
.weather-card.weather-icon-partly  .weather-card-icon { color: var(--wx-partly); }
.weather-card.weather-icon-cloud   .weather-card-icon { color: var(--wx-cloud); }
.weather-card.weather-icon-rain    .weather-card-icon { color: var(--wx-rain); }
.weather-card.weather-icon-snow    .weather-card-icon { color: var(--wx-snow); }
.weather-card.weather-icon-fog     .weather-card-icon { color: var(--wx-fog); }
.weather-card.weather-icon-thunder .weather-card-icon { color: var(--wx-thunder); }

/* Home page footer — fixed to the viewport bottom so the Start (and
   Continue, when an unfinished session exists) CTAs sit where the thumb
   naturally rests, regardless of page length or iOS address-bar
   collapse. Mirrors the .convo-voice frosted-glass treatment used on
   the in-session voice composer so the visual language is consistent
   across the app. The 36rem inner column matches <main>. */
.home-footer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 30;
  background: rgba(243, 238, 229, 0.78);
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  padding: 1rem 1.5rem calc(1rem + env(safe-area-inset-bottom, 0px));
}
.home-footer .inner {
  max-width: 36rem;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.home-footer .home-cta-form { margin: 0; }
.home-footer .home-cta {
  width: 100%;
  box-sizing: border-box;
  /* Zero the inherited .home-cta vertical margin (0.75rem 0) inside the
     footer — the .inner flex gap is the single source of vertical rhythm
     between Start-new and Continue, so the anchor button doesn't double
     up with its own margin. */
  margin: 0;
}
/* Home footer specifically: the "Start new session" secondary button now
   carries its hover border at rest (Justin's "use the hover border as the
   idle border"), and reveals the surface colour on hover for a clear
   change without shifting the border weight. */
.home-footer .home-cta.secondary {
  border-color: var(--accent-soft);
}
@media (hover: hover) {
  .home-footer .home-cta.secondary:hover {
    background: var(--surface);
    border-color: var(--accent-soft);
  }
}
/* Reserve space at the bottom of <main> so page content doesn't slide
   under the fixed footer when the page is short. ~8rem covers footer
   height + comfortable gap. Plus centre the home content vertically
   between the header and the footer so "Hello, Justin." sits in the
   middle of the viewport rather than hugging the top. dvh, not vh, so
   iOS Safari's address-bar collapse doesn't drift the centre. */
body:has(.home-footer) main {
  padding-bottom: 8rem;
  min-height: calc(100dvh - 8rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* /auth/request "Check your email" lands as a small block in an otherwise
   empty viewport; centre it vertically so it doesn't sit pinned to the
   top of the page. dvh tracks the visible area on iOS Safari. */
body:has(.auth-sent) main {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 100dvh;
}
@media (max-width: 480px) {
  .home-footer {
    padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px));
  }
}

/* Voice composer — fixed to the viewport bottom so it never moves on scroll. */
/* Dev-queue #347 / #360: voice composer is a floating bottom-right mic
   FAB rather than the old full-width pill bar. The FAB's colour and
   pulse animation are also the always-visible mic-state indicator, so
   #360 (mic-active indicator) piggy-backs on the same element. The
   .convo-voice container is now a transparent positioning shell; the
   button itself carries all the chrome. */
.convo-voice {
  position: fixed;
  /* Pin to the right edge of the 36rem app column, not the viewport edge.
     On narrow viewports (≤36rem) the column fills the screen so the
     max() picks the 1rem viewport inset; on wider viewports it picks the
     gutter between the column and the viewport edge, so the FAB sits at
     the bottom-right of the app column instead of drifting out into the
     empty page margin. */
  right: max(
    calc(1rem + env(safe-area-inset-right, 0px)),
    calc((100vw - 36rem) / 2 + 1rem)
  );
  /* Anchored at the bottom-right of the app column. Justin: "the mic
     button is way too high — move to the bottom right of the 'app'". The
     FAB overlaps the always-visible composer's Send button visually; the
     typed-send path remains usable via Enter inside the textarea. */
  bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
  left: auto;
  z-index: 30;
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  padding: 0;
  border: 0;
  display: block;
  pointer-events: none; /* FAB itself re-enables; container is just positioning */
}
.convo-voice > * { pointer-events: auto; }
@media (max-width: 640px) {
  .convo-voice {
    right: calc(0.85rem + env(safe-area-inset-right, 0px));
    bottom: calc(0.85rem + env(safe-area-inset-bottom, 0px));
  }
}
/* Stream padding in voice mode: composer is always visible now (see #6
   "press Send anytime") so the bottom reservation has to clear the
   composer just like in text mode. Unified with .convo.mode-text at
   9rem so the last bubble doesn't slide under the composer in either
   mode. The FAB floats over the corner above the composer without
   claiming its own band. */
.convo.mode-voice .convo-stream {
  padding-bottom: 9rem;
}
.convo-interim {
  /* Legacy fallback strip; the live interim renders as an inline
     italic draft bubble in the stream (interimDraftBubble). Kept in
     the DOM hidden so any old reader-script lookups don't crash. */
  display: none;
}
.convo-mic {
  width: 64px;
  height: 64px;
  padding: 0;
  border-radius: 50%;
  background: var(--convo-mic-bg);
  color: var(--ink-on-accent);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  border: none;
  font-family: inherit;
  font-weight: 600;
  font-size: 0;
  cursor: pointer;
  position: relative;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  box-shadow: 0 6px 18px rgba(61, 138, 111, 0.35), 0 2px 6px rgba(0,0,0,0.12);
  transition: transform 120ms ease, background 120ms ease, box-shadow 120ms ease;
}
.convo-mic:hover  { background: var(--convo-mic-bg-hover); }
.convo-mic:active { transform: scale(0.95); }
.convo-mic .mic-glyph { line-height: 0; }
.convo-mic .mic-glyph svg { width: 28px; height: 28px; }
/* Label text is now screenreader-only; the FAB's colour + pulse is the
   visual state cue. aria-label gets the state-specific copy set from
   JS so assistive tech still hears "Listening" / "Tap to skip" etc. */
.convo-mic .mic-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}
.convo-mic .mic-label > span { display: none; }
.convo-mic[data-state="idle"]      .mic-label > [data-state-idle]      { display: inline; }
.convo-mic[data-state="listening"] .mic-label > [data-state-listening] { display: inline; }
.convo-mic[data-state="thinking"]  .mic-label > [data-state-thinking]  { display: inline; }
.convo-mic[data-state="speaking"]  .mic-label > [data-state-speaking]  { display: inline; }
/* Muted mic state (user toggled the mic itself, not Bryn's voice).
   Faint grey FAB with a slash overlay so the user always sees "not
   listening" at a glance — closes #360's "mic state always visible"
   beat for the off case too. */
.convo-mic[data-state="muted"] {
  background: var(--convo-mic-muted);
  box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
.convo-mic[data-state="muted"]::after {
  content: '';
  position: absolute;
  inset: 14px;
  border-top: 2px solid rgba(255,255,255,0.85);
  transform: rotate(-45deg);
  transform-origin: center;
  border-radius: 1px;
  pointer-events: none;
}

.convo-mic[data-state="listening"] {
  background: var(--error);
  animation: bryn-mic-pulse 1.4s ease-out infinite;
}
/* A1 / item 309: visually distinct thinking vs speaking states so the user
   can tell whether Bryn is composing or actively delivering. Thinking gets
   a soft breathing tone (slow opacity pulse, no glow ring) for "working on
   it"; speaking gets the accent colour with the same outward glow ring as
   listening, just rhythmically slower, for "actively replying". */
.convo-mic[data-state="thinking"] {
  background: var(--accent-soft);
  animation: bryn-mic-breathe 1.8s ease-in-out infinite;
}
.convo-mic[data-state="speaking"] {
  background: var(--accent);
  animation: bryn-mic-pulse-soft 1.8s ease-out infinite;
}
@keyframes bryn-mic-breathe {
  0%, 100% { opacity: 0.78; }
  50%      { opacity: 1; }
}
@keyframes bryn-mic-pulse-soft {
  0%   { box-shadow: 0 0 0 0   rgba(61, 138, 111, 0.5); }
  70%  { box-shadow: 0 0 0 14px rgba(61, 138, 111, 0); }
  100% { box-shadow: 0 0 0 0   rgba(61, 138, 111, 0); }
}
.convo-mic:disabled { opacity: 0.6; cursor: not-allowed; }

@keyframes bryn-mic-pulse {
  0%   { box-shadow: 0 0 0 0   rgba(161, 58, 42, 0.5); }
  70%  { box-shadow: 0 0 0 18px rgba(161, 58, 42, 0); }
  100% { box-shadow: 0 0 0 0   rgba(161, 58, 42, 0); }
}

.convo.mode-voice .convo-text-row { display: none; }
.convo.mode-text  .convo-voice    { display: none; }

/* History */
.period-tabs {
  display: flex;
  gap: 0.25rem;
  margin: 0 0 1rem;
  flex-wrap: wrap;
}
.period-tabs a {
  display: inline-block;
  padding: 0.4rem 0.75rem;
  border-radius: 999px;
  text-decoration: none;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--ink-soft);
  font-size: 0.9rem;
}
.period-tabs a.active {
  background: var(--accent);
  color: var(--ink-on-accent);
  border-color: var(--accent);
}

.period-summary p { font-size: 0.95rem; }
.period-summary a { color: var(--accent); text-decoration: none; }
.period-summary a:hover { text-decoration: underline; }

.filter-card summary { cursor: pointer; font-weight: 500; color: var(--accent); }

.strain-checks {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.4rem;
}
.check-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.7rem;
  border-radius: 999px;
  background: var(--bg);
  border: 1px solid var(--border);
  font-size: 0.9rem;
  cursor: pointer;
  font-weight: 400;
  margin: 0;
}
.check-pill input { margin: 0; width: auto; }

.history-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.history-list .card { padding: 0; margin-bottom: 0.75rem; }
.history-row {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1rem 1.25rem;
  text-decoration: none;
  color: inherit;
}
.history-row:hover { background: var(--bg); }
.history-row-when { flex-shrink: 0; }
.history-row-body { min-width: 0; }

/* In-conversation deep-link buttons. Bryn emits `[[link:URL|Label]]`
   tokens at the end of a reply when the user asks to see their shelf,
   kit, or a specific item; conversation.js strips the tokens and appends
   one of these chips per link below the bubble's text. The chip looks
   distinct from a suggestion chip (filled, not outlined) so the user
   reads it as "tap to navigate" rather than "tap to reply with this". */
.convo-link-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.5rem;
}
.convo-link-btn {
  display: inline-flex;
  align-items: center;
  padding: 0.35rem 0.75rem;
  background: var(--accent);
  color: var(--ink-on-accent);
  border-radius: 999px;
  text-decoration: none;
  font-size: 0.9rem;
  font-weight: 500;
  line-height: 1.2;
}
.convo-link-btn:hover { background: var(--accent-deep); color: var(--ink-on-accent); }

/* A7 / item 311: inline save/dismiss reminder card. Sits under the assistant
   bubble that surfaced it, styled like a soft callout rather than a chip
   row so it reads as "confirm or drop", not "tap to navigate". */
.convo-remind-card {
  /* #431: render as its own full-width row (sibling of the assistant
     bubble) rather than tucked inside the message column. Bolder border
     so it reads as a callout from the conversation, not a quiet hint. */
  margin: 0.6rem 0 0.4rem;
  width: 100%;
  align-self: stretch;
  padding: 0.7rem 0.85rem;
  background: var(--bg-soft, rgba(0, 0, 0, 0.03));
  border: 2px solid var(--accent-soft);
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  box-sizing: border-box;
}
.convo-remind-head {
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 700;
}
.convo-remind-text {
  font-size: 0.95rem;
  line-height: 1.35;
  color: var(--ink);
}
.convo-remind-time {
  font-size: 0.82rem;
  color: var(--ink-soft);
  font-weight: 500;
}
.convo-remind-btns {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.convo-remind-save {
  background: var(--accent);
  color: var(--ink-on-accent);
}
.convo-remind-dismiss {
  background: transparent;
  color: var(--ink-soft);
  border: 1px solid var(--border);
}
.convo-remind-dismiss:hover {
  background: var(--bg-soft, rgba(0, 0, 0, 0.04));
  color: var(--ink);
}

/* /reminders rows. Tap-to-edit on .reminder-text; Done / × forms sit beside
   it. Designed so that no JS = the existing forms still work and tapping the
   text simply focuses the container. */
.reminder-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 0.5rem;
  padding: 0.6rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: 10px;
}
.reminder-text {
  flex: 1;
  min-width: 0;
  cursor: text;
  border-radius: 6px;
  padding: 0.2rem 0.4rem;
  margin: -0.2rem -0.4rem;
  transition: background 120ms ease;
  outline: none;
}
.reminder-text:hover,
.reminder-text:focus-visible { background: var(--bg); }
.reminder-text[data-editing="true"] {
  cursor: auto;
  background: var(--surface);
  padding: 0.5rem 0.6rem;
  margin: -0.5rem -0.6rem;
}
.reminder-text-display {
  word-break: break-word;
}
.reminder-edited {
  margin-left: 0.35rem;
  white-space: nowrap;
}
.reminder-edit-form textarea {
  width: 100%;
  font: inherit;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.4rem 0.5rem;
  resize: none;
  box-sizing: border-box;
  background: var(--surface);
  color: var(--ink);
}
.reminder-edit-actions {
  display: flex;
  gap: 0.4rem;
  margin-top: 0.4rem;
}
.reminder-action-form { margin: 0; }
.reminder-action-form button { padding: 0.25rem 0.6rem; }

/* "Saving summary…" indicator on a row whose digest is still running in the
   background. The pulsing dot signals "wait, working on it". history-pending.js
   swaps the inner contents of .history-row-summary out once the summary lands. */
.history-saving {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--ink-soft);
  font-style: italic;
}
.history-saving::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
  background: var(--accent);
  animation: bryn-pulse 1.4s ease-in-out infinite;
}
@keyframes bryn-pulse {
  0%, 100% { opacity: 0.35; transform: scale(0.92); }
  50%      { opacity: 1;    transform: scale(1.08); }
}
@media (prefers-reduced-motion: reduce) {
  .history-saving::before { animation: none; opacity: 0.7; }
}

.kv-list { display: grid; grid-template-columns: 8rem 1fr; gap: 0.4rem 1rem; margin: 0.75rem 0 0; }
.kv-list dt { color: var(--ink-soft); font-size: 0.9rem; }
.kv-list dd { margin: 0; }

.chip {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  background: var(--bg);
  border: 1px solid var(--border);
  margin-right: 0.3rem;
  margin-bottom: 0.2rem;
  font-size: 0.9rem;
}

.transcript {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

.notes-list { list-style: none; margin: 0.5rem 0 1rem; padding: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.notes-list li {
  border-left: 3px solid var(--border);
  padding-left: 0.75rem;
}

/* Memory facts: small coloured tag pills + a filter row that toggles
   per-tag visibility on the list below. The pill palette is intentionally
   muted — these are quiet categorisation hints, not chrome. Each tag slug
   gets its own pastel; the filter chip variant adds a subtle border so
   the unselected state still reads as tappable. */
.memory-filter {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.25rem 0 1rem;
}
.memory-filter-chip {
  font: inherit;
  font-size: 0.78rem;
  font-weight: 500;
  line-height: 1.2;
  padding: 0.3rem 0.65rem;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--ink-soft);
  border-radius: 999px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.memory-filter-chip[aria-pressed="true"] {
  background: var(--accent);
  color: var(--surface);
  border-color: var(--accent);
}
.memory-filter-count {
  font-size: 0.7rem;
  opacity: 0.75;
}
.memory-tag-row {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-left: 0.45rem;
  vertical-align: middle;
}
.memory-tag {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 500;
  line-height: 1.2;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  color: #3b2c12;
  background: #efe2c8;
  white-space: nowrap;
}
[data-theme="dark"] .memory-tag { background: #3b2c12; color: #efe2c8; }
@media (prefers-color-scheme: dark) {
  [data-theme="auto"] .memory-tag { background: #3b2c12; color: #efe2c8; }
}
/* Per-category pastel palette. Subtle saturation so the pills sit
   quietly next to the fact text without competing with it. */
.memory-tag-people   { background: #e7d6f0; color: #3b2858; }
.memory-tag-pets     { background: #f3e1c8; color: #553414; }
.memory-tag-health   { background: #f3d7d7; color: #5a2828; }
.memory-tag-place    { background: #d4e9d6; color: #1f3c25; }
.memory-tag-routine  { background: #dfe5f2; color: #1f2c50; }
.memory-tag-setup    { background: #e3e5d2; color: #36381e; }
.memory-tag-strain   { background: #e8d8c0; color: #4a3414; }
.memory-tag-work     { background: #d6e6ef; color: #1c3a4a; }
.memory-tag-personal { background: #ece4d8; color: #443628; }
/* Dark mode flips bg ↔ text for each category — the hue stays the
   recogniser, only the value inverts so the pill reads against the
   dark surface. Filter-chip borders also swap to the lighter tone. */
[data-theme="dark"] .memory-tag-people   { background: #3b2858; color: #e7d6f0; }
[data-theme="dark"] .memory-tag-pets     { background: #553414; color: #f3e1c8; }
[data-theme="dark"] .memory-tag-health   { background: #5a2828; color: #f3d7d7; }
[data-theme="dark"] .memory-tag-place    { background: #1f3c25; color: #d4e9d6; }
[data-theme="dark"] .memory-tag-routine  { background: #1f2c50; color: #dfe5f2; }
[data-theme="dark"] .memory-tag-setup    { background: #36381e; color: #e3e5d2; }
[data-theme="dark"] .memory-tag-strain   { background: #4a3414; color: #e8d8c0; }
[data-theme="dark"] .memory-tag-work     { background: #1c3a4a; color: #d6e6ef; }
[data-theme="dark"] .memory-tag-personal { background: #443628; color: #ece4d8; }
@media (prefers-color-scheme: dark) {
  [data-theme="auto"] .memory-tag-people   { background: #3b2858; color: #e7d6f0; }
  [data-theme="auto"] .memory-tag-pets     { background: #553414; color: #f3e1c8; }
  [data-theme="auto"] .memory-tag-health   { background: #5a2828; color: #f3d7d7; }
  [data-theme="auto"] .memory-tag-place    { background: #1f3c25; color: #d4e9d6; }
  [data-theme="auto"] .memory-tag-routine  { background: #1f2c50; color: #dfe5f2; }
  [data-theme="auto"] .memory-tag-setup    { background: #36381e; color: #e3e5d2; }
  [data-theme="auto"] .memory-tag-strain   { background: #4a3414; color: #e8d8c0; }
  [data-theme="auto"] .memory-tag-work     { background: #1c3a4a; color: #d6e6ef; }
  [data-theme="auto"] .memory-tag-personal { background: #443628; color: #ece4d8; }
}
/* Filter-chip variant of each tag colour: same hue, but only the
   border is tinted in the unselected state so the row of chips
   doesn't shout. Selected state already inherits accent fill above. */
.memory-filter-chip.memory-tag-people   { border-color: #c5add6; }
.memory-filter-chip.memory-tag-pets     { border-color: #d9b889; }
.memory-filter-chip.memory-tag-health   { border-color: #d8a8a8; }
.memory-filter-chip.memory-tag-place    { border-color: #a8c8ac; }
.memory-filter-chip.memory-tag-routine  { border-color: #b4bfd6; }
.memory-filter-chip.memory-tag-setup    { border-color: #b8bd9d; }
.memory-filter-chip.memory-tag-strain   { border-color: #c9a87f; }
.memory-filter-chip.memory-tag-work     { border-color: #a6c0cd; }
.memory-filter-chip.memory-tag-personal { border-color: #c4b6a0; }
.memory-filter-chip:not([aria-pressed="true"]).memory-tag {
  background: transparent;
  color: var(--ink-soft);
}

/* #393/#394: shelf inventory-state pills. Amber for ordered, muted grey for
   out-of-stock. Light + dark variants. Sit inline next to the strain name. */
.shelf-stock-pill {
  display: inline-block;
  margin-left: 0.4rem;
  font-size: 0.65rem;
  font-weight: 600;
  line-height: 1.2;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  vertical-align: middle;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
}
.shelf-stock-ordered { background: #f4e2c1; color: #6a4814; }
.shelf-stock-out     { background: #e0ddd6; color: #6b6862; }
[data-theme="dark"] .shelf-stock-ordered { background: #5a3f12; color: #f4e2c1; }
[data-theme="dark"] .shelf-stock-out     { background: #4d4944; color: #d0cdc6; }
@media (prefers-color-scheme: dark) {
  [data-theme="auto"] .shelf-stock-ordered { background: #5a3f12; color: #f4e2c1; }
  [data-theme="auto"] .shelf-stock-out     { background: #4d4944; color: #d0cdc6; }
}

/* #392: kit cleaning-cycle counter chip. Quiet, sits next to the device
   name; lifts visibility only when count > 0 (renderer-side guard). */
.kit-use-chip {
  display: inline-block;
  margin-left: 0.4rem;
  font-size: 0.65rem;
  font-weight: 600;
  line-height: 1.2;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  vertical-align: middle;
  background: var(--accent-soft);
  color: var(--ink);
  white-space: nowrap;
}

/* Modal */
.modal {
  position: fixed; inset: 0;
  z-index: 100;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
@media (min-width: 560px) {
  .modal { align-items: center; }
}
.modal[hidden] { display: none; }
.modal-backdrop {
  position: absolute; inset: 0;
  background: rgba(0, 0, 0, 0.4);
}
.modal-card {
  position: relative;
  background: var(--surface);
  border-radius: var(--radius) var(--radius) 0 0;
  padding: 1.25rem;
  width: 100%;
  max-width: 36rem;
  max-height: 92vh;
  overflow-y: auto;
  box-shadow: 0 -8px 24px rgba(0,0,0,0.12);
}
@media (min-width: 560px) {
  .modal-card { border-radius: var(--radius); box-shadow: 0 8px 24px rgba(0,0,0,0.18); }
}

.consent-group {
  margin-top: 1rem;
  padding: 1rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.consent-group label.checkbox {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin-top: 0;
  font-weight: 500;
  cursor: pointer;
}
.consent-group input[type="checkbox"] {
  margin-top: 0.25rem;
  width: 1.1rem; height: 1.1rem;
  flex-shrink: 0;
}
.consent-group .body {
  font-weight: 400;
  color: var(--ink-soft);
  margin-top: 0.25rem;
  font-size: 0.95rem;
}


/* Admin pages: mobile polish. The users index table and the per-user
   notification log are full grids on desktop; on phones we stack the
   users grid as cards and drop low-signal columns from the notif log so
   it reads in a single column. The admin-kv definition lists already
   wrap fine. */
.admin-users { font-size: 0.92rem; }
.admin-users th,
.admin-users td { vertical-align: top; }
.admin-notif { font-size: 0.9rem; }

@media (max-width: 600px) {
  /* Users table -> card list. Hide the column headers and reflow each
     row as a stacked card: email link on its own line, then a compact
     meta row underneath. The minimal columns chosen are the ones a
     mobile admin glance actually wants. */
  .admin-users thead { display: none; }
  .admin-users,
  .admin-users tbody,
  .admin-users tr { display: block; width: 100%; }
  .admin-users tr {
    padding: 0.55rem 0;
    border-bottom: 1px solid var(--border);
  }
  .admin-users td { display: inline; padding: 0; border: 0; }
  .admin-users td:nth-child(1) { /* id */
    font-weight: 600;
    margin-right: 0.4rem;
  }
  .admin-users td:nth-child(2) { /* email */
    display: block;
    margin-bottom: 0.15rem;
  }
  .admin-users td:nth-child(3)::before { content: ""; }
  .admin-users td:nth-child(3):not(:empty) + td::before { content: " · "; color: var(--ink-soft); }
  .admin-users td:nth-child(4)::before { content: " · joined "; color: var(--ink-soft); }
  .admin-users td:nth-child(5)::before { content: " · "; color: var(--ink-soft); }
  .admin-users td:nth-child(6)::before { content: " · "; color: var(--ink-soft); }
  .admin-users td:nth-child(7)::before { content: " · seen "; color: var(--ink-soft); }
  .admin-users td:nth-child(8)::before { content: " · "; color: var(--ink-soft); }
  .admin-users td:nth-child(8)::after  { content: " pushes"; color: var(--ink-soft); }
  .admin-users td { text-align: left !important; }

  /* Notif log: hide the HTTP column on phones; Notes wraps. */
  .admin-notif th:nth-child(4),
  .admin-notif td:nth-child(4) { display: none; }
  .admin-notif td { word-break: break-word; }

  /* Identity / activity key-value blocks tighter on mobile. */
  .admin-kv { gap: 0.3rem 0.7rem !important; }
}

/* In-page banner used by push.js for "notifications turned off in iOS
   settings" and similar transient warnings. Sits at the top of <main>,
   styled to match the project's warm tone (warn ink, soft warn fill). */
.banner {
  padding: 0.7rem 0.9rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface);
  margin: 0 0 1rem;
  font-size: 0.95rem;
}
.banner-warn {
  border-color: var(--warn);
  background: rgba(138, 75, 26, 0.08);
  color: var(--warn);
}

/* Quick-reply and settings chips share .convo-link-btn but get a small
   visual confirmation when tapped. is-chosen on a chip means: the user
   has chosen this option / the setting was persisted. Disabled state
   carries the visual; we just tweak the colour. */
.convo-link-btn.is-chosen,
.convo-link-btn.is-chosen[disabled] {
  background: var(--accent-soft);
  color: var(--ink-on-accent);
  opacity: 1;
  cursor: default;
}
/* Quick-reply chip default: same shape, slightly softer tone so it reads
   as "your reply" not "navigate". The user sees these every time Bryn
   asks a closed question, so a fully-saturated accent green every time
   would feel shouty. */
.convo-link-btn.convo-link-btn-quick {
  background: var(--accent-deep);
}
.convo-link-btn.convo-link-btn-quick:hover { background: var(--accent); }
