/* Master stylesheet — imports every layer in order.
   Order matters: tokens first, base next, then layout, then components. */

/* Design tokens — spacing, typography, radii, shadows, colors.
   Color structure:
     1. :root, [data-theme="light"]  — light (default + explicit)
     2. [data-theme="dark"]          — dark explicit
     3. @media prefers-color-scheme  — OS-preference catch-all (no data-theme) */


/* ─── Static tokens (theme-independent) ──────────────────────────────────── */

:root {
  /* Motion (2026-10-23): a section folding up/down and a pane sliding
     left/right share one timing — 300ms on the deceleration curve iOS
     sheets and folds use. Reduced motion turns each off where it's used. */
  --motion-slide: 300ms;
  --ease-slide: cubic-bezier(0.32, 0.72, 0, 1);

  /* Spacing */
  --sp-4xs: 2px;
  --sp-3xs: 4px;
  --sp-xxs: 6px;
  --sp-xs: 8px;
  --sp-sm: 12px;
  --sp-md: 16px;
  --sp-lg: 20px;
  --sp-xl: 24px;
  --sp-xxl: 32px;
  --sp-3xl: 40px;
  --sp-4xl: 48px;

  /* Sizing */
  --size-4xs: 2px;
  --size-3xs: 4px;
  --size-xxs: 6px;
  --size-xs: 8px;
  --size-sm: 12px;
  --size-md: 16px;
  --size-lg: 20px;
  --size-xl: 24px;
  --size-xxl: 32px;
  --size-3xl: 40px;
  --size-4xl: 48px;
  --size-5xl: 56px;

  /* Typography */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --font-serif: "New York", ui-serif, Georgia, serif;
  --text-xs: 11px;
  --text-sm: 13px;
  --text-base: 14px;
  --text-md: 15px;
  /* iOS `Title - Card` (19 / 600 / 140%, −0.5%): the toDo card's title. */
  --text-title-card: 19px;
  --text-lg: 16px;
  --text-xl: 18px;
  --text-2xl: 22px;
  --text-3xl: 28px;
  --text-4xl: 32px;
  /* Type styles as `font` shorthands (sheet chrome, 2026-10-26): body bold
     18/120% with --tracking-body, body regular, footnote 13/18, title
     20/600/25. A feature uses these, never restates the numbers. */
  --type-body-bold: 700 18px/1.2 var(--font-sans);
  --type-body-semibold: 600 18px/1.2 var(--font-sans);
  --type-body: 400 18px/140% var(--font-sans);
  --type-footnote: 400 13px/18px var(--font-sans);
  --type-title: 600 20px/25px var(--font-sans);
  /* iOS callout regular, 16/21: the Home widget card's body. */
  --type-callout: 400 16px/21px var(--font-sans);
  --tracking-body: -0.43px;

  /* Sheets (ui/sheet): the family radius; the default fill, flipped by
     .app-shell--surface-secondary where the page paints --bg-secondary; the
     material blur. --display-corner and --sheet-bottom-radius are written on
     <html> by ffi/dom.mjs from the device's panel (0 and the family radius
     where the page doesn't reach the glass). */
  --sheet-radius: 32px;
  --sheet-fill: var(--bg-secondary);
  --material-blur: 100px;

  /* Radius */
  --radius-sm: 4px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 24px;
  --radius-2xl: 32px;
  --radius-full: 9999px;

  /* brand gradient */
  --fill-gradient:linear-gradient(180deg, #62D475 0%, #58BE68 100%);

  /* Brand pastel — the Please-agent tint (iOS `ColorSystem.Gradients.
     brandPastelColors`, the single source: #F0B6F6 @0, #89C3FD @0.49,
     #56E06D @1). Vertical for glyphs/rings, horizontal for the glow
     plane, and the bare stop list for the comet's conic wheel. */
  --pastel-1: #F0B6F6;
  --pastel-2: #89C3FD;
  --pastel-3: #56E06D;
  --gradient-pastel: linear-gradient(180deg, var(--pastel-1) 0%, var(--pastel-2) 49%, var(--pastel-3) 100%);
  --gradient-pastel-horizontal: linear-gradient(90deg, var(--pastel-1) 0%, var(--pastel-2) 49%, var(--pastel-3) 100%);

  /* The brief masthead's skies (iOS `ColorSystem.Gradients.Sky`, nine
     mesh stops row-major top→bottom): the middle column as the vertical
     run, the top-left and bottom-right stops as corner tints. Theme-
     invariant (iOS: "the sky is the same in light and dark"), so defined
     once, here; the dark scopes inherit them. White copy stays ≥ 3:1 on
     every palette's lower rows. --sky-scrim steadies the bottom edge. */
  --sky-scrim: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.22) 100%);
  --sky-ink: #FFFFFF;
  /* Copy on a solid accent fill (iOS whiteCanvas): white in both modes. */
  --label-on-accent: #FFFFFF;
  --sky-pre-dawn: radial-gradient(90% 80% at 0% 0%, #F0B6F6 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #B5721F 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #B9BCFA 0%, #7F70D0 50%, #B45A4E 100%);
  --sky-dawn: radial-gradient(90% 80% at 0% 0%, #3A2F7A 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #B96A2A 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #4A3C8F 0%, #B0587A 50%, #C98332 100%);
  --sky-day: radial-gradient(90% 80% at 0% 0%, #6FB6F2 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #22489C 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #5FA8EE 0%, #3572D2 50%, #2650A8 100%);
  --sky-dusk: radial-gradient(90% 80% at 0% 0%, #5A3E9C 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #A85226 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #6E4A9E 0%, #A85C74 50%, #C46A2E 100%);
  --sky-night: radial-gradient(90% 80% at 0% 0%, #1A2650 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #131D42 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #1C2A5A 0%, #182450 50%, #111A3C 100%);
  --sky-day-overcast: radial-gradient(90% 80% at 0% 0%, #9AA9BC 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #424F60 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #91A1B5 0%, #68798F 50%, #485668 100%);
  --sky-dusk-overcast: radial-gradient(90% 80% at 0% 0%, #8A7A96 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #423849 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #857390 0%, #655672 50%, #473D52 100%);
  --sky-night-overcast: radial-gradient(90% 80% at 0% 0%, #2A3140 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #121620 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #28303E 0%, #1D222E 50%, #141822 100%);
  --sky-day-rain: radial-gradient(90% 80% at 0% 0%, #7C8CA0 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #344050 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #76869A 0%, #526278 50%, #384454 100%);
  --sky-night-rain: radial-gradient(90% 80% at 0% 0%, #232B38 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #10141B 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #222A36 0%, #191F28 50%, #11161D 100%);
  --sky-storm: radial-gradient(90% 80% at 0% 0%, #4E5868 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #20262E 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #4A5464 0%, #343C49 50%, #222831 100%);
  --sky-day-snow: radial-gradient(90% 80% at 0% 0%, #B8C6D6 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #4D5D72 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #B0BFD0 0%, #76889E 50%, #516176 100%);
  --sky-night-snow: radial-gradient(90% 80% at 0% 0%, #3A4458 0%, rgba(0, 0, 0, 0) 70%), radial-gradient(90% 80% at 100% 100%, #1A202E 0%, rgba(0, 0, 0, 0) 70%), linear-gradient(180deg, #384256 0%, #293143 50%, #1B2130 100%);


  /* Accent color base rgb values to inject into RGB functions.
     iOS ColorSystem parity (Aug 2026): *-1 is the light-mode value,
     *-2 the dark-mode value. */
  --blue-1:       0, 93, 214;   /* #005DD6, the accent blue base (2026-10-23; was #0066EB) */
  --blue-2:       20, 122, 255;
  --red-1:        255, 56, 60;
  --red-2:        255, 66, 69;
  --green-1:      52, 199, 89;
  --green-2:      48, 209, 88;
  --yellow-1:     255, 214, 0;
  --yellow-2:     255, 204, 0;
  --orange-1:     255, 146, 48;
  --orange-2:     255, 141, 40;
  --purple-1:     175, 82, 222;
  --purple-2:     191, 90, 242;

  --neutral-white:    255, 255, 255;
  --neutral-01:       242, 242, 247;
  --neutral-02:       58, 58, 60;
  --neutral-03:       44, 44, 46;
  --neutral-04:       28, 28, 30;
  --neutral-05:       13, 13, 13;
  --neutral-black:    0, 0, 0;


--neutral-01: 242, 242, 247; /* 1 */
--fill-tertiary: 237, 237, 237; /* 2 */
--fill-secondary: 224, 224, 224; /* 3 */
--fill-primary: 204, 204, 204; /* 4 */
--neutral-02: 58, 58, 60; /* 5 */
--fill-primary-inverse: 51, 51, 51; /* 6 */
--neutral-03: 44, 44, 46; /* 7 */
--fill-secondary-inverse: 31, 31, 31; /* 8 */
--neutral-04: 28, 28, 30; /* 9 */
--fill-tertiary-inverse: 18, 18, 18; /* 10 */
--neutral-05: 13, 13, 13; /* 11 */


--neutral-01: 242, 242, 247;
--neutral-02: 237, 237, 237;
--neutral-03: 224, 224, 224;
--neutral-04: 204, 204, 204;
--neutral-05: 58, 58, 60;
--neutral-06: 51, 51, 51;
--neutral-07: 44, 44, 46;
--neutral-08: 31, 31, 31;
--neutral-09: 28, 28, 30;
--neutral-10: 18, 18, 18;
--neutral-11: 13, 13, 13;
}


/* ─── Light theme — default + explicit ───────────────────────────────────── */

:root,
[data-theme="light"] {

  --blend-mode:      darken;

  --bg-base:               var(--neutral-white);
  --bg-base-inverse:       var(--neutral-black);

  --color-gray-1:          var(--neutral-01);
  --color-gray-2:          var(--neutral-05);
  --color-gray-3:          var(--neutral-07);
  --color-gray-4:          var(--neutral-09);
  --color-gray-5:          var(--neutral-11);

  --color-red:             var(--red-1);
  --color-orange:          var(--orange-1);
  --color-yellow:          var(--yellow-1);
  --color-green:           var(--green-1);
  --color-blue:            var(--blue-1);
  --color-purple:          var(--purple-1);

  --color-red-inverse:     var(--red-2);
  --color-orange-inverse:  var(--orange-2);
  --color-yellow-inverse:  var(--yellow-2);
  --color-green-inverse:   var(--green-2);
  --color-blue-inverse:    var(--blue-2);
  --color-purple-inverse:  var(--purple-2);

  /* Background */
  --bg-body:                       rgba(var(--bg-base), 1);
  --bg-primary:                    rgba(var(--bg-base), 1);
  --bg-secondary:                  rgba(var(--color-gray-1), 1);
  --bg-tertiary:                   rgba(var(--bg-base), .04);
  --bg-quarternary:                rgba(var(--bg-base), .02);

  --bg-elevated-primary:           rgba(var(--bg-base), 1);
  --bg-elevated-secondary:         rgba(var(--color-gray-1), 1);
  --bg-elevated-tertiary:          rgba(var(--bg-base), 1);

  --bg-body-inverse:               rgba(var(--color-gray-5), 1);
  --bg-primary-inverse:            rgba(var(--bg-base-inverse), 1);
  --bg-elevated-primary-inverse:   rgba(var(--color-gray-4), 1);
  --bg-elevated-secondary-inverse: rgba(var(--color-gray-3), 1);
  --bg-elevated-tertiary-inverse:  rgba(var(--color-gray-2), 1);    
  --bg-secondary-inverse:          rgba(var(--color-gray-4), 1);
  --bg-tertiary-inverse:           rgba(var(--color-gray-3), 1);
  --bg-quarternary-inverse:        rgba(var(--bg-base-inverse), .02);

  --bg-disabled:                   var(--bg-quarternary);
  --bg-disabled-inverse:           var(--bg-quarternary-inverse);
  --bg-0:                          rgba(var(--bg-base),0);
  --bg-70:                         rgba(var(--bg-base),.7);
  /* Phone chrome glass (iOS BarChrome .glass below iOS 26: ultraThin
     material, stood in for by the scrim tuner's fill), 2026-10-23. */
  --glass-fill:                    rgba(255, 255, 255, 0.72);
  /* Sheets (ui/sheet, 2026-10-26 spec): the overlay behind every phone sheet
     and the material backgrounds — two layers each ("bg layer 1" is the
     card's own fill, "bg layer 2" sits behind it), over --material-blur. */
  --overlay:                       rgba(0, 0, 0, 0.20);
  --material-ultrathin-1:          rgba(255, 255, 255, 0.03);
  --material-ultrathin-2:          rgba(255, 255, 255, 0.07);
  --material-thin-1:               rgba(255, 255, 255, 0.40);
  --material-thin-2:               rgba(255, 255, 255, 0.05);
  --material-regular-1:            rgba(255, 255, 255, 0.60);
  --material-regular-2:            rgba(255, 255, 255, 0.25);
  --material-thick-1:              rgba(255, 255, 255, 0.84);
  --material-thick-2:              rgba(255, 255, 255, 0.34);
  --material-chrome-1:             rgba(255, 255, 255, 0.75);
  --material-chrome-2:             transparent;
  --material-chrome-blur:          50px;
  /* The glass surface (styles/glass.css, 2026-10-27): the slab under the
     link page's composer, from Julian's 358×80 mock — a material over the
     backdrop blur, tinted top to bottom (darker under the top edge, clear
     by the foot), with a rim: a 1px light on the top edge, a soft glow
     along the bottom, a faint ring. --glass-tint is the tint's gradient,
     the first background layer over the material pair; --glass-rim the
     rim's inset shadows, composed before a component's own drop shadow:
     `box-shadow: var(--glass-rim), var(--shadow-float)`. */
  --glass-tint:                    linear-gradient(180deg, rgba(0, 0, 0, 0.065) 0%, rgba(0, 0, 0, 0.03) 45%, rgba(0, 0, 0, 0) 100%);
  --glass-rim:                     inset 0 1px 0 rgba(255, 255, 255, 0.95), inset 0 -6px 6px -3px rgba(255, 255, 255, 0.90), inset 0 0 0 1px rgba(255, 255, 255, 0.35);
  /* iOS CountBadge brandRed (white label in both modes) and the Home
     header glow (EphemeralHeaderGlow: 1 light / .45 dark), 2026-10-23. */
  --count-badge-fill:              #FF383C;
  --count-badge-label:             #FFFFFF;
  --home-glow-green:               #62D475;
  --home-glow-blue:                #8EC3F8;
  --home-glow-pink:                #EABDEF;
  --home-glow-opacity:             1;
  --bg-80:                         rgba(var(--bg-base),.8);
  --bg-70-inverse:                 rgba(var(--bg-base-inverse),.7);
  --bg-80-inverse:                 rgba(var(--bg-base-inverse),.8);

  /* Fill — base (solid) + elevated (translucent overlay) */
  --fill-primary:                     rgba(var(--neutral-04));
  --fill-secondary:                   rgba(var(--neutral-03));
  --fill-tertiary:                    rgba(var(--neutral-02));
  --fill-elevated-primary:            rgba(120, 120, 120, 0.20);
  --fill-elevated-secondary:          rgba(120, 120, 128, 0.16);
  --fill-elevated-tertiary:           rgba(118, 118, 128, 0.12);
  --fill-elevated-quaternary:         rgba(116, 116, 128, 0.08);
  
  --fill-primary-inverse:             rgba(var(--neutral-06));
  --fill-secondary-inverse:           rgba(var(--neutral-08));
  --fill-tertiary-inverse:            rgba(var(--neutral-10));
  --fill-elevated-primary-inverse:    rgba(120, 120, 128, 0.36);
  --fill-elevated-secondary-inverse:  rgba(120, 120, 128, 0.32);
  --fill-elevated-tertiary-inverse:   rgba(118, 118, 128, 0.24);
  --fill-elevated-quaternary-inverse: rgba(118, 118, 128, 0.18);

  /* Brand gradients */
  --fill-gradient-green: linear-gradient(180deg, #62D475 0%, #58BE68 100%);
  
  /* Label (Figma variables, 2026-10-23) — named as Figma names them:
     Label, Label / Inverse (the other mode's ramp), Label / Control. */
  --label-primary:            #000000;
  --label-secondary:          #5C5C5C;
  --label-tertiary:           #727272;
  --label-quaternary:         #D9D9D9;
  /* PlsLabelSpecial — the reminder BODY reading colour, between primary
     (too heavy for a wall of copy) and secondary (too faint). */
  --label-special:            #454545;
  /* UIKit's tertiaryLabel (4b, 2026-10-25), which is not --label-tertiary:
     the phone notes placeholder, and 4c's completion ring. */
  --label-tertiary-system:    rgba(60, 60, 67, 0.30);

  --label-inverse-primary:    #FFFFFF;
  --label-inverse-secondary:  #8A8A8A;
  --label-inverse-tertiary:   #404040;
  --label-inverse-quaternary: #262626;

  --label-control-primary:    #1A1A1A;
  --label-control-secondary:  #727272;
  --label-control-tertiary:   #BFBFBF;

  /* The toDo card's counts beside their glyphs (iOS
     TodoActionRow.Layout.countTintByAppearance): label tertiary in light. */
  --action-count:             var(--label-tertiary);

  /* The phone file row's size line ("482 KB"): tertiary, as iOS's
     FileAttachmentRow draws it. Dark reads --label-special for now (below);
     when --label-tertiary's dark value gets its proper update, point this
     back at --label-tertiary in every scheme (Julian, 2026-10-25). */
  --file-size-text:           var(--label-tertiary);

  /* DEPRECATED (2026-10-23): the pre-Figma names, kept as aliases only so
     branches in flight keep their colours. Use the label names above —
     test/label_tokens_test.mjs fails any new use. Delete once those
     branches have landed. */
  --text-primary:               var(--label-primary);
  --text-secondary:             var(--label-secondary);
  --text-tertiary:              var(--label-tertiary);
  --text-quarternary:           var(--label-quaternary);
  --text-disabled:              var(--label-quaternary);
  --text-primary-inverse:       var(--label-inverse-primary);
  --text-secondary-inverse:     var(--label-inverse-secondary);
  --text-tertiary-inverse:      var(--label-inverse-tertiary);
  --text-quarternary-inverse:   var(--label-inverse-quaternary);
  --text-disabled-inverse:      var(--label-inverse-quaternary);
  --text-primary-vibrant:       var(--label-control-primary);
  --text-secondary-vibrant:     var(--label-control-secondary);
  --text-tertiary-vibrant:      var(--label-control-tertiary);
  --text-quarternary-vibrant:   var(--label-quaternary);
  --text-disabled-vibrant:      var(--label-quaternary);
  --text-quarternary-vibrant-inverse: var(--label-inverse-quaternary);
  --tl-text-primary:            var(--label-primary);
  --tl-text-secondary:          var(--label-secondary);
  /* END DEPRECATED */

  /* Border */
  --border:           #e5e5e5;
  --border-strong:    #d4d4d4;
  --border-primary:   #C6C6C8;
  --border-secondary: rgba(0, 0, 0, 0.12);
  --border-tertiary:  rgba(0, 0, 0, 0.05);
  --border-vibrant:   #E6E6E6;

  /* Accent */
  --accent-blue:          rgba(var(--color-blue), 1);
  --accent-blue-inverse:  rgba(var(--color-blue-inverse), 1);
  --accent-red:           rgba(var(--color-red), 1);
  --accent-red-inverse:   rgba(var(--color-red-inverse), 1);
  --accent-green:         rgba(var(--color-green), 1);
  --accent-green-inverse: rgba(var(--color-green-inverse), 1);
  --accent-yellow:        rgba(var(--color-yellow), 1);
  --accent-yellow-inverse:rgba(var(--color-yellow-inverse), 1);
  --accent-orange:        rgba(var(--color-orange), 1);
  --accent-orange-inverse:rgba(var(--color-orange-inverse), 1);
  --accent-purple:        rgba(var(--color-purple), 1);
  --accent-purple-inverse:rgba(var(--color-purple-inverse), 1);

  /* iOS `*Elevated` tonal fill: the accent hue at low alpha, composited
     live (never a frozen hex). Alpha deliberately diverges by mode
     (10% light / 16% dark), matching ColorSystem.accentBlueElevated. */
  --accent-blue-elevated: rgba(var(--color-blue), 0.10);
  --accent-red-elevated: rgba(var(--color-red), 0.10);

  /* Timeline list (iOS parity) */
  --canvas-bg:         #F2F2F7;
  --ui-borders:        #C6C6C8;
  /* Card swipe strips (iOS TimelineListRow, round 3a 2026-10-24): Pin's
     indigo (Palette.indigo) and its 10% wash; Share always blue (the
     base channel — never --accent-blue, which the user accent re-points)
     and its wash; the purple wash bulk select takes in 3c. */
  --swipe-pin:               #481ADA;
  --swipe-pin-wash:          rgba(72, 26, 218, 0.10);
  --swipe-share:             rgba(var(--color-blue), 1);
  --swipe-share-wash:        rgba(var(--color-blue), 0.10);
  --accent-purple-elevated:  rgba(var(--color-purple), 0.10);
  /* Completion confetti (iOS CompletionCelebration): piece 1 is the
     accent; these four are fixed, the same in both themes. */
  --confetti-2:              #7F77DD;
  --confetti-3:              #1D9E75;
  --confetti-4:              #EF9F27;
  --confetti-5:              #D4537E;
  /* Collection card gradients (iOS BucketGridView, round 2 2026-10-23):
     each tint lifted +0.04 HSL lightness at the top-left, base at the
     bottom-right, both at 22%. Blue, orange, red, teal, green. */
  --bucket-grad-blue-from:   rgba(20, 146, 255, 0.22);
  --bucket-grad-blue-to:     rgba(0, 136, 255, 0.22);
  --bucket-grad-orange-from: rgba(255, 152, 60, 0.22);
  --bucket-grad-orange-to:   rgba(255, 141, 40, 0.22);
  --bucket-grad-red-from:    rgba(255, 76, 80, 0.22);
  --bucket-grad-red-to:      rgba(255, 56, 60, 0.22);
  --bucket-grad-teal-from:   rgba(0, 220, 197, 0.22);
  --bucket-grad-teal-to:     rgba(0, 200, 179, 0.22);
  --bucket-grad-green-from:  rgba(66, 206, 101, 0.22);
  --bucket-grad-green-to:    rgba(52, 199, 89, 0.22);
  /* Calendar card badge weekday (iOS system red). */
  --calendar-weekday:        #FF3B30;
  --brand-blue:        rgba(var(--color-blue), 1);
  --brand-blue-12:     rgba(var(--color-blue), 0.12);

  /* Shadow */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow:     0 2px 8px rgba(0, 0, 0, 0.06);
  /* Floating capsule over content (the desktop detail's Messages pill,
     mock 2026-09-24): hairline edge + soft drop. */
  --shadow-float: 0 4px 12px rgba(0, 0, 0, 0.14);
  /* The Home widget card (iOS AgentUpdateHeroCard, Julian 2026-10-15:
     "0,4,12,0 black 10%") — the same in both modes, so defined once. */
  --shadow-widget: 0 4px 12px rgba(0, 0, 0, 0.10);
  --border-float: rgba(0, 0, 0, 0.08);

  /* The signed-out link page (link_page.css): the page behind the card,
     the card, and the two pastel tints the bar's glow is mixed from —
     pastel-1 and pastel-2 at an alpha that reads as a wash, not a fill. */
  --link-page-bg:       rgba(var(--neutral-01), 1);
  --link-page-card:     rgba(var(--bg-base), 1);
  --link-page-glow-1:   rgba(240, 182, 246, 0.55);
  --link-page-glow-2:   rgba(137, 195, 253, 0.55);

  /* Deprecated aliases — remove once consumers are migrated */
  --bg:         #ffffff;
  --bg-subtle:  #f5f5f4;
  --bg-elevated: #ffffff;
  --fg:         #0a0a0a;
  --accent:     #0a0a0a;
  --accent-fg:  #ffffff;

  color-scheme: light;
}


/* ─── Dark theme — explicit ───────────────────────────────────────────────── */

[data-theme="dark"] {

  --blend-mode:      lighten;

  --bg-base:                var(--neutral-black);
  --bg-base-inverse:        var(--neutral-white);

  --color-gray-1:           var(--neutral-01);
  --color-gray-2:           var(--neutral-05);
  --color-gray-3:           var(--neutral-07);
  --color-gray-4:           var(--neutral-09);
  --color-gray-5:           var(--neutral-11);

  --color-red:              var(--red-2);
  --color-orange:           var(--orange-2);
  --color-yellow:           var(--yellow-2);
  --color-green:            var(--green-2);
  --color-blue:             var(--blue-2);
  --color-purple:           var(--purple-2);

  --color-red-inverse:      var(--red-1);
  --color-orange-inverse:   var(--orange-1);
  --color-yellow-inverse:   var(--yellow-1);
  --color-green-inverse:    var(--green-1);
  --color-blue-inverse:     var(--blue-1);
  --color-purple-inverse:   var(--purple-1);

  /* Background */
  --bg-body:               rgba(var(--color-gray-5), 1);
  --bg-primary:            rgba(var(--bg-base), 1); 
  --bg-secondary:          rgba(var(--color-gray-4), 1);
  --bg-tertiary:           rgba(var(--color-gray-3), 1);
  --bg-quarternary:        rgba(var(--bg-base), .02);

  --bg-elevated-primary:   rgba(var(--color-gray-4), 1);
  --bg-elevated-secondary: rgba(var(--color-gray-3), 1);
  --bg-elevated-tertiary:  rgba(var(--color-gray-2), 1);   

  --bg-body-inverse:                       rgba(var(--bg-base-inverse), 1);
  --bg-primary-inverse:                    rgba(var(--bg-base-inverse), 1);
  --bg-elevated-primary-inverse:           rgba(var(--bg-base-inverse), 1);
  --bg-elevated-secondary-inverse:         rgba(var(--color-gray-1), 1);
  --bg-elevated-tertiary-inverse:          rgba(var(--bg-base-inverse), 1);
  --bg-secondary-inverse:                  rgba(var(--color-gray-1), 1);
  --bg-tertiary-inverse:                   rgba(var(--bg-base-inverse), .04);
  --bg-quarternary-inverse:                rgba(var(--bg-base-inverse), .02);

  --bg-disabled:            var(--bg-quarternary);
  --bg-disabled-inverse:    var(--bg-quarternary-inverse);
  --bg-0:                   rgba(var(--bg-base),0);
  --bg-70:                  rgba(var(--bg-base),.7);
  --glass-fill:                    rgba(40, 40, 42, 0.72);
  /* Sheets (ui/sheet): overlay + materials, dark values. */
  --overlay:                       rgba(0, 0, 0, 0.48);
  --material-ultrathin-1:          rgba(0, 0, 0, 0.02);
  --material-ultrathin-2:          transparent;
  --material-thin-1:               rgba(0, 0, 0, 0.26);
  --material-thin-2:               transparent;
  --material-regular-1:            rgba(0, 0, 0, 0.41);
  --material-regular-2:            transparent;
  --material-thick-1:              rgba(0, 0, 0, 0.60);
  --material-thick-2:              transparent;
  --material-chrome-1:             #7C7C7C;
  --material-chrome-2:             rgba(28, 28, 28, 0.90);
  --material-chrome-blur:          100px;
  /* The glass surface, dark: the tint lifts the material off a dark page
     (white, not black) and the rim is a hint. */
  --glass-tint:                    linear-gradient(180deg, rgba(255, 255, 255, 0.10) 0%, rgba(255, 255, 255, 0.06) 45%, rgba(255, 255, 255, 0.03) 100%);
  --glass-rim:                     inset 0 1px 0 rgba(255, 255, 255, 0.22), inset 0 -6px 6px -3px rgba(255, 255, 255, 0.10), inset 0 0 0 1px rgba(255, 255, 255, 0.08);
  --count-badge-fill:              #FF4245;
  --count-badge-label:             #FFFFFF;
  --home-glow-green:               #62D475;
  --home-glow-blue:                #8EC3F8;
  --home-glow-pink:                #EABDEF;
  --home-glow-opacity:             0.45;
  --bg-80:                  rgba(var(--bg-base),.8);
  --bg-70-inverse:          rgba(var(--bg-base-inverse),.7);
  --bg-80-inverse:          rgba(var(--bg-base-inverse),.8);
  
  /* Fill — base (solid) + elevated (translucent overlay) */
  --fill-primary:                      rgba(var(--neutral-06));
  --fill-secondary:                    rgba(var(--neutral-08));
  --fill-tertiary:                     rgba(var(--neutral-10));
  --fill-elevated-primary:            rgba(120, 120, 128, 0.36);
  --fill-elevated-secondary:          rgba(120, 120, 128, 0.32);
  --fill-elevated-tertiary:           rgba(118, 118, 128, 0.24);
  --fill-elevated-quaternary:         rgba(118, 118, 128, 0.18);
  
  --fill-primary-inverse:               rgba(var(--neutral-04));
  --fill-secondary-inverse:             rgba(var(--neutral-03));
  --fill-tertiary-inverse:              rgba(var(--neutral-02));
  --fill-elevated-primary-inverse:    rgba(120, 120, 120, 0.20);
  --fill-elevated-secondary-inverse:  rgba(120, 120, 128, 0.16);
  --fill-elevated-tertiary-inverse:   rgba(118, 118, 128, 0.12);
  --fill-elevated-quaternary-inverse: rgba(116, 116, 128, 0.08);


  /* Label (Figma variables, 2026-10-23) — named as Figma names them:
     Label, Label / Inverse (the other mode's ramp), Label / Control. */
  --label-primary:            #FFFFFF;
  --label-secondary:          #8A8A8A;
  --label-tertiary:           #404040;
  --label-quaternary:         #262626;
  /* PlsLabelSpecial — the reminder BODY reading colour, between primary
     (too heavy for a wall of copy) and secondary (too faint). */
  --label-special:            #EBEBF5;
  /* UIKit's tertiaryLabel (4b, 2026-10-25), which is not --label-tertiary:
     the phone notes placeholder, and 4c's completion ring. */
  --label-tertiary-system:    rgba(235, 235, 245, 0.30);

  --label-inverse-primary:    #000000;
  --label-inverse-secondary:  #5C5C5C;
  --label-inverse-tertiary:   #727272;
  --label-inverse-quaternary: #D9D9D9;

  --label-control-primary:    #F5F5F5;
  --label-control-secondary:  #8A8A8A;
  --label-control-tertiary:   #404040;

  /* The toDo card's counts beside their glyphs (iOS
     TodoActionRow.Layout.countTintByAppearance): label SECONDARY in dark, where tertiary all but vanishes on the card. */
  --action-count:             var(--label-secondary);

  /* The file row's size line: --label-special, not tertiary's #404040
     (1.6:1 on the row's fill), until --label-tertiary's dark value gets its
     proper update; then this goes back to --label-tertiary (Julian,
     2026-10-25). */
  --file-size-text:           var(--label-special);

  /* DEPRECATED (2026-10-23): the pre-Figma names, kept as aliases only so
     branches in flight keep their colours. Use the label names above —
     test/label_tokens_test.mjs fails any new use. Delete once those
     branches have landed. */
  --text-primary:               var(--label-primary);
  --text-secondary:             var(--label-secondary);
  --text-tertiary:              var(--label-tertiary);
  --text-quarternary:           var(--label-quaternary);
  --text-disabled:              var(--label-quaternary);
  --text-primary-inverse:       var(--label-inverse-primary);
  --text-secondary-inverse:     var(--label-inverse-secondary);
  --text-tertiary-inverse:      var(--label-inverse-tertiary);
  --text-quarternary-inverse:   var(--label-inverse-quaternary);
  --text-disabled-inverse:      var(--label-inverse-quaternary);
  --text-primary-vibrant:       var(--label-control-primary);
  --text-secondary-vibrant:     var(--label-control-secondary);
  --text-tertiary-vibrant:      var(--label-control-tertiary);
  --text-quarternary-vibrant:   var(--label-quaternary);
  --text-disabled-vibrant:      var(--label-quaternary);
  --text-quarternary-vibrant-inverse: var(--label-inverse-quaternary);
  --tl-text-primary:            var(--label-primary);
  --tl-text-secondary:          var(--label-secondary);
  /* END DEPRECATED */

  /* Border */
  --border:           rgba(255, 255, 255, 0.3);
  --border-strong:    rgba(255, 255, 255, 0.5);
  --border-primary:   #38383A;
  --border-secondary: rgba(255, 255, 255, 0.17);
  --border-tertiary:  #1A1A1A;

  /* Accent */
  --accent-blue:                  rgba(var(--color-blue), 1);
  --accent-red:                   rgba(var(--color-red), 1);
  --accent-green:                 rgba(var(--color-green), 1);
  --accent-yellow:                rgba(var(--color-yellow), 1);
  --accent-orange:                rgba(var(--color-orange), 1);
  --accent-purple:                rgba(var(--color-purple), 1);
  
  --accent-blue-inverse:          rgba(var(--color-blue-inverse), 1);
  --accent-red-inverse:           rgba(var(--color-red-inverse), 1);
  --accent-green-inverse:         rgba(var(--color-green-inverse), 1);
  --accent-yellow-inverse:        rgba(var(--color-yellow-inverse), 1);
  --accent-orange-inverse:        rgba(var(--color-orange-inverse), 1);
  --accent-purple-inverse:        rgba(var(--color-purple-inverse), 1);

  
  /* iOS `*Elevated` tonal fill — dark mode runs blue at a higher alpha
     (16%); red stays 10% in both (PlsAccentRedElevated). */
  --accent-blue-elevated: rgba(var(--color-blue), 0.16);
  --accent-red-elevated: rgba(var(--color-red), 0.10);

  
  /* Timeline list (iOS parity) */
  --canvas-bg:         #0C0C0C; /* iOS backgroundCanvas dark (2026-09-23): darker than the
                                   black cards so they read as lifted, as on iOS. Was #1C1C1E. */
  --ui-borders:        #38383A;
  --swipe-pin:               #B5A7FF;
  --swipe-pin-wash:          rgba(181, 167, 255, 0.10);
  --swipe-share:             rgba(var(--color-blue), 1);
  --swipe-share-wash:        rgba(var(--color-blue), 0.16);
  --accent-purple-elevated:  rgba(var(--color-purple), 0.10);
  --confetti-2:              #7F77DD;
  --confetti-3:              #1D9E75;
  --confetti-4:              #EF9F27;
  --confetti-5:              #D4537E;
  --bucket-grad-blue-from:   rgba(20, 154, 255, 0.22);
  --bucket-grad-blue-to:     rgba(0, 145, 255, 0.22);
  --bucket-grad-orange-from: rgba(255, 157, 68, 0.22);
  --bucket-grad-orange-to:   rgba(255, 146, 48, 0.22);
  --bucket-grad-red-from:    rgba(255, 86, 89, 0.22);
  --bucket-grad-red-to:      rgba(255, 66, 69, 0.22);
  --bucket-grad-teal-from:   rgba(0, 238, 213, 0.22);
  --bucket-grad-teal-to:     rgba(0, 218, 195, 0.22);
  --bucket-grad-green-from:  rgba(65, 213, 101, 0.22);
  --bucket-grad-green-to:    rgba(48, 209, 88, 0.22);
  --calendar-weekday:        #FF453A;
  --brand-blue:        rgba(var(--color-blue), 1);
  --brand-blue-12:     rgba(var(--color-blue), 0.12);

  /* Shadow */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow:     0 2px 8px rgba(0, 0, 0, 0.4);
  --shadow-float: 0 4px 12px rgba(0, 0, 0, 0.5);
  --border-float: rgba(255, 255, 255, 0.12);

  /* link_page.css — see the light scope. The glow is dimmer on black so the
     bar stays a tint rather than a neon band. */
  --link-page-bg:       rgba(var(--bg-base), 1);
  --link-page-card:     rgba(var(--color-gray-4), 1);
  --link-page-glow-1:   rgba(240, 182, 246, 0.22);
  --link-page-glow-2:   rgba(137, 195, 253, 0.22);

  /* Deprecated aliases — remove once consumers are migrated */
  --bg:         #0a0a0a;
  --bg-subtle:  #171717;
  --bg-elevated: #1c1c1c;
  --fg:         #fafafa;
  --accent:     #fafafa;
  --accent-fg:  #0a0a0a;

  color-scheme: dark;
}


/* ─── OS dark preference — catch-all when no data-theme is set ────────────── */

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

  --blend-mode:      lighten;

  --bg-base:                var(--neutral-black);
  --bg-base-inverse:        var(--neutral-white);

  --color-gray-1:           var(--neutral-01);
  --color-gray-2:           var(--neutral-05);
  --color-gray-3:           var(--neutral-07);
  --color-gray-4:           var(--neutral-09);
  --color-gray-5:           var(--neutral-11);

  --color-red:              var(--red-2);
  --color-orange:           var(--orange-2);
  --color-yellow:           var(--yellow-2);
  --color-green:            var(--green-2);
  --color-blue:             var(--blue-2);
  --color-purple:           var(--purple-2);

  --color-red-inverse:      var(--red-1);
  --color-orange-inverse:   var(--orange-1);
  --color-yellow-inverse:   var(--yellow-1);
  --color-green-inverse:    var(--green-1);
  --color-blue-inverse:     var(--blue-1);
  --color-purple-inverse:   var(--purple-1);

  /* Background */
  --bg-body:               rgba(var(--color-gray-5), 1);
  --bg-primary:            rgba(var(--bg-base), 1); 
  --bg-secondary:          rgba(var(--color-gray-4), 1);
  --bg-tertiary:           rgba(var(--color-gray-3), 1);
  --bg-quarternary:        rgba(var(--bg-base), .02);

  --bg-elevated-primary:   rgba(var(--color-gray-4), 1);
  --bg-elevated-secondary: rgba(var(--color-gray-3), 1);
  --bg-elevated-tertiary:  rgba(var(--color-gray-2), 1);   

  --bg-body-inverse:                       rgba(var(--bg-base-inverse), 1);
  --bg-primary-inverse:                    rgba(var(--bg-base-inverse), 1);
  --bg-elevated-primary-inverse:           rgba(var(--bg-base-inverse), 1);
  --bg-elevated-secondary-inverse:         rgba(var(--color-gray-1), 1);
  --bg-elevated-tertiary-inverse:          rgba(var(--bg-base-inverse), 1);
  --bg-secondary-inverse:                  rgba(var(--color-gray-1), 1);
  --bg-tertiary-inverse:                   rgba(var(--bg-base-inverse), .04);
  --bg-quarternary-inverse:                rgba(var(--bg-base-inverse), .02);

  --bg-disabled:            var(--bg-quarternary);
  --bg-disabled-inverse:    var(--bg-quarternary-inverse);
  --bg-0:                   rgba(var(--bg-base),0);
  --bg-70:                  rgba(var(--bg-base),.7);
  --glass-fill:                    rgba(40, 40, 42, 0.72);
  /* Sheets (ui/sheet): overlay + materials, dark values. */
  --overlay:                       rgba(0, 0, 0, 0.48);
  --material-ultrathin-1:          rgba(0, 0, 0, 0.02);
  --material-ultrathin-2:          transparent;
  --material-thin-1:               rgba(0, 0, 0, 0.26);
  --material-thin-2:               transparent;
  --material-regular-1:            rgba(0, 0, 0, 0.41);
  --material-regular-2:            transparent;
  --material-thick-1:              rgba(0, 0, 0, 0.60);
  --material-thick-2:              transparent;
  --material-chrome-1:             #7C7C7C;
  --material-chrome-2:             rgba(28, 28, 28, 0.90);
  --material-chrome-blur:          100px;
  /* The glass surface, dark: the tint lifts the material off a dark page
     (white, not black) and the rim is a hint. */
  --glass-tint:                    linear-gradient(180deg, rgba(255, 255, 255, 0.10) 0%, rgba(255, 255, 255, 0.06) 45%, rgba(255, 255, 255, 0.03) 100%);
  --glass-rim:                     inset 0 1px 0 rgba(255, 255, 255, 0.22), inset 0 -6px 6px -3px rgba(255, 255, 255, 0.10), inset 0 0 0 1px rgba(255, 255, 255, 0.08);
  --count-badge-fill:              #FF4245;
  --count-badge-label:             #FFFFFF;
  --home-glow-green:               #62D475;
  --home-glow-blue:                #8EC3F8;
  --home-glow-pink:                #EABDEF;
  --home-glow-opacity:             0.45;
  --bg-80:                  rgba(var(--bg-base),.8);
  --bg-70-inverse:          rgba(var(--bg-base-inverse),.7);
  --bg-80-inverse:          rgba(var(--bg-base-inverse),.8);
  
  /* Fill — base (solid) + elevated (translucent overlay) */
  --fill-primary:                      rgba(var(--neutral-06));
  --fill-secondary:                    rgba(var(--neutral-08));
  --fill-tertiary:                     rgba(var(--neutral-10));
  --fill-elevated-primary:            rgba(120, 120, 128, 0.36);
  --fill-elevated-secondary:          rgba(120, 120, 128, 0.32);
  --fill-elevated-tertiary:           rgba(118, 118, 128, 0.24);
  --fill-elevated-quaternary:         rgba(118, 118, 128, 0.18);
  
  --fill-primary-inverse:               rgba(var(--neutral-04));
  --fill-secondary-inverse:             rgba(var(--neutral-03));
  --fill-tertiary-inverse:              rgba(var(--neutral-02));
  --fill-elevated-primary-inverse:    rgba(120, 120, 120, 0.20);
  --fill-elevated-secondary-inverse:  rgba(120, 120, 128, 0.16);
  --fill-elevated-tertiary-inverse:   rgba(118, 118, 128, 0.12);
  --fill-elevated-quaternary-inverse: rgba(116, 116, 128, 0.08);


  /* Label (Figma variables, 2026-10-23) — named as Figma names them:
     Label, Label / Inverse (the other mode's ramp), Label / Control. */
  --label-primary:            #FFFFFF;
  --label-secondary:          #8A8A8A;
  --label-tertiary:           #404040;
  --label-quaternary:         #262626;
  /* PlsLabelSpecial — the reminder BODY reading colour, between primary
     (too heavy for a wall of copy) and secondary (too faint). */
  --label-special:            #EBEBF5;
  /* UIKit's tertiaryLabel (4b, 2026-10-25), which is not --label-tertiary:
     the phone notes placeholder, and 4c's completion ring. */
  --label-tertiary-system:    rgba(235, 235, 245, 0.30);

  --label-inverse-primary:    #000000;
  --label-inverse-secondary:  #5C5C5C;
  --label-inverse-tertiary:   #727272;
  --label-inverse-quaternary: #D9D9D9;

  --label-control-primary:    #F5F5F5;
  --label-control-secondary:  #8A8A8A;
  --label-control-tertiary:   #404040;

  /* The toDo card's counts beside their glyphs (iOS
     TodoActionRow.Layout.countTintByAppearance): label SECONDARY in dark, where tertiary all but vanishes on the card. */
  --action-count:             var(--label-secondary);

  /* The file row's size line: --label-special, not tertiary's #404040
     (1.6:1 on the row's fill), until --label-tertiary's dark value gets its
     proper update; then this goes back to --label-tertiary (Julian,
     2026-10-25). */
  --file-size-text:           var(--label-special);

  /* DEPRECATED (2026-10-23): the pre-Figma names, kept as aliases only so
     branches in flight keep their colours. Use the label names above —
     test/label_tokens_test.mjs fails any new use. Delete once those
     branches have landed. */
  --text-primary:               var(--label-primary);
  --text-secondary:             var(--label-secondary);
  --text-tertiary:              var(--label-tertiary);
  --text-quarternary:           var(--label-quaternary);
  --text-disabled:              var(--label-quaternary);
  --text-primary-inverse:       var(--label-inverse-primary);
  --text-secondary-inverse:     var(--label-inverse-secondary);
  --text-tertiary-inverse:      var(--label-inverse-tertiary);
  --text-quarternary-inverse:   var(--label-inverse-quaternary);
  --text-disabled-inverse:      var(--label-inverse-quaternary);
  --text-primary-vibrant:       var(--label-control-primary);
  --text-secondary-vibrant:     var(--label-control-secondary);
  --text-tertiary-vibrant:      var(--label-control-tertiary);
  --text-quarternary-vibrant:   var(--label-quaternary);
  --text-disabled-vibrant:      var(--label-quaternary);
  --text-quarternary-vibrant-inverse: var(--label-inverse-quaternary);
  --tl-text-primary:            var(--label-primary);
  --tl-text-secondary:          var(--label-secondary);
  /* END DEPRECATED */

  /* Border */
  --border:           rgba(255, 255, 255, 0.3);
  --border-strong:    rgba(255, 255, 255, 0.5);
  --border-primary:   #38383A;
  --border-secondary: rgba(255, 255, 255, 0.17);
  --border-tertiary:  #1A1A1A;

  /* Accent */
  --accent-blue:                  rgba(var(--color-blue), 1);
  --accent-red:                   rgba(var(--color-red), 1);
  --accent-green:                 rgba(var(--color-green), 1);
  --accent-yellow:                rgba(var(--color-yellow), 1);
  --accent-orange:                rgba(var(--color-orange), 1);
  --accent-purple:                rgba(var(--color-purple), 1);
  
  --accent-blue-inverse:          rgba(var(--color-blue-inverse), 1);
  --accent-red-inverse:           rgba(var(--color-red-inverse), 1);
  --accent-green-inverse:         rgba(var(--color-green-inverse), 1);
  --accent-yellow-inverse:        rgba(var(--color-yellow-inverse), 1);
  --accent-orange-inverse:        rgba(var(--color-orange-inverse), 1);
  --accent-purple-inverse:        rgba(var(--color-purple-inverse), 1);

  
  /* iOS `*Elevated` tonal fill — dark mode runs blue at a higher alpha
     (16%); red stays 10% in both (PlsAccentRedElevated). */
  --accent-blue-elevated: rgba(var(--color-blue), 0.16);
  --accent-red-elevated: rgba(var(--color-red), 0.10);

  
  /* Timeline list (iOS parity) */
  --canvas-bg:         #0C0C0C; /* iOS backgroundCanvas dark (2026-09-23): darker than the
                                   black cards so they read as lifted, as on iOS. Was #1C1C1E. */
  --ui-borders:        #38383A;
  --swipe-pin:               #B5A7FF;
  --swipe-pin-wash:          rgba(181, 167, 255, 0.10);
  --swipe-share:             rgba(var(--color-blue), 1);
  --swipe-share-wash:        rgba(var(--color-blue), 0.16);
  --accent-purple-elevated:  rgba(var(--color-purple), 0.10);
  --confetti-2:              #7F77DD;
  --confetti-3:              #1D9E75;
  --confetti-4:              #EF9F27;
  --confetti-5:              #D4537E;
  --bucket-grad-blue-from:   rgba(20, 154, 255, 0.22);
  --bucket-grad-blue-to:     rgba(0, 145, 255, 0.22);
  --bucket-grad-orange-from: rgba(255, 157, 68, 0.22);
  --bucket-grad-orange-to:   rgba(255, 146, 48, 0.22);
  --bucket-grad-red-from:    rgba(255, 86, 89, 0.22);
  --bucket-grad-red-to:      rgba(255, 66, 69, 0.22);
  --bucket-grad-teal-from:   rgba(0, 238, 213, 0.22);
  --bucket-grad-teal-to:     rgba(0, 218, 195, 0.22);
  --bucket-grad-green-from:  rgba(65, 213, 101, 0.22);
  --bucket-grad-green-to:    rgba(48, 209, 88, 0.22);
  --calendar-weekday:        #FF453A;
  --brand-blue:        rgba(var(--color-blue), 1);
  --brand-blue-12:     rgba(var(--color-blue), 0.12);

  /* Shadow */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow:     0 2px 8px rgba(0, 0, 0, 0.4);
  --shadow-float: 0 4px 12px rgba(0, 0, 0, 0.5);
  --border-float: rgba(255, 255, 255, 0.12);

  /* link_page.css — see the light scope. */
  --link-page-bg:       rgba(var(--bg-base), 1);
  --link-page-card:     rgba(var(--color-gray-4), 1);
  --link-page-glow-1:   rgba(240, 182, 246, 0.22);
  --link-page-glow-2:   rgba(137, 195, 253, 0.22);

  /* Deprecated aliases — remove once consumers are migrated */
  --bg:         #0a0a0a;
  --bg-subtle:  #171717;
  --bg-elevated: #1c1c1c;
  --fg:         #fafafa;
  --accent:     #fafafa;
  --accent-fg:  #0a0a0a;

    color-scheme: dark;
  }
}

/* The user's accent colour over the tokens — see accent.css. */
/* The user's accent colour (Settings → Accent Color), after iOS's
   AccentPreference. `data-accent` on <html> (app/settings
   apply_accent_effect, restored pre-paint by index.html) re-points the
   accent tokens the app already paints with: --accent-blue and its 10% /
   16% elevated wash. No attribute = the stock accent from tokens.css.

   Values are iOS's PleaseUI Colors.xcassets bases. Three blocks per colour,
   matching tokens.css's theme resolution: light (default), explicit dark,
   and OS-dark when no theme is forced. The .settings-accent--* classes are
   the swatches, which always show their own colour whatever is chosen. */

:root[data-accent="orange"],
.settings-accent--orange {
  --accent-blue: rgb(120, 61, 2);
  --accent-blue-elevated: rgba(120, 61, 2, 0.10);
  --swatch: rgb(120, 61, 2);
}

:root[data-theme="dark"][data-accent="orange"],
:root[data-theme="dark"] .settings-accent--orange {
  --accent-blue: rgb(255, 146, 48);
  --accent-blue-elevated: rgba(255, 146, 48, 0.10);
  --swatch: rgb(255, 146, 48);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-accent="orange"],
  :root:not([data-theme]) .settings-accent--orange {
    --accent-blue: rgb(255, 146, 48);
    --accent-blue-elevated: rgba(255, 146, 48, 0.10);
    --swatch: rgb(255, 146, 48);
  }
}

:root[data-accent="yellow"],
.settings-accent--yellow {
  --accent-blue: rgb(94, 75, 7);
  --accent-blue-elevated: rgba(94, 75, 7, 0.10);
  --swatch: rgb(94, 75, 7);
}

:root[data-theme="dark"][data-accent="yellow"],
:root[data-theme="dark"] .settings-accent--yellow {
  --accent-blue: rgb(255, 214, 0);
  --accent-blue-elevated: rgba(255, 214, 0, 0.10);
  --swatch: rgb(255, 214, 0);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-accent="yellow"],
  :root:not([data-theme]) .settings-accent--yellow {
    --accent-blue: rgb(255, 214, 0);
    --accent-blue-elevated: rgba(255, 214, 0, 0.10);
    --swatch: rgb(255, 214, 0);
  }
}

:root[data-accent="green"],
.settings-accent--green {
  --accent-blue: rgb(18, 89, 36);
  --accent-blue-elevated: rgba(18, 89, 36, 0.10);
  --swatch: rgb(18, 89, 36);
}

:root[data-theme="dark"][data-accent="green"],
:root[data-theme="dark"] .settings-accent--green {
  --accent-blue: rgb(48, 209, 88);
  --accent-blue-elevated: rgba(48, 209, 88, 0.10);
  --swatch: rgb(48, 209, 88);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-accent="green"],
  :root:not([data-theme]) .settings-accent--green {
    --accent-blue: rgb(48, 209, 88);
    --accent-blue-elevated: rgba(48, 209, 88, 0.10);
    --swatch: rgb(48, 209, 88);
  }
}

:root[data-accent="blue"],
.settings-accent--blue {
  --accent-blue: rgb(0, 102, 235);
  --accent-blue-elevated: rgba(0, 102, 235, 0.10);
  --swatch: rgb(0, 102, 235);
}

:root[data-theme="dark"][data-accent="blue"],
:root[data-theme="dark"] .settings-accent--blue {
  --accent-blue: rgb(20, 122, 255);
  --accent-blue-elevated: rgba(20, 122, 255, 0.16);
  --swatch: rgb(20, 122, 255);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-accent="blue"],
  :root:not([data-theme]) .settings-accent--blue {
    --accent-blue: rgb(20, 122, 255);
    --accent-blue-elevated: rgba(20, 122, 255, 0.16);
    --swatch: rgb(20, 122, 255);
  }
}

:root[data-accent="indigo"],
.settings-accent--indigo {
  --accent-blue: rgb(72, 26, 218);
  --accent-blue-elevated: rgba(72, 26, 218, 0.10);
  --swatch: rgb(72, 26, 218);
}

:root[data-theme="dark"][data-accent="indigo"],
:root[data-theme="dark"] .settings-accent--indigo {
  --accent-blue: rgb(181, 167, 255);
  --accent-blue-elevated: rgba(181, 167, 255, 0.10);
  --swatch: rgb(181, 167, 255);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-accent="indigo"],
  :root:not([data-theme]) .settings-accent--indigo {
    --accent-blue: rgb(181, 167, 255);
    --accent-blue-elevated: rgba(181, 167, 255, 0.10);
    --swatch: rgb(181, 167, 255);
  }
}

:root[data-accent="purple"],
.settings-accent--purple {
  --accent-blue: rgb(132, 1, 148);
  --accent-blue-elevated: rgba(132, 1, 148, 0.10);
  --swatch: rgb(132, 1, 148);
}

:root[data-theme="dark"][data-accent="purple"],
:root[data-theme="dark"] .settings-accent--purple {
  --accent-blue: rgb(242, 137, 255);
  --accent-blue-elevated: rgba(242, 137, 255, 0.10);
  --swatch: rgb(242, 137, 255);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-accent="purple"],
  :root:not([data-theme]) .settings-accent--purple {
    --accent-blue: rgb(242, 137, 255);
    --accent-blue-elevated: rgba(242, 137, 255, 0.10);
    --swatch: rgb(242, 137, 255);
  }
}

/* Reset + base element styles. Consumes tokens.css. */

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

/* Active panel drag-resize (dom.mjs startPanelResize sets/clears this):
   keep the col-resize cursor everywhere the pointer wanders, stop text
   selection, and pin the resizing panels' width transitions off so they
   track the pointer 1:1. */
body.panel-resizing {
  user-select: none;
  -webkit-user-select: none;
}
body.panel-resizing * {
  cursor: col-resize !important;
}
body.panel-resizing .sidebar,
body.panel-resizing .detail-ai-panel {
  transition: none !important;
}

/* No rubber-band/overscroll bounce anywhere. html/body alone (below) only
   covers the root scroller — every inner overflow container (detail scroll,
   chat lists, sidebars, sheets) still bounces on macOS/iOS without this.
   `none` also stops scroll chaining out of a scroller that hit its edge,
   which is what makes the app feel like a fixed shell rather than a page. */
* {
  overscroll-behavior: none;
}

/* Except inside the pages that are one scroller each, outside the shell
   (sign-in, OAuth consent): there a swipe must reach the page, wherever it
   starts. Every overflow: hidden box (a card's rounded corners, a segment's
   ellipsis) is a scroll container too, and `none` stopped the swipe at it,
   so the page would not scroll from most of its surface (2026-10-27). The
   page itself keeps `none`: nothing above it scrolls. */
.auth-screen *,
.oauth-consent * {
  overscroll-behavior: auto;
}

html, body {
  /* No background here, on purpose (2026-09-23). The app canvas lives on
     .app-shell (shell.css), not on body: html's colour is written at runtime
     by syncChromeColor (ffi/dom.mjs) to mirror the visible pane, and it is
     what shows in the iOS safe-area strip under the shell — a body fill
     would sit between the two and paint that strip the canvas colour under a
     white detail page. Pages outside the shell (boot, auth, share, join)
     keep painting their own surfaces. */
  color: var(--label-primary);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  overscroll-behavior: none;
  /* Ensure the body never exceeds the true visual viewport on Android */
  max-height: 100dvh;
}

a { color: inherit; text-decoration: none; }
a:hover { text-decoration: underline; }

ul, ol { list-style: none; }

button {
  font: inherit;
  color: inherit;
  background: transparent;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  padding: var(--sp-xs) var(--sp-sm);
  cursor: pointer;
}
button:hover {
  border-color: var(--border-primary);
}

:focus-visible {
  outline: 2px solid var(--bg-secondary);
  outline-offset: 2px;
}

/* Prevent iOS Safari autozoom on field focus. The browser zooms whenever a
   focused field's font-size is < 16px, so floor every text-entry surface at
   16px on mobile — native controls (input / textarea / select) AND the
   contenteditable editors (Milkdown / ProseMirror), which the bare element
   selectors miss. `!important` is required because component classes set
   sub-16px sizes that win on specificity; `max(16px, 1em)` only ever raises,
   never shrinks. The `:not()`s preserve the few intentionally-large fields
   (phone entry, reminder title) and the off-screen file-input sizing hack.
   This intentionally does not disable pinch-zoom. */
@media (max-width: 834px) {
  input:not(.auth-phone-input):not(.capture-file-input-hidden),
  textarea:not(.nr-title),
  select,
  [contenteditable="true"],
  .ProseMirror {
    font-size: max(16px, 1em) !important;
  }

  /* Placeholder text. Real ::placeholder inherits the (now ≥16px) field size;
     the editors render their placeholder as a ::before pseudo-element that
     carries its own font-size, so floor those too. */
  .chat-composer-milkdown::before {
    font-size: max(16px, 1em) !important;
  }
}

/* Lucide-style icons rendered via CSS mask so they inherit currentColor.
   Each icon span carries a `--icon-mask` custom property pointing to an
   inline SVG data URI, and explicit width/height. See `ui/icons.gleam`. */
.icon {
  display: inline-block;
  background-color: currentColor;
  -webkit-mask-image: var(--icon-mask);
          mask-image: var(--icon-mask);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size: contain;
          mask-size: contain;
  flex-shrink: 0;
  vertical-align: middle;
  line-height: 0;
  /* WebKit hit-tests against the mask alpha channel, so only painted pixels
     would register clicks. Opt out entirely — let the parent button/anchor
     be the sole click target. */
  pointer-events: none;
}

/* Brand-gradient tint through the icon mask (the mask only reads alpha,
   so a background gradient shows through the glyph). Generic sibling of
   sidebar.css's .sidebar-row-icon-gradient-green — wrap any icon:
     html.span([class("icon-gradient-green")], [icons.x(size)]) */
.icon-gradient-green .icon {
  background: var(--fill-gradient-green);
}

/* ── <waveform-tile> — 1:1 audio waveform preview ──────────────────────────
   Canvas bars draw in `currentColor`; size by setting a width on the
   element (or letting a square slot like the Activity media box or
   .tl-row-thumb size it). */
.wf-tile {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  background: var(--fill-elevated-quaternary);
  border-radius: var(--radius-md);
  color: var(--label-tertiary);
  overflow: hidden;
  pointer-events: none; /* parent anchor owns the click, like .icon */
}

/* Animated inline-SVG icons (e.g. check_hollow_animated).
   Cannot use the mask approach — browsers don't animate SVG data URIs.
   Instead the SVG is injected as real DOM via unsafe_raw_html and wrapped
   in this span so it flows like a regular .icon. */
.icon-animated-check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  vertical-align: middle;
  line-height: 0;
  pointer-events: none;
}

.icon-animated-check svg {
  display: block;
}

.pill {
    border: none;
    font: inherit;
    cursor: pointer;
}

/* iOS DueBadgePill: bell 16 + 13/500/18 label, 4px icon gap, 8px/4px
   padding, accentBlue on accentBlueElevated capsule. */
.due-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 8px;
    border-radius: var(--radius-full);
    background: var(--accent-blue-elevated);
    color: var(--accent-blue);
}

.due-pill > .due-pill-label {
    font-size: var(--text-sm);
    font-weight: 500;
    line-height: 18px;
    letter-spacing: 0;
}

/* Empty state ("Set alert"): same geometry, neutral/grey capsule. Carried
   over from feed.css's old .due-pill--empty. */
.due-pill--empty {
    background: var(--fill-elevated-quaternary);
    color: var(--label-primary);
}

/* Notifications off (due_pill.view_silenced, 2026-09-24): the toDo card
   mock's grey pill — background secondary, label-secondary bell and text.
   Built ahead of its data; nothing renders it until the server says an
   alert is muted (see the view's doc). */
.due-pill--silenced {
    background: var(--bg-secondary);
    color: var(--label-secondary);
}

/* Spoken-only text inside a pill (view_silenced's ", notifications off"). */
.due-pill-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
}
.button {
    width: 100%;
    padding: var(--sp-xs) var(--sp-md);
    background: transparent;
    /* Neutralize the UA <button> border; variants (.danger) re-add their
       own. */
    border: none;
    border-radius: var(--radius-full);
    font-family: inherit;
    font-size: var(--text-base);
    font-weight: 500;
    cursor: pointer;
}

.button.destructive {
    color: var(--accent-red);
    background: rgba(var(--color-red), .1);
    background-blend-mode: var(--blend-mode);
}

.button.danger {
    color: var(--accent-red);
    background: transparent;
    border: 1px solid var(--accent-red);
}
/* ── Sizes (sheet chrome, 2026-10-26 spec §4.4) ──────────────────────────
   48px: the sheet footer's actions. A text button carries body regular; an
   icon button is a 48 disc around a 24 glyph. */
.button--lg {
    height: 48px;
    padding: 0 var(--sp-lg);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font: var(--type-body);
}

.button--icon-lg {
    width: 48px;
    height: 48px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-full);
}

/* Phone + OTP authentication screen — centered card on a muted background. */

/* Its own scroller, the height of what is visible (2026-10-27): html/body never
   scroll (base.css), and an in-app browser tray, or the keyboard up for the
   code, leaves a few hundred pixels, so a taller step lost its bottom. The
   card's auto margin centres it, and gives way to the top when it is taller
   than the screen (align-items: center would push the top off-screen). */
.auth-screen {
  height: 100vh;
  height: 100dvh;
  overflow-y: auto;
  display: flex;
  background: var(--bg-subtle);
  padding: var(--sp-xl);
}

.auth-card {
  margin: auto;
  width: 100%;
  max-width: 440px;
  padding: var(--sp-xxl);
  background: var(--bg-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

.auth-logo {
  font-family: var(--font-serif);
  font-size: var(--text-3xl);
  font-style: italic;
  font-weight: 400;
  text-align: center;
  margin: 0 0 var(--sp-xs);
  color: var(--label-primary);
}

.auth-title {
  font-size: var(--text-lg);
  font-weight: 500;
  text-align: center;
  color: var(--label-primary);
  margin: 0 0 var(--sp-xl);
}

.auth-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

.auth-field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3xs);
}

.auth-label {
  font-size: var(--text-xs);
  color: var(--label-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.auth-field input,
.auth-field-input {
  padding: var(--sp-sm);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--label-primary);
  font: inherit;
  outline: none;
  transition: border-color 0.15s ease;
  width: 100%;
  box-sizing: border-box;
}

.auth-field input:focus,
.auth-field-input:focus {
  border-color: var(--bg-secondary);
}

.auth-error {
  font-size: var(--text-sm);
  color: var(--accent-red);
  text-align: center;
  margin: 0;
}

.auth-submit {
  padding: var(--sp-sm);
  border: none;
  border-radius: var(--radius-md);
  background: var(--bg-secondary);
  color: var(--label-secondary);
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
  margin-top: var(--sp-xs);
  transition: background 0.15s ease, color 0.15s ease;
}

.auth-submit:not(:disabled) {
  background: var(--fg);
  color: var(--bg);
}

.auth-submit:disabled {
  cursor: not-allowed;
}

/* Phone input */
.auth-phone-input {
  font-size: var(--text-2xl);
  text-align: center;
  letter-spacing: 0.1em;
  padding: var(--sp-md);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--label-primary);
  font-family: var(--font-sans);
  width: 100%;
  box-sizing: border-box;
}
.auth-phone-input:focus { border-color: var(--bg-secondary); outline: none; }

.auth-phone-row {
  display: flex;
  gap: 8px;
  width: 100%;
}

.auth-country-button {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 var(--sp-md);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  background: var(--bg);
  cursor: pointer;
  font: inherit;
  white-space: nowrap;
}

.auth-country-button:hover {
  background: var(--bg-subtle);
}

.auth-country-flag {
  font-size: 18px;
  line-height: 1;
}

.auth-country-code {
  font-weight: 500;
}

.auth-country-caret {
  font-size: 10px;
  opacity: 0.6;
}

.auth-phone-row .auth-phone-input {
  flex: 1;
  min-width: 0;
}

.auth-country-picker {
  position: relative;
  margin-top: 8px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  background: var(--bg);
  max-height: 300px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  z-index: 10;
}

.auth-country-search {
  padding: 10px 12px;
  border: none;
  border-bottom: 1px solid var(--border-primary);
  font: inherit;
  outline: none;
  background: transparent;
}

.auth-country-list {
  overflow-y: auto;
  flex: 1;
}

.auth-country-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 12px;
  border: none;
  background: transparent;
  cursor: pointer;
  font: inherit;
  text-align: left;
}

.auth-country-row:hover {
  background: var(--bg-subtle);
}

.auth-country-row-flag {
  font-size: 18px;
  line-height: 1;
}

.auth-country-row-name {
  flex: 1;
}

.auth-country-row-code {
  opacity: 0.6;
}

/* OTP display */
.auth-otp-wrapper { position: relative; }

.auth-otp-hidden-input {
  position: absolute;
  inset: 0;
  opacity: 0;
  width: 100%;
  height: 100%;
  font-size: 24px;
}

.auth-otp-display {
  display: flex;
  gap: var(--sp-sm);
  justify-content: center;
}

.auth-otp-box {
  width: 48px;
  height: 56px;
  border: 2px solid var(--border-primary);
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-2xl);
  font-weight: 500;
  color: var(--label-primary);
  background: var(--bg);
}
.auth-otp-box-active { border-color: var(--bg-secondary); }

/* OTP sub-text */
.auth-phone-hint {
  font-size: var(--text-sm);
  color: var(--label-primary);
  text-align: center;
  margin-top: var(--sp-xs);
}

.auth-resend {
  font-size: var(--text-sm);
  color: var(--label-tertiary);
  text-align: center;
  margin-top: var(--sp-sm);
}
.auth-resend-btn {
  background: none;
  border: none;
  color: var(--label-secondary);
  cursor: pointer;
  font: inherit;
  padding: 0;
}
.auth-resend-btn:hover { text-decoration: underline; }

/* Interest picker */
.auth-interest-form {
  gap: var(--sp-md);
}

.auth-subtitle {
  font-size: var(--text-sm);
  color: var(--label-tertiary);
  text-align: center;
  margin: 0 0 var(--sp-xs);
}

.auth-interest-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs);
  justify-content: center;
  min-height: 80px;
}

.auth-interest-loading {
  text-align: center;
  color: var(--label-tertiary);
  font-size: var(--text-sm);
  padding: var(--sp-xl) 0;
}

.auth-interest-chip {
  padding: 6px 14px;
  border-radius: 20px;
  border: 1px solid var(--border-primary);
  background: transparent;
  color: var(--label-primary);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.auth-interest-chip:hover {
  background: var(--bg-subtle);
}

.auth-interest-chip.selected {
  background: var(--fg);
  color: var(--bg);
  border-color: var(--fg);
}

.auth-interest-hint {
  font-size: var(--text-xs);
  color: var(--label-tertiary);
  text-align: center;
  margin: 0;
}

.auth-interest-buttons {
  display: flex;
  gap: var(--sp-sm);
}

.auth-interest-buttons .auth-submit {
  flex: 1;
  margin-top: 0;
}

.auth-submit-secondary {
  background: transparent;
  color: var(--label-primary);
  border: 1px solid var(--border-primary);
}

.auth-submit-secondary:hover:not(:disabled) {
  background: var(--bg-subtle);
}

/* Back link */
.auth-back {
  display: flex;
  align-items: center;
  gap: var(--sp-3xs);
  font-size: var(--text-sm);
  color: var(--label-primary);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  margin-bottom: var(--sp-md);
}
.auth-back:hover { color: var(--label-primary); }

/* The App Store button an iPhone visitor sees above the wordmark — the way to
   the app from /open when Safari's Smart App Banner isn't there (in-app
   browsers, Chrome, a dismissed banner). The pill is .share-duplicate-btn,
   the same one the join page's "Get the app to join" uses; this only gives
   an <a> the box a <button> gets for free and stretches it to the card. */
.auth-install-btn {
  display: block;
  text-align: center;
  text-decoration: none;
  margin: 0 0 var(--sp-xl);
}

/* Shell layout styles — app-shell root, shared page-header primitives,
 * main content pane, and the mobile bottom action bar.
 * Sidebar navigation lives in sidebar.css (imported right after this file).
 */

.app-shell {
  display: flex;
  height: 100vh;                  /* fallback: no dvh support */
  height: 100dvh;                 /* fallback: dvh but no visualViewport JS */
  height: var(--vvh, 100dvh);    /* primary: set in real-time by dom.mjs from
                                     visualViewport.height — reliably shrinks when
                                     the iOS Safari keyboard appears */
  overflow: hidden;
  /* Respect the gesture-nav bar on Android and iOS notch devices */
  padding-bottom: env(safe-area-inset-bottom);
  /* The app canvas (2026-09-23): the rail and sidebar paint nothing and show
     this through; .main-pane paints bg-primary on top. --canvas-bg, not
     --bg-secondary (2026-09-25): the two are the same #F2F2F7 in light, but
     in dark bg-secondary is #1C1C1E while the canvas is iOS's #0C0C0C —
     which .settings-canvas paints, so desktop Settings (rail → main, no
     list pane) showed a seam at the rail's edge. Anything that paints
     itself to blend with the shell uses --canvas-bg too. content-box, not
     the default border-box, so the safe-area padding above stays
     transparent and the runtime-mirrored html colour shows there, as it
     did before the canvas existed. */
  background: var(--canvas-bg);
  background-clip: content-box;
}

/* Keyboard up: the on-screen keyboard covers the gesture-nav zone, but
   env() keeps reporting the inset — collapse it so bottom-anchored UI
   hugs the keyboard instead of floating above it (.kb-open is set by
   the --vvh visualViewport updater in dom.mjs). */
.kb-open .app-shell {
  padding-bottom: 0;
}

/* ── Shared header icon button ──────────────────────────────────────────
   Ghost, fully-round icon button used for every header action across
   Activity, Messages, Please AI chat, Reminders/Timeline, Profile, and
   Settings — sidebar toggle, back/close, search, compose, menu, settings.
   Desktop: 32x32 hit box / 20px icon. Mobile: 48x48 hit box / 32px icon.
   Icon calls should pass size 0 so this class controls the icon size. */

/* ── Standardized page header (ui/page_header) ─────────────────────────
   Extracted from .chat-header (chat.css) — values mirrored exactly.
   [leading action] [centered flex:1 title] [trailing actions]; the
   leading slot swaps back-link (desktop) ↔ sidebar-toggle (≤768px). */
.page-header {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  /* Top: sp-sm + sp-xs = 20px — the same two-token stack as the sidebar
     (.sidebar 12px + .sidebar-header 8px), so the page header and the
     sidebar header top-align exactly. Page roots must not add their own
     top padding above this. */
  padding: calc(var(--sp-sm) + var(--sp-xs)) var(--sp-md) 0;
  flex-shrink: 0;
  min-height: 68px;
}

/* Action slots: equal flex share on both sides keeps the title truly
   centered regardless of how many actions each side holds; an empty slot
   still occupies its share. min-width reserves at least one icon-button
   footprint. */
.page-header-slot {
  flex: 1 1 0;
  min-width: 32px;
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
}
.page-header-slot--left {
  justify-content: flex-start;
}
.page-header-slot--right {
  justify-content: flex-end;
}

.page-header-title {
  flex: 0 1 auto;
  min-width: 0;
  text-align: center;
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--label-primary);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.page-header-title-link {
  text-decoration: none;
  cursor: pointer;
}
.page-header-title-link:hover {
  text-decoration: none;
  color: var(--accent-blue);
}

@media (max-width: 834px) {
  .page-header-title {
    font-size: var(--text-2xl);
  }
}

.header-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--label-primary);
  text-decoration: none;
  cursor: pointer;
  /* Fade the ghost background in on hover/focus and back out on leave,
     instead of snapping instantly. */
  transition: background-color 150ms ease, color 150ms ease;
}

.header-icon-btn:hover,
.header-icon-btn:focus-visible {
  background: var(--bg-subtle);
  color: var(--label-primary);
  text-decoration: none;
}

/* Quick fade-down on press for tactile feedback. */
.header-icon-btn:active {
  background: var(--bg-subtle);
  transition-duration: 60ms;
}

.header-icon-btn .icon {
  width: 20px;
  height: 20px;
}

@media (max-width: 834px) {
  .header-icon-btn {
    width: 48px;
    height: 48px;
  }

  .header-icon-btn .icon {
    width: 24px;
    height: 24px;
  }
}

/* Main content pane beside the sidebar */
.main-pane {
  flex: 1;
  min-width: 0;
  min-height: 0;
  height: 100%;
  overflow: hidden;
  /* The one surface that is bg-primary (2026-09-23): the shell is the
     canvas, the rail and sidebar are transparent on it, and the content
     pane is the white sheet lifted off it — the mock's only white. */
  background: var(--bg-primary);
}

/* ── Breakpoint hosts ─────────────────────────────────────────────────────
   The home route renders two main-pane bodies and lets CSS pick one: the
   timeline page on mobile (where the sidebar drawer holds nav only) and
   the new-chat landing on desktop (where the timeline lives in the
   sidebar). Both hosts fill the pane so the child owns its own scroll. */
.main-mobile-only,
.main-desktop-only {
  height: 100%;
  min-height: 0;
}

@media (min-width: 835px) {
  .main-mobile-only {
    display: none;
  }
}

@media (max-width: 834px) {
  .main-desktop-only {
    display: none;
  }
}


/* Sidebar navigation — desktop persistent rail / mobile slide-in drawer.
 * Extracted from shell.css for granularity (shell.css now holds only the
 * app-shell root, the shared page-header/header-icon-btn primitives, the
 * main content pane, and the mobile bottom action bar).
 *
 * Responsive breakpoints:
 *   Mobile  : max-width 834px  — sidebar hidden, slide-in drawer
 *   Desktop : min-width 835px  — beside the rail, 406–480px wide; collapsed =
 *             display:none (the rail's header then offers the expand button)
 */

.sidebar {
  /* --sidebar-width is the drag-resize override (absolute px, persisted;
     set inline by client.view_sidebar and live-updated by the resize FFI
     during a drag). The mobile drawer's own width rules below ignore it. */
  /* 406 default and minimum (2026-09-24): room for the toDo card's 370px
     min-width inside the list's 16px inset each side (the mock's, 2026-09-25;
     it was the pane's 10px padding plus the list's 8px), which leaves the
     card 374 at the default. Was 320 default / 395 max, which left cards
     284px wide. */
  width: var(--sidebar-width, 406px);
  max-width: 480px;
  /* No background: like the rail, the sidebar is canvas and shows the
     shell's --canvas-bg through. The one exception is the mobile drawer below,
     which slides OVER the white main pane and has to paint itself. */
  flex-shrink: 0;
  border-right: 1px solid var(--border-tertiary);
  display: flex;
  flex-direction: column;
  /* 10px horizontal padding: the mobile drawer's. Desktop drops it — the
     lists carry the pane's 16px inset themselves (see the min-width: 835px
     block below). */
  padding: var(--sp-sm) 10px;
  gap: var(--sp-md);
  box-sizing: border-box;
  position: sticky;
  top: 0;
  height: 100vh;   /* fallback */
  height: 100dvh;  /* dynamic viewport — keeps sidebar flush with the shell */
  overflow-x: hidden;
  overflow-y: hidden; /* .sidebar-scroll owns scrolling; header/footer stay pinned */
  transition: width 0.3s ease-in-out, padding 0.3s ease-in-out;
}

.sidebar-section {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.sidebar-section-header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: var(--sp-sm) var(--sp-xs) var(--sp-xs) var(--sp-xs);
  overflow: hidden;
  white-space: nowrap;
  opacity: 1;
  transition: opacity 0.3s ease-in-out;
  /* Pins to the top of .sidebar-scroll once scrolled up to it, so chat rows
     scroll underneath instead of the header scrolling out of view. Opaque
     the canvas colour, so rows scrolling under it are hidden — the sidebar
     itself paints nothing and shows the shell's --canvas-bg through (not
     bg-secondary, 2026-09-25: #1C1C1E against the #0C0C0C canvas in dark;
     the mobile drawer, which paints bg-secondary, overrides it below).
     Unstuck, it's visually identical to the old static header. */
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--canvas-bg);
}

.sidebar-section-header__label {
  font-size: var(--text-lg); /* 16px */
  color: var(--label-primary);
}

/* Collapsed-rail label swap: "Messages" ↔ "Msg". The short label hides by
   default; the desktop collapsed state flips both (and drops the + action,
   which has nowhere sane to live in the 72px rail). The mobile drawer
   never collapses visually, so the mobile block below restores the full
   set like the other collapsed resets. */
.sidebar-section-header__label--collapsed {
  display: none;
}

/* "+" action in a section header (e.g. Messages → new chat). */
.sidebar-section-header-action {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--label-primary);
  cursor: pointer;
}

@media (hover: hover) {
  .sidebar-section-header-action:hover {
    background: var(--fill-elevated-secondary);
  }
}

.sidebar-row {
  /* Button reset — ensures <button> rows are identical to <a> rows */
  font: inherit;
  border: none;
  background: none;
  box-sizing: border-box;
  /* Row layout */
  display: flex;
  align-items: center;
  gap: var(--sp-xxs);
  padding: 6px var(--sp-sm);
  border-radius: var(--radius-md);
  font-size: var(--text-base);
  color: var(--label-primary);
  min-height: 44px;
  text-decoration: none !important;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  /* Smooth the padding change between expanded (6px 12px) and collapsed (12px) */
  transition: padding 0.3s ease-in-out;
  position: relative;
  flex-shrink: 0;
}

@media (hover: hover) {
  .sidebar-row:hover {
  background: var(--fill-elevated-secondary);
  text-decoration: none;
  transition: .3s ease-in-out;
}
}
.sidebar-row-active {
  background: var(--fill-elevated-secondary);
  font-weight: 500;
  box-shadow: none;
}
@media (hover: hover) {
  .sidebar-row-active:hover {
  background: var(--fill-elevated-secondary);
  font-weight: 500;
  box-shadow: none;
}
}

.sidebar-row-icon {
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--label-primary);
  font-size: var(--text-base);
}

/* Brand-gradient icon tint (e.g. New Reminder). The base .icon paints
   currentColor through an alpha mask, so a background-image gradient
   shows through the same mask. */
.sidebar-row-icon-gradient-green .icon {
  background: var(--fill-gradient-green);
}

.sidebar-row-active .sidebar-row-icon {
  color: var(--label-primary);
}

.sidebar-label {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--label-secondary);
  font-size: 16px;
  line-height: 21px;
  letter-spacing: -0.31px;
  max-width: 200px;
  opacity: 1;
  transition: max-width 0.3s ease-in-out, opacity 0.3s ease-in-out;
}

.sidebar-row-active .sidebar-label,
.sidebar-row-button .sidebar-label {
  color: var(--label-primary);
}

.sidebar-label--with-you {
  display: flex;
  align-items: center;
  gap: var(--sp-4xs);
}

.sidebar-label-name {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  min-width: 0;
}

.sidebar-label-you {
  color: var(--label-secondary);
  font-weight: 400;
  flex-shrink: 0;
}

.sidebar-row-unread .sidebar-label {
  font-weight: 600;
  color: var(--label-primary);
}

/* Typing indicator: animated gradient sweep on the label */
.sidebar-row-typing .sidebar-label {
  font-weight: 600;
  background: radial-gradient(
    circle at 100%,
    #fda8a8,
    #f97878 50%,
    #fec7c7 75%,
    #fd8d8d 75%
  );
  background-size: 200% auto;
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: sidebar-typing-gradient 1.5s linear infinite;
}

.sidebar-unread-badge {
  display: none;
}

/* Scroll region for the reminder/chat LIST only. Nav rows, the daily
   brief, and the Reminders/Chats mode toggle are pinned above it as
   direct children of .sidebar (header/footer were always pinned) — the
   list alone fills the remaining space and scrolls. The Messages section
   header (.sidebar-section-header) sticks to the top of this container
   once scrolled up to it. */
.sidebar-scroll {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.sidebar-scroll::-webkit-scrollbar {
  display: none;
}

/* The pinned sections above the scroll region must never get crushed by
   a long list — the scroll region is the only child that flexes. */
.sidebar > .sidebar-section,
.sidebar > .sidebar-mode-header,
.sidebar > .sidebar-search-field {
  flex-shrink: 0;
}

/* ── Drag-resize handle (desktop) ─────────────────────────────────────
   Invisible 5px strip along the sidebar's right edge; shows a hairline
   on hover and while a drag is running. Dragging below the collapse
   threshold collapses the sidebar (see shell.ResizeEnded). Collapsed, the
   pane and this handle are display:none, so expanding is the rail header's
   button or ⌘\ — there is no edge left to drag back out. */
.sidebar-resize-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: 5px;
  cursor: col-resize;
  z-index: 10;
}

@media (hover: hover) {
  .sidebar-resize-handle:hover {
    background: var(--border-primary);
  }
}

body.panel-resizing .sidebar-resize-handle {
  background: var(--border-primary);
}

/* Spacer no longer needed — .sidebar-scroll grows to fill the gap */
.sidebar-spacer {
  display: none;
}

.sidebar-ai-avatar {
  width: 22px;
  height: 22px;
  border-radius: var(--radius-full);
  background: var(--fg);
  color: var(--bg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.user-menu-backdrop {
  position: fixed;
  inset: 0;
  z-index: 99;
  cursor: default;
}

/* (Feed column toggle removed — internal test UI; the feed still reads
   the persisted column count via feed_cols_ffi.) */

/* ── Mobile responsive (≤ 768px) ─────────────────────────────────────── */

/* ─── Keyframe animations ────────────────────────────────────────────── */

@keyframes sidebar-typing-gradient {
  to {
    background-position: -200% center;
  }
}

/* ─── Media queries ──────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .sidebar-row-typing .sidebar-label {
    animation: none;
  }
}

/* Phones have no list pane and no drawer (iOS parity, 2026-10-23): the tab
   bar, the root header and the pages themselves carry navigation. */
@media (max-width: 834px) {
  .sidebar {
    display: none;
  }
}

/* ── Sidebar mode toggle: full-width Reminders | Chats segmented pill ──────
   Replaces the static "Reminders" heading. A capsule track with two equal
   segments; the active one is a raised pill. */
.sidebar-mode-header {
  padding: var(--sp-xs) var(--sp-xs) 0;
}

.sidebar-mode-toggle {
  display: flex;
  width: 100%;
  padding: 3px;
  box-sizing: border-box;
  gap: 2px;
  border-radius: var(--radius-full);
  background: var(--bg-secondary);
}

.sidebar-mode-segment {
  flex: 1;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xs);
  border: none;
  background: none;
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: var(--radius-full);
  font: inherit;
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--label-secondary);
  cursor: pointer;
}

@media (hover: hover) {
  .sidebar-mode-segment:hover {
    color: var(--label-primary);
  }
}

.sidebar-mode-segment--active,
.sidebar-mode-segment--active:hover {
  background: var(--bg-primary);
  color: var(--label-primary);
  box-shadow: var(--shadow-sm);
}

/* Chats-with-unread count on the toggle — the blue read/unread language
   of the chat list, not the old red page badge. border-box keeps a
   single digit a true 16px circle (content-box let the side padding
   widen it into a subtle pill, reading as an off-center digit). */
.sidebar-mode-badge {
  box-sizing: border-box;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background: var(--accent-blue);
  color: #ffffff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
}

/* ── Sidebar chat list (Chats mode) ─────────────────────────────────────── */
.sidebar-chats {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-xs);
}

@media (max-width: 834px) {
  /* The drawer keeps its labeled nav rows as the primary controls; the
     heading toggle is desktop chrome. The chat list itself does render in
     the drawer — the Messages row flips the mode to reveal it. */
  .sidebar-mode-header {
    display: none;
  }

  .sidebar--chats-mode .sidebar-chats {
    display: flex;
  }

  .sidebar:not(.sidebar--chats-mode) .sidebar-chats {
    display: none;
  }
}

/* ── Sidebar timeline (desktop, expanded only) ──────────────────────────── */
.sidebar-timeline {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  padding: var(--sp-md) var(--sp-xs) var(--sp-sm);
}

/* The section rhythm (Julian, 2026-10-23): header → first card 8px (the
   folding body's own top padding, section_header.css), card → card 4px
   (.tl-list--compact), last card → next header 24px (.sidebar-timeline's
   12px gap + the header's 12px top margin). */
.sidebar-tl-section {
  display: flex;
  flex-direction: column;
  margin: 0;
}

/* The foldable header (styles/section_header.css), flush with the cards:
   the mock's "Today" starts on the card edge. */
.sidebar-tl-section > .section-header {
  margin-top: var(--sp-sm);
}

.sidebar-timeline-empty {
  /* Vertical only: .sidebar-timeline carries the column's inset. */
  padding: var(--sp-sm) 0;
  font-size: var(--text-base);
  color: var(--label-secondary);
}

.sidebar-timeline-skeleton {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  padding: var(--sp-sm) 0;
}

@media (max-width: 834px) {
  .sidebar-timeline {
    display: none;
  }
}


/* ── Desktop: collapsed = hidden ──────────────────────────────────────────
   The app rail (rail.css) is the always-present column; the sidebar is
   the panel beside it — collapsed by its own header's button, expanded by
   the rail header's (2026-09-24). Nothing "folds" any more — no icon rail,
   no label fades — so a hidden sidebar costs zero chrome. The drawer
   header and footer are mobile chrome (the rail carries their jobs). */
@media (min-width: 835px) {
  /* Collapsed = slid away left (2026-10-23; was display:none, a cut). The
     pane eases to zero width while its contents keep the open width and
     ride the closing edge out (same timing, so they move with it), then
     goes visibility:hidden — out of the tab order and the accessibility
     tree — once the slide ends. Expanding runs it back: visible at once,
     sliding right. A drag turns the transitions off (body.panel-resizing). */
  .sidebar {
    transition:
      width var(--motion-slide) var(--ease-slide),
      min-width var(--motion-slide) var(--ease-slide),
      visibility 0s;
  }
  .sidebar > :not(.sidebar-resize-handle) {
    box-sizing: border-box;
    width: clamp(406px, var(--sidebar-width, 406px), 480px);
    transition: transform var(--motion-slide) var(--ease-slide);
  }
  /* Two classes: the desktop `.sidebar` rule's 406px min-width comes later
     in this file and would win a tie. */
  .sidebar.sidebar-collapsed {
    width: 0;
    min-width: 0;
    visibility: hidden;
    transition:
      width var(--motion-slide) var(--ease-slide),
      min-width var(--motion-slide) var(--ease-slide),
      visibility 0s linear var(--motion-slide);
  }
  .sidebar.sidebar-collapsed > :not(.sidebar-resize-handle) {
    transform: translateX(-100%);
  }

  @media (prefers-reduced-motion: reduce) {
    .sidebar,
    .sidebar.sidebar-collapsed,
    .sidebar > :not(.sidebar-resize-handle) {
      transition: none;
    }
  }
}

/* ── Desktop list header: the collapse button (2026-09-24, the mock) ──────
   One 56px row at the very top of the list pane — whatever it lists
   (timeline, messages, collections) — holding the collapse button at its right,
   right-aligned with the cards below. Collapsed, the pane is display:none
   and the rail's header shows the expand button instead. The pane's 12px
   top padding goes on desktop so this row sits flush with the window top,
   level with the rail's own 56px header. */
.sidebar-list-header {
  display: none;
}

@media (min-width: 835px) {
  .sidebar {
    padding-top: 0;
    /* No side padding on desktop (2026-09-25): the mock insets the cards
       16px from the pane's edges, and that inset is the lists' own padding
       (.sidebar-timeline, .sidebar-chats, .sidebar-search below) so it sits
       INSIDE .sidebar-scroll's clip — the selected card's ring draws 4px
       outside the card and would be cut off at a padding-box edge. */
    padding-left: 0;
    padding-right: 0;
    /* Never narrower than a 370px toDo card needs (see .sidebar). */
    min-width: 406px;
    /* No divider on desktop (2026-09-24): the mock has none between the
       list pane and the main pane — canvas meets the white sheet — and
       Julian confirmed leaving it out. The mobile drawer keeps its edge:
       it slides OVER the page and needs one. */
    border-right: none;
  }

  .sidebar-list-header {
    display: flex;
    flex: 0 0 56px;
    align-items: center;
    justify-content: space-between;
    /* The headline lines up with the cards' left edge (the lists' 16px
       inset), as the button does with their right. */
    padding-left: var(--sp-md);
    /* The lists' 16px inset (the pane has no side padding on desktop), so
       the button's right edge lines up with the cards' (2026-09-25: cards
       80→454, button 414→454 at the default 406 width; was 8 on top of the
       pane's old 10px padding). */
    padding-right: var(--sp-md);
  }

  /* The list column's 16px inset from the pane's edges (the mock,
     2026-09-25), whichever list the pane shows: the timeline's cards, the
     chat list, Search's field and collections. 406 - 2×16 = a 374px card. */
  .sidebar-timeline,
  .sidebar-chats,
  .sidebar-search-field,
  .sidebar-search {
    padding-left: var(--sp-md);
    padding-right: var(--sp-md);
  }
}

/* ── The list pane in Search mode: Collections (round 5, desktop) ────────
   The field is pinned between the header and .sidebar-scroll, a direct
   .sidebar child, so it takes the pane's width clamp and collapse slide.
   Inside the scroller, .sidebar-search holds the keep bar, then the grid,
   a collection's list or typed results. Both carry the 16px inset above. */

/* Header → field is the pane's 16px gap. The same gap would sit under the
   field, where field → first card must be 12, so it goes: the whole space
   under the field is .sidebar-search's top padding, inside the scroller,
   where a selected ring (3px out + 1px) never clips at the top. */
.sidebar-search-field {
  margin-bottom: calc(var(--sp-md) * -1);
}

/* The base rule's 24px under the field row would add a third gap. */
.sidebar-search-field .search-header {
  margin: 0;
}

/* The empty field's clear chip has no width and no ink (search.css), but a
   button stays a Tab stop until it is hidden: an invisible one after the
   field. Hidden outright here, with no fade (desktop has no ✕ to trade
   places with). */
.sidebar-search-field .search-input:placeholder-shown + .search-clear {
  visibility: hidden;
}

/* Desktop's sign of focus (phones show the ✕ instead): the hairline turns
   accent. */
.sidebar-search-field .search-pill:focus-within {
  box-shadow: inset 0 0 0 1px var(--accent-blue);
}

/* Field → first card 12; field → the keep bar, a section header or the
   first row 16 (phone parity). It fills the scroller's height so a state
   can centre in it, and keeps 12 under the last card or row for its focus
   or selected ring. */
.sidebar-search {
  display: flex;
  flex: 1;
  flex-direction: column;
  padding-top: var(--sp-md);
  padding-bottom: var(--sp-sm);
}

.sidebar-search:has(> .bucket-grid:first-child) {
  padding-top: var(--sp-sm);
}

/* Keyboard focus in the list pane rings in accent, as a card's does
   (search.css): base.css's ring is the canvas colour here, so Keep, No
   thanks, a section header or a row would show none. A row's ring hugs
   its title link, inside the card's 16px padding, so the scroller's clip
   never cuts it. */
.sidebar-search
  :is(.search-keep, .search-no-thanks, .section-header__button, .tl-row-content):focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

/* The 12 under the last row is .sidebar-search's alone: the results'
   own bottom padding (search.css) would stack on it. */
.sidebar-search .search-results {
  padding-bottom: 0;
}

/* Bar → first section header or row 16; the field → bar 16 is the padding
   above. */
.sidebar-search .search-keep-bar {
  margin: 0 0 var(--sp-md);
}

/* Centred in the scroller's height, not the phone's 100dvh calc. */
.sidebar-search .search-state,
.sidebar-search .search-spinner {
  flex: 1;
}

/* Entering a collection, its list (or its empty state) and the title come
   in from 24px right; leaving, the grid and "Collections" from 24px left
   (spec §9). Only grandchildren of .sidebar and deeper move: a transform
   on a direct child would fight the collapse slide's. */
.sidebar-search .browse-slides-in,
.sidebar-list-header .sidebar-list-title--enter {
  animation: sidebar-slide-forward var(--motion-slide) var(--ease-slide);
}

.sidebar-search .bucket-grid--enter,
.sidebar-list-header .sidebar-list-title--back {
  animation: sidebar-slide-back var(--motion-slide) var(--ease-slide);
}

@keyframes sidebar-slide-forward {
  from {
    opacity: 0;
    transform: translateX(24px);
  }
}

@keyframes sidebar-slide-back {
  from {
    opacity: 0;
    transform: translateX(-24px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .sidebar-search .browse-slides-in,
  .sidebar-search .bucket-grid--enter,
  .sidebar-list-header .sidebar-list-title--enter,
  .sidebar-list-header .sidebar-list-title--back {
    animation: none;
  }
}

/* "Home" / "Messages" / "Collections" — which list the pane holds, or the
   open collection's name. A long one truncates, so the collapse button
   never moves. */
.sidebar-list-title {
  min-width: 0;
  margin: 0;
  overflow: hidden;
  font-size: var(--text-xl);
  font-weight: 700;
  line-height: 1.2;
  color: var(--label-primary);
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Inside a collection: the back chevron, then the title, 4 apart. */
.sidebar-list-heading {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}

/* The 40px button centres its 24px glyph 8 in, so −8 puts the glyph's left
   edge on the cards' 16px edge. */
.sidebar-list-back {
  margin-left: -8px;
}

/* The rail's button, in the list pane: 40px hollow, 24px glyph, label
   primary, the elevated-secondary wash on hover. The collapse button and
   Search's back chevron share it (a selector list: shell.gleam's focus
   hand-offs take the first .sidebar-list-header-button, which must stay
   the collapse button). Neither shrinks for a long title. */
.sidebar-list-header-button,
.sidebar-list-back {
  flex: none;
  font: inherit;
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  color: var(--label-primary);
  transition: background-color 150ms ease;
}

.sidebar-list-header-button .icon,
.sidebar-list-back .icon {
  width: 24px;
  height: 24px;
}

@media (hover: hover) {
  .sidebar-list-header-button:hover,
  .sidebar-list-back:hover {
    background: var(--fill-elevated-secondary);
  }
}

/* A header led by an action (Messages' Ask Please disc): the title sits
   centred between the two discs, as the 2026-10-26 mock. */
.sidebar-list-header--led .sidebar-list-title {
  flex: 1;
  text-align: center;
}

/* Ask Please in the list header (the 2026-10-26 mock): 40 round in the
   elevated fill with the 24 sparkle. The list's first row is the other
   door (iOS keeps both). */
.sidebar-list-ai {
  border-radius: var(--radius-full);
  background: var(--fill-elevated-secondary);
  text-decoration: none;
}

.sidebar-list-header-button:focus-visible,
.sidebar-list-back:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 1px;
}

/* Settings is rail → main on desktop (2026-09-24): no list pane beside it
   (client.view_session tags the shell). Phones keep the aside — it is their
   navigation drawer. */
@media (min-width: 835px) {
  .app-shell--no-list > .sidebar {
    display: none;
  }
}

/* The app rail left of the sidebar — see app/shell.view_rail. */
/* App rail — the narrow always-present column left of the list pane (see
   app/shell.view_rail). Desktop only: the mobile drawer carries labeled
   rows for the same actions.

   Three stacked parts (2026-09-24 spec): a 56px header, the nav column
   filling the height between, and a 56px footer. Header and footer are
   fixed rows, full rail width, so the nav's centre never moves with what
   they hold — the header's expand button comes and goes with the list
   pane, and the row stays. Replaced a 1fr/auto/1fr grid (2026-09-23) whose
   bottom group sat in the last row with 12px padding above and below. */

.app-rail {
  width: 64px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  /* No background of its own: the rail is canvas, and the canvas is the
     shell's --canvas-bg showing through. No divider either (2026-09-24):
     the mock runs one flat canvas from the rail into the list pane, and
     Julian confirmed leaving the line out. It was border-right, then an
     inset shadow — a border takes its pixel out of the content box and
     put every 40px button at x = 11.5, half-pixel soft. With no line at
     all, the full 64px is content and the buttons sit on x = 12. */
  z-index: 20;
}

.app-rail-header,
.app-rail-footer {
  flex: 0 0 56px;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The nav spans everything between header and footer and centres its
   items both ways. 36px between the 40px buttons is the spec's gap — a
   76px pitch, centre to centre. */
.app-rail-nav {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 36px;
}

/* 40px round buttons, 24px glyphs, label primary for every item. The
   current page is the filled drawing (Search keeps its hollow one) on a
   raised disc — revised 2026-09-24: the filled glyph alone didn't read as
   a state. Fully round (100px radius) like the New Reminder circle; the
   relative position anchors the alert badge. */
.app-rail-button {
  /* Button + anchor reset */
  font: inherit;
  border: none;
  background: transparent;
  text-decoration: none;
  cursor: pointer;
  padding: 0;
  position: relative;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 100px;
  color: var(--label-primary);
  transition: background-color 150ms ease;
}

.app-rail-button .icon {
  width: 24px;
  height: 24px;
}

@media (hover: hover) {
  .app-rail-button:hover {
    background: var(--fill-elevated-secondary);
    text-decoration: none;
  }
}

/* After the hover rule so the current page keeps its disc under the
   pointer too. Background elevated primary, not background primary
   (2026-09-25): the two are the same white in light, but in dark
   bg-primary is #000, which all but vanished (about 1.07:1) once the shell
   became the #0C0C0C canvas; it had read at about 1.23:1 on the old
   #1C1C1E shell. Elevated primary is iOS's raised #1C1C1E, about 1.15:1
   on the canvas. */
.app-rail-button[aria-current="page"] {
  background: var(--bg-elevated-primary);
}

/* Settings: the viewer's avatar at the rail's 40px (2026-10-26, as the
   phone tab bar and iOS). At rest it is dimmed to 65% and drained to a
   quarter of its saturation; the current section shows it in full. The
   disc hover/current fill behind it is hidden by the avatar itself. */
.app-rail-button--avatar .avatar {
  opacity: 0.65;
  filter: saturate(0.25);
  transition: opacity 150ms ease, filter 150ms ease;
}
.app-rail-button--avatar[aria-current="page"] .avatar {
  opacity: 1;
  filter: none;
}

/* Alert badge (2026-09-24): 12px, accent red, the count in label primary
   inverse — Messages' unread chats (DMs, replies, mentions). A 12px circle
   for one digit, a capsule past that ("99+" caps it). Its corner sits on
   the 24px glyph's top-right (the glyph spans 8–32 of the 40px button). */
.app-rail-badge {
  position: absolute;
  top: 4px;
  left: 24px;
  box-sizing: border-box;
  min-width: 12px;
  height: 12px;
  padding: 0 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 100px;
  background: var(--accent-red);
  color: var(--label-inverse-primary);
  font-size: 9px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

.app-rail-button:focus-visible,
.app-rail-new:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 1px;
}

/* New Reminder: a 32px accent circle inside a 40px slot, so the one
   creative action keeps the column's 40px rhythm. The thick plus (lg art)
   at 20px in label-primary-inverse. The accent is web's default blue —
   there is no persisted user accent on web yet. */
.app-rail-slot {
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.app-rail-new {
  font: inherit;
  border: none;
  padding: 0;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--accent-blue);
  color: var(--label-inverse-primary);
  cursor: pointer;
}

.app-rail-new .icon {
  width: 20px;
  height: 20px;
}

@media (max-width: 834px) {
  .app-rail {
    display: none;
  }
}

/* Feed view styles — tabs, item cards, grid, loading/empty/error states. */

/* ---------- Pull-to-refresh indicator ---------- */

/*
 * #feed-ptr-indicator sits as the first child of .feed-items-wrap.
 * Its height is driven by JS during the gesture (inline style). When the
 * gesture completes, JS removes inline styles and Gleam adds --active.
 * The indicator collapses back to height:0 when --active is absent.
 */
.feed-ptr-indicator {
  width: 100%;
  height: 0;
  /* A flex item of the column wrap: without this, a Home taller than the
     screen shrank the pull to nothing (overflow: hidden makes its minimum
     0) and the spinner never showed (found by card_swipe.spec, round 3a). */
  flex-shrink: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: height 0.25s ease, opacity 0.2s ease, margin-bottom 0.25s ease;
  opacity: 0;
  /* Idle at height:0 the indicator still occupies a flex slot, so the
     wrap's gap rendered a phantom --sp-xl between the header and the
     first card. Cancel that gap while idle; --active restores it so the
     spinner keeps its spacing during a refresh. */
  margin-bottom: calc(-1 * var(--feed-wrap-gap, var(--sp-xl)));
}

/* JS sets --pulling during the drag; Lustre sets --active while the
   manifest refresh is in flight. Both restore the gap slot the idle
   negative margin cancels. */
.feed-ptr-indicator--pulling {
  margin-bottom: 0;
}

.feed-ptr-indicator--active {
  height: 52px;
  opacity: 1;
  margin-bottom: 0;
}

/* Spinner ring — pure CSS, no external assets. */
.feed-ptr-spinner {
  width: 22px;
  height: 22px;
  border: 2.5px solid var(--border-primary);
  border-top-color: var(--label-secondary);
  border-radius: 50%;
  animation: feed-ptr-spin 0.7s linear infinite;
}

@keyframes feed-ptr-spin {
  to { transform: rotate(360deg); }
}

/*
 * During the drag phase JS mutates `style.height` and `style.opacity`
 * directly on .feed-ptr-indicator, bypassing the CSS transition so the
 * motion tracks the finger in real time.
 *
 * On release with a confirmed refresh:
 *   1. onRefresh() fires → Lustre queues a re-render with --active
 *   2. One rAF later, JS clears the inline styles
 *   3. Lustre's render has already applied --active (height: 52px), so no
 *      visual jump occurs — the indicator stays pinned at 52px
 *   4. When ManifestRefreshed clears pull_refreshing, Lustre removes --active
 *      and the CSS transition collapses height back to 0
 *
 * `feed-ptr-indicator--ready` is toggled by JS when drag >= threshold.
 * It brightens the spinner to signal "release to refresh".
 */
.feed-ptr-indicator.feed-ptr-indicator--ready .feed-ptr-spinner {
  border-top-color: var(--label-primary);
}

/* Full-page wrapper: hosts the standardized page header (full width,
   own inset) above the column-constrained .feed body. */
.feed-page {
  height: 100%;
  background: var(--bg-secondary);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.feed {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  flex-direction: column;
  padding: var(--sp-xs) var(--sp-xl) 0 var(--sp-xl);
  width: 54vw;
  max-width: 900px;
  margin: 0 auto;
  overflow: hidden;
}

/* Search entry below the header — shares .search-input's chrome (border,
   radius, bg, padding); these overrides make the <a> behave as a static
   pill rather than a live input. */
.feed-search-pill {
  flex: 0 0 auto !important;
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  color: var(--label-tertiary);
  text-decoration: none;
  cursor: pointer;
  margin-bottom: var(--sp-md);
  height: 48px;
}
@media (hover: hover) {
  .feed-search-pill:hover {
  text-decoration: none;
  border-color: var(--border-primary);
}
}

@media (min-width: 835px) {
    .feed-search-pill {
        width: 100%;
        max-width: 800px;
    }
}

/* Mobile: search lives in the bottom action bar — no pill under the header. */
@media (max-width: 834px) {
  .feed-search-pill {
    display: none;
  }
}

/* ---------- Grid ---------- */

.feed-items-wrap {
  flex: 1;
  min-height: 0;
  width: 100%;
  overflow-y: auto;
  /* Explicit x-axis lock: when overflow-y is non-visible, CSS computes
     overflow-x as `auto` unless we say otherwise — on iOS that means the
     container can scroll horizontally if any child is 1px too wide.
     `touch-action: pan-y` tells iOS to only recognise vertical panning here
     so the PTR gesture doesn't open a sideways scroll context. */
  overflow-x: hidden;
  touch-action: pan-y;
  display: flex;
  flex-direction: column;
  /* One gap for the sections and the idle pull indicator's cancel. */
  gap: var(--feed-wrap-gap, var(--sp-xl));
  padding-bottom: var(--sp-xl);
  scrollbar-width: none;
  -ms-overflow-style: none;
}
/* Timeline sections: the foldable header (ui/section_header) over its
   folding body, stacked in the wrap. Header → first card is the body's own
   top padding, so a folded section is its header alone. */
.feed-section {
  display: flex;
  flex-direction: column;
}

/* Desktop: constrain sections (centered by .feed-items-wrap--timeline). */
@media (min-width: 835px) {
  .feed-section {
    width: 100%;
    max-width: 800px;
  }
}

.feed-items-wrap::-webkit-scrollbar {
  display: none;
}

.feed-grid {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: var(--sp-xl);
}

.feed-col {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  gap: var(--sp-xl);
}

/* 4-col needs more room */
.feed--cols-4 {
  width: 80vw;
  max-width: 80vw;
}

/* ---------- Card ---------- */

/* Wrapper: inner grid drives the collapse animation (grid-template-rows: 1fr → 0fr).
   No margin-bottom needed — gap on .feed-grid handles row spacing. */
.feed-card-wrapper {
  display: grid;
  grid-template-rows: 1fr;
}

.feed-card {
  width: 100%;
  display: inline-flex;
  flex-direction: column;
  background: var(--bg-primary);
  border: none;
  border-radius: 16px;
  overflow: hidden; /* clips media image to card's rounded corners */
  transition: transform 0.15s ease;
  break-inside: avoid;
  min-height: 0; /* required for grid-template-rows collapse */
}

@media (hover: hover) {
  .feed-card:hover {
  transform: translateY(-1px);
}
}

/* Text cards use the same chrome-free style as photo/bookmark cards.
   When a thumbnail is present it shows above the title just like a photo. */
.feed-card-text {
  color: var(--label-primary);
}

/* The anchor wraps only the media area. The card body (title, author, likes)
   sits as a sibling outside the anchor so the like button doesn't trigger
   modem's anchor-intercepted navigation. */
.feed-card-link {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
  position: relative;
}
@media (hover: hover) {
  .feed-card-link:hover {
  text-decoration: none;
}
}

/* ---------- Media area ---------- */

.feed-card-media {
  position: relative;
  display: flex;
  align-items: flex-end;
  width: 100%;
  padding: var(--sp-sm);
  color: #ffffff;
  font-size: var(--text-xs);
  overflow: hidden;
  /* No border-radius here — the card's overflow:hidden + border-radius:16px
     clips the media to rounded corners at the top automatically. */
}

.feed-card-media-photo {
  padding: 0;
}
.feed-card-media-voice {
  aspect-ratio: 4 / 3;
}
.feed-card-media-bookmark {
  aspect-ratio: 16 / 9;
  min-width: 100%;
  min-height: 120px;
}

/* Tiles with no media (calendar tiles; text tiles without a thumbnail)
   reserve a slim band when a manifest badge is present, so the
   absolute-positioned `.feed-badge-pill` has room to sit without overlapping
   the card body. The pill is ~32px tall plus its top inset, so 48px clears it. */
.feed-card-badge-spacer {
  height: 48px;
  width: 100%;
}

.feed-card-photo-placeholder {
  width: 100%;
  height: 100%;
}

/* Deterministic gradients for placeholders */
.feed-card-gradient-1 { background: linear-gradient(135deg, #667eea, #764ba2); }
.feed-card-gradient-2 { background: linear-gradient(135deg, #f093fb, #f5576c); }
.feed-card-gradient-3 { background: linear-gradient(135deg, #4facfe, #00f2fe); }
.feed-card-gradient-4 { background: linear-gradient(135deg, #43e97b, #38f9d7); }
.feed-card-gradient-5 { background: linear-gradient(135deg, #fa709a, #fee140); }
.feed-card-gradient-6 { background: linear-gradient(135deg, #30cfd0, #330867); }

.feed-card-voice-duration,
.feed-card-bookmark-site {
  background: rgba(0, 0, 0, 0.5);
  padding: var(--sp-3xs) var(--sp-xs);
  border-radius: var(--radius-md);
  backdrop-filter: blur(4px);
  font-weight: 500;
}

.feed-card-voice-icon {
  position: absolute;
  top: var(--sp-sm);
  left: var(--sp-sm);
  background: rgba(0, 0, 0, 0.5);
  width: 32px;
  height: 32px;
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(4px);
  color: #fff;
}

/* ---------- Header (avatar + name) ---------- */

.feed-card-header {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-xs);
  padding: var(--sp-sm);
  text-decoration: none;
  color: var(--label-tertiary);
  min-width: 0;
  overflow: hidden;
}
@media (hover: hover) {
  .feed-card-header:hover {
  text-decoration: none;
  color: var(--label-primary);
}
}

.feed-card-header-info {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.feed-card-header-name {
  font-size: var(--text-sm);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: inherit;
}

.feed-card-header-timestamp {
  font-size: var(--text-xs);
  color: var(--label-tertiary);
  line-height: 1.3;
}

/* ---------- Body (title row + footer) ---------- */

.feed-card-body {
  padding: var(--sp-sm) var(--sp-xs);
  width: 100%;
}

/* Title row: completion checkbox + title link side by side */
.feed-card-title-row {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-xs);
}

/* Title link fills remaining width */
.feed-card-title-link {
  flex: 1;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}
@media (hover: hover) {
  .feed-card-title-link:hover {
  text-decoration: none;
}
}

.feed-card-title {
  font-size: 14px;
  font-weight: 500;
  line-height: 1.3;
  color: var(--label-primary);
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Completed state: struck-through, lighter weight and color */
.feed-card-title--done {
  text-decoration: line-through;
  font-weight: 400;
  color: var(--label-tertiary);
}

/* Event time line shown above the title on calendar tiles. Sits inside
   the title link so it inherits the same hit area as the title. */
.feed-card-event-time {
  display: block;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--label-tertiary);
  margin-bottom: var(--sp-3xs);
}

/* Calendar metadata row: source-calendar name badge under the title.
   Lives inside `.feed-card-title-link` so it inherits the title's
   left indent automatically. */
.feed-card-meta-row {
  display: flex;
  margin-top: var(--sp-3xs);
}
.feed-card-meta-badge {
  font-size: var(--text-xs);
  color: var(--label-tertiary);
  background: var(--bg-secondary);
  padding: var(--sp-3xs) var(--sp-xs);
  border-radius: var(--radius-md);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Completion checkbox button */
.feed-card-complete-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  margin-top: 1px; /* optical alignment with first line of title */
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: var(--label-tertiary);
  font-family: inherit;
}
@media (hover: hover) {
  .feed-card-complete-btn:hover {
  color: var(--label-primary);
}
}
.feed-card-complete-btn--done {
  color: var(--color-primary, var(--accent));
}
@media (hover: hover) {
  .feed-card-complete-btn--done:hover {
  color: var(--color-primary, var(--accent));
}
}

/* Footer: timestamp left, engagement actions right */
/* Please-agent comment preview on the tile. */
.feed-card-agent {
  margin: var(--sp-xs) 0 0;
  padding: var(--sp-xs) var(--sp-sm);
  background: var(--surface-secondary, rgba(127, 127, 127, 0.06));
  border-radius: var(--radius-sm, 8px);
}
.feed-card-agent-label {
  display: block;
  font-size: var(--text-2xs, 0.625rem);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--accent-blue);
  margin-bottom: 2px;
}
.feed-card-agent-text {
  margin: 0;
  font-size: var(--text-xs);
  line-height: 1.4;
  color: var(--label-secondary);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.feed-card-agent-pending {
  font-size: var(--text-xs);
  font-style: italic;
  color: var(--label-tertiary);
}

.feed-card-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  margin-top: var(--sp-sm);
}

.feed-card-footer-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
}

/* Comments count */
.feed-card-comments {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3xs);
  color: var(--label-secondary);
  flex-shrink: 0;
}

.feed-card-comments-icon {
  display: inline-flex;
  align-items: center;
}

.feed-card-comments .icon {
  width: 13px;
  height: 13px;
}

.feed-card-comments-count {
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

/* Bookmark button */
.feed-card-bookmark-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3xs);
  color: var(--label-secondary);
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: inherit;
  flex-shrink: 0;
}
@media (hover: hover) {
  .feed-card-bookmark-btn:hover {
  color: var(--label-primary);
}
}
.feed-card-bookmark-btn--active {
  color: var(--color-primary, var(--accent));
}
@media (hover: hover) {
  .feed-card-bookmark-btn--active:hover {
  color: var(--color-primary, var(--accent));
}
}
.feed-card-bookmark-btn .icon {
  width: 14px;
  height: 14px;
}

/* Likes */
.feed-card-likes {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3xs);
  color: var(--label-secondary);
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: inherit;
  flex-shrink: 0;
}
@media (hover: hover) {
  .feed-card-likes:hover {
  color: var(--label-primary);
}
}
.feed-card-likes-active {
  color: var(--accent-red);
}
@media (hover: hover) {
  .feed-card-likes-active:hover {
  color: var(--accent-red);
}
}

.feed-card-likes-icon {
  display: inline-flex;
  align-items: center;
  color: var(--label-secondary);
}

.feed-card-likes-count {
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

.feed-card-likes .icon {
  width: 13px;
  height: 13px;
}

/* ---------- States ---------- */

.feed-loading,
.feed-empty,
.feed-error {
  padding: var(--sp-xxl) 0;
  color: var(--label-secondary);
  font-size: var(--text-sm);
  text-align: center;
}
.feed-error {
  color: var(--accent-red);
}

/* Real images in cards — absolutely positioned so they fill the media
   container without competing with overlay siblings (e.g. bookmark site
   badge) in the flex layout. The parent's overflow:hidden + border-radius
   handles corner clipping. */
.feed-card-img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.feed-card-media-text {
  aspect-ratio: 1;
  overflow: hidden;
  width: 100%;
  padding: 0;
}

/* ---------- Badge pill ---------- */

.feed-badge-pill {
  position: absolute;
  top: var(--sp-sm);
  left: var(--sp-sm);
  z-index: 2;
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1.4;
  white-space: nowrap;
  max-width: 240px;
  overflow: hidden;
  text-overflow: ellipsis;
  background: var(--bg-70);
  color: var(--label-primary);
  /* border: 1px solid var(--border-primary); */
  width: fit-content;
  backdrop-filter: blur(12px);
}

.feed-card-calendar .feed-badge-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3xs);
}

.feed-card-calendar .feed-badge-pill::before {
  content: '';
  display: inline-block;
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  background-color: currentColor;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 xmlns=%22http://www.w3.org/2000/svg%22%3E %3Cg clip-path=%22url(%23clip0_17956_8822)%22%3E %3Cpath d=%22M8.94719 14.0525C8.94751 10.1868 12.081 7.05296 15.9467 7.05276C19.8124 7.05282 22.9459 10.1866 22.9464 14.0522C22.9461 17.918 19.8119 21.0525 15.9461 21.0525C12.0806 21.052 8.94744 17.918 8.94719 14.0525ZM13.267 9.41761C11.9629 10.303 11.1607 11.6717 10.9324 13.1377L10.7741 12.8935C10.5787 12.5917 10.1817 12.5092 9.88702 12.7092C9.59259 12.9094 9.51248 13.3158 9.7076 13.6177L10.9007 15.4642C11.0961 15.7657 11.4934 15.847 11.788 15.6471L13.5891 14.4244C13.8837 14.2242 13.9646 13.8173 13.7693 13.5154C13.5739 13.2139 13.1768 13.1313 12.8822 13.3311L12.1495 13.8272C12.2035 12.532 12.8405 11.2816 13.9744 10.5117C15.8561 9.2345 18.3932 9.76175 19.64 11.6895C20.8862 13.6174 20.3705 16.2158 18.4891 17.4931C17.1863 18.3773 15.569 18.3973 14.282 17.6836C13.9712 17.5114 13.583 17.6298 13.4145 17.9479C13.2463 18.2664 13.3615 18.6648 13.6723 18.8373C15.3588 19.7727 17.4837 19.7496 19.1964 18.5871C21.6675 16.9096 22.3443 13.4966 20.7073 10.9648C19.0698 8.43296 15.7382 7.74014 13.267 9.41761ZM4.04413 3.97441C3.97353 3.56673 4.24666 3.1788 4.65408 3.1075C5.0622 3.03673 5.45075 3.31015 5.52152 3.71827L5.68865 4.67945L11.0449 3.74977L10.8786 2.78805C10.808 2.38009 11.0814 1.99142 11.4894 1.92061C11.897 1.85032 12.2857 2.1239 12.3568 2.53138L12.5228 3.49445L13.4054 3.34309C14.7569 3.10909 15.9553 4.10536 16.1831 5.41884C16.2537 5.82678 15.9789 6.21513 15.571 6.28601C15.1633 6.35622 14.7752 6.08241 14.7044 5.6747C14.5978 5.06188 14.0845 4.74664 13.6602 4.82021L3.61189 6.5628C3.18756 6.63644 2.81138 7.10668 2.9177 7.71982L3.16715 9.16415L8.42199 8.2759C8.82999 8.20714 9.21678 8.48097 9.28618 8.88883C9.35514 9.29698 9.08047 9.6842 8.67244 9.75355L3.41029 10.6431L4.42524 16.4958C4.50337 16.9448 4.94649 17.2691 5.43981 17.1837L7.85129 16.7655C8.25931 16.6948 8.64792 16.9682 8.71873 17.3762C8.78909 17.7839 8.51635 18.1721 8.10878 18.2431L5.69595 18.6611C4.41368 18.8831 3.172 18.0443 2.94784 16.752L1.71726 9.65628C1.70941 9.61091 1.70563 9.56528 1.706 9.52082L1.43841 7.97486C1.21116 6.6617 2.00349 5.31987 3.3544 5.08512L4.21044 4.93612L4.04413 3.97441Z%22 fill=%22black%22/%3E %3C/g%3E %3Cdefs%3E %3CclipPath id=%22clip0_17956_8822%22%3E %3Crect width=%2224%22 height=%2224%22 fill=%22white%22/%3E %3C/clipPath%3E %3C/defs%3E %3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 xmlns=%22http://www.w3.org/2000/svg%22%3E %3Cg clip-path=%22url(%23clip0_17956_8822)%22%3E %3Cpath d=%22M8.94719 14.0525C8.94751 10.1868 12.081 7.05296 15.9467 7.05276C19.8124 7.05282 22.9459 10.1866 22.9464 14.0522C22.9461 17.918 19.8119 21.0525 15.9461 21.0525C12.0806 21.052 8.94744 17.918 8.94719 14.0525ZM13.267 9.41761C11.9629 10.303 11.1607 11.6717 10.9324 13.1377L10.7741 12.8935C10.5787 12.5917 10.1817 12.5092 9.88702 12.7092C9.59259 12.9094 9.51248 13.3158 9.7076 13.6177L10.9007 15.4642C11.0961 15.7657 11.4934 15.847 11.788 15.6471L13.5891 14.4244C13.8837 14.2242 13.9646 13.8173 13.7693 13.5154C13.5739 13.2139 13.1768 13.1313 12.8822 13.3311L12.1495 13.8272C12.2035 12.532 12.8405 11.2816 13.9744 10.5117C15.8561 9.2345 18.3932 9.76175 19.64 11.6895C20.8862 13.6174 20.3705 16.2158 18.4891 17.4931C17.1863 18.3773 15.569 18.3973 14.282 17.6836C13.9712 17.5114 13.583 17.6298 13.4145 17.9479C13.2463 18.2664 13.3615 18.6648 13.6723 18.8373C15.3588 19.7727 17.4837 19.7496 19.1964 18.5871C21.6675 16.9096 22.3443 13.4966 20.7073 10.9648C19.0698 8.43296 15.7382 7.74014 13.267 9.41761ZM4.04413 3.97441C3.97353 3.56673 4.24666 3.1788 4.65408 3.1075C5.0622 3.03673 5.45075 3.31015 5.52152 3.71827L5.68865 4.67945L11.0449 3.74977L10.8786 2.78805C10.808 2.38009 11.0814 1.99142 11.4894 1.92061C11.897 1.85032 12.2857 2.1239 12.3568 2.53138L12.5228 3.49445L13.4054 3.34309C14.7569 3.10909 15.9553 4.10536 16.1831 5.41884C16.2537 5.82678 15.9789 6.21513 15.571 6.28601C15.1633 6.35622 14.7752 6.08241 14.7044 5.6747C14.5978 5.06188 14.0845 4.74664 13.6602 4.82021L3.61189 6.5628C3.18756 6.63644 2.81138 7.10668 2.9177 7.71982L3.16715 9.16415L8.42199 8.2759C8.82999 8.20714 9.21678 8.48097 9.28618 8.88883C9.35514 9.29698 9.08047 9.6842 8.67244 9.75355L3.41029 10.6431L4.42524 16.4958C4.50337 16.9448 4.94649 17.2691 5.43981 17.1837L7.85129 16.7655C8.25931 16.6948 8.64792 16.9682 8.71873 17.3762C8.78909 17.7839 8.51635 18.1721 8.10878 18.2431L5.69595 18.6611C4.41368 18.8831 3.172 18.0443 2.94784 16.752L1.71726 9.65628C1.70941 9.61091 1.70563 9.56528 1.706 9.52082L1.43841 7.97486C1.21116 6.6617 2.00349 5.31987 3.3544 5.08512L4.21044 4.93612L4.04413 3.97441Z%22 fill=%22black%22/%3E %3C/g%3E %3Cdefs%3E %3CclipPath id=%22clip0_17956_8822%22%3E %3Crect width=%2224%22 height=%2224%22 fill=%22white%22/%3E %3C/clipPath%3E %3C/defs%3E %3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-position: center;
          mask-position: center;
}

/* ---------- Mobile header (hidden on desktop) ---------- */

/* ── Mid-desktop (769px – 1440px) ────────────────────────────────────── */

@media (min-width: 835px) and (max-width: 1440px) {
  .feed {
    width: 68vw;
    max-width: 68vw;
    padding-left: var(--sp-xs);
    padding-right: var(--sp-xs);
  }
}

/* ── Mobile responsive (≤ 768px) ─────────────────────────────────────── */

/* ─── Media queries ──────────────────────────────────────────────────── */

@media (max-width: 834px) {
  /* The 16px gutter is inside the scroller (round 3a), so a swiped card
     slides to the screen edge instead of under a clip 16px in. */
  .feed {
    width: 100%;
    max-width: 100vw;
    padding: var(--sp-xl) 0 var(--sp-sm);
    overflow: hidden;
  }

  .feed-items-wrap,
  .feed > .feed-empty,
  .feed > .error-state {
    padding-left: var(--sp-md);
    padding-right: var(--sp-md);
  }

  /* iOS ListFeed: 24px headers, header → first card 12, card → card 4, a
     section's last card → next header 32. */
  .feed-items-wrap {
    --feed-wrap-gap: var(--sp-xxl);
  }

  .feed-section .section-header__title {
    font-size: 24px;
    font-weight: 700;
    line-height: 33.6px;
  }

  .feed-section .section-body__content {
    padding-top: var(--sp-sm);
  }

  .feed-items-wrap .tl-list {
    gap: var(--sp-3xs);
  }

  /* A fold clips its rows vertically only: a swiped card's sideways
     travel isn't cut at the section's edge. */
  .feed-section .section-body__clip,
  .search-browse .section-body__clip {
    overflow-x: visible;
    overflow-y: clip;
  }

  /* Tighter gap on mobile */
  .feed-grid {
    gap: var(--sp-md);
  }

  .feed-col {
    gap: var(--sp-md);
  }

  /* Reset 4-col container override on mobile */
  .feed--cols-4 {
    width: 100%;
    max-width: 100vw;
  }

  .feed-card-title {
    font-size: 13px;
  }

  /* .feed-header-notifications sizing/color/radius/hover come from
     .header-icon-btn — only the badge-dot anchor is kept here. */
  .feed-header-notifications {
    position: relative;
  }

  .feed-header-notifications-dot {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 8px;
    height: 8px;
    border-radius: var(--radius-full);
    background: var(--accent-red);
    border: 2px solid var(--bg-body);
    box-sizing: content-box;
    pointer-events: none;
  }
}


/* Shared by the reminders list and search — see ui/timeline_list. */
/* Timeline reminder rows (iOS TimelineListRow parity).

   Lifted out of feed.css: the reminders list and search now render the same
   rows through ui/timeline_list, so this is a shared component's stylesheet,
   not a feed-specific one. Same move .due-pill made when it stopped being
   feed-only. */


/* The Timeline tab swaps the masonry grid for a vertical list of reminder
   rows on the iOS canvas color. Only the Timeline tab gets this class —
   Saved/ForYou/Following/search/profile keep the grid. */
.feed-items-wrap--timeline {
  align-items: center;
}

.tl-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm); /* 12px between rows — distinct from the 16px
                         (--sp-md) title→content gap on .feed-section */
}

/* The toDo card (iOS TimelineListRow, 2026-10-14→22 card spec): 16px
   padding around a 27px header row and, 6px under it, a 22px second row —
   the card hugs them: 16 + 27 + 6 + 22 + 16 = 87. Both rows are fixed
   heights, so every card is 87 and every title in a list sits on the same
   line. The 370px min-width holds in the list pane, which is sized to fit
   it (see the .sidebar rule below); elsewhere — a phone, the search pane
   at a narrow window — the card takes its column's width rather than
   scroll sideways. */
.tl-row {
  position: relative;
  display: flex;
  align-items: flex-start;
  box-sizing: border-box;
  padding: 16px;
  /* iOS TimelineListRow: 24px continuous-corner card. */
  border-radius: 24px;
  background: var(--bg-primary);
  border: none;
  box-shadow: none;
  transition: background-color 150ms ease;
}

.sidebar .tl-row {
  min-width: 370px;
}

/* Whole-card affordance: animated background tint on hover. (Solid color
   → solid color interpolates fine through the tokens — unlike gradients,
   which is why the Done button needed the opacity-overlay trick.) */
@media (hover: hover) {
  .tl-row:hover {
    background: var(--fill-elevated-secondary);
  }
}

/* Never underline the card's nested anchors on hover (overrides the
   global `a:hover` underline from base.css). */
.tl-row a {
  text-decoration: none;
}
@media (hover: hover) {
  .tl-row a:hover {
  text-decoration: none;
}
}

/* Stretched link: the content anchor's ::after covers the whole card, so
   any tap that isn't on a nested action navigates to the reminder's
   detail view. Nested actions (completion circle) sit above it. */
.tl-row-content::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 24px;
}


/* ── Processing border: the comet (iOS `AnimatedLoadingComet`) ─────────
   A rotating CONIC gradient stroking the card's outline: two soft pastel
   lobes (90° each, centred at 90° and 270°) that fade in through
   transparent shoulders, sweep the perimeter and compress through the
   corners — constant angular velocity, the projection supplies the
   easing. One revolution = 3s. Two rings painted by the same wheel at the
   same angle: a 1.4px core and, behind it, a wider stroke blurred 15px at
   0.4 — the halo. Both are overlay pseudo-elements masked to a ring, so
   the card fill stays var(--bg-primary). Reduce-motion (and browsers
   without `@property`) get the static 1.5px pastel ring instead. */
@property --tl-comet-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.tl-row--processing::before,
.tl-row--processing::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 24px;
  pointer-events: none;
  background: var(--gradient-pastel);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
          mask-composite: exclude;
}

/* Static fallback: the core ring alone, 1.5px, no halo. */
.tl-row--processing::before {
  padding: 1.5px;
}
.tl-row--processing::after {
  display: none;
}

@supports (background: conic-gradient(from var(--tl-comet-angle), #000, #fff)) {
  @media (prefers-reduced-motion: no-preference) {
    .tl-row--processing::before,
    .tl-row--processing::after {
      /* Per lobe: transparent shoulder → 45% ramp-in → full core → 45%
         ramp-out → transparent shoulder, at the iOS stop fractions
         (centre ± half, ± half·0.55). Lobe 1 centred at 90°, lobe 2 at
         270°; the gaps between them are the reveal. */
      background: conic-gradient(
        from var(--tl-comet-angle),
        transparent 45deg,
        color-mix(in srgb, var(--pastel-1) 45%, transparent) 65.25deg,
        var(--pastel-2) 90deg,
        color-mix(in srgb, var(--pastel-3) 45%, transparent) 114.75deg,
        transparent 135deg,
        transparent 225deg,
        color-mix(in srgb, var(--pastel-1) 45%, transparent) 245.25deg,
        var(--pastel-2) 270deg,
        color-mix(in srgb, var(--pastel-3) 45%, transparent) 294.75deg,
        transparent 315deg
      );
      animation: tl-comet-spin 3s linear infinite;
    }
    .tl-row--processing::before {
      padding: 1.4px;
      filter: blur(0.5px);
    }
    .tl-row--processing::after {
      display: block;
      padding: 2.4px;
      filter: blur(15px);
      opacity: 0.4;
    }
  }
}

@keyframes tl-comet-spin {
  to {
    --tl-comet-angle: 360deg;
  }
}

/* ── Unread-insight halo debut (iOS `BreathingGradientGlow` + debut) ────
   The brand pastel plane spread 4px past the card and blurred 20px,
   BEHIND the opaque card fill (a sibling under it), so only the bleed
   past the corners shows. It debuts at the crest and breathes toward the
   trough over 3.6s while its hue cycles (14s, never restarted), then
   fades out over 0.4s ease-out; the wrapper is unmounted by the tracker
   once the window elapses. */
/* Every row sits in one stable wrapper (round 3a, 2026-10-24): the keyed
   .tl-list child, exactly the card's box (no padding or margin). The glow
   plane and the swipe strips sit behind the card in it. */
.tl-row-wrap {
  position: relative;
}
.tl-row-glow {
  position: absolute;
  inset: -4px;
  z-index: 0;
  border-radius: 28px;
  background: var(--gradient-pastel-horizontal);
  filter: blur(20px);
  pointer-events: none;
  opacity: 0;
  /* Out of layout at rest: its 4px bleed past the card otherwise widened
     a sideways-scrollable list (desktop Search, final review). */
  display: none;
}
.tl-row-glow--debut {
  display: block;
  opacity: 0.62;
  animation:
    tl-glow-breathe 3.6s ease-in-out 1 forwards,
    tl-glow-hue 14s linear infinite,
    tl-glow-fade 0.4s ease-out 3.6s 1 forwards;
}
.tl-row-wrap > .tl-row {
  position: relative;
  z-index: 1;
}
@keyframes tl-glow-breathe {
  from {
    opacity: 0.62;
  }
  to {
    opacity: 0.38;
  }
}
@keyframes tl-glow-hue {
  to {
    filter: blur(20px) hue-rotate(360deg);
  }
}
@keyframes tl-glow-fade {
  to {
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .tl-row-glow--debut {
    animation: tl-glow-fade 0.4s ease-out 3.6s 1 forwards;
  }
}

/* Completion circle: hollow ring (ui-borders) unchecked, filled check
   (text-secondary) when done. Rendered as a <button> when tappable
   (reminder present) or a <span> otherwise. */
/* Layout-only overrides for the shared completion circle
   (ui/completion_circle → .nr-completion-circle states) inside a
   timeline row: keep the unchecked ring in the row's border color. The
   header row centres it on the title line. */
.tl-row-check.nr-completion-circle {
  flex-shrink: 0;
  /* Reset .nr-completion-circle's 6px heading margin (new_reminder.css):
     left in, the header grew to 30px and the circle sat 3px below the
     title's centre — measured, 2026-09-24 review. */
  margin-top: 0;
  border-color: var(--ui-borders);
  /* Above the stretched card link so the toggle stays tappable. */
  position: relative;
  z-index: 1;
}
.tl-row-check.nr-completion-circle--done {
  border-color: transparent;
}
/* On the grey hover fill the ring's --ui-borders grey all but matched the
   card — 1.02:1 in dark mode, the ring vanished — so the ring steps up to
   label secondary there, the same step the circle takes when hovered
   directly (Julian, 2026-09-24). A selected card stays white under the
   pointer (2026-09-25), so it keeps the resting ring. */
@media (hover: hover) {
  .tl-row:not(.tl-row--selected):hover .tl-row-check.nr-completion-circle:not(.nr-completion-circle--done):not(.tl-row-check--tint) {
    border-color: var(--label-secondary);
  }
}

/* A completed card's glyph rests at label-secondary, as on iOS (round 3a,
   Q4) — card circles only; the detail heading and New Reminder keep
   label-tertiary. */
.tl-row-check.nr-completion-circle--done {
  color: var(--label-secondary);
}

/* The accent after the viewer's own tick (iOS CompletionCelebration): the
   done glyph — or, un-ticked inside the 3 s, the empty ring — wears
   --accent-blue at once, then eases back over 0.45 s when the class goes.
   Only the colour eases: the ring snaps back so a resting read never lands
   mid-transition. */
.tl-row-check.nr-completion-circle {
  transition: color 450ms ease;
}
.tl-row-check.nr-completion-circle.tl-row-check--tint {
  color: var(--accent-blue);
  transition: none;
}
.tl-row-check.tl-row-check--tint:not(.nr-completion-circle--done) {
  border-color: var(--accent-blue);
}

/* The press (iOS: 1.2 on spring 0.3 / 0.6, approximated): it grows while
   held and never once a drag has claimed the touch (card_swipe sets
   data-pressing="cancelled"). Phones and mice alike; not under reduced
   motion. */
@media (prefers-reduced-motion: no-preference) {
  .tl-row-check.nr-completion-circle {
    transition:
      color 450ms ease,
      transform 300ms cubic-bezier(0.34, 1.4, 0.64, 1);
  }
  .tl-row-check.nr-completion-circle.tl-row-check--tint {
    transition: transform 300ms cubic-bezier(0.34, 1.4, 0.64, 1);
  }
  .tl-row-check:active:not([data-pressing="cancelled"]) {
    transform: scale(1.2);
  }
}

/* The swipe strips (iOS SwipeRevealContainer, round 3a): the action
   circles behind the card, on the side the card moves away from. 56px
   circles with 28px glyphs, 24px apart, centred on the 87px card; at full
   open the inner circle sits 16px from the card's moved edge and the outer
   one lines up with the card's resting edge. At rest the visuals are
   transparent but the buttons keep their size (screen readers reach
   them). Phones only: a mouse uses the popover. */
.tl-swipe-strip {
  display: none;
}
@media (max-width: 834px) {
  .tl-swipe-strip {
    position: absolute;
    top: 0;
    bottom: 0;
    z-index: 0;
    display: flex;
    align-items: center;
    gap: 24px;
  }
  .tl-swipe-strip--leading {
    left: 0;
  }
  .tl-swipe-strip--trailing {
    right: 0;
  }
  .tl-swipe-action {
    width: 56px;
    height: 56px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: none;
    background: none;
    cursor: pointer;
  }
  .tl-swipe-visual {
    width: 56px;
    height: 56px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    opacity: var(--grow-o, 0);
    transform: scale(var(--grow-s, 0));
  }
  .tl-swipe-action--pin .tl-swipe-visual {
    color: var(--swipe-pin);
    background: var(--swipe-pin-wash);
  }
  .tl-swipe-action--delete .tl-swipe-visual,
  .tl-swipe-action--leave .tl-swipe-visual {
    color: var(--accent-red);
    background: var(--accent-red-elevated);
  }
  .tl-swipe-action--share .tl-swipe-visual {
    color: var(--swipe-share);
    background: var(--swipe-share-wash);
  }
  /* iOS's default button style: the glyph dims while held; the wash and
     the size don't change. */
  .tl-swipe-action:active .tl-swipe-visual > .icon {
    opacity: 0.5;
  }
  /* The card is the swipe surface: no long-press link preview or link
     drag on phones (desktop keeps drag-to-tab). */
  .tl-row-content {
    -webkit-touch-callout: none;
    -webkit-user-drag: none;
  }
  .feed-items-wrap .tl-row-wrap,
  .search .tl-row-wrap {
    touch-action: pan-y;
  }
  /* A hold before a slow swipe started a text selection on the title in
     Mobile Safari; iOS cards select nothing. */
  .feed-items-wrap .tl-row-wrap > .tl-row,
  .search .tl-row-wrap > .tl-row {
    -webkit-user-select: none;
    user-select: none;
  }
  /* ffi/card_swipe.mjs moves the card: `--swipe-x` while a row is away
     from rest (`data-swipe`), written per frame — no transition here. */
  .tl-row-wrap[data-swipe] > .tl-row {
    transform: translateX(var(--swipe-x, 0px));
  }
}

/* A Search result sliding off (round 3a): its snapshot moves in a fixed
   layer (ffi/card_motion.mjs) while the row itself keeps its place,
   unseen, until the slide ends and the row is dropped. */
.tl-row-wrap[data-departing] {
  visibility: hidden;
}

/* A Search row the viewer can't edit draws no circle (iOS
   SearchListPresentation): the second row and footer lose the circle's
   indent. */
.tl-row--no-circle .tl-row-second,
.tl-row--no-circle .tl-row-footer {
  padding-left: 0;
}

/* The card's two rows, 6px apart (iOS `rowGap`). */
.tl-row-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* A footer under the second row (the chat's collab strip): 8 below it —
   the rows' 6 plus 2 — on the title's column, inert like the second row
   so the card's link takes the tap. */
.tl-row-footer {
  display: flex;
  margin-top: 2px;
  padding-left: 32px;
  pointer-events: none;
}

/* Header row: the 24px circle, the title, the media at its end — 8px
   apart, centred on the title's 27px line box (26.6, rounded). The 32px
   media overhangs it 2.5px each way, into the padding and the gap. */
.tl-row-header {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  height: 27px;
  min-width: 0;
}

/* The title's link — its ::after stretches over the whole card. */
.tl-row-content {
  flex: 1;
  min-width: 0;
  display: block;
  text-decoration: none;
  color: inherit;
}

/* iOS `Title - Card` (2026-10-15): 19 / 600 / 26.6, −0.5% tracking,
   single line, tail truncation. */
.tl-row-title {
  margin: 0;
  font-size: var(--text-title-card);
  font-weight: 600;
  line-height: 140%;
  letter-spacing: -0.005em;
  color: var(--label-primary);
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tl-row-title--done {
  text-decoration: line-through;
}

/* Titleless capture (iOS `NewCapturePhase`): the dated media label wears
   the agent shimmer while a run is expected or live, in the title's own
   type; plain secondary text once the server has given up. */
.tl-row .tl-row-title--placeholder .agent-status-line-text {
  font-size: inherit;
  font-weight: inherit;
  line-height: inherit;
  letter-spacing: inherit;
}
.tl-row-title--untitled {
  color: var(--label-secondary);
}

/* ── Second row (iOS TimelineListRow.secondRow, 2026-10-14) ───────────
   22px tall, indented 32px to the title's text (the circle's 24 + 8),
   items left-aligned 24px apart: the alert (bare bell + due), the AI
   glyph, the paper plane — 18px glyphs, a count 4px after its glyph, all
   in footnote (13/18) and label secondary. The alert gives way first; a
   count never truncates. Inert: the card's link is the stretched ::after
   underneath, and the glyphs' masks (own stacking contexts) must not
   swallow its clicks. */
.tl-row-second {
  display: flex;
  align-items: center;
  gap: var(--sp-xl);
  height: 22px;
  min-width: 0;
  padding-left: 32px;
  overflow: hidden;
  pointer-events: none;
  color: var(--label-secondary);
  font-size: var(--text-sm);
  font-weight: 400;
  line-height: 18px;
}

.tl-row-item {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  gap: var(--sp-3xs);
  min-width: 0;
}

.tl-row-item--alert {
  flex-shrink: 1;
}

.tl-row-item-glyph {
  display: inline-flex;
  flex-shrink: 0;
}

.tl-row-item-label,
.tl-row-cal-meta {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.tl-row-item-count {
  color: var(--action-count);
}

/* iOS `ShimmerBar`: a faint track with a brighter band sweeping across
   one way (1.2s linear, no autoreverse). */
.tl-shimmer-bar {
  position: relative;
  overflow: hidden;
  height: 10px;
  border-radius: 5px;
  background: var(--fill-elevated-tertiary);
}
.tl-shimmer-bar::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    color-mix(in srgb, var(--label-primary) 14%, transparent) 50%,
    transparent 100%
  );
  transform: translateX(-100%);
  animation: tl-shimmer-sweep 1.2s linear infinite;
}
@keyframes tl-shimmer-sweep {
  to {
    transform: translateX(100%);
  }
}
@media (prefers-reduced-motion: reduce) {
  .tl-shimmer-bar::after {
    animation: none;
    transform: none;
    opacity: 0.5;
  }
}

/* The media at the title's end (iOS TimelineThumbnailDeck, the
   2026-10-14 card spec): a 32×32 slot 8px after the title (the header's
   gap), with 6px of slack each side for the tilt (iOS
   `horizontalOverhang`). Each card in it is 32×32, cover, radius 8/42 of
   its side, with a soft shadow. The front card tilts +8°; a fanned deck's
   two back cards sit behind it at 0° and −8°, at 90%. */
.tl-row-thumb {
  position: relative;
  flex-shrink: 0;
  display: block;
  width: 32px;
  height: 32px;
  margin: 0 6px;
}
.tl-thumb-card {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: calc(32px * 8 / 42);
  box-shadow: 0 1.5px 3px rgba(0, 0, 0, 0.18);
}
.tl-thumb-card--0 {
  transform: rotate(8deg);
}
.tl-thumb-card--1 {
  opacity: 0.9;
}
.tl-thumb-card--2 {
  transform: rotate(-8deg);
  opacity: 0.9;
}
/* "+N" on the front card's corner: white on a 60% black capsule, over
   the photo in either theme (iOS TimelineThumbnailDeck.badge). */
.tl-thumb-badge {
  position: absolute;
  right: 3px;
  bottom: 3px;
  padding: 1.5px 5px;
  border-radius: var(--radius-full);
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0;
}
.tl-thumb-img,
.tl-thumb-media {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Calendar date tile (calendar items without artwork) — iOS
   CalendarDateBadge: a white tile with a 1px border, the short weekday in
   bold red (side × 0.22 ≈ 7px at 32) over the day number (side × 0.52 ≈
   16.6px), tucked 1.5px. */
.tl-thumb-calendar {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  border: 1px solid var(--ui-borders);
  border-radius: inherit;
  background: var(--bg-primary);
}
.tl-thumb-cal-weekday {
  font-size: 7px;
  font-weight: 700;
  line-height: 8.5px;
  color: var(--accent-red);
}
.tl-thumb-cal-day {
  margin-top: -1.5px;
  font-size: 16.6px;
  font-weight: 400;
  line-height: 20px;
  color: var(--label-primary);
}

@media (max-width: 834px) {
  .feed-section {
    width: 100%;
  }
  .tl-list {
    width: 100%;
  }
}

/* ── The list pane's list ───────────────────────────────────────────────
   Same card as everywhere (2026-09-24 — the denser 10px / 15px-title
   sidebar variant is gone); only the gap between cards differs. */
.tl-list--compact {
  /* 4px between cards (Julian, 2026-10-23; iOS interGroupSpacing). */
  gap: var(--sp-3xs);
}

/* Selected: the open detail route's card (the mock, 2026-09-25). The card
   keeps its white fill and wears a 1px accent ring 3px outside its edge,
   following its corners — under the pointer too, where an unselected card
   takes the grey hover fill. Measured in mock3/mock4's selected "Today"
   card: 1.1px of ink, clear of the card by 3.2 top, 2.6 bottom, 3.8 left,
   5.1 right — the mock's ring box is wider than the card (about 4.5 each
   side against 2.9 top and bottom), not just shifted. An outline takes one
   offset on every side, so the vertical 3px sets it: the side gaps run
   about 1.5px tighter than the mock's. The grey fill was the 2026-09-24
   "for now" choice.

   An outline, so nothing moves and it draws outside the card: its corners
   follow the card's 24px radius out by the offset, concentric (27.5 at the
   stroke's centre; the mock's corner is tighter, ~24), and it paints
   over the insight glow (a sibling behind the card) and around the
   processing comet (pseudo-elements inside the card's edge) without
   touching either. The list pane's 16px inset leaves it room inside
   .sidebar-scroll's clip. Cards sit 4px apart (.tl-list--compact), so
   the ring's outer edge meets a neighbour's edge without overlapping it. */
.tl-row--selected,
.tl-row--selected:hover {
  background: var(--bg-primary);
  outline: 1px solid var(--accent-blue);
  outline-offset: 3px;
}

/* An always-shown day with nothing in it: "Nothing due today" (iOS
   TimelineListPresentation's empty Today/Tomorrow rows). */
/* "Nothing here" — a home list with nothing open (iOS `EmptyBucketRow`):
   footnote 13/18, label secondary, 12 / 16 insets. */
.tl-empty-day {
  margin: 0;
  padding: var(--sp-sm) var(--sp-md);
  color: var(--label-secondary);
  font-size: var(--text-sm);
  line-height: 18px;
}

/* The foldable section header (ui/section_header, iOS
   CollapsibleSectionHeader): the title left at 22px bold (Julian's spec,
   2026-10-23), a 24px label-secondary chevron right — up open, down
   folded — and the whole row one button. The gaps around it belong to the
   host list (sidebar.css, search.css). */

.section-header {
  margin: 0;
  font: inherit;
}

.section-header__button {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-sm);
  width: 100%;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  color: var(--label-primary);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.section-header__title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  font-size: var(--text-2xl);
  font-weight: 700;
  line-height: 140%;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.section-header__chevron {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--label-secondary);
}

/* The rows under a header (section_header.body): folded, the grid row
   eases from 1fr to 0fr and the rows slide up under the header; unfolded,
   back down (iOS ListFeedFoldLayout). The rows stay mounted, `inert`. */
.section-body {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--motion-slide) var(--ease-slide);
}

.section-body--folded {
  grid-template-rows: 0fr;
}

/* Clips the sliding rows. 4px of slack at the sides and below, given back
   by negative margins, so a selected card's ring (1px, 3px outside the
   card) isn't cut while the layout stays exact — folded, the clip nets to
   zero height. */
.section-body__clip {
  min-height: 0;
  overflow: hidden;
  margin: 0 -4px -4px;
  padding: 0 4px 4px;
}

/* Header → first card, 8px — inside the clip, so a folded section is its
   header alone. */
.section-body__content {
  padding-top: var(--sp-xs);
}

@media (prefers-reduced-motion: reduce) {
  .section-body {
    transition: none;
  }
}


/* The member pile on shared reminders — see ui/shared_badge. */
/* The member pile on a shared reminder — see ui/shared_badge (iOS
   SharedReminderBadgeView parity). Avatars stack at the MIDPOINT (each
   face's edge lands on the centre of the one under it), trailing on top,
   each wearing a host-coloured halo OUTSIDE its edge so the pile punches
   out of whatever it sits on. The halo colour is inherited from the host
   via --pile-ring (defaults to the surface background). */

.shared-pile {
  display: inline-flex;
  align-items: center;
  padding: 0;
  margin: 0;
  border: none;
  background: transparent;
  color: inherit;
  --pile-ring: var(--bg-primary);
}

.shared-pile--button {
  cursor: pointer;
  border-radius: var(--radius-full);
}
.shared-pile--button:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

.shared-pile-avatar {
  display: inline-flex;
  border-radius: 50%;
  box-shadow: 0 0 0 1px var(--pile-ring);
  flex-shrink: 0;
}
.shared-pile-avatar + .shared-pile-avatar {
  /* Midpoint stack: overlap by half the diameter. The inline width is
     set by the view; a negative margin of half of it is the overlap. */
  margin-left: calc(var(--pile-overlap, 13px) * -1);
}

/* Invited: dimmed, still in colour. Departed: desaturated and dimmed. */
.shared-pile-avatar--invited {
  opacity: 0.7;
}
.shared-pile-avatar--departed {
  opacity: 0.7;
  filter: saturate(0);
}

/* On a toDo card (2026-09-24 spec): the stack "cuts through" to the card.
   Not a ring in the card's colour — a hovered or selected card is the
   translucent elevated fill, and a ring in that colour stacks a second
   tint over the face beneath. Instead each face under another is masked
   away where the next one lands (its centre sits at this face's right
   edge — the midpoint stack), 1px wider than the face, so whatever the
   card paints shows through in every state and theme. */
.tl-row .shared-pile {
  --pile-ring: transparent;
}
.tl-row .shared-pile-avatar:not(:last-child) {
  -webkit-mask: radial-gradient(circle at 100% 50%, transparent 14px, #000 14.5px);
          mask: radial-gradient(circle at 100% 50%, transparent 14px, #000 14.5px);
}

/* The Collaborators sheet — see app/detail/view_collab. */
/* The Collaborators sheet — see app/detail/view_collab (iOS
   CollaboratorsSheet parity): one list, three states; the avatar's
   dressing (ui/shared_badge presence) and the row's own subtitle carry
   the state, not headers. The sheet, its header and its rows are the
   shared chrome (ui/sheet, sheet.css); what is here is the rows' own
   content — the pills, the departed note — and the invite banner. */

/* A row whose mutation is out: dimmed, and not for tapping again. */
.collab-row--busy {
  opacity: 0.5;
  pointer-events: none;
}

/* The link row's two pills, side by side in the row's trailing slot. */
.collab-link-pills {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
}

/* The invite banner's text column (the sheet's rows use the chrome's). */
.collab-row__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}
.collab-row__name {
  font-size: var(--text-md);
  color: var(--label-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Footnote 13/18 at label-secondary (2026-09-08 spec). */
.collab-row__sub {
  font-size: 13px;
  line-height: 18px;
  color: var(--label-secondary);
}

/* The worded pill, matching the chat menu's member row: height IS the
   contract (40) with 16 on the sides, body regular on the accent pair. */
.collab-pill {
  height: 40px;
  padding: 0 var(--sp-md);
  border-radius: var(--radius-full);
  border: none;
  background: var(--bg-tertiary);
  color: var(--label-primary);
  font: inherit;
  font-size: var(--text-md);
  cursor: pointer;
  flex-shrink: 0;
}
.collab-pill--danger {
  background: rgba(var(--color-red), 0.12);
  color: var(--accent-red);
}
.collab-pill:disabled {
  opacity: 0.5;
  cursor: default;
}

/* The departed note, a child in the sheet's 24 rhythm: footnote 13/18. */
.collab-sheet__note {
  margin: 0;
  font-size: 13px;
  line-height: 18px;
  color: var(--label-secondary);
}

/* The invitation banner at the top of an invited viewer's detail page. */
.collab-invite-banner {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-md);
  margin-bottom: var(--sp-md);
  border-radius: var(--radius-lg);
  background: var(--accent-blue-elevated);
  color: var(--accent-blue);
  flex-wrap: wrap;
}
.collab-invite-banner .collab-row__name {
  color: var(--label-primary);
}
.collab-pill--primary {
  background: var(--accent-blue);
  color: var(--bg-primary);
}
.collab-pill--muted {
  background: transparent;
  color: var(--label-secondary);
}

/* A shared toDo in chat (iOS CollabInviteTileCard, Julian 2026-10-23):
   the timeline's toDo card on its own — no bubble — wearing a hairline,
   since the chat canvas is the card's own fill. One tap surface: tapping
   joins or opens; the strip inside only names that. As wide as the list
   pane's card, or the column when that's narrower. */
.chat-att-collab-card {
  width: 375px;
  max-width: 100%;
  cursor: pointer;
}
.chat-att-collab-card--inert {
  cursor: default;
}
.chat-att-collab-card .tl-row {
  box-shadow: inset 0 0 0 1px var(--border-secondary);
}

/* The strip's words: footnote in label secondary; "Tap to join" / "Tap to
   try again" as iOS's ghost pill — 26 tall, 8 a side, background
   secondary. */
.chat-att-collab-footer {
  font-size: var(--text-sm);
  line-height: 18px;
  color: var(--label-secondary);
}
.chat-att-collab-footer--pill {
  display: inline-flex;
  align-items: center;
  height: 26px;
  padding: 0 var(--sp-xs);
  border-radius: 999px;
  background: var(--bg-secondary);
}

/* Agent working-status line — iOS AgentStatusShimmerText parity.

   A single line of muted text with a brighter band sweeping through it
   once per cycle (one-way marquee, never a bounce — a bounce reads as a
   broken loading bar; see the iOS component's grammar notes). Web port:
   the gradient is painted through background-clip:text and slid with
   background-position, which approximates the mask-band original within
   one declaration.

   Sizes: `.agent-status-line` is the chat variant (agentStatus token:
   18/400/25.2, −0.43px tracking — the only token that kept tracking);
   `--tile` is the timeline-row variant (metadata: 14/400/20, 1 line). */

.agent-status-line {
  display: flex;
  align-items: center;
  min-width: 0;
  padding: var(--sp-3xs) var(--sp-xs);
}

.agent-status-line-text {
  font-size: 18px;
  font-weight: 400;
  line-height: 140%;
  letter-spacing: -0.43px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: transparent;
  background-image: linear-gradient(
    90deg,
    var(--label-secondary) 0%,
    var(--label-secondary) 35%,
    var(--label-primary) 50%,
    var(--label-secondary) 65%,
    var(--label-secondary) 100%
  );
  background-size: 250% 100%;
  background-repeat: no-repeat;
  background-position: 100% 0;
  -webkit-background-clip: text;
  background-clip: text;
  /* Band travel: fully off one end to fully off the other (no reverse),
     1.5s linear with the iOS 0.35s lead-in; keyed re-mounts on text swap
     restart the fade below. */
  animation:
    agent-status-sweep 1.5s linear 0.35s infinite,
    agent-status-fade-in 0.25s ease-in-out;
}

@keyframes agent-status-sweep {
  from {
    background-position: 100% 0;
  }
  to {
    background-position: 0% 0;
  }
}

@keyframes agent-status-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* Reduce-motion: park at full opacity, no sweep (iOS parity). */
@media (prefers-reduced-motion: reduce) {
  .agent-status-line-text {
    animation: none;
    color: var(--label-secondary);
    background: none;
  }
}

/* AI chats on phones (2026-10-27): iOS's band DIMS the text — a 45% band
   that removes up to 70% of the alpha — where the shared rule brightens
   it. The same sweep, the band faded instead of lit. */
@media (max-width: 834px) {
  .chat--ai .agent-status-line-text {
    background-image: linear-gradient(
      90deg,
      var(--label-secondary) 0%,
      var(--label-secondary) 32%,
      color-mix(in srgb, var(--label-secondary) 30%, transparent) 50%,
      var(--label-secondary) 68%,
      var(--label-secondary) 100%
    );
  }
}

/* Agent suggestion pills — iOS ChatSuggestionPills parity.

   Not chips in a row: a vertical stack of full-width capsule buttons,
   16px gaps, accent-blue label on the accent-blue-elevated tonal fill
   (the tonal pair reads as an offered action rather than as another
   surface). A leading model-emitted emoji is split out with a controlled
   12px gap so stray whitespace can't leak into layout; the emoji keeps
   its own colors. Capsule (not fixed radius) so a two-line suggestion
   stays fully rounded. Accept-only — there is no dismiss. */

.suggestion-pills {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 100%;
  box-sizing: border-box;
  padding-top: var(--sp-sm);
}

/* AI threads: the message text is indented past the row's avatar column
   (28px .chat-message-avatar-slot + --sp-xs row gap); the pills render
   outside any row, so give them the same left inset — their outer edge
   lines up with the text's left edge. Right edges already coincide
   (ai-bare rows stretch to the container edge). */
.chat--ai .suggestion-pills {
  padding-left: calc(28px + var(--sp-xs));
}

.suggestion-pill {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 16px;
  border: none;
  border-radius: var(--radius-full);
  background: var(--accent-blue-elevated);
  color: var(--accent-blue);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: transform 120ms ease;
}

@media (hover: hover) {
  .suggestion-pill:hover:not(:disabled) {
    transform: scale(1.02);
  }
}

.suggestion-pill:active:not(:disabled) {
  transform: scale(1.04);
}

.suggestion-pill:disabled {
  opacity: 0.4;
  cursor: default;
}

/* Sized to the AI-chat message text (--text-md) so accepted suggestions
   don't read louder than the conversation they join. */
.suggestion-pill-emoji {
  flex-shrink: 0;
  font-size: var(--text-md);
  line-height: 140%;
}

.suggestion-pill-label {
  min-width: 0;
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 140%;
}

/* AI chats on phones (iOS ChatSuggestionPills, 2026-10-27): 16 under the
   newest reply, 4 apart, the body-semibold label 18/25.2 — a capsule on
   one line, 32 corners once it wraps (one radius does both) — and no
   left inset: the bare replies have none either. */
@media (max-width: 834px) {
  .chat--ai .suggestion-pills {
    padding-left: 0;
    padding-top: var(--sp-md);
    gap: var(--sp-3xs);
  }
  .chat--ai .suggestion-pill {
    border-radius: 32px;
  }
  .chat--ai .suggestion-pill-emoji,
  .chat--ai .suggestion-pill-label {
    font-size: 18px;
    line-height: 25.2px;
  }
}

/* The daily-brief card at the top of the Timeline — see ui/daily_brief_card. */
/* The daily brief's glow (ui/daily_brief_card.view_hero — the desktop
   hero, unused since 2026-09-24; home.css restyles it): the brand pastel
   spread and blurred BEHIND the card, a separate absolutely-positioned
   sibling under the opaque card fill, so only the bleed past the corners
   shows. Breathing at full emphasis while the brief is unread; halved and
   still once readAt lands. The timeline card's own rules left with it on
   2026-10-26 — the brief's live surface is the Home widget card
   (widget_card.css). */

.brief-card-glow {
  position: absolute;
  inset: -4px;
  border-radius: 20px;
  background: var(--gradient-pastel);
  filter: blur(20px);
  pointer-events: none;
}

/* Unread: full emphasis — the halo breathes around the static midpoint. */
.brief-card-glow--unread {
  opacity: 0.5;
  animation: brief-glow-breathe 3.2s ease-in-out infinite;
}

/* Read: halved, still. Seen state never changes the visuals. */
.brief-card-glow--read {
  opacity: 0.25;
}

@keyframes brief-glow-breathe {
  0%,
  100% {
    opacity: 0.38;
  }
  50% {
    opacity: 0.62;
  }
}

/* The Home widget space above Today on phones — see ui/widget_card. */
/* The Home widget space (ui/widget_card) — iOS AgentUpdateHeroCard's brief
   lead card and AgentUpdateEmptyTray, measured off Julian's 2026-10-26 mock
   at 393: the card 16 in from either edge, radius 24, 16 of padding, its
   pieces 8 apart; the band 91 tall at radius 24 with 16 of padding; the
   body two 16/21 lines; a 49 capsule button; the 13/18 stamp. Phone only
   by placement: the timeline page this sits on is CSS-hidden at ≥835px
   (.main-mobile-only), so nothing here needs a breakpoint. */

.widget-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  box-sizing: border-box;
  width: 100%;
  padding: var(--sp-md);
  border-radius: var(--radius-xl);
  background: var(--bg-primary);
  box-shadow: var(--shadow-widget);
  color: var(--label-primary);
}

.widget-card--brief {
  cursor: pointer;
}

/* Phone opt-outs (the rows' precedent, timeline_row.css): a held card
   selects nothing and shows no callout or tap wash. */
@media (max-width: 834px) {
  .widget-card {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
  }
}

/* "🌅 Your Daily Brief": Title - Card (19/600/140%, −0.5%), 8 in from the
   padding on the body text's column, the emoji 8 before the words. */
.widget-card__title {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  margin: 0;
  padding-left: var(--sp-xs);
  font-size: var(--text-title-card);
  font-weight: 600;
  line-height: 140%;
  letter-spacing: -0.005em;
  color: var(--label-primary);
  white-space: nowrap;
  overflow: clip;
  text-overflow: ellipsis;
}

.widget-card__emoji {
  flex-shrink: 0;
}

/* The body: callout 16/21, two lines tail-truncated, in 8 / 4 of its own
   padding like running text inside a bubble's chrome.

   Every clipped box here clips (`overflow: clip`), never hides: an
   `overflow: hidden` box is a scroll container, and Chromium latches a
   touch scroll that starts on one to it — a finger on the title, the
   band or these two lines then scrolled nothing (measured 2026-10-27). */
.widget-card__body {
  margin: 0;
  padding: var(--sp-3xs) var(--sp-xs);
  font: var(--type-callout);
  color: var(--label-primary);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: clip;
  overflow-wrap: anywhere;
}

/* "View your brief" / "Ask Please": a full-width gray capsule, body
   medium centred in 12 above and below (25.2 + 24 = 49.2), 8 at the ends;
   the quiet counterpart of the chat pills' accent capsule. */
.widget-card__button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xs);
  box-sizing: border-box;
  width: 100%;
  margin: 0;
  padding: var(--sp-sm) var(--sp-xs);
  border: none;
  border-radius: var(--radius-full);
  background: var(--bg-secondary);
  color: var(--label-primary);
  font: var(--type-body);
  font-weight: 500;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  overflow: clip;
  text-overflow: ellipsis;
  cursor: pointer;
  transition: background-color 150ms ease, transform 150ms ease;
}

.widget-card__button .icon {
  flex-shrink: 0;
}

@media (hover: hover) {
  .widget-card__button:hover {
    background: var(--fill-elevated-secondary);
  }
}

/* iOS .pressable(scale: 1.02). */
.widget-card__button:active {
  transform: scale(1.02);
}

/* The creation stamp, bottom-left on the body's column. */
.widget-card__stamp {
  margin: 0;
  padding: 0 var(--sp-xs);
  font: var(--type-footnote);
  color: var(--label-secondary);
}

/* ── The stack ─────────────────────────────────────────────────────────
   iOS AgentUpdateStackRow: the FRONT face in flow (the row is its height),
   the NEXT face behind it hung from the top edge — at rest 0.84 and
   invisible, growing and fading in with the front's travel — and the card
   coming back on top, off the left edge until a right drag. Everything
   the finger moves is a custom property on .widget-stack, written by
   ffi/update_stack.mjs (the faces' own style is Lustre's). */

.widget-stack {
  position: relative;
  width: 100%;
  /* The widget → Today header gap is 24 (the mock), not the sections'
     32: give 8 of the wrap's gap back. */
  margin-bottom: calc(var(--sp-xl) - var(--feed-wrap-gap, var(--sp-xl)));
}

.widget-face {
  width: 100%;
}

.widget-face--front {
  position: relative;
  z-index: 1;
  transform: translateX(var(--stack-x, 0px)) rotate(var(--stack-tilt, 0deg));
  transform-origin: center;
}

/* Covered by a card coming back: the front recedes to the back face's
   rest as the other arrives — the read's reveal in reverse. */
.widget-stack[data-covered] .widget-face--front {
  transform: scale(var(--stack-recede-scale, 1));
  transform-origin: top center;
  opacity: var(--stack-recede-opacity, 1);
}

.widget-face--back {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 0;
  transform: scale(var(--stack-back-scale, 0.84));
  transform-origin: top center;
  opacity: var(--stack-back-opacity, 0);
  pointer-events: none;
}

.widget-face--entering {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 2;
  transform: translateX(var(--stack-enter-x, -120%)) rotate(var(--stack-enter-tilt, 0deg));
  transform-origin: center;
  pointer-events: none;
}

/* A finger on the stack: no transition fights it. */
.widget-stack[data-dragging] .widget-face,
.widget-stack[data-phase] .widget-face {
  transition: none;
}

/* ── The band ──────────────────────────────────────────────────────────
   iOS BriefMastheadView: the sky under white copy centred on the band, a
   scrim over the sky's bottom edge. The static gradient is the token for
   the band's class (tokens.css --sky-*); the live sky (.sky, painted by
   ffi/brief_sky.mjs) sits over it, the scrim over that, the copy on top. */

.widget-band {
  --sky-h: 91px;
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  box-sizing: border-box;
  width: 100%;
  height: var(--sky-h);
  padding: var(--sp-md);
  border-radius: var(--radius-xl);
  overflow: clip;
  color: var(--sky-ink);
  background-color: var(--accent-blue);
  background-image: var(--sky-day);
}

.widget-band.sky--pre-dawn { background-image: var(--sky-pre-dawn); }
.widget-band.sky--dawn { background-image: var(--sky-dawn); }
.widget-band.sky--day { background-image: var(--sky-day); }
.widget-band.sky--dusk { background-image: var(--sky-dusk); }
.widget-band.sky--night { background-image: var(--sky-night); }
.widget-band.sky--day-overcast { background-image: var(--sky-day-overcast); }
.widget-band.sky--dusk-overcast { background-image: var(--sky-dusk-overcast); }
.widget-band.sky--night-overcast { background-image: var(--sky-night-overcast); }
.widget-band.sky--day-rain { background-image: var(--sky-day-rain); }
.widget-band.sky--night-rain { background-image: var(--sky-night-rain); }
.widget-band.sky--storm { background-image: var(--sky-storm); }
.widget-band.sky--day-snow { background-image: var(--sky-day-snow); }
.widget-band.sky--night-snow { background-image: var(--sky-night-snow); }

/* The scrim: clear to black at .22 down the band, over the sky, under
   the copy. */
.widget-band::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--sky-scrim);
  pointer-events: none;
}

.widget-band > :not(.sky) {
  position: relative;
  z-index: 2;
}

/* ── The sky (iOS BriefSky) ─────────────────────────────────────────────
   L0 the mesh canvas and the grain, L1 the disc, L2 the clouds. Sizes are
   the band's height: the disc 0.42 of it, its halo 1.6 × that, the moon
   0.22; a cloud bank's size and home are fractions of the band. */

.sky {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: clip;
  border-radius: inherit;
  pointer-events: none;
}

.sky__mesh {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

.sky__halo,
.sky__disc,
.sky__moon,
.sky__cloud {
  position: absolute;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}

.sky__halo {
  left: var(--sun-x, 50%);
  top: var(--sun-y, 50%);
  width: calc(var(--sky-h) * 0.672);
  height: calc(var(--sky-h) * 0.672);
  background: rgba(255, 255, 255, 0.25);
  filter: blur(18px);
}

.sky__disc {
  left: var(--sun-x, 50%);
  top: var(--sun-y, 50%);
  width: calc(var(--sky-h) * 0.42);
  height: calc(var(--sky-h) * 0.42);
  background: rgba(255, 255, 255, 0.9);
  filter: blur(6px);
}

.sky__moon {
  left: 78%;
  top: 30%;
  width: calc(var(--sky-h) * 0.22);
  height: calc(var(--sky-h) * 0.22);
  background: rgba(255, 255, 255, 0.6);
}

.sky__cloud {
  background: #ffffff;
  filter: blur(22px);
  opacity: var(--cloud-opacity, 0);
  transform: translate(calc(-50% + var(--cloud-drift, 0px)), -50%);
}

.sky__cloud--1 { width: 70%; height: 55%; left: 22%; top: 38%; }
.sky__cloud--2 { width: 50%; height: 45%; left: 58%; top: 26%; }
.sky__cloud--3 { width: 60%; height: 40%; left: 86%; top: 50%; }
.sky__cloud--4 { width: 45%; height: 50%; left: 40%; top: 62%; }

/* Film grain at 4% (iOS Grain.metal: a static per-pixel noise mixed in
   at 0.04) — an SVG noise tile, never animated: texture, not weather. */
.sky__grain {
  position: absolute;
  inset: 0;
  opacity: 0.04;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='128' height='128'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.95' numOctaves='1' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='128' height='128' filter='url(%23g)'/></svg>");
  background-size: 128px 128px;
}

/* The temperature: 48 light on one line, the glyph box as its line. */
.widget-band__temperature {
  flex-shrink: 0;
  font-size: 48px;
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

/* The place over the high / low and the condition: body semibold 18/140%
   and footnote 13/18, 2 apart, both full white. */
.widget-band__place-col {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4xs);
  min-width: 0;
}

.widget-band__place {
  font: var(--type-body);
  font-weight: 600;
  white-space: nowrap;
  overflow: clip;
  text-overflow: ellipsis;
}

.widget-band__summary {
  font: var(--type-footnote);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: clip;
}

/* The fallback's greeting: subheading 16/600/20, "Good evening," over the
   first name — two lines in the text, so nothing truncates. */
.widget-band__greeting {
  display: flex;
  flex-direction: column;
  margin: 0;
  min-width: 0;
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 20px;
}

.widget-band__salutation,
.widget-band__name {
  white-space: nowrap;
  overflow: clip;
  text-overflow: ellipsis;
}

/* Detail screen — single item view with header, body, and right-side rail. */

.detail {
  display: flex;
  flex-direction: column;
  height: 100vh;   /* fallback */
  height: 100dvh;  /* dynamic viewport */
  overflow: hidden;
  position: relative; /* stacking context for the nav toast */
}

.detail-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 0 var(--sp-xl);
}

.detail-content {
  display: flex;
  flex-direction: column;
  /* 100%, not the old 64vw: a VIEWPORT fraction inside a pane that is the
     viewport minus rail and sidebar defeated the auto margins at every width
     below ~1172px — the column pinned left, overflowed .detail-scroll and
     clipped the prev/next chevrons (12px gone at 1100, both gone at 900).
     100% + max-width is the only pair that is centred at every width. */
  width: 100%;
  /* 740, centred: the editable column is capped at what an eye can scan in
     one pass (2026-09-23). Was 720 — nearly home-body's cap already, and
     the two should read as one measure. */
  max-width: 740px;
  margin: 0 auto;
}

/* Header: full width with border below, outside the grid. */
.detail-header {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-md) var(--sp-xl);
  flex-shrink: 0;
}

.detail-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--sp-md);
}

.detail-meta-nav {
  display: flex;
  gap: var(--sp-3xs);
}


.detail-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  color: var(--label-primary);
  text-decoration: none;
  font-size: var(--text-lg);
}
.detail-back:hover {
  background: var(--bg-subtle);
  color: var(--label-primary);
  text-decoration: none;
}

.detail-author-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
}

.detail-author-avatar-link {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  text-decoration: none;
  color: inherit;
}

.detail-author-info {
  display: flex;
  flex-direction: column;
}

.detail-author-name {
  font-size: var(--text-lg);
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: -0.02em;
  color: var(--label-primary);
}

.detail-author-timestamp {
  font-size: var(--text-base);
  line-height: 1.3;
  letter-spacing: -0.02em;
  color: var(--label-secondary);
}

/* keep .detail-author-type selectable for any other consumers */
.detail-author-type {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  color: var(--label-primary);
  font-size: var(--text-sm);
  font-weight: 500;
}

.detail-archived-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--label-secondary);
  background: var(--bg-tertiary-inverse);
  font-size: var(--text-xs);
  padding: 2px var(--sp-xs);
  border-radius: var(--radius-full);
}

.detail-nav-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: none;
  background: transparent;
  color: var(--label-primary);
  font-size: var(--text-xl);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.detail-nav-btn:hover {
  background: var(--bg-secondary);
  color: var(--label-primary);
}
.detail-nav-btn:disabled {
  opacity: 0.3;
  cursor: not-allowed;
}
.detail-nav-btn:disabled:hover {
  background: transparent;
  color: var(--label-quaternary);
}

.detail-nav-menu {
  margin-left: var(--sp-xs);
  font-size: var(--text-2xl);
}

/* Wrapper for the ellipsis menu button + dropdown */
.detail-menu-wrapper {
  position: relative;
}

/* Ellipsis dropdown menu */
.detail-menu-dropdown {
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 50;
  margin-top: var(--sp-3xs);
  padding: var(--sp-3xs);
  background: var(--bg-primary);
  border-radius: var(--radius-lg);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  min-width: 240px;
  display: flex;
  flex-direction: column;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.detail-menu-item {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  width: 100%;
  padding: var(--sp-xs) var(--sp-sm);
  border: none;
  background: transparent;
  color: var(--label-primary);
  font-size: var(--text-sm);
  font-family: inherit;
  text-align: left;
  border-radius: var(--radius-md);
  cursor: pointer;
}
.detail-menu-item:hover {
  background: var(--bg-subtle);
}

.detail-menu-item-danger {
  color: var(--accent-red);
}
.detail-menu-item-danger:hover {
  background: var(--danger-subtle, var(--bg-subtle));
}

/* Delete confirmation dialog */
.detail-confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.5);
}
/* Tap-out-to-cancel layer (the timeline row's delete confirm passes one). */
.detail-confirm-backdrop {
  position: absolute;
  inset: 0;
}
.detail-confirm-dialog {
  position: relative;
  background: var(--bg);
  border-radius: var(--radius-lg);
  padding: var(--sp-xl);
  max-width: 360px;
  width: 90%;
  box-shadow: var(--shadow-lg, 0 8px 32px rgba(0,0,0,.2));
}
.detail-confirm-title {
  font-weight: 600;
  font-size: 1.1rem;
  margin: 0 0 var(--sp-xs);
}
.detail-confirm-body {
  color: var(--label-tertiary);
  margin: 0 0 var(--sp-lg);
  font-size: 0.9rem;
}
.detail-confirm-actions {
  display: flex;
  gap: var(--sp-sm);
  justify-content: flex-end;
}
.detail-confirm-btn {
  padding: var(--sp-xs) var(--sp-md);
  border-radius: var(--radius-md);
  border: none;
  font-size: 0.9rem;
  font-weight: 500;
  cursor: pointer;
}
.detail-confirm-cancel {
  background: var(--bg-subtle);
  color: var(--label-primary);
}
.detail-confirm-cancel:hover {
  background: var(--bg-hover, var(--bg-subtle));
}
.detail-confirm-delete {
  background: var(--accent-red);
  color: white;
}
.detail-confirm-delete:hover {
  opacity: 0.9;
}

/* Body column */
.detail-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
  min-width: 0;
}

.detail-hero {
  width: 100%;
  overflow: hidden;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

.detail-hero-photo,
.detail-hero-voice,
.detail-hero-bookmark {
  width: -webkit-fill-available;
  height: auto;
  aspect-ratio: 4 / 3;
  
}

.detail-hero-text {
  aspect-ratio: 1;
  max-width: 400px;
}

.detail-hero-img {
  width: -webkit-fill-available;
  display: block;
  height: auto;
  object-fit: cover;
}

.detail-hero-zoomable {
  cursor: zoom-in;
  transition: opacity 120ms ease;
}

.detail-hero-zoomable:hover {
  opacity: 0.9;
}

.detail-hero-text,
.detail-hero-photo,
.detail-hero-voice,
.detail-hero-bookmark,
.detail-hero-img {
  border-radius: var(--radius-lg);
}

.detail-hero-voice .detail-hero-play-icon,
.detail-hero-voice .detail-hero-duration,
.detail-hero-bookmark .detail-hero-site,
.detail-hero-bookmark .detail-hero-external {
  color: #fff;
}

.detail-hero-play-icon {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-full);
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 28px;
  backdrop-filter: blur(8px);
}

/* Click-to-preview thumbnail for a single video capture (view_hero.gleam's
   `video_hero`) — same visual footprint as the plain photo hero, plus a
   centered play badge (reusing `.detail-hero-play-icon`'s look) signalling
   the tap opens the fullscreen video viewer instead of playing inline. */
.detail-hero-video-btn {
  display: block;
  position: relative;
  width: 100%;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
}

.detail-hero-video-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 64px;
  height: 64px;
  border-radius: var(--radius-full);
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(8px);
}

.detail-hero-duration {
  position: absolute;
  bottom: var(--sp-sm);
  right: var(--sp-sm);
  background: rgba(0, 0, 0, 0.5);
  padding: var(--sp-3xs) var(--sp-xs);
  border-radius: var(--radius-md);
  font-size: var(--text-xs);
  backdrop-filter: blur(4px);
}

.detail-hero-site {
  position: absolute;
  top: var(--sp-sm);
  left: var(--sp-sm);
  background: rgba(0, 0, 0, 0.45);
  padding: var(--sp-3xs) var(--sp-sm);
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  backdrop-filter: blur(4px);
}

.detail-hero-external {
  position: absolute;
  top: var(--sp-sm);
  right: var(--sp-sm);
  font-size: var(--text-xl);
  background: rgba(0, 0, 0, 0.45);
  width: 32px;
  height: 32px;
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
}

.detail-text-content {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.detail-title {
  font-size: var(--text-3xl);
  font-weight: 600;
  color: var(--label-primary);
  line-height: 1.2;
  margin: 0;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  overflow: hidden;
}

.detail-title-row {
  display: flex;
  align-items: baseline;
  gap: var(--sp-xs);
  flex-wrap: wrap;
}

.detail-title-editable {
  background: transparent;
  border: none;
  resize: none;
  width: 100%;
  padding: 0;
  font-family: var(--font-sans);
  color: inherit;
  field-sizing: content;
  max-height: calc(3 * 1.25em);
  overflow: hidden;
  outline: none;
}

.detail-save-indicator {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--text-sm);
  color: var(--label-secondary);
  white-space: nowrap;
}

.detail-save-indicator-saved {
  color: var(--accent-green);
}

.detail-save-indicator-failed {
  color: var(--accent-red);
}

.detail-divider {
  border: none;
  border-top: 1px solid var(--border-primary);
  margin: var(--sp-md) 0;
}

.detail-description {
  font-size: var(--text-base);
  color: var(--label-special);
  line-height: 1.6;
  margin: 0;
}

/* Inline images inside a note body (markdown or the Milkdown editor) — keep them
   modestly sized instead of rendering at full natural resolution. */
.detail-description img,
.detail-body-editor img {
  max-width: 100%;
  max-height: 240px;
  width: auto;
  height: auto;
  border-radius: var(--radius-md, 12px);
  object-fit: contain;
}

.detail-description p {
  margin: 0 0 var(--sp-xs) 0;
}
.detail-description p:last-child {
  margin-bottom: 0;
}
/* A blank row the author made (markdown.mjs renderBodyMarkdown): one
   line's height, no extra gap, as the editor shows it. */
.detail-description p.md-blank-row {
  margin: 0;
}

.detail-description img {
  max-width: 100%;
  height: auto;
}

/* iOS has two heading sizes, both under the 32px title: `#` at .title2
   (22) and everything deeper at .title3 (20), bold (MarkdownStyleAttributes;
   `###`+ parse as H2). The browser defaults (2em = 36px) out-shouted the
   title. */
.detail-description h1,
.detail-description h2,
.detail-description h3,
.detail-description h4,
.detail-body-editor .ProseMirror h1,
.detail-body-editor .ProseMirror h2,
.detail-body-editor .ProseMirror h3,
.detail-body-editor .ProseMirror h4,
.detail-body-editor .ProseMirror h5,
.detail-body-editor .ProseMirror h6 {
  font-weight: 700;
  font-size: 20px;
  line-height: 1.3;
  letter-spacing: 0;
  color: var(--label-special);
  margin: var(--sp-sm) 0 var(--sp-3xs);
}
.detail-description h1,
.detail-body-editor .ProseMirror h1 {
  font-size: 22px;
}

.detail-description ul,
.detail-description ol {
  margin: var(--sp-xs) 0;
  padding-left: var(--sp-lg);
}
.detail-description ul { list-style: disc; }
.detail-description ol { list-style: decimal; }
.detail-description li { margin-bottom: var(--sp-3xs); }

/* ── Task list shared icon variables ────────────────────────────────────── */
/* SVG data URIs for the three checklist icons. Used as CSS mask-image so
   the icons inherit currentColor via background-color. The two checkbox
   states are client/icons/check-box/empty/lg/hollow.svg and
   client/icons/check-box/checked/lg/filled.svg — the glyphs iOS's editor
   draws at 20pt (MarkdownMarkerAttachment), re-copied 2026-10-21. They
   serve the editor (.pm-task-check) AND the read-only checklist
   (.md-task-checkbox: description, share page, public item), as on iOS;
   test/icon_parity_test.mjs fails if either drifts from those files. */

:root {
  --icon-task-unchecked: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E %3Cg clip-path='url(%23clip0_22606_32032)'%3E %3Cpath d='M5.88889 23H18.1111C20.8112 23 23 20.8112 23 18.1111V5.88889C23 3.18883 20.8112 1 18.1111 1H5.88889C3.18883 1 1 3.18883 1 5.88889V18.1111C1 20.8112 3.18883 23 5.88889 23Z' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' stroke='black'/%3E %3C/g%3E %3Cdefs%3E %3CclipPath id='clip0_22606_32032'%3E %3Crect width='24' height='24' fill='white'/%3E %3C/clipPath%3E %3C/defs%3E %3C/svg%3E");
  --icon-task-checked: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E %3Cg clip-path='url(%23clip0_22606_33446)'%3E %3Cpath d='M18.1113 0C21.3635 0.000117509 23.9999 2.6365 24 5.88867V18.1113C23.9999 21.3635 21.3635 23.9999 18.1113 24H5.88867C2.6365 23.9999 0.000117506 21.3635 0 18.1113V5.88867C0.000116994 2.6365 2.6365 0.000117249 5.88867 0H18.1113ZM18.1455 6.73633C17.7239 6.37995 17.093 6.43302 16.7363 6.85449L9.81152 15.0381L7.19531 12.5088C6.79829 12.1249 6.16516 12.1352 5.78125 12.5322C5.39735 12.9292 5.40769 13.5624 5.80469 13.9463L9.18945 17.2188C9.38839 17.4111 9.65819 17.5128 9.93457 17.499C10.211 17.4852 10.4697 17.3568 10.6484 17.1455L18.2637 8.14551C18.6201 7.7239 18.567 7.09296 18.1455 6.73633Z' fill-rule='evenodd' clip-rule='evenodd' fill='black'/%3E %3C/g%3E %3Cdefs%3E %3CclipPath id='clip0_22606_33446'%3E %3Crect width='24' height='24' fill='white'/%3E %3C/clipPath%3E %3C/defs%3E %3C/svg%3E");
  --icon-task-drag: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath d='M9.25 16.5C10.2165 16.5 11 17.2835 11 18.25C11 19.2165 10.2165 20 9.25 20C8.2835 20 7.5 19.2165 7.5 18.25C7.5 17.2835 8.2835 16.5 9.25 16.5Z' fill='black'/%3E %3Cpath d='M14.75 16.5C15.7165 16.5 16.5 17.2835 16.5 18.25C16.5 19.2165 15.7165 20 14.75 20C13.7835 20 13 19.2165 13 18.25C13 17.2835 13.7835 16.5 14.75 16.5Z' fill='black'/%3E %3Cpath d='M9.25 10.1504C10.2165 10.1504 11 10.9339 11 11.9004V12C11 12.9665 10.2165 13.75 9.25 13.75C8.2835 13.75 7.5 12.9665 7.5 12V11.9004C7.5 10.9339 8.2835 10.1504 9.25 10.1504Z' fill='black'/%3E %3Cpath d='M14.75 10.1504C15.7165 10.1504 16.5 10.9339 16.5 11.9004V12C16.5 12.9665 15.7165 13.75 14.75 13.75C13.7835 13.75 13 12.9665 13 12V11.9004C13 10.9339 13.7835 10.1504 14.75 10.1504Z' fill='black'/%3E %3Cpath d='M9.25 4C10.2165 4 11 4.7835 11 5.75C11 6.7165 10.2165 7.5 9.25 7.5C8.2835 7.5 7.5 6.7165 7.5 5.75C7.5 4.7835 8.2835 4 9.25 4Z' fill='black'/%3E %3Cpath d='M14.75 4C15.7165 4 16.5 4.7835 16.5 5.75C16.5 6.7165 15.7165 7.5 14.75 7.5C13.7835 7.5 13 6.7165 13 5.75C13 4.7835 13.7835 4 14.75 4Z' fill='black'/%3E %3C/svg%3E");
}

/* Shared mask helper — applies to both .md-task-drag/.md-task-checkbox spans
   and the Milkdown editor ::before pseudo-element. */
.md-task-drag, .md-task-checkbox,
.detail-body-editor .ProseMirror li[data-item-type="task"]::before {
  display: inline-block;
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  background-color: var(--label-special);
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
}

/* The checkbox is iOS's 20pt list marker (MarkdownStyleAttributes
   listMarkerSide); the drag handle keeps 16. */
.md-task-checkbox {
  width: 20px;
  height: 20px;
}

/* ── Task list (checklist) — read path (marked.js + transformTaskItems) ── */

.detail-description ul:has(.md-task-item),
.detail-description .md-list--tasks {
  list-style: none;
  padding-left: 0;
}

/* A plain bullet that shares its list with checklist items ("- [ ] milk"
   then "- eggs" — iOS writes exactly this, and typing "- " right under a
   checklist joins it into that list) keeps its bullet: the rule above
   strips every marker in the list, which left it bare (QA 2026-09-29). */
.detail-description ul:has(.md-task-item) > li:not(.md-task-item),
.detail-body-editor .ProseMirror ul:has(li[data-item-type="task"])
  > li:not([data-item-type="task"]) {
  list-style: disc;
  margin-left: var(--sp-lg);
}

.detail-description .md-task-item {
  display: flex;
  align-items: flex-start;
  gap: 4px;
  padding: var(--sp-3xs) 0;
  margin-bottom: 0;
  line-height: 1.5;
}

/* The share link and public item pages are read-only: nothing drags, so
   no handle (prod stress test 2026-09-29). */
.share-page .md-task-drag,
.public-item-page .md-task-drag {
  display: none;
}

.detail-description .md-task-drag {
  -webkit-mask-image: var(--icon-task-drag);
          mask-image: var(--icon-task-drag);
  margin-top: 2px;
  opacity: 0.4;
  cursor: grab;
}

.detail-description .md-task-checkbox {
  -webkit-mask-image: var(--icon-task-unchecked);
          mask-image: var(--icon-task-unchecked);
  margin-top: 2px;
}

.detail-description .md-task-item--checked .md-task-checkbox {
  -webkit-mask-image: var(--icon-task-checked);
          mask-image: var(--icon-task-checked);
}

.detail-description .md-task-item--checked {
  color: color-mix(in srgb, var(--label-special) 70%, transparent);
  text-decoration: line-through;
}

.detail-description .md-task-item--checked .md-task-drag,
.detail-description .md-task-item--checked .md-task-checkbox {
  text-decoration: none;
  background-color: var(--label-special);
}

/* ── Milkdown editor task list items (owner edit path) ─────────────────── */
/* Milkdown GFM: <li data-item-type="task" data-checked="true|false">.
   Icons are real DOM elements (.pm-task-drag, .pm-task-check) injected by
   a Decoration.widget plugin — ::before inside contenteditable doesn't
   receive pointer events reliably in WebKit. */

.detail-body-editor .ProseMirror ul:has(li[data-item-type="task"]) {
  list-style: none;
  padding-left: 0;
}

/* Layout: iOS Reminders anatomy — checkbox gutter on the LEFT, drag handle
   pinned to the row's RIGHT edge. Both buttons are absolutely positioned,
   NOT flow siblings of the text: inline contenteditable=false neighbors
   reproduce a known Chromium/WebKit bug where the native caret fails to
   render/blink next to them. Out-of-flow placement keeps the text run's
   inline layout free of non-editable neighbors on BOTH ends.
   `top: 0.75em - 8px` centers each 16px icon on the first text line
   (line-height 1.5), which reads as vertical centering on single-line
   rows and stays pinned to the first line when the text wraps. */
.detail-body-editor .ProseMirror li[data-item-type="task"] {
  position: relative;
  /* 20px checkbox + 12px gap (iOS's 34pt marker column, less its 2pt
     lead-in). */
  padding: var(--sp-3xs) 28px var(--sp-3xs) 32px;
  margin-bottom: 0;
  line-height: 1.5;
}

.detail-body-editor .ProseMirror .pm-task-check {
  position: absolute;
  left: 0;
  top: calc(var(--sp-3xs) + 0.75em - 10px);
  user-select: none;
}

.detail-body-editor .ProseMirror .pm-task-drag {
  position: absolute;
  right: 0;
  top: calc(var(--sp-3xs) + 0.75em - 8px);
  user-select: none;
}

/* Button reset — strip browser defaults so we get a clean 16×16 icon button */
.detail-body-editor .ProseMirror .pm-task-drag,
.detail-body-editor .ProseMirror .pm-task-check {
  appearance: none;
  -webkit-appearance: none;
  display: inline-block;
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  padding: 0;
  margin: 0;
  border: none;
  /* The global `button { border-radius }` (12px) made the 20px checkbox a
     circle that cropped the square glyph's corners (2026-10-21). */
  border-radius: 0;
  background-color: var(--label-primary);
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  cursor: pointer;
  outline: none;
}

.detail-body-editor .ProseMirror .pm-task-check {
  width: 20px;
  height: 20px;
}

.detail-body-editor .ProseMirror .pm-task-drag {
  -webkit-mask-image: var(--icon-task-drag);
          mask-image: var(--icon-task-drag);
  cursor: grab;
  opacity: 0.5;
}

.detail-body-editor .ProseMirror .pm-task-check {
  -webkit-mask-image: var(--icon-task-unchecked);
          mask-image: var(--icon-task-unchecked);
}

.detail-body-editor .ProseMirror .pm-task-check--checked {
  -webkit-mask-image: var(--icon-task-checked);
          mask-image: var(--icon-task-checked);
}

/* iOS MarkdownStyleAttributes: strikethrough at 70% — not italic. */
.detail-body-editor .ProseMirror li[data-item-type="task"][data-checked="true"] {
  color: color-mix(in srgb, var(--label-special) 70%, transparent);
  text-decoration: line-through;
}

/* Visual feedback during pointer drag-to-reorder: the row itself rides
   under the pointer (inline translateY set by the NodeView) as a lifted
   card. pointer-events:none is load-bearing — trackDrop's elementFromPoint
   must see the rows BENEATH the lifted one. */
.detail-body-editor .ProseMirror li.task-dragging {
  backdrop-filter: blur(2px);
  pointer-events: none;
  position: relative;
  z-index: 5;
  background: rgba(var(--color-blue), .4);
  border-radius: var(--radius-sm);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
  opacity: 0.95;
  will-change: transform;
}
.detail-body-editor .ProseMirror li.task-drop-above {
  border-top: 2px solid var(--accent-blue);
}
.detail-body-editor .ProseMirror li.task-drop-below {
  border-bottom: 2px solid var(--accent-blue);
}

.detail-description code {
  font-family: "SF Mono", "Fira Code", monospace;
  font-size: 0.9em;
  background: rgba(0, 0, 0, 0.06);
  padding: 1px 4px;
  border-radius: var(--radius-sm);
}
[data-theme="dark"] .detail-description code { background: rgba(255, 255, 255, 0.1); }
[data-theme="light"] .detail-description code { background: rgba(0, 0, 0, 0.06); }

.detail-description pre {
  background: rgba(0, 0, 0, 0.06);
  padding: var(--sp-sm);
  border-radius: var(--radius-md);
  overflow-x: auto;
  margin: var(--sp-xs) 0;
}
[data-theme="dark"] .detail-description pre { background: rgba(255, 255, 255, 0.08); }
[data-theme="light"] .detail-description pre { background: rgba(0, 0, 0, 0.06); }
.detail-description pre code { background: none; padding: 0; }

.detail-description blockquote {
  border-left: 3px solid var(--border-primary);
  padding-left: var(--sp-sm);
  color: var(--label-secondary);
  margin: var(--sp-xs) 0;
}

.detail-description strong { font-weight: 600; color: var(--label-special); }
.detail-description em { font-style: italic; }

.detail-description a {
  color: var(--accent-blue);
  text-decoration: underline;
}

/* Owner-only mount point for the Milkdown body editor. Milkdown injects
 * .milkdown > .ProseMirror once initialised. Mirrors the chat composer's
 * styling pattern (chat.css §"Milkdown rich-text composer") but scaled
 * to detail-page typography so the editable surface visually matches
 * `.detail-description` (the non-owner read path). */
.detail-body-editor {
  min-height: 1.6em;
  cursor: text;
  outline: none;
}

.detail-body-editor .milkdown {
  all: unset;
  display: block;
  width: 100%;
}

.detail-body-editor .ProseMirror {
  outline: none;
  font-family: inherit;
  font-size: var(--text-base);
  color: var(--label-special);
  line-height: 1.6;
  margin: 0;
  padding: 0;
  white-space: pre-wrap;
  word-break: break-word;
}

.detail-body-editor .ProseMirror p.is-editor-empty:first-child::before,
.detail-body-editor .ProseMirror:empty::before {
  content: "Add a description…";
  float: left;
  height: 0;
  color: var(--label-tertiary);
  pointer-events: none;
}

.detail-body-editor .ProseMirror p {
  margin: 0 0 var(--sp-xs) 0;
}
.detail-body-editor .ProseMirror p:last-child {
  margin-bottom: 0;
}

.detail-body-editor .ProseMirror strong { font-weight: 700; }
.detail-body-editor .ProseMirror em     { font-style: italic; }
.detail-body-editor .ProseMirror code {
  font-family: monospace;
  font-size: 0.9em;
  background: var(--bg-secondary, rgba(0,0,0,0.08));
  border-radius: 3px;
  padding: 0.1em 0.3em;
}
.detail-body-editor .ProseMirror a {
  color: var(--accent-blue);
  text-decoration: underline;
}

.detail-body-editor .ProseMirror ul,
.detail-body-editor .ProseMirror ol {
  margin: var(--sp-xs) 0;
  padding-left: 1.4em;
}
.detail-body-editor .ProseMirror ul { list-style-type: disc; }
.detail-body-editor .ProseMirror ol { list-style-type: decimal; }
.detail-body-editor .ProseMirror li { margin: 0; }

.detail-body-editor .ProseMirror pre {
  background: var(--bg-secondary, rgba(0,0,0,0.08));
  border-radius: var(--radius-sm, 4px);
  padding: var(--sp-xs) var(--sp-sm);
  overflow-x: auto;
  font-family: monospace;
  font-size: 0.9em;
  margin: var(--sp-xs) 0;
}
.detail-body-editor .ProseMirror pre code {
  background: none;
  padding: 0;
}

.detail-body-editor .ProseMirror blockquote {
  border-left: 3px solid var(--border-primary, #ccc);
  margin: var(--sp-xs) 0;
  padding-left: var(--sp-sm);
  color: var(--label-secondary);
}

.detail-body-editor .ProseMirror h1,
.detail-body-editor .ProseMirror h2,
.detail-body-editor .ProseMirror h3 {
  font-weight: 700;
  margin: var(--sp-sm) 0 var(--sp-xs) 0;
  line-height: 1.3;
  color: var(--label-special);
}

.detail-body-editor .ProseMirror ::selection {
  background: var(--accent-blue-elevated);
}

.detail-description hr {
  border: none;
  border-top: 1px solid var(--border-primary);
  margin: var(--sp-md) 0;
}

.detail-text-body {
  font-size: var(--text-base);
  color: var(--label-primary);
  line-height: 1.7;
  margin-top: var(--sp-xs);
}

.detail-text-body p {
  margin: 0 0 var(--sp-sm) 0;
}
.detail-text-body p:last-child {
  margin-bottom: 0;
}

.detail-text-body h1,
.detail-text-body h2,
.detail-text-body h3,
.detail-text-body h4 {
  font-weight: 600;
  margin: var(--sp-md) 0 var(--sp-xs);
}
.detail-text-body h1 { font-size: var(--text-2xl); }
.detail-text-body h2 { font-size: var(--text-xl); }
.detail-text-body h3 { font-size: var(--text-lg); }
.detail-text-body h4 { font-size: var(--text-base); }

.detail-text-body ul,
.detail-text-body ol {
  margin: var(--sp-xs) 0;
  padding-left: var(--sp-lg);
}
.detail-text-body ul { list-style: disc; }
.detail-text-body ol { list-style: decimal; }
.detail-text-body li { margin-bottom: var(--sp-3xs); }

.detail-text-body code {
  font-family: "SF Mono", "Fira Code", monospace;
  font-size: 0.9em;
  background: rgba(0, 0, 0, 0.06);
  padding: 1px 4px;
  border-radius: var(--radius-sm);
}
[data-theme="dark"] .detail-text-body code { background: rgba(255, 255, 255, 0.1); }
[data-theme="light"] .detail-text-body code { background: rgba(0, 0, 0, 0.06); }

.detail-text-body pre {
  background: rgba(0, 0, 0, 0.06);
  padding: var(--sp-sm);
  border-radius: var(--radius-md);
  overflow-x: auto;
  margin: var(--sp-xs) 0;
}
[data-theme="dark"] .detail-text-body pre { background: rgba(255, 255, 255, 0.08); }
[data-theme="light"] .detail-text-body pre { background: rgba(0, 0, 0, 0.06); }
.detail-text-body pre code { background: none; padding: 0; }

.detail-text-body blockquote {
  border-left: 3px solid var(--border-primary);
  padding-left: var(--sp-sm);
  color: var(--label-secondary);
  margin: var(--sp-xs) 0;
}

.detail-text-body strong { font-weight: 600; }
.detail-text-body em { font-style: italic; }

.detail-text-body a {
  color: var(--accent-blue);
  text-decoration: underline;
}

.detail-text-body hr {
  border: none;
  border-top: 1px solid var(--border-primary);
  margin: var(--sp-md) 0;
}

/* ── Extra text section (transcript / extracted page text) ──── */

.detail-extra-section {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

.detail-extra-label {
  margin: 0;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--label-secondary);
}

.detail-extra-content {
  position: relative;
  max-height: 220px;
  overflow: hidden;
}

.detail-extra-content--expanded {
  max-height: none;
  overflow: visible;
}

.detail-extra-fade {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 80px;
  background: linear-gradient(to bottom, transparent, var(--bg-body));
  pointer-events: none;
}

.detail-extra-toggle {
  align-self: flex-start;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--label-secondary);
  cursor: pointer;
}
.detail-extra-toggle:hover {
  color: var(--label-primary);
}

.detail-bookmark-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  margin-top: var(--sp-xs);
  color: var(--accent-blue);
  font-size: var(--text-sm);
  text-decoration: none;
}
.detail-bookmark-link:hover {
  text-decoration: underline;
}

.detail-actions {
  display: flex;
  gap: var(--sp-xs);
  flex-wrap: wrap;
  margin-top: var(--sp-xs);
}

.detail-action-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-md);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-full);
  background: var(--bg-primary);
  color: var(--label-primary);
  font-size: var(--text-sm);
  font-family: inherit;
  cursor: pointer;
  text-align: left;
  white-space: nowrap;
}
.detail-action-btn:hover {
  border-color: var(--border-primary);
  background: var(--bg-subtle);
}

.detail-action-icon {
  display: inline-flex;
  align-items: center;
  color: var(--label-tertiary);
}

.detail-action-text {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}

.detail-action-label {
  color: var(--label-primary);
  font-size: var(--text-sm);
  font-weight: 500;
}

/* Right-side vertical action rail */
.detail-rail {
  position: absolute;
  bottom: 44px;
  right: 44px;
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

.detail-rail-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var( --size-3xl);
  height: var( --size-3xl);
  padding: var(--sp-3xs);
  border: none;
  background: var(--bg-primary);
  border-radius: var(--radius-full);
  color: var(--label-primary);
  cursor: pointer;
  font-family: inherit;
}
.detail-rail-btn:hover {
  color: var(--label-primary);
  opacity: 0.7;
}

@media (min-width: 835px) {
  .detail-rail-btn:has(.detail-rail-count:not(:empty)) {
    padding-inline: var(--sp-xs);
  }
}

.detail-rail-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.detail-rail-count {
  font-size: 9px;
  line-height: 1;
  color: var(--label-secondary);
}

.detail-rail-icon .icon {
  width: 18px;
  height: 18px;
}

/* The like rail entry is a real <button>; reset the default chrome so it
   matches the surrounding rail_btn divs, and show the filled/colored
   state when the viewer has already liked the item. */
.detail-rail-btn-like {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  width: fit-content;
  border: none;
  font-family: inherit;
  gap: 4px;
}


.detail-rail-btn-like > .detail-rail-count {
  font-size: var(--size-sm);
}

.detail-rail-btn-like-active,
.detail-rail-btn-like-active:hover {
  color: var(--label-primary);
}

.detail-rail-btn-like-active {
  padding-left: 8px;
  padding-right: 8px;
  transition: .3s ease all;
}

/* Bookmark rail button — same reset as the like button. */
.detail-rail-btn-bookmark {
  border: none;
  font-family: inherit;
}
.detail-rail-btn-bookmark-active,
.detail-rail-btn-bookmark-active:hover {
  color: var(--label-primary);
}
/* Wrapper that stacks the toggle + groups-pill vertically in the rail */
.detail-visibility-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

/* "N People" pill shown when visibility is GROUPS */
.detail-groups-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border-primary);
  background: var(--bg-secondary);
  color: var(--label-secondary);
  font-size: var(--text-xs);
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s;
}
.detail-groups-pill:hover {
  background: var(--bg-tertiary);
}

/* Chat-with-AI rail button — accent-colored to signal the primary action,
   dimmed while the Stream create-channel request is in flight. */
.detail-rail-btn-ai {
  color: var(--label-primary);
  background: var(--bg-primary);
}
.detail-rail-btn-ai:hover {
  /* color: var(--bg-secondary); */
  opacity: 0.8;
}
.detail-rail-btn-ai-loading {
  opacity: 0.5;
  cursor: progress;
}

/* Unread dot on the Chat-with-AI rail button. Item AI chats are hidden
   from the sidebar, so this dot is the only place users see new agent
   replies. Positioned over the top-right corner of the round button. */
.detail-rail-btn-ai {
  position: relative;
}
.detail-rail-btn-ai-unread-dot {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  background: var(--accent-red);
  border: 2px solid var(--bg-primary);
  box-sizing: content-box;
  pointer-events: none;
}

/* Voice hero + player wrapper — player renders below the gradient hero. */
.detail-voice-wrapper {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

/* ── Audio player for voice detail ────────────────────────────── */

.detail-player {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-md) var(--sp-lg);
  background: var(--bg-subtle);
  border-radius: var(--radius-lg);
}

.detail-player-btn {
  width: 48px;
  height: 48px;
  border-radius: var(--radius-full);
  background: var(--bg-secondary);
  color: var(--label-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  cursor: pointer;
  flex-shrink: 0;
}

.detail-player-progress { flex: 1; }

.detail-player-bar {
  height: 4px;
  background: var(--border-primary);
  border-radius: var(--radius-full);
  overflow: hidden;
  cursor: pointer;
}
.detail-player-bar-fill {
  height: 100%;
  background: var(--bg-secondary);
  transition: width 0.1s linear;
}

.detail-player-times {
  display: flex;
  justify-content: space-between;
  margin-top: var(--sp-3xs);
  font-size: var(--text-xs);
  color: var(--label-secondary);
}

.detail-player-transcript {
  margin: var(--sp-sm) 0 0 0;
  font-size: var(--text-sm);
  color: color-mix(in srgb, var(--label-primary) 85%, transparent);
  line-height: 1.5;
}

.detail-loading,
.detail-error {
  padding: var(--sp-xxl);
  text-align: center;
  color: var(--label-secondary);
}

/* ── Container attachment grid ──────────────────────────────────── */

.detail-container-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: var(--sp-sm);
}

.detail-container-thumb {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--radius-md, 12px);
  background: var(--surface-secondary, rgba(127, 127, 127, 0.08));
  display: block;
}

.detail-container-attach {
  margin-top: var(--sp-sm);
}

.detail-container-attach-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-md);
  border-radius: var(--radius-md, 12px);
  border: 1px dashed var(--border-subtle, rgba(127, 127, 127, 0.35));
  background: transparent;
  color: var(--label-secondary);
  font-size: var(--text-sm);
  cursor: pointer;
}

.detail-container-attach-btn:hover {
  background: var(--surface-secondary, rgba(127, 127, 127, 0.08));
  color: var(--label-primary);
}

.detail-container-file {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--sp-sm);
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-md, 12px);
  background: var(--surface-secondary, rgba(127, 127, 127, 0.08));
  font-size: var(--text-xs);
  color: var(--label-secondary);
  overflow: hidden;
  overflow-wrap: anywhere;
}

/* ── Attached files (ui/file_chip) ─────────────────────────────────
   Under the heading; the reminder card's own inset lines them up with
   the body text. */

.detail-file-chips {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  padding-top: var(--sp-sm);
}

.file-chip {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  width: 100%;
  padding: var(--sp-xs) var(--sp-sm);
  background: var(--bg-subtle);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  font: inherit;
  font-size: var(--text-sm);
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.file-chip:disabled {
  cursor: default;
}

@media (hover: hover) {
  .file-chip:not(:disabled):hover {
    background: var(--bg-elevated, var(--bg-subtle));
  }
}

.file-chip-info {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.file-chip-name {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.file-chip-size {
  font-size: var(--text-xs);
  color: var(--label-secondary);
}

/* The phone's row (spec §4c.9, ruling C13): only the phone block below draws
   it, in place of the clip and the whole name above. */
.file-chip-row {
  display: none;
}

/* ── Inline Please AI chat (replaces comments section) ──────────── */

.detail-ai-chat {
  margin-top: var(--sp-lg);
  padding-bottom: var(--sp-4xl);
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

/* Owner reminder surface (.nr-root): inset the AI-chat section like the
   rest of the page's content (it previously ran flush on desktop), but
   leave it unboxed — it should blend into the plain page background
   rather than read as its own colored panel like the reminder card. */
.nr-root .detail-ai-chat {
  padding: var(--sp-lg) var(--sp-xl);
  background: var(--bg-secondary);
}

.detail-ai-chat-title {
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--label-primary);
  margin: 0;
}

.detail-ai-chat-status {
  color: var(--label-secondary);
  font-size: var(--text-sm);
  padding: var(--sp-md) 0;
}

/* Offline: Please AI is entirely server-side, so it can't be used at all
   without a connection. Framed as a state to wait out rather than as an
   error — the old copy ("Couldn't open the chat.") read like the feature
   was broken and gave no hint that reconnecting would fix it. */
.detail-ai-chat-offline {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-sm) var(--sp-md);
  margin-bottom: var(--sp-sm);

  border-radius: var(--radius-md, 10px);
  background: var(--fill-elevated-secondary);
  color: var(--label-secondary);
}

.detail-ai-chat-offline-text {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 18px;
}

/* ── Camera capture modal (owner-container hardware camera) ───────── */

.camera-modal-body {
  display: flex;
  flex-direction: column;
  background: #000;
  border-radius: inherit;
  overflow: hidden;
}

.camera-viewfinder {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
  background: #111;
}

.camera-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-lg) var(--sp-md);
  background: #000;
}

.camera-shutter-btn {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-full);
  border: 4px solid #fff;
  background: rgba(255, 255, 255, 0.85);
  cursor: pointer;
  transition: transform 80ms ease, background 80ms ease;
  flex-shrink: 0;
}

.camera-shutter-btn:hover {
  background: #fff;
  transform: scale(1.06);
}

.camera-shutter-btn:active {
  transform: scale(0.94);
  background: rgba(255, 255, 255, 0.65);
}

.detail-ai-chat-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

/* No independent scrollbox here — messages flow in the same single
   page-level scroll as the reminder card above (.nr-body owns the only
   overflow-y: auto on this page). */
.detail-ai-chat-messages {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

.detail-ai-chat-empty {
  color: var(--label-secondary);
  font-size: var(--text-sm);
  padding: var(--sp-lg) 0;
  text-align: center;
}

.detail-ai-chat-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

/* Plain doc-like thread, not two-sided chat bubbles: the agent's messages
   (.detail-ai-msg-them) render as bare full-width text with a name+time
   header; only the viewer's own messages (.detail-ai-msg-me) get a bubble,
   right-aligned and width-capped like a normal chat reply. */
.detail-ai-msg {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3xs);
}

.detail-ai-msg-them {
  align-self: stretch;
  max-width: 100%;
}

.detail-ai-msg-me {
  align-self: flex-end;
  align-items: flex-end;
  max-width: 85%;
}

.detail-ai-msg-header {
  font-size: var(--text-xs);
  color: var(--label-secondary);
}

.detail-ai-msg-header-name {
  font-weight: 600;
  color: var(--label-primary);
}

/* An agent reply's media leads it (iOS ChatAttachmentView; the drawer's
   media, on the panel since 2026-10-26): one piece is a 260 square, more
   a two-column grid of 100-tall cells 2 apart, 220 wide. Thumbnails crop
   to fill on 12 corners; a fill holds the box until the image lands. A
   video wears a play badge. 16 above what follows. */
.detail-ai-msg-media {
  align-self: flex-start;
  margin-bottom: var(--sp-sm);
}

.detail-ai-msg-media--single {
  width: min(260px, 100%);
}

.detail-ai-msg-media--grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
  width: min(220px, 100%);
  border-radius: 12px;
  overflow: hidden;
}

.detail-ai-msg-media-item {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 12px;
  background: var(--fill-tertiary);
  cursor: zoom-in;
}

.detail-ai-msg-media--single .detail-ai-msg-media-item {
  aspect-ratio: 1;
}

.detail-ai-msg-media--grid .detail-ai-msg-media-item {
  height: 100px;
}

.detail-ai-msg-media-item[data-media="video"] {
  cursor: pointer;
}

.detail-ai-msg-media-thumb {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 12px;
  object-fit: cover;
}

.detail-ai-msg-media-play {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  transform: translate(-50%, -50%);
  pointer-events: none;
}

/* The toDo cards a reply embeds (iOS ChatFeedTileCard; the drawer's
   cards, on the panel since 2026-10-26): above the rich cards and the
   text, full width, 16 apart and 16 above what follows. The card is the
   timeline row (timeline_row.css): bg-primary on the panel's
   bg-secondary. */
.detail-ai-msg-todos {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
  width: 100%;
  align-self: stretch;
  margin-bottom: var(--sp-sm);
}

/* Rich agent cards above the text — full width across the thread for
   every sender (iOS InlineChatMessageRows.richCards: a card is an item,
   not a bubble). */
.detail-ai-msg-cards {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  width: 100%;
  align-self: stretch;
}

.detail-ai-msg-bubble {
  padding: var(--sp-sm) var(--sp-md);
  border-radius: var(--radius-lg, 16px);
  background: var(--surface-secondary, rgba(127, 127, 127, 0.08));
}

.detail-ai-msg-me .detail-ai-msg-bubble {
  border-bottom-right-radius: var(--radius-sm);
  background: var(--fill-elevated-primary);
  color: var(--label-primary);
  backdrop-filter: contrast(2);
}

.detail-ai-msg-body {
  /* AI prose reads a step larger than human-chat bubbles. */
  font-size: var(--text-md);
  line-height: 1.5;
  overflow-wrap: break-word;
}

.detail-ai-msg-body p {
  margin: 0;
}

.detail-ai-msg-body p + p {
  margin-top: var(--sp-sm);
}

.detail-ai-attach-input {
  display: none;
}

/* ── Transparent backdrop for closing dropdowns on outside click ── */

.detail-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9;
}

/* ── Reminder picker dropdown ────────────────────────────────── */

.detail-action-remind-wrapper {
  position: relative;
}

.detail-reminder-picker {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 10;
  margin-top: var(--sp-xs);
  padding: var(--sp-xs);
  background: var(--bg-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-lg);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3xs);
  min-width: 220px;
}

/* When the reminder picker is rendered inside the owner-container slide-up
   sheet (Ping Me), drop the absolute dropdown positioning and let it sit
   in-flow, full-width, without the border/shadow the sheet already provides. */
.nr-reminder-sheet .detail-reminder-picker {
  position: static;
  margin-top: 0;
  padding: 0;
  border: none;
  box-shadow: none;
  min-width: 0;
  width: 100%;
}

.detail-reminder-option {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  width: 100%;
  padding: var(--sp-xs) var(--sp-sm);
  border: none;
  background: transparent;
  color: var(--label-primary);
  font-size: var(--text-sm);
  font-family: inherit;
  text-align: left;
  border-radius: var(--radius-md);
  cursor: pointer;
}
.detail-reminder-option:hover {
  background: var(--bg-subtle);
}
.detail-reminder-option-time {
  margin-left: auto;
  color: var(--label-secondary);
  font-size: var(--text-xs);
}

/* The opened alert's own actions (Snooze, Dismiss), above the quick picks. */
.detail-reminder-actions {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3xs);
  padding-bottom: var(--sp-3xs);
  margin-bottom: var(--sp-3xs);
  border-bottom: 1px solid var(--border-primary);
}

/* "Repeats [Never ▾]" — sits between the quick picks and the custom row. */
.detail-reminder-repeat {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-sm);
  border-top: 1px solid var(--border-primary);
  margin-top: var(--sp-3xs);
  color: var(--label-primary);
  font-size: var(--text-sm);
}
.detail-reminder-repeat-label {
  flex: 1;
}
.detail-reminder-repeat-select {
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  padding: var(--sp-3xs) var(--sp-xs);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--label-primary);
  background: var(--bg);
  cursor: pointer;
}

.detail-reminder-label-input {
  flex: 0 1 12rem;
  min-width: 0;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  padding: var(--sp-3xs) var(--sp-xs);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--label-primary);
  background: var(--bg);
}

.detail-reminder-custom {
  display: flex;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-sm);
  border-top: 1px solid var(--border-primary);
  margin-top: var(--sp-3xs);
  padding-top: var(--sp-sm);
}

.detail-reminder-custom-input {
  flex: 1;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  padding: var(--sp-3xs) var(--sp-xs);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--label-primary);
  background: var(--bg);
}

.detail-reminder-custom-btn {
  padding: var(--sp-3xs) var(--sp-sm);
  border: none;
  border-radius: var(--radius-md);
  background: var(--bg-secondary);
  color: var(--label-secondary);
  font-size: var(--text-sm);
  font-family: inherit;
  cursor: pointer;
}
.detail-reminder-custom-btn:hover {
  opacity: 0.9;
}

/* ── Existing reminders (chips below action row) ─────────────── */

.detail-reminders-list {
  display: flex;
  flex-direction: row;
  gap: 16px;
  margin-top: var(--sp-sm);
  justify-content: flex-start;
  align-items: center;
}

.detail-reminder-chip-wrap {
  position: relative;
  display: inline-flex;
}

.detail-reminder-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  height: var(--size-3xl);
  padding: var(--sp-3xs) var(--sp-sm);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-full);
  background: var(--bg-subtle);
  font-size: var(--text-sm);
  font-family: inherit;
  color: var(--label-primary);
  cursor: pointer;
}
.detail-reminder-chip:hover {
  background: var(--bg-hover, var(--bg-subtle));
  border-color: var(--border-primary);
}

.detail-reminder-chip-icon {
  display: inline-flex;
  align-items: center;
  color: var(--label-tertiary);
  line-height: 1;
}

.detail-reminder-chip-date {
  color: var(--label-primary);
}

.detail-reminder-chip-overdue {
  border-color: var(--accent-red);
  color: var(--accent-red);
}
.detail-reminder-chip-overdue .detail-reminder-chip-icon,
.detail-reminder-chip-overdue .detail-reminder-chip-date {
  color: var(--accent-red);
}

/* In-flight CompleteReminder request — chip taps are inert (see
   completing_reminder in app/detail/reminders.gleam). */
.detail-reminder-chip--completing {
  opacity: 0.6;
  cursor: default;
  pointer-events: none;
}

/* Chip action dropdown */
.detail-reminder-chip-menu {
  position: absolute;
  top: calc(100% + var(--sp-3xs));
  left: 0;
  z-index: 20;
  min-width: 140px;
  padding: var(--sp-3xs);
  background: var(--bg-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-lg);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.detail-reminder-chip-menu-item {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  width: 100%;
  padding: var(--sp-xs) var(--sp-sm);
  border: none;
  background: transparent;
  color: var(--label-primary);
  font-size: var(--text-sm);
  font-family: inherit;
  text-align: left;
  border-radius: var(--radius-md);
  cursor: pointer;
  white-space: nowrap;
}
.detail-reminder-chip-menu-item:hover {
  background: var(--bg-subtle);
}
.detail-reminder-chip-menu-item-danger {
  color: var(--accent-red);
}
.detail-reminder-chip-menu-item-danger:hover {
  background: var(--danger-subtle, var(--bg-subtle));
}

/* ── Keyboard-nav end-of-list toast ──────────────────────────────────── */

.detail-nav-toast {
  position: absolute;
  bottom: var(--sp-xl);
  left: 50%;
  transform: translateX(-50%);
  padding: var(--sp-xs) var(--sp-md);
  background: var(--bg-elevated, var(--bg-primary));
  color: var(--label-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  white-space: nowrap;
  pointer-events: none;
  z-index: 10;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  animation: detail-toast-in 0.18s ease-out, detail-toast-out 0.3s ease-in 2.2s forwards;
}

/* ─── Keyframe animations ────────────────────────────────────────────── */

@keyframes detail-toast-in {
  from { opacity: 0; transform: translateX(-50%) translateY(6px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

@keyframes detail-toast-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* ─── Media queries ──────────────────────────────────────────────────── */

@media (max-width: 834px) {
  .detail-content {
    width: 100%;
    max-width: 100%;
  }

  .detail-back { display: none; }

  /* Remove scroll-container horizontal padding so media bleeds edge-to-edge,
     then re-apply the same value directly to content sections. */
  .detail-scroll {
    padding-left: 0;
    padding-right: 0;
  }

  .detail-meta,
  .detail-text-content {
    padding-left: var(--sp-xl);
    padding-right: var(--sp-xl);
  }

  /* Full-width media on mobile — let natural aspect ratio dictate height. */
  .detail-hero {
    border-radius: 0;
  }

  .detail-hero-photo,
  .detail-hero-voice,
  .detail-hero-bookmark,
  .detail-hero-text {
    width: 100%;
    height: auto;
    aspect-ratio: unset;
    max-width: 100%;
    border-radius: 0;
  }

  .detail-hero-img {
    width: 100%;
    height: auto;
    object-fit: contain;
    border-radius: 0;
  }
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .detail-description code { background: rgba(255, 255, 255, 0.1); }
  :root:not([data-theme]) .detail-description pre { background: rgba(255, 255, 255, 0.08); }
  :root:not([data-theme]) .detail-text-body code { background: rgba(255, 255, 255, 0.1); }
  :root:not([data-theme]) .detail-text-body pre { background: rgba(255, 255, 255, 0.08); }
}

.detail-no-access {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-sm);
  padding: var(--sp-xl);
  color: var(--label-secondary);
  font-size: var(--text-lg);
  text-align: center;
}

.detail-no-access p { margin: 0; }

.detail-no-access-title {
  color: var(--label-primary);
  font-weight: 600;
}

.detail-no-access-detail {
  max-width: 360px;
  font-size: var(--text-sm);
}

/* ── Title-row attachment thumbnail deck ─────────────────────────────────
   Port of iOS CaptureThumbnailStack: up to three tilted 80×80 cards inside
   a 96×88 footprint, trailing the container title. One click target. */

.detail-title-row--with-thumb {
  flex-wrap: nowrap;
  align-items: flex-start;
}

.detail-title-row--with-thumb .detail-title {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
}

.thumb-stack {
  position: relative;
  flex: 0 0 auto;
  align-self: flex-start;
  /* Tight 80px box — the fan/rotation is transform-only and overhangs
     the box visibly without costing any layout height. */
  width: 80px;
  height: 80px;
  margin-left: 6px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}

.thumb-stack-card {
  position: absolute;
  top: 0;
  left: 0;
  width: 80px;
  height: 80px;
  box-sizing: border-box;
  border: 2px solid var(--bg-body); /* base background — flips with theme */
  border-radius: 16px;
  background: var(--bg-body);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18);
}

/* The deck as designed (Julian, 2026-10-27): the top thumbnail turned +8°,
   the middle flat, the last −8° — the turns the phone block below has drawn
   since 4c, now on every deck. It was a −7 / +6 / −3 fan with nudges. */
.thumb-stack-front {
  transform: rotate(8deg);
  z-index: 3;
}

.thumb-stack-back1 {
  transform: none;
  opacity: 0.9;
  z-index: 2;
}

.thumb-stack-back2 {
  transform: rotate(-8deg);
  opacity: 0.9;
  z-index: 1;
}

/* The "+N" badge's layer, above the front card (spec §4c.3, C11): a copy of
   the card's box, turned with it, so the badge rides the turned corner as
   it did inside the card. The transparent 2px border copies the card's
   padding box, so the badge's right/bottom 4 resolve exactly as before.
   Unscoped and at every width: /new, share, join and the read-only
   container body draw this deck. The phone detail lays it flat. */
.thumb-stack-badge-layer {
  position: absolute;
  top: 0;
  left: 0;
  width: 80px;
  height: 80px;
  box-sizing: border-box;
  border: 2px solid transparent;
  transform: rotate(8deg);
  z-index: 4;
  pointer-events: none;
}

/* Inner clip so media cover-crops to the rounded card. The badge sits in
   its own layer, outside the clip (mirrors iOS, where the overlay draws
   after clipShape). */
.thumb-stack-clip {
  position: absolute;
  inset: 0;
  border-radius: 14px; /* card radius minus border width */
  overflow: hidden;
}

.thumb-stack-media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.thumb-stack-placeholder {
  background: var(--bg-secondary);
}

/* Calendar date badge card — scaled-up sibling of the 56px timeline badge
   (.tl-thumb-calendar): red uppercase month over the day numeral. */
.thumb-stack-calendar {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  background: var(--fill-elevated-quaternary);
}

.thumb-stack-cal-month {
  font-size: 14px;
  font-weight: 600;
  line-height: 17px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--accent-red);
}

.thumb-stack-cal-day {
  font-size: 32px;
  font-weight: 500;
  line-height: 36px;
  color: var(--label-primary);
}

.thumb-stack-voice {
  position: relative; /* anchors the play chip */
  display: flex;
  align-items: center;
  justify-content: center;
}

.thumb-stack-voice .wf-tile {
  height: 100%;
  border-radius: 0; /* the card's clip owns the rounding */
}

/* Translucent play chip centered on the voice card — signals that tapping
   opens the fullscreen voice viewer. */
.thumb-stack-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

.thumb-stack-badge {
  position: absolute;
  right: 4px;
  bottom: 4px;
  z-index: 1;
  padding: 2px 6px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
}

/* ── The phone detail's deck (spec §4c.3) ───────────────────────────────
   Over the title block's top-right corner: a 96×88 frame whose right edge
   is on the heading's 16 gutter (x 281 -> 377 at 393) and whose top is the
   title's (the heading has no top padding). The 80 cards are centred in
   it, turned +8° / 0° / -8° with no offsets, at radius 80 × 8/42, ringed
   and backed in the page's colour (C12). The badge layer lies flat where
   the cards sit, so the "+N" lands 4 in from the unturned front card's
   bottom-right corner, then turns +8° about its own centre (C11). Scoped to
   the detail's heading: the base deck above stays everywhere else, and
   desktop. The title's room beside it is new_reminder.css's. */
@media (max-width: 834px) {
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack {
    top: 0;
    right: 16px;
    width: 96px;
    height: 88px;
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack-card,
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack-badge-layer {
    top: 4px;
    left: 8px;
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack-card {
    border-color: var(--detail-page-bg);
    border-radius: 15.24px;
    background: var(--detail-page-bg);
  }

  /* The card's radius inside its 2px border. */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack-clip {
    border-radius: 13.24px;
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack-front {
    transform: rotate(8deg);
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack-back1 {
    transform: none;
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack-back2 {
    transform: rotate(-8deg);
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack-badge-layer {
    transform: none;
    border-width: 0;
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack-badge {
    transform: rotate(8deg);
  }
}

/* The deck grows to 1.2 on press, on the circle's curve (C10), and pops in
   when media arrive after the page has drawn (`data-pop`, C17): from 0.6
   and -45° to rest after 80 ms, on an overshoot standing in for iOS's
   spring (response 0.35, damping 0.65), with no fade. Neither under
   reduced motion. `backwards` holds the start through the delay and
   drops the animation at its end, so the press's transform still lands. */
@media (max-width: 834px) and (prefers-reduced-motion: no-preference) {
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack {
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack:active {
    transform: scale(1.2);
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack[data-pop] {
    animation: thumb-stack-pop 350ms cubic-bezier(0.34, 1.56, 0.64, 1) 80ms
      backwards;
  }
}

@keyframes thumb-stack-pop {
  from {
    transform: scale(0.6) rotate(-45deg);
  }
  to {
    transform: none;
  }
}

/* ============================================================
   Inline "Audio transcript:" block — standalone voice notes on the
   unified owner view (between the heading container and the body editor).
   ============================================================ */

.detail-audio-transcript {
  padding: var(--sp-sm) var(--sp-xl) 0;
}

.detail-audio-transcript-label {
  margin: 0 0 var(--sp-3xs);
  font-size: 13px;
  font-weight: 500;
  color: var(--label-secondary);
}

.detail-audio-transcript-text {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--label-secondary);
  white-space: pre-wrap;
}

/* ============================================================
   Fullscreen media viewer (voice notes + videos) — web port of iOS's
   VoiceNoteViewer / MediaViewer. Forced-dark surface like the image
   lightbox (.mo-lightbox); sits above the modal system (900), below
   the chat lightbox (1000).
   ============================================================ */

.media-viewer {
  position: fixed;
  inset: 0;
  z-index: 950;
  display: flex;
  flex-direction: column;
  background: #0b0b0d;
  color: #fff;
  animation: mo-lb-in 0.3s ease-in-out;
}

/* ── top bar ── */

.media-viewer-top {
  display: flex;
  align-items: center;
  padding: 12px 8px 8px;
  flex-shrink: 0;
}

.media-viewer-top-spacer { flex: 1; }

/* Uploaded/captured file name + extension, centered between the flex
   spacers (the ghost below balances a hidden ⋯ side). */
.media-viewer-title {
  max-width: 60%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-sm);
  font-weight: 500;
  color: rgba(255, 255, 255, 0.85);
}

/* Same footprint as an icon button — keeps the title centered when the
   right side has no ⋯ menu (local previews). */
.media-viewer-top-ghost {
  width: 44px;
  height: 44px;
  flex-shrink: 0;
}

.media-viewer-icon-btn {
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: var(--radius-full);
  color: #fff;
  cursor: pointer;
  transition: background 120ms ease;
}
.media-viewer-icon-btn:hover { background: rgba(255, 255, 255, 0.08); }

/* ── pager (dots / chevrons / image page) ── */

.media-viewer-image {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* Animated page container: the keyed child remounts on page change, so
   the slide-in replays; direction class picks the entry side. */
.media-viewer-pagewrap {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.media-viewer-page {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.media-viewer-pagewrap--fwd > .media-viewer-page {
  animation: mv-page-fwd 240ms cubic-bezier(0.25, 0.1, 0.25, 1);
}
.media-viewer-pagewrap--back > .media-viewer-page {
  animation: mv-page-back 240ms cubic-bezier(0.25, 0.1, 0.25, 1);
}
@keyframes mv-page-fwd {
  from {
    transform: translateX(48px);
    opacity: 0.3;
  }
  to {
    transform: none;
    opacity: 1;
  }
}
@keyframes mv-page-back {
  from {
    transform: translateX(-48px);
    opacity: 0.3;
  }
  to {
    transform: none;
    opacity: 1;
  }
}

/* Bottom pagination dots (iOS MediaViewer parity). */
.media-viewer-dots {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-sm);
  padding: var(--sp-md) 0;
  flex-shrink: 0;
}
.media-viewer-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  border: none;
  padding: 0;
  background: rgba(255, 255, 255, 0.35);
  cursor: pointer;
}
.media-viewer-dot--current {
  background: #ffffff;
}

/* Desktop-only hover chevrons at the viewer edges. */
.media-viewer-chevrons {
  display: none;
}
@media (min-width: 835px) {
  .media-viewer-chevrons {
    display: block;
  }
  .media-viewer-chevron {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: var(--radius-full);
    background: rgba(118, 118, 128, 0.28);
    color: #ffffff;
    cursor: pointer;
    opacity: 0;
    transition: opacity 120ms ease;
    z-index: 2;
  }
  .media-viewer:hover .media-viewer-chevron {
    opacity: 1;
  }
  .media-viewer-chevron--prev { left: var(--sp-md); }
  .media-viewer-chevron--next { right: var(--sp-md); }
}


/* Delete-attachment confirm layer above the viewer (shared
   ui/confirm_modal markup under the media-viewer-confirm prefix). */
.media-viewer-confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
}
.media-viewer-confirm-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
}
.media-viewer-confirm-dialog {
  position: relative;
  width: min(360px, calc(100% - 2 * var(--sp-xl)));
  padding: var(--sp-lg);
  border-radius: var(--radius-lg);
  background: #2c2c2e;
  color: #ffffff;
}
.media-viewer-confirm-body {
  margin: 0 0 var(--sp-md);
  font-size: var(--text-base);
}
.media-viewer-confirm-actions {
  display: flex;
  gap: var(--sp-sm);
  justify-content: flex-end;
}
.media-viewer-confirm-btn {
  padding: var(--sp-xs) var(--sp-lg);
  border: none;
  border-radius: var(--radius-full);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  background: rgba(118, 118, 128, 0.24);
  color: #ffffff;
}
.media-viewer-confirm-delete {
  background: var(--accent-red);
}

/* ── centered media ── */

.media-viewer-center {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  padding: 0 16px;
}

.media-viewer-canvas {
  width: min(320px, 78vw);
  height: 64px;
  cursor: pointer;
  touch-action: none; /* pointer-capture drag scrubbing */
}

.media-viewer-video {
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--radius-sm);
}

/* ── playback bar + transcript ── */

.media-viewer-lower {
  flex-shrink: 0;
  width: min(560px, 100%);
  margin: 0 auto;
  padding: 0 24px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.media-viewer-playbar {
  display: flex;
  align-items: center;
  gap: 12px;
}

.media-viewer-play {
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: #fff;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}

.media-viewer-progress {
  flex: 1;
  height: 4px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.25);
  overflow: hidden;
}

.media-viewer-progress-fill {
  height: 100%;
  border-radius: 999px;
  background: #fff;
  transition: width 0.1s linear;
}

.media-viewer-time {
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, 0.6);
  flex-shrink: 0;
}

.media-viewer-transcript {
  max-height: 32vh;
  overflow-y: auto;
}

.media-viewer-transcript-label {
  margin: 0 0 6px;
  font-size: 13px;
  font-weight: 500;
  color: rgba(255, 255, 255, 0.5);
}

.media-viewer-transcript-text {
  margin: 0;
  font-size: 17px;
  line-height: 1.45;
}

.media-viewer-seg {
  color: rgba(255, 255, 255, 0.45);
  transition: color 120ms ease;
}

.media-viewer-seg--current {
  color: #fff;
  font-weight: 600;
}

/* ── bottom action row (voice) ── */

.media-viewer-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 20px 8px 32px;
  flex-shrink: 0;
  color: rgba(255, 255, 255, 0.6);
}

/* ─── Share picker (app/detail/view_share) in the sheet chrome ──────────────
   The field, the grid and its tiles keep chat.css's .new-chat-* / .share-*
   rules; these re-scope them to the sheet's stack (its 16 gutters and 24
   rhythm do the spacing) and dress the footer's 48 actions. */
.share-picker .new-chat-search {
  margin: 0;
}
.share-picker .people-search-error {
  padding: 0;
}
/* The grid fills the fluid stack (sheet__fill); never shorter than a row
   of tiles while the people load. */
.share-picker__body {
  min-height: 120px;
}
.share-picker .share-grid,
.share-picker .new-chat-contacts {
  margin: 0;
  padding: 0;
  max-height: none;
}
.share-picker .share-empty-bar {
  display: flex;
  justify-content: center;
}
.share-picker .share-picker__primary {
  background: var(--accent);
  color: var(--accent-fg);
}
.share-picker .share-picker__primary:disabled {
  background: var(--bg-secondary);
  color: var(--label-tertiary);
  cursor: not-allowed;
}
.share-picker .share-picker__secondary {
  color: var(--label-primary);
  border: 1px solid var(--border-primary);
}
.share-picker .share-picker__secondary:disabled {
  opacity: 0.5;
  cursor: default;
}

/* ── The desktop panel's AI composer (Messages-parity pill) ─────────────
   The composer reuses chat.css's .chat-composer* classes wholesale; these
   rules only re-scope chat-pane layout assumptions to the panel. Phones
   never draw it: their composer is the AI drawer's (chat_drawer.css). */

/* Chat pane gives .chat-composer grid placement + gutter padding — the
   detail column provides its own spacing. */
.detail-ai-chat .chat-composer {
  padding: 0;
  max-width: none;
}

/* No pill overrides here: the component keeps chat.css's exact chrome
   (including the 2px transparent → colored focus border) so it looks and
   feels identical to the Messages composer, except where the panel makes
   it the mock's white "Chat" card — see the `.detail-ai-panel
   .chat-composer-form` rule in the panel section. */

/* Generic (non image/video) staged file chip inside the preview strip. */
.detail-ai-composer-file {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3xs);
  width: 80px;
  height: 80px;
  padding: var(--sp-xs);
  color: var(--label-secondary);
}

.detail-ai-composer-file-name {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-xs);
}

/* chat.css hardcodes the post-mount empty-state placeholder to
   "Type a message..." — keep the AI composer's own prompt (the pre-mount
   ::before already reads data-placeholder). */
.detail-ai-chat .chat-composer-milkdown .ProseMirror p.is-editor-empty:first-child::before,
.detail-ai-chat .chat-composer-milkdown .ProseMirror:empty::before {
  content: "Chat";
}

/* ── Desktop detail: editor column + Please AI third column ─────────────
   (Web mock 2026-09-24.) The columns wrapper takes over .nr-body's slot
   in the .nr-root flex column; on mobile it degrades to a plain column and
   the panel isn't rendered at all (the chat lives in the AI drawer). */
.nr-detail-columns {
  flex: 1;
  min-height: 0;
  display: flex;
}

.nr-detail-columns > .nr-body {
  flex: 1;
  min-width: 0;
}

/* Desktop-only chrome. `is_mobile()` picks the markup at render time; a
   live resize across 834 swaps it on the next render, and this covers the
   gap. */
@media (max-width: 834px) {
  .nr-detail-columns {
    flex-direction: column;
  }

  .detail-ai-panel,
  .detail-ai-entry,
  .detail-messages-pill {
    display: none;
  }
}

/* ── Please AI button (header action strip's trailing slot) ─────────────
   Closed (mock3): white disc, hairline edge, grey agent glyph. Open
   (mock4): black glyph in a static pastel halo, no edge (the halo gives
   it). Working (the agent latch is composing, open or closed): the halo
   breathes (iOS AIActionSurface timings, AIActionCircle.swift:158-193).
   The halo is a plain span, not an .icon, so the blur works. */
.detail-ai-entry {
  position: relative;
  width: 36px;
  height: 36px;
  flex-shrink: 0;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background: none;
  cursor: pointer;
}

.detail-ai-entry-halo {
  position: absolute;
  inset: -2px; /* 40px disc (mock fit: RMS 3.6/255) */
  border-radius: var(--radius-full);
  background: var(--gradient-pastel);
  filter: blur(2px);
  opacity: 0;
  transition: opacity 300ms ease-in-out; /* iOS isLit easeInOut(0.3) */
  pointer-events: none;
}

.detail-ai-entry-disc {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-tertiary); /* mock .06 */
  border-radius: var(--radius-full);
  background: var(--bg-primary);
  color: var(--label-secondary);
  transition: color 150ms ease, border-color 300ms ease-in-out;
}

@media (hover: hover) {
  .detail-ai-entry:hover .detail-ai-entry-disc {
    color: var(--label-primary);
  }
}

.detail-ai-entry[aria-expanded="true"] .detail-ai-entry-halo {
  opacity: 0.7;
}

.detail-ai-entry[aria-expanded="true"] .detail-ai-entry-disc {
  color: var(--label-primary);
}

.detail-ai-entry[aria-expanded="true"] .detail-ai-entry-disc,
.detail-ai-entry--working .detail-ai-entry-disc {
  border-color: transparent;
}

.detail-ai-entry--working .detail-ai-entry-halo {
  animation: detail-ai-entry-breathe 2.6s ease-in-out infinite alternate;
}

@keyframes detail-ai-entry-breathe {
  from {
    opacity: 0.5;
    transform: scale(0.97);
  }
  to {
    opacity: 1;
    transform: scale(1.05);
  }
}

@media (prefers-reduced-motion: reduce) {
  .detail-ai-entry--working .detail-ai-entry-halo {
    animation: none;
    opacity: 1;
    transform: scale(1.05);
  }
}

.detail-ai-entry:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

/* Dark (no mock): --border-tertiary is #1A1A1A, invisible on black, so the
   closed disc takes the next hairline up; the pastel glow, drawn for a
   light panel, reads as a bright band at full strength. */
[data-theme="dark"]
  .detail-ai-entry:not([aria-expanded="true"]):not(.detail-ai-entry--working)
  .detail-ai-entry-disc {
  border-color: var(--border-secondary);
}
[data-theme="dark"] .detail-ai-panel-glow {
  opacity: 0.45;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme])
    .detail-ai-entry:not([aria-expanded="true"]):not(.detail-ai-entry--working)
    .detail-ai-entry-disc {
    border-color: var(--border-secondary);
  }
  :root:not([data-theme]) .detail-ai-panel-glow {
    opacity: 0.45;
  }
}

@media (min-width: 835px) {
  /* ── Main pane chrome (detail only) ──────────────────────────────────
     The mock's detail main pane is white edge to edge — no grey canvas
     (new_reminder.css's desktop .nr-root), no card chrome. */
  .nr-root.nr-root--detail {
    background: var(--bg-primary);
  }

  /* Breathing room around the editor now that the top nav is gone. The
     bottom is reserved for what floats over the pane's bottom corners —
     the accessory rail (52, bottom-left while editing) and the Messages
     pill (58, bottom-right) — so scrolled to the end, a long note's last
     line clears both (prod QA 2026-10-01: they sat on it). .nr-body's own
     bottom margin matches their 24 inset, so this is their height + a gap. */
  .nr-root--detail .nr-detail-columns > .nr-body {
    padding: var(--sp-xl) var(--sp-xl) calc(58px + var(--sp-sm));
  }

  /* Reading column: 692 max, centred; .nr-body's 24px side padding
     (above) keeps the gutters at max(24, (pane - 692) / 2) — 64.6 in
     mock3's 821 pane, 24 in mock4's 490 pane. Card padding-top 20 + body
     padding-top 24 = the title line box at y 44. */
  .nr-root.nr-root--detail .nr-reminder-card {
    width: 100%;
    max-width: 692px;
    margin-inline: auto;
    padding: var(--sp-lg) 0;
    border-radius: 0;
    background: none;
  }

  .nr-root--detail .reminder-heading-container {
    gap: var(--sp-sm); /* title box 84 → action row 96 */
    padding: 0 0 var(--sp-lg); /* row 132 → hairline 152 (mock 151) */
    border-bottom-color: var(--border-secondary); /* mock #E0E0E0 = .12 */
  }

  /* Ring 30 / stroke 2 in a 32 slot (48→80); the title starts at slot +
     12 gap = content-left + 44. */
  .nr-root--detail .reminder-heading-container .nr-completion-circle {
    width: 30px;
    height: 30px;
    margin: 5px 1px 0;
    border-color: var(--label-secondary); /* mock #8E8E8E */
  }

  .nr-root--detail .reminder-heading-container .nr-completion-circle:hover {
    border-color: var(--label-primary);
  }

  .nr-root--detail .reminder-heading-container .nr-completion-circle--done {
    width: 32px;
    height: 32px;
    margin: 4px 0 0;
  }

  /* 24px glyph → the 32 slot. */
  .nr-root--detail
    .reminder-heading-container
    .nr-completion-circle--done
    .icon {
    transform: scale(1.3333);
  }

  /* Flat tracking, as in the mocks (and iOS FontSystem's tracking 0 "for
     Figma parity"): Chromium applies SF Pro's size-specific tracking —
     measured +0.43px a letter at 32, −0.45 at 18, −0.23 at 15 (Apple's
     trak table) — and these letter-spacings cancel it. Segoe UI has no
     such tracking, so Windows renders these runs ~2.5% looser. */
  /* New toDo reads like the page it becomes (running notes: capture text
     styles didn't match detail) — same title and body type. */
  .nr-root .nr-title {
    font-size: var(--text-4xl); /* 32 */
    font-weight: 700;
    line-height: 40px;
    letter-spacing: -0.43px; /* flat tracking, above */
    min-height: 40px;
    max-height: 120px; /* still 3 lines */
  }

  .nr-root--detail .reminder-heading-container .thumb-stack {
    right: 0;
  }

  /* Deck clearance (iOS EditableTitle.swift:226-233, deckHeight 88;
     CaptureThumbnailStack.swift:22-26): the strip starts 8 under the
     deck — 44 + 84 + 12 = 140 = deck top 44 + 88 + 8. */
  .nr-root--detail
    .reminder-heading-container:has(.thumb-stack):has(.nr-action-strip)
    .nr-title-row {
    min-height: calc(88px + var(--sp-xs) - var(--sp-sm));
  }

  /* Card gap 8 + 12 = the body's first line 20 below the hairline. */
  .nr-root--detail .nr-body-editor .ProseMirror {
    padding: var(--sp-sm) 0 var(--sp-lg);
  }

  /* Colour stays --label-special (.detail-body-editor .ProseMirror). */
  .nr-root .detail-body-editor .ProseMirror {
    font-size: var(--text-xl);
    line-height: 25px;
    letter-spacing: 0.45px; /* flat tracking (.nr-title) */
  }

  /* Children that carried the old card's 24px inset. */
  .nr-root--detail .detail-audio-transcript {
    padding-inline: 0;
  }

  .nr-root--detail .nr-body .detail-reminders-list,
  .nr-root--detail .nr-body .detail-action-remind-wrapper {
    padding: 0;
  }

  .nr-root--detail .nr-save-indicator {
    margin-left: calc(-1 * var(--sp-xs));
  }

  /* ── The Please AI third column ──────────────────────────────────────
     Mock4: a flat bg-secondary column — no margin, radius, border or
     shadow. Mounted while closed ([hidden]) so the composer's lazy init,
     scroll and draft survive.

     Its width: --ai-panel-width is the drag-resize override (absolute px,
     persisted), set inline on .nr-root--detail. --ai-panel-w is what
     renders: that width (default 405, mock 405.1), narrowed so the main
     pane keeps 378 — the accessory rail (330) + 24 each side — down to
     the 300 floor. It resolves where it's used (the panel's width, the
     rail and pill insets below), each against the same pane-wide box, so
     all three agree. A drag past the clamp stops growing (the saved width
     still applies once the window is wide enough). */
  .nr-root--detail {
    --ai-panel-w: clamp(
      300px,
      calc(100% - 378px),
      var(--ai-panel-width, 405px)
    );
  }

  .detail-ai-panel {
    width: var(--ai-panel-w);
    flex-shrink: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    position: relative; /* anchors the resize handle and the glow */
    overflow: hidden;
    background: var(--bg-secondary);
    transition:
      margin-right var(--motion-slide) var(--ease-slide),
      visibility 0s;
  }

  /* Closed = slid away right (2026-10-23; was display:none, a cut). The
     panel keeps its width and a negative right margin of that width eases
     it out past the columns' right edge (which clips it), so the main
     column grows into the space while nothing inside the panel reflows;
     then it goes visibility:hidden (out of the tab order and the
     accessibility tree) once the slide ends. Opening runs it back. Still
     mounted either way, as before. A drag turns the transition off
     (body.panel-resizing). */
  .detail-ai-panel[hidden] {
    display: flex;
    margin-right: calc(-1 * var(--ai-panel-w));
    visibility: hidden;
    transition:
      margin-right var(--motion-slide) var(--ease-slide),
      visibility 0s linear var(--motion-slide);
  }

  /* Clips the panel as it slides off the right. `clip`, not `hidden`: no
     scroll container, and vertical overflow stays as it was. */
  .nr-root--detail .nr-detail-columns {
    overflow-x: clip;
  }

  @media (prefers-reduced-motion: reduce) {
    .detail-ai-panel,
    .detail-ai-panel[hidden] {
      transition: none;
    }
  }

  /* Drag-resize handle — invisible 5px strip along the panel's left
     edge; hairline on hover / during a drag. Dragging below the collapse
     threshold closes the panel (detail_msg.AiPanelResizeEnded). */
  .detail-ai-panel-resize-handle {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 5px;
    cursor: col-resize;
    z-index: 10;
  }

  @media (hover: hover) {
    .detail-ai-panel-resize-handle:hover {
      background: var(--border-primary);
    }
  }

  body.panel-resizing .detail-ai-panel-resize-handle {
    background: var(--border-primary);
  }

  /* Inside the panel the chat owns its own scroll: messages flex-grow and
     scroll independently, the composer docks at the panel bottom. */
  /* position: relative, no z-index: the chat paints above the glow by DOM
     order (the glow comes first) without becoming a stacking context. */
  .detail-ai-panel .detail-ai-chat {
    position: relative;
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    margin-top: 0;
    padding: 0 var(--sp-xl) var(--sp-xl); /* composer 24 above the bottom */
    /* The stacked layout's `.nr-root .detail-ai-chat` paints bg-secondary;
       the panel's own surface shows through. */
    background: none;
  }

  /* No title bar in the mock; the header's button names the panel. */
  .detail-ai-panel .detail-ai-chat-title {
    display: none;
  }

  .detail-ai-panel .detail-ai-chat-body {
    flex: 1;
    min-height: 0;
    gap: var(--sp-sm); /* chips → composer 13 */
  }

  /* Top fade (mock ~112px, linear); short threads sit on the composer. */
  .detail-ai-panel .detail-ai-chat-messages {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding-top: var(--sp-xl);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 112px);
    mask-image: linear-gradient(to bottom, transparent 0, #000 112px);
  }

  .detail-ai-panel .detail-ai-chat-messages > :first-child {
    margin-top: auto;
  }

  /* Agent prose ("What would you like to do next?" is agent text). */
  .detail-ai-panel .detail-ai-msg-body {
    font-size: var(--text-xl);
    line-height: 25px;
    letter-spacing: 0.45px; /* flat tracking (.nr-title) */
    color: var(--label-control-primary);
  }

  /* The prose after a card run sits 48 below the last card (mock: card
     bottom 471 → question ink 540.8). */
  .detail-ai-panel .detail-ai-msg:has(.detail-ai-msg-cards) + .detail-ai-msg {
    margin-top: var(--sp-4xl);
  }

  .detail-ai-panel .detail-ai-msg-cards {
    gap: var(--sp-xxl); /* mock ~31 */
  }

  /* Rich cards (chat_card.css), panel only. */
  .detail-ai-panel .chat-card {
    border: none;
    border-radius: var(--radius-xl);
  }

  .detail-ai-panel .chat-card__footer {
    padding: var(--sp-md);
    gap: var(--sp-md);
  }

  /* Line boxes 25 / 20, not the inherited 1.5× (27 / 22.5): iOS
     subHeading / subline (FontSystem.swift) — title top → subtitle top
     28.8 (mock 28.2). */
  .detail-ai-panel .chat-card__title {
    font-size: var(--text-xl);
    font-weight: 500;
    line-height: 25px;
    letter-spacing: 0.45px; /* flat tracking (.nr-title) */
    color: var(--label-control-primary);
  }

  .detail-ai-panel .chat-card__subtitle {
    font-size: var(--text-md);
    line-height: 20px;
    letter-spacing: 0.23px; /* flat tracking (.nr-title) */
  }

  .detail-ai-panel .chat-card__action {
    width: var(--size-5xl);
    height: var(--size-5xl);
    background: var(--accent-blue-elevated);
    color: var(--accent-blue);
  }

  .detail-ai-panel .chat-card__action .icon {
    transform: scale(1.2); /* 20 inline → 24 */
  }

  /* Suggestion chips (suggestion_pills.css), panel only: 56 tall, pitch
     72; question → first chip 16. */
  .detail-ai-panel .suggestion-pills {
    padding-top: 0;
  }

  /* 16 top and bottom around the 25 line (iOS pads 16 all round):
     57 tall, mock 57.6 / 57. The emoji → label gap is the mock's space
     (~5), not suggestion_pills.css's 12. */
  .detail-ai-panel .suggestion-pill {
    min-height: 0;
    padding: var(--sp-md) var(--sp-xl);
    gap: 5px;
  }

  .detail-ai-panel .suggestion-pill-emoji,
  .detail-ai-panel .suggestion-pill-label {
    font-size: var(--text-xl);
    line-height: 25px;
  }

  /* Composer → the mock's white "Chat" card. */
  .detail-ai-panel .chat-composer-form {
    min-height: 90px;
    background: var(--bg-primary);
    border-color: transparent;
    border-radius: var(--radius-xl);
  }

  /* Text hugs the top: the 90 min-height's slack goes under the line,
     not above it (chat.css aligns flex-end) — mock "Chat" ink 21 below
     the card's top edge. */
  .detail-ai-panel .chat-composer-input-wrap {
    align-items: flex-start;
    padding: var(--sp-md) var(--sp-md) 0;
  }

  .detail-ai-panel .chat-composer-milkdown {
    min-height: 25px;
    letter-spacing: 0.45px; /* flat tracking (.nr-title) */
  }

  .detail-ai-panel .chat-composer-milkdown:not(:has(.milkdown))::before,
  .detail-ai-panel .chat-composer-milkdown .ProseMirror {
    font-size: var(--text-xl);
    line-height: 25px;
    padding: 0;
    min-height: 25px;
  }

  .detail-ai-panel .chat-composer-toolbar {
    padding: var(--sp-xs) var(--sp-md) var(--sp-sm);
    gap: var(--sp-sm);
  }

  .detail-ai-panel .chat-composer-add,
  .detail-ai-panel .chat-composer-emoji-btn {
    width: 24px;
    height: 24px;
    color: var(--label-secondary);
  }

  .detail-ai-panel .chat-composer-send {
    width: 24px;
    height: 24px;
  }

  .detail-ai-panel .chat-composer-send:disabled {
    background: var(--bg-secondary); /* mock #F2F2F4 */
    color: var(--label-tertiary); /* mock #C4C3C8 */
  }

  /* Static pastel glow under the composer (mock: blue → green at 45% →
     blue; alpha falls off over ~96px). The mock's ends are tealer and
     duller than pure pastel-2, its middle a duller green: each stop mixes
     in the other two pastels (bottom row within ~10/255 of the mock). iOS
     draws it breathing at the top; the mock is still. */
  .detail-ai-panel-glow {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 96px;
    pointer-events: none;
    background: linear-gradient(
      90deg,
      color-mix(
          in srgb,
          var(--pastel-2) 70%,
          color-mix(in srgb, var(--pastel-3), var(--pastel-1))
        )
        0%,
      color-mix(
          in srgb,
          var(--pastel-3) 80%,
          color-mix(in srgb, var(--pastel-2), var(--pastel-1))
        )
        45%,
      color-mix(
          in srgb,
          var(--pastel-2) 70%,
          color-mix(in srgb, var(--pastel-3), var(--pastel-1))
        )
        100%
    );
    -webkit-mask-image: linear-gradient(
      to top,
      #000 0,
      rgba(0, 0, 0, 0.72) 32px,
      rgba(0, 0, 0, 0.28) 58px,
      rgba(0, 0, 0, 0.12) 70px,
      transparent 96px
    );
    mask-image: linear-gradient(
      to top,
      #000 0,
      rgba(0, 0, 0, 0.72) 32px,
      rgba(0, 0, 0, 0.28) 58px,
      rgba(0, 0, 0, 0.12) 70px,
      transparent 96px
    );
  }

  /* ── Accessory rail + Messages pill ──────────────────────────────────
     No card any more: both anchor 24 from the main pane's bottom edge —
     the pill on the right (the pane edge moves left by the panel's live,
     drag-resizable, clamped width while it is open), the rail on the
     left. */
  /* Bottom-LEFT of the main pane, level with the Messages pill on the
     right (running notes 2026-10-01: move it left to make room for
     floating messages) — so it neither stacks over the pill nor shifts
     with the AI panel. */
  .nr-root--detail .acc-rail {
    right: auto;
    left: var(--sp-xl);
    bottom: var(--sp-xl);
    transition: opacity 150ms ease;
  }

  /* The mocks (2026-09-24) show the reading state with no rail: the
     header strip now carries its media and ⋯ (share), leaving checklist
     and dictation, which are body-editing tools. So the rail shows while
     the title or body is being edited, or while it holds focus itself
     (Tab from the editor lands on it and keeps it up); at rest it is
     transparent and click-through. */
  .nr-root--detail:not(
      :has(
        .nr-title:focus,
        .detail-body-editor:focus-within,
        .acc-rail:focus-within
      )
    )
    .acc-rail {
    opacity: 0;
    pointer-events: none;
  }

  .nr-root--detail:has(.detail-ai-panel:not([hidden])) .detail-messages-pill {
    right: calc(var(--ai-panel-w) + var(--sp-xl));
  }

  /* The pill rides along with the panel's slide. */
  .detail-messages-pill {
    transition: right var(--motion-slide) var(--ease-slide);
  }

  @media (prefers-reduced-motion: reduce) {
    .detail-messages-pill {
      transition: none;
    }
  }

  /* Mock: 220×58 capsule — chat glyph, bold "Messages", the member pile;
     hairline edge + a soft float shadow. */
  .detail-messages-pill {
    position: absolute;
    right: var(--sp-xl);
    bottom: var(--sp-xl);
    z-index: 5;
    display: inline-flex;
    align-items: center;
    gap: var(--sp-3xs);
    height: 58px;
    padding: 0 var(--sp-md);
    border: 1px solid var(--border-float);
    border-radius: var(--radius-full);
    background: var(--bg-primary);
    box-shadow: var(--shadow-float);
    color: var(--label-primary);
    font: inherit;
    cursor: pointer;
  }

  .detail-messages-pill-label {
    font-size: var(--text-xl);
    font-weight: 700;
    line-height: 25px;
    letter-spacing: 0.45px; /* flat tracking (.nr-title) */
  }

  .detail-messages-pill-faces {
    display: inline-flex;
    margin-left: var(--sp-lg);
    --pile-overlap: 14px; /* mock pitch 9.6 */
  }

  @media (hover: hover) {
    .detail-messages-pill:hover {
      background: var(--bg-secondary);
    }

    .detail-messages-pill:hover .shared-pile {
      --pile-ring: var(--bg-secondary);
    }
  }

  .detail-messages-pill:focus-visible {
    outline: 2px solid var(--accent-blue);
    outline-offset: 2px;
  }
}

/* ── Save indicator (bottom of the editor card) ──────────────────────────
   The old header ✓'s replacement: quiet "Saved ✓" at rest, "Saving…"
   while a write is in flight (check pulses), tappable "Save" while
   drafts are dirty. Bottom-LEFT of the card: the accessory rail floats
   over the bottom-right corner, and the two must not overlap. The left
   margin aligns the pill's text with the editor's var(--sp-xl) content
   inset (minus the pill's own horizontal padding). */
.nr-save-indicator {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3xs);
  align-self: flex-start;
  margin-top: var(--sp-sm);
  margin-left: calc(var(--sp-xl) - var(--sp-xs));
  padding: 2px var(--sp-xs);
  border: none;
  border-radius: var(--radius-full);
  background: none;
  font: inherit;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--label-secondary);
}

.nr-save-indicator-check {
  display: inline-flex;
  color: var(--accent-green);
}

.nr-save-indicator--saving .nr-save-indicator-check {
  animation: nr-save-pulse 1s ease-in-out infinite;
}

@keyframes nr-save-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.3;
  }
}

.nr-save-indicator--dirty {
  color: var(--label-secondary);
  cursor: pointer;
}

.nr-save-indicator--dirty .nr-save-indicator-check {
  color: var(--label-tertiary);
}

@media (hover: hover) {
  .nr-save-indicator--dirty:hover {
    background: var(--fill-elevated-quaternary);
    color: var(--label-primary);
  }
}

.nr-save-indicator--failed {
  color: var(--accent-red);
  cursor: pointer;
}

/* Phones (iOS LiveFeedDetailView, 2026-10-23): back and ⋯ on top, no
   title. An owner's toDo scrolls under the header (it's absolute over
   `.nr-root`); a read-only or refused toDo stacks it above, and the
   desktop header (with its arrow and drawer toggle) goes. */
.detail-menu-wrapper--phone {
  display: none;
}

/* The card's tap catcher (spec §4c.9, ruling C15) is the phone's alone: the
   phone block below draws it. */
.detail-tap-catcher {
  display: none;
}

@media (max-width: 834px) {
  /* The top: the content starts 12 under the nav bar (spec §4c.1), the
     title box at B + 12. The bottom: the AI drawer's clearance (spec
     §4a.15), room down to the resting docked pill's top, the 88 card and
     its gap above the glass, measured from the shell's content box (the
     glass less the bottom safe area). It reads the <html> gap, never the
     moving card, so the page holds still while the drawer moves or grows. */
  :is(.nr-root--detail, .nr-root--capture) .nr-body {
    padding-top: calc(env(safe-area-inset-top) + 44px + 12px);
    padding-bottom: max(
      0px,
      calc(88px + var(--drawer-docked-gap, 22px) - env(safe-area-inset-bottom))
    );
  }

  /* The page's own dropdowns and its toast stay above the drawer (z 25):
     the outside-click catcher, then the reminder picker and the chip menu
     over it. The toast floats 12 above the docked card. */
  .detail-backdrop {
    z-index: 30;
  }

  .detail-reminder-picker,
  .detail-reminder-chip-menu {
    z-index: 31;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-nav-toast {
    z-index: 31;
    bottom: calc(
      max(0px, 88px + var(--drawer-docked-gap, 22px) - env(safe-area-inset-bottom)) +
        12px
    );
  }

  /* The card's children 12 apart (spec §4c.9): the hairline to the file
     chips or the notes. Positioned and isolated for the tap catcher, which
     lies behind all of them ("The tap catcher" below). Scoped: /new, share
     and join use the card too. */
  :is(.nr-root--detail, .nr-root--capture) .nr-reminder-card {
    position: relative;
    isolation: isolate;
    gap: 12px;
  }

  .detail > .detail-header {
    display: none;
  }

  .detail-menu-wrapper--phone {
    display: block;
    position: absolute;
    z-index: 30;
    top: calc(env(safe-area-inset-top) + 44px);
    right: 16px;
  }

  /* ── The notes, as iOS sets them (spec §4b.8) ─────────────────────────
     Every rule is scoped .nr-root--detail: /new's mount carries
     .detail-body-editor too and keeps today's look, and this specificity
     beats the desktop rules above without touching them. */

  /* "Add notes" (the copy rides in as --notes-ph on the mount) in UIKit's
     tertiaryLabel. Desktop keeps "toDo" (new_reminder.css). */
  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor .ProseMirror p.is-editor-empty:first-child::before,
  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor .ProseMirror:empty::before {
    content: var(--notes-ph);
    color: var(--label-tertiary-system);
  }

  /* Headings: 4 below, nothing above. The air above is the blank row
     before a heading, kept as an empty paragraph. */
  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor .ProseMirror :is(h1, h2, h3, h4, h5, h6) {
    margin: 0 0 4px;
  }

  /* Lists share one 34px gutter: a 2 lead-in, a 20 marker zone, a 12 gap.
     The marker is drawn in it from the item's data-label ("•", "1.") rather
     than the browser's, which sits where the browser wants it. Items sit 8
     apart as 4 of padding each side, which doesn't collapse; a nested list
     steps in 20 (margin -14 under its parent's 34). */
  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor .ProseMirror :is(ul, ol) {
    list-style: none;
    padding-left: 0;
  }

  /* Longhands, not the `padding` shorthand: a checklist item ties this
     rule's specificity (0,3,1) with the desktop task rule and this one comes
     later, so a shorthand would zero the task's 28px right gutter and run its
     text under the drag handle. */
  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor .ProseMirror li {
    position: relative;
    padding-block: 4px;
    padding-left: 34px;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor .ProseMirror li p {
    margin: 0;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor .ProseMirror li :is(ul, ol) {
    margin: 0 0 0 calc(20px - 34px);
  }

  /* A parent's 4 after sit under its nested list instead (that list's last
     item carries them), and its text keeps them as the gap to the first
     nested item. */
  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor .ProseMirror li:has(:is(ul, ol)) {
    padding-bottom: 0;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor .ProseMirror li p:has(+ :is(ul, ol)) {
    margin-bottom: 4px;
  }

  /* A bullet that shares a list with checklist items keeps the same gutter
     (the desktop rule gives it a disc and a 20px margin). */
  :is(.nr-root--detail, .nr-root--capture)
    .detail-body-editor
    .ProseMirror
    ul:has(li[data-item-type="task"])
    > li:not([data-item-type="task"]) {
    margin-left: 0;
    list-style: none;
  }

  :is(.nr-root--detail, .nr-root--capture)
    .detail-body-editor
    .ProseMirror
    li:not([data-item-type="task"])::before {
    content: attr(data-label);
    position: absolute;
    left: 0;
    top: 4px;
    /* The first line's height, so the marker sits on its line. */
    line-height: 1lh;
    pointer-events: none;
    user-select: none;
    /* A bullet: 1.4× the body, centred 12 into the gutter (a 24 box). */
    width: 24px;
    text-align: center;
    font-size: 1.4em;
  }

  /* A number: right-aligned to 22, at the body's size. The box's right edge
     is anchored (the li is its containing block) and it is as wide as its
     text: a fixed 22 box would start-align a wider "10." and push it past
     22. */
  :is(.nr-root--detail, .nr-root--capture)
    .detail-body-editor
    .ProseMirror
    ol
    > li:not([data-item-type="task"])::before {
    left: auto;
    right: calc(100% - 22px);
    width: max-content;
    white-space: nowrap;
    text-align: right;
    font-size: 1em;
  }

  /* The checklist box: 20×20 at 2, in --label-special like the markers.
     (The pinned desktop blocks above stay as they are.) */
  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor .ProseMirror .pm-task-check {
    left: 2px;
    background-color: var(--label-special);
  }

  /* ── Saved goes (spec §4b.10) ─────────────────────────────────────────
     iOS shows no save line. A failed save stays: web has no other surface
     for it. */
  :is(.nr-root--detail, .nr-root--capture) .nr-save-indicator:not(.nr-save-indicator--failed) {
    display: none;
  }

  /* ── File rows (spec §4c.9, ruling C13) ───────────────────────────────
     iOS FileAttachmentRow: the first row 12 under the hairline (the card's
     gap, so no padding of its own on top), rows 4 apart in the 16 gutters,
     each full width on --bg-secondary. Desktop's clip and whole name step
     aside for the row (ui/file_chip draws both). */
  :is(.nr-root--detail, .nr-root--capture) .detail-file-chips {
    padding: 0 16px;
    gap: 4px;
  }

  :is(.nr-root--detail, .nr-root--capture) .file-chip {
    padding: 12px;
    border: none;
    border-radius: 12px;
    background: var(--bg-secondary);
  }

  :is(.nr-root--detail, .nr-root--capture) .file-chip > .icon,
  :is(.nr-root--detail, .nr-root--capture) .file-chip > .file-chip-info {
    display: none;
  }

  /* A 44 tile in the brand's 12% blue holding the 24 glyph in its blue,
     then 12, then the name over the size. */
  :is(.nr-root--detail, .nr-root--capture) .file-chip-row {
    display: flex;
    flex: 1;
    align-items: center;
    gap: 12px;
    min-width: 0;
  }

  :is(.nr-root--detail, .nr-root--capture) .file-chip-tile {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 8px;
    background: var(--brand-blue-12);
    color: var(--brand-blue);
  }

  :is(.nr-root--detail, .nr-root--capture) .file-chip-text {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
  }

  /* Up to 2 lines. ui/file_chip has already cut the middle of a long name,
     its extension kept; a name with nowhere to break still wraps. */
  :is(.nr-root--detail, .nr-root--capture) .file-chip-row-name {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    overflow-wrap: anywhere;
    font-size: 16px;
    font-weight: 600;
    line-height: 20px;
    color: var(--label-primary);
  }

  :is(.nr-root--detail, .nr-root--capture) .file-chip-row-size {
    font-size: 13px;
    line-height: 18px;
    /* Tertiary in light; --label-special in dark until tertiary's dark
       value is updated (tokens.css, --file-size-text). */
    color: var(--file-size-text);
  }

  /* The whole name, which names the chip in place of the cut one (that one
     is aria-hidden): kept in the accessibility tree, drawn nowhere. */
  :is(.nr-root--detail, .nr-root--capture) .file-chip-row-full-name {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* The bookmark's source link and the voice note's transcript (ruling
     C14): in the 16 gutters, the link shrink-wrapped so the room beside it
     falls to the tap catcher. */
  :is(.nr-root--detail, .nr-root--capture) .detail-bookmark-link {
    align-self: flex-start;
    margin-left: 16px;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-audio-transcript {
    padding-inline: 16px;
  }

  /* ── The notes' column (spec §4c.9, ruling C15) ───────────────────────
     18 / 25.2 in --label-special, the text from x 21 (16 + UITextView's 5 of
     line padding) to 21 from the right, the first line 12 (the card's gap)
     + 6 under the hairline or the last chip. iOS hangs a line's extra
     height below the glyphs, where CSS splits it above and below, so a 6
     (not the editor's 8) puts the first line's ink where iOS draws it
     (Julian, 2026-10-25). No tracking: iOS keeps the face's own on the
     body.
     The size goes on the mount: base.css's phone floor sets `.ProseMirror {
     font-size: max(16px, 1em) !important }`, so an 18 on .ProseMirror loses,
     while .milkdown (`all: unset`) inherits the mount's 18 and the floor's
     1em resolves to it.
     The 168 at the bottom is the trailing room, the editor's 8 plus the
     spec's 160: a tap below the last line lands in the editor and puts the
     caret at the end. (0,3,0) beats detail.css's and new_reminder.css's
     ProseMirror rules. */
  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor {
    font-size: 18px;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-body-editor .ProseMirror {
    line-height: 25.2px;
    padding: 6px 21px 168px;
    letter-spacing: 0;
    color: var(--label-special);
  }

  /* ── The tap catcher (spec §4c.9, ruling C15) ─────────────────────────
     A clear layer over the whole card, behind all of its children (the
     heading too): at z-index -1 inside the card (positioned and isolated
     above, with its gap) it paints over the card's background and under
     every in-flow child, so it takes only taps on the card's own empty
     space (the 12 under the hairline or the chips, the room beside the
     source link or the transcript), which put the caret at the end of the
     notes. No child needs a z-index or a position for it. Nothing in the
     card has to rise above the page: the page's dropdowns that do (z 30, 31
     above) render for non-editors, who never get this card. No tap
     highlight: iOS Safari would flash the whole card grey. */
  :is(.nr-root--detail, .nr-root--capture) .detail-tap-catcher {
    display: block;
    position: absolute;
    inset: 0;
    z-index: -1;
    -webkit-tap-highlight-color: transparent;
  }
}

/* Public (unauthenticated) item page — /item/<id> for anonymous visitors.
   The content column reuses the signed-in detail page's .detail-* classes
   (see detail.css); this file only supplies the page wrapper and the
   sign-in footer link. */

.public-item-page {
  height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg-primary);
}

.public-item-page .detail-scroll {
  flex: 1;
  padding-top: var(--sp-xl);
}

.public-item-signin {
  padding: var(--sp-md);
  text-align: center;
  font-weight: 600;
  text-decoration: none;
  color: var(--accent-blue);
  border-top: 1px solid var(--border-primary);
}

/* Shared snapshot page — /s/<token>. */
/* Shared snapshot page — /s/<token>, the SIGNED-IN mount. The content column
   reuses the signed-in detail page's .detail-* classes (see detail.css) plus
   the reminder anatomy's .nr-reminder-card / .reminder-heading-container /
   thumbnail deck (see new_reminder.css); this file supplies only the page
   wrapper, the Duplicate row and the single not-found rendering. The
   signed-out mount wraps the same card in link_page.css's page instead. */

.share-page {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-height: 0;
  background: var(--bg-primary);
}

/* Signed-in mount: `.main-pane` is a BLOCK box (shell.css) with
   `overflow: hidden`, so the `flex: 1` above is inert there — the page would
   collapse to content height, `.detail-scroll` would never be bounded, and
   anything past the fold would be clipped with no document scroller to fall
   back on (base.css puts `overflow: hidden` on html/body). An explicit
   height is what every other main-pane child does (.chat, .search-page). */
.main-pane > .share-page {
  height: 100%;
}

.share-page .detail-scroll {
  flex: 1;
  padding-top: var(--sp-xl);
}

/* Centres only because the page has a resolved height in both mounts —
   `height: 100%` under `.main-pane`, the card's min-height under
   `.link-page-card--unavailable`. Without that these two are no-ops against
   an auto-height box. */
.share-page-unavailable {
  align-items: center;
  justify-content: center;
}

.share-unavailable-text {
  padding: var(--sp-xl);
  text-align: center;
  color: var(--label-secondary);
}

/* The Duplicate action, below the reminder card. Present for every
   visitor (see app/share_view.view) — the button styling itself mirrors
   .auth-submit's filled/muted-disabled treatment; the failure banner reuses
   .people-search-error (chat.css) rather than inventing a new one. */
.share-duplicate-row {
  padding: var(--sp-lg) var(--sp-xl) var(--sp-xxl);
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

.share-duplicate-btn {
  align-self: flex-start;
  padding: var(--sp-xs) var(--sp-lg);
  border: none;
  border-radius: var(--radius-full);
  background: var(--fg);
  color: var(--bg);
  font-size: var(--text-sm);
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.share-duplicate-btn:disabled {
  background: var(--bg-secondary);
  color: var(--label-secondary);
  cursor: not-allowed;
}

/* The body, rendered from markdown into .detail-description (detail.css owns
   the list/heading/link/image rules). Two things the owner's editor gets from
   its ProseMirror inset and this static block would otherwise lack: the
   heading container's horizontal padding, so the text lines up under the
   title rather than at the card edge, and the body reading colour — iOS
   labelSpecial, the same as .detail-description (this rule loads later at
   equal specificity, so it must name the colour itself). */
.share-body {
  padding: var(--sp-lg) var(--sp-xl);
  color: var(--label-special);
}

/* The join page's attribution row sits inside the card above the heading;
   inset it the same way so name, title and body share one left edge. */
.nr-reminder-card > .join-inviter,
.nr-reminder-card > p.join-kicker {
  padding: 0 var(--sp-xl);
}

/* The thumbnail deck hangs off the heading container's top-right, out of
   flow (detail.css pulls .thumb-stack absolute so a short title doesn't get
   an 80px row). On the signed-in page the alert-pill row sits under the
   title and carries the hairline clear of the deck; these read-only pages
   render no pill row, so with a one-line title the hairline landed at
   ~60px and the 80px deck ran straight through it. Reserve the deck's
   height — the box, the few pixels the ±8° fan and its shadow overhang,
   and the container's own bottom padding — so the line waits for it.
   Border-box: the min-height includes that padding, and the hairline is
   the border itself. A two-line title is taller than this and unaffected. */
.share-page .reminder-heading-container:has(.thumb-stack) {
  min-height: calc(80px + 10px + var(--sp-md));
}

/* Collaboration invite page — /join/<token>.

   Almost nothing lives here on purpose. The page is built on the share page's
   skeleton AND its anatomy (.share-page > .detail-scroll > .detail-content >
   .nr-reminder-card, reminder_heading with a thumbnail_stack deck, .share-body
   for the body, .share-duplicate-row / .share-duplicate-btn for the action), so
   the two anonymous link pages read as one product and share every rule that
   governs the card, the paddings, the scroll behaviour and the button.

   What follows is only what the share page has no equivalent for: the
   attribution row (a share is published content; an invitation comes from a
   person). There was an expiry line here too until collab links stopped
   expiring (2026-09-09); nothing replaced it. */

.join-inviter {
  margin-bottom: var(--sp-sm);
}

/* The line under the inviter's name, and the whole prompt on its own when the
   owner row could not be read — join_view.view_inviter renders it either way,
   so this must read correctly standing alone as well as stacked. */
.join-kicker {
  color: var(--label-secondary);
  font-size: var(--text-sm);
  line-height: 1.3;
}

/* Standalone: it is a <p> rather than a <span>, and carries the spacing the
   author pill would otherwise have supplied. */
p.join-kicker {
  margin: 0 0 var(--sp-sm);
}

/* The iOS-only action row. On iOS this page is only ever seen by a visitor
   WITHOUT the app — an installed one takes the universal link before Safari
   loads — so the store leads and signing in is demoted. Both reuse
   .share-duplicate-btn; these rules only re-weight it. */

/* The iOS row is a STACK of three — action, explanation, alternative — where
   every other use of .share-duplicate-row holds a single button. Its
   content-width `align-self: flex-start` and one flat `--sp-xs` gap are tuned
   for that single button, and applied to three items they read as a cramped
   ragged pile on a phone. So the two actions stretch to the column and the
   sentence between them gets its own margins. */

/* An <a> needs the box back that <button> gave for free. */
.join-install-btn {
  align-self: stretch;
  display: block;
  text-align: center;
  text-decoration: none;
}

/* Muted fill rather than a new outline treatment: it's the same pill the
   disabled state already uses, so the page grows no new button vocabulary.
   Stretched to match the primary above it — two actions of different widths
   stacked left-aligned is what made this look unfinished. */
.join-secondary-btn {
  align-self: stretch;
  text-align: center;
  background: var(--bg-secondary);
  color: var(--fg);
}

/* Sits BETWEEN the two actions, so it needs air on both sides rather than the
   row's single gap — see join_view.view_join_action. */
.join-after-install {
  margin: var(--sp-xs) 0;
  color: var(--label-secondary);
  font-size: var(--text-sm);
  line-height: 1.4;
}

/* The signed-out chrome around both link pages — see app/link_page. After
   share.css and join.css: it re-flows their card inside its own scroller. */
/* The signed-out link page — /s/<token> and /join/<token> for a visitor with
   no account (app/link_page; Julian's mock, 2026-10-21).

   Three stacked pieces on the brand's pastel glow: the bar (sender, Log in,
   Sign up), the white card holding the same .nr-reminder-card the signed-in
   mount shows, and the wordmark. The card hugs its content at every width
   (Julian, 2026-10-27: no explicit height on the reminder or its container);
   the composer is the card's foot, sticky, so it floats over the body while
   a long one scrolls and rests at the card's end once it has. Desktop
   follows the mock at 1470 (a 620 card, 32 radius); under 835 the card runs
   edge to edge.

   The page is its own scroller: base.css puts overflow: hidden on html/body,
   so without this the document would never scroll and a long body would be
   clipped. Everything the card contains is the share page's anatomy
   (share.css, join.css, new_reminder.css); the rules under .link-page only
   re-flow it inside this scroller. */

.link-page {
  height: 100dvh;
  overflow-y: auto;
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  color: var(--label-primary);
  /* The glow: pink left of centre, blue right of it, both fading out by
     ~220px — a wash over the page colour, not a band with an edge. */
  background:
    radial-gradient(60% 220px at 38% -40px, var(--link-page-glow-1), transparent 100%),
    radial-gradient(55% 240px at 70% -60px, var(--link-page-glow-2), transparent 100%),
    var(--bg-secondary);
  background-repeat: no-repeat;
  padding-top: 36px;
  padding-bottom: 44px;
}

/* ── Bar ─────────────────────────────────────────────────────────────── */

.link-page-bar {
  width: 100%;
  max-width: 620px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-md);
}

/* Left side takes the slack; the buttons keep their size. */
.link-page-sender {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  min-width: 0;
}

.link-page-sender-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* iOS callout, semibold. */
.link-page-sender-name {
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.25;
  color: var(--label-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* iOS footnote. */
.link-page-sender-kicker {
  font-size: var(--text-sm);
  line-height: 1.25;
  color: var(--label-secondary);
  /* One line, like the name: on a phone the auth buttons leave the text
     column ~150px, and a wrapped kicker pushes the bar to three lines. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Where the avatar would be when the link carries no person: the wordmark
   in a card-coloured disc, the avatar's size. */
.link-page-sender-mark {
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background: var(--link-page-card);
  box-shadow: var(--shadow-sm);
  color: var(--label-primary);
}

.link-page-auth {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
}

/* Both labels iOS subheadline (15) semibold. Log in is the hollow one:
   12/8 padding, no fill, accent-blue type. */
.link-page-login {
  display: inline-flex;
  align-items: center;
  padding: var(--sp-sm) var(--sp-xs);
  border-radius: var(--radius-full);
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 20px;
  color: var(--accent-blue);
  text-decoration: none;
}

/* The filled pill: 12/32 padding on accent blue, label primary inverse —
   white on light, black on dark (Julian, 2026-10-21). */
.link-page-signup {
  display: inline-flex;
  align-items: center;
  padding: var(--sp-sm) var(--sp-xxl);
  border-radius: var(--radius-full);
  background: var(--accent-blue);
  color: var(--label-inverse-primary);
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 20px;
  text-decoration: none;
}

/* ── Card ────────────────────────────────────────────────────────────── */

.link-page-card {
  position: relative;
  width: 100%;
  height: fit-content;
  max-width: 620px;
  max-height: 820px;
  /* Hugs the reminder and the composer: a short body makes a short card
     with the page's glow under the wordmark, a long one grows the card and
     the page scrolls. It used to fill what the bar and the foot left. */
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  background: var(--bg-primary);
  border-radius: 50px;
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.link-page-card .reminder-heading-container {
  padding: 32px 24px 16px 24px;
  border: none;
}

.link-page-card > .nr-reminder-card {
  flex: 0 0 auto;
  gap: 0;
  height: fit-content;
  max-height: 820px;
}

.link-page-card > .nr-title {
  font-size: 32px;
  line-height: 124%;
  letter-spacing: 0%;
}

.link-page-card .reminder-heading-container:has(.thumb-stack) .nr-title {
    margin-right: 0px;
}

.link-page-card .nr-title-row {
    position: relative;
}


.link-page-card .nr-title-row::after {
  content: "";
  width: 100%;
  bottom: -17px;
  height: 1px;
  background: var(--border-secondary);
  position: absolute;
}

.link-page-card .nr-completion-circle {
  margin-top: 2px;
  height: 32px;
  width: 32px;
}


.link-page-card .thumb-stack {
  position: relative !important;
  margin-left: 0 !important;
  right: 0 !important;
  border: none;
}


/* The deck hangs off the heading's top-right, out of flow (detail.css). On
   the signed-in share page share.css reserves its height so a one-line
   title's hairline waits for it; the card is outside .share-page here, so
   the same reservation is restated. */
.link-page .reminder-heading-container:has(.thumb-stack) {
  min-height: calc(80px + 10px + var(--sp-md));
}

/* The body reads larger than the signed-in page's 14 — this is the whole
   page, not a pane — and leaves room under its last line so the sticky
   composer never hides it before the card's end scrolls in. */
.link-page .share-body {
  padding: 20px 24px 0px 24px;
  font-size: 18px;
  line-height: 140%;
  height: fit-content;
  padding-bottom: 180px;
  overflow: scroll;
}

/* The iOS-only store row under the body on an invite (join_view.view_install):
   the same pill the old action row used, stretched to the card's text width. */
.link-page-install {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  padding: 0 var(--sp-xl) var(--sp-lg);
}

/* A dead link: the one sentence, centred in a card that keeps its height
   rather than collapsing to a strip. The share page's wrapper carries its
   own background (share.css); the card's shows through instead. */
.link-page-card--unavailable {
  min-height: 40vh;
  justify-content: center;
}

.link-page .share-page-unavailable {
  flex: 1;
  background: transparent;
}

/* ── Composer ────────────────────────────────────────────────────────── */

/* Sticky to the scroller's bottom while the card is longer than the
   viewport; in flow at the card's foot once it is not. The card hugs its
   content, so the foot is right under the body. */
.link-page-composer {
  position: absolute;
  left: 0;
  bottom: 0;
  right: 0;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Mock: the row's bottom 38 above the card's. */
  padding: var(--sp-md) var(--sp-md) var(--sp-3xl);
  background: linear-gradient(180deg, rgba(var(--bg-base), 0%) 0%, var(--bg-primary) 80%);
  border-bottom-right-radius: 50px;
  border-bottom-left-radius: 50px;
}

/* The floating tray, Julian's 358×80 component (2026-10-27): pill and + on
   one slab of glass — .glass-surface (glass.css): the thick material over
   the blur, the mock's tint and rim — with the float shadow composed after
   the rim, so the body text scrolling under it frosts rather than
   disappears (it was a solid of the card colour). 12 over and under, 8 at
   the sides and between; positioned for the hairline it seats. */
.link-page-composer-row {
  position: relative;
  width: min(100%, 358px);
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-sm) var(--sp-xs);
  border-radius: var(--radius-full);
  box-shadow: var(--glass-rim), var(--shadow-float);
}

/* The brand hairline, the pastel run left to right: 60×4, centred in the
   slab's top padding — 4 under the slab's edge, 4 over the pill. It used to
   float 8 above the slab, outside the component (Julian, 2026-10-27). */
.link-page-composer-accent {
  position: absolute;
  top: var(--sp-3xs);
  left: 50%;
  transform: translateX(-50%);
  /* Mock: 60 wide. */
  width: 60px;
  height: var(--size-3xs);
  border-radius: var(--radius-full);
  background: var(--gradient-pastel-horizontal);
}

.link-page-composer-pill {
  flex: 1;
  min-width: 0;
  height: var(--size-5xl);
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: 0 var(--sp-md) 0 var(--sp-xs);
  border-radius: var(--radius-full);
  border: 1px solid var(--border-tertiary);
  /* Its own fill now the slab is translucent: the mock's white pill on the
     frosted slab, the card's colour. */
  background: var(--bg-primary);
  color: var(--label-primary);
  text-decoration: none;
}

.link-page-composer-attach {
  flex-shrink: 0;
  width: var(--size-xxl);
  height: var(--size-xxl);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background: var(--fill-elevated-quaternary);
  color: var(--label-primary);
}

/* Reads as the placeholder it stands in for: primary, like
   .ai-composer-pill-input::placeholder on the real composer. */
.link-page-composer-placeholder {
  flex: 1;
  min-width: 0;
  font-size: var(--text-lg);
  line-height: 1.25;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.link-page-composer-send {
  flex-shrink: 0;
  width: var(--size-5xl);
  height: var(--size-5xl);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background: var(--accent-blue);
  /* Same ink as the Sign up pill: the two blue controls flip together. */
  color: var(--label-inverse-primary);
  text-decoration: none;
}

/* ── Foot ────────────────────────────────────────────────────────────── */

.link-page-foot {
  flex-shrink: 0;
  display: flex;
  justify-content: center;
  padding: var(--sp-xl) 0 var(--sp-3xl);
  color: var(--label-primary);
}

/* ── Phone ───────────────────────────────────────────────────────────── */

@media (max-width: 834px) {
  .link-page {
    align-items: stretch;
  }

  /* Mock: a 36 avatar 16 under the top, the card 16 under it, and 12 of
     side gutter — tighter than the desktop bar because the two buttons
     leave the text column ~140px, and the invite kicker needs all of it. */
  .link-page-bar {
    max-width: none;
    padding: var(--sp-md) var(--sp-sm);
  }

  .link-page-sender {
    gap: var(--sp-xs);
  }

  .link-page-sender .avatar,
  .link-page-sender-mark {
    width: var(--size-3xl) !important;
    height: var(--size-3xl) !important;
  }

  .link-page-sender-name {
    font-size: var(--text-md);
  }

  .link-page-sender-kicker {
    font-size: var(--text-xs);
  }

  /* The desktop pill's 32 of side padding costs the sender column its
     kicker at 375; the phone keeps 20, and the shorter height. */
  .link-page-signup {
    padding: var(--sp-xs) var(--sp-lg);
  }

  .link-page-login {
    padding: var(--sp-xs);
  }

  /* Edge to edge, rounded at the top only, and no shadow against the
     page it spans. */
  .link-page-card {
    max-width: none;
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    box-shadow: none;
    padding-top: var(--sp-xl);
  }

  /* The mock's phone title: a size up from the desktop card's 28. */
  .link-page .nr-title {
    font-size: var(--text-4xl);
    line-height: 1.25;
  }

  /* On Safari's toolbar rather than 40 above the card's foot, and clear of
     the home indicator where there is one. */
  .link-page-composer {
    padding: var(--sp-sm) var(--sp-sm) calc(var(--sp-md) + env(safe-area-inset-bottom));
  }

  .link-page-composer-row {
    width: 100%;
  }

  .link-page-foot {
    display: none;
  }
}

/* Capture modal — overlay + dialog for the New Capture flow. */

.capture-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}

.capture-modal {
  background: var(--bg-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  width: min(440px, calc(100vw - 48px));
  min-height: 480px;
  max-height: calc(100vh - 48px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  padding: var(--sp-md);
  gap: var(--sp-xl);
}

.capture-modal-body {
  flex: 1;
  display: flex;
  flex-direction: column;
}

/* Header ------------------------------------------------------------------ */
.capture-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.capture-title {
  font-size: 16px;
  font-weight: 400;
  color: var(--label-primary);
  margin: 0;
}

.capture-close {
  background: transparent;
  border: none;
  color: var(--label-primary);
  cursor: pointer;
  padding: 0;
  width: 24px;
  height: 24px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}
.capture-close:hover { background: var(--bg-secondary); }
.capture-close .icon { width: 14px; height: 14px; }

/* Drop zone --------------------------------------------------------------- */
.capture-dropzone {
  position: relative; /* contains the absolutely-positioned file input */
  background: rgba(0, 0, 0, 0.04); /* --bg-quaternary not available in tokens */
  border-radius: var(--radius-md);
  padding: 15px var(--sp-xl);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.capture-dropzone-icon {
  display: flex;
  align-items: center;
  color: var(--label-primary);
}

.capture-dropzone-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
}

/* Nested inside its parent label and sized to cover it fully so the user
   is physically tapping the file input itself — bypasses iOS Safari's
   label-for activation quirks entirely. The label needs position:relative. */
.capture-file-input-hidden {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
  /* font-size: 100px pushes the "Choose File" text off-screen on some
     Android browsers that render it inside the input bounds */
  font-size: 100px;
}

.capture-dropzone-text {
  font-size: 14px;
  font-weight: 400;
  color: var(--label-primary);
  margin: 0;
  text-align: center;
}
.capture-dropzone-link {
  color: var(--accent-blue);
  text-decoration: none;
  cursor: pointer;
}
.capture-dropzone-link:hover {
  text-decoration: underline;
}
.capture-dropzone-formats {
  font-size: 12px;
  color: var(--label-secondary);
  margin: var(--sp-3xs) 0 0;
}

/* Uploads list ------------------------------------------------------------ */
.capture-uploads {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.capture-upload {
  display: flex;
  gap: 12px;
  padding: 12px 14px;
  background: rgba(0, 0, 0, 0.7);
  border-radius: 12px;
  align-items: flex-start;
}

.capture-upload-type-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #ffffff;
  flex-shrink: 0;
  padding-top: 1px;
}

.capture-upload-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.capture-upload-header {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 8px;
  align-items: center;
}

.capture-upload-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: #ffffff;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.capture-upload-remove {
  background: transparent;
  border: none;
  color: rgba(255, 255, 255, 0.5);
  padding: 0;
  width: 18px;
  height: 18px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.capture-upload-remove:hover { color: #ffffff; }

.capture-upload-percent {
  font-size: var(--text-xs);
  color: rgba(255, 255, 255, 0.5);
  font-variant-numeric: tabular-nums;
  min-width: 36px;
  text-align: right;
  flex-shrink: 0;
}
.capture-upload-percent--done {
  color: #22c55e;
}

.capture-upload-bar {
  height: 3px;
  background: rgba(255, 255, 255, 0.2);
  border-radius: var(--radius-full);
  overflow: hidden;
}
.capture-upload-bar-fill {
  height: 100%;
  background: var(--accent-blue);
  transition: width 0.15s ease;
}
.capture-upload-bar-fill--done {
  background: #22c55e;
}

/* Image preview shown in drop mode after a file is selected */
.capture-drop-preview {
  position: relative;
  width: 100%;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-secondary);
  max-height: 240px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.capture-drop-preview-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  max-height: 240px;
  display: block;
}
.capture-drop-preview-change {
  position: absolute;
  bottom: var(--sp-xs);
  right: var(--sp-xs);
  padding: 4px 10px;
  font-size: var(--text-xs);
  font-weight: 500;
  color: #ffffff;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  border-radius: var(--radius-full);
  cursor: pointer;
  user-select: none;
  transition: background 0.15s ease;
}
.capture-drop-preview-change:hover {
  background: rgba(0, 0, 0, 0.75);
}


/* Nav tabs (mode switches) ------------------------------------------------ */
.capture-nav {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-xs);
}

.capture-nav-title {
  font-size: 14px;
  font-weight: 500;
  color: var(--label-tertiary);
  margin: 0;
}

.capture-nav-tabs {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 4px 0;
  gap: var(--sp-lg);
}

.capture-nav-tab {
  background: transparent;
  border: none;
  font: inherit;
  font-size: 16px;
  font-weight: 500;
  color: var(--label-secondary);
  cursor: pointer;
  padding: var(--sp-3xs) var(--sp-sm);
  border-radius: var(--radius-full);
  transition: color 0.12s, background 0.12s;
}
.capture-nav-tab:hover {
  color: var(--label-primary);
}
.capture-nav-tab--active {
  font-weight: 600;
  color: var(--label-primary);
  background: var(--bg-secondary);
}

/* Footer ------------------------------------------------------------------ */
.capture-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-sm);
  padding-top: var(--sp-sm);
  border-top: 1px solid var(--border-primary);
}

.capture-btn {
  padding: var(--sp-xs) var(--sp-lg);
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
  border: 1px solid var(--border-primary);
  font-family: inherit;
}
.capture-btn-cancel {
  background: var(--bg-primary);
  color: var(--label-primary);
}
.capture-btn-cancel:hover {
  background: var(--bg-subtle);
}
.capture-btn-save {
  background: var(--bg-body);
  color: var(--label-primary);
  border-color: var(--border-primary);
}
.capture-btn-save:hover:not(:disabled) {
  background: var(--bg-subtle);
}
.capture-btn-save-disabled {
  background: var(--bg-subtle);
  color: var(--label-secondary);
  border-color: var(--border-primary);
  cursor: not-allowed;
}


/* Mode forms -------------------------------------------------------------- */
.capture-link-form,
.capture-text-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.capture-voice-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.capture-camera-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  align-items: center;
}

/* Desktop webcam viewfinder + shutter ------------------------------------ */
.capture-webcam-wrap {
  width: 100%;
  position: relative; /* needed for the absolute-positioned flip button */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-md);
}

.capture-webcam-viewfinder {
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-md);
  background: #000;
  display: block;
  object-fit: cover;
}

.capture-webcam-shutter {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: 3px solid var(--border-primary);
  background: var(--bg-primary);
  cursor: pointer;
  position: relative;
  flex-shrink: 0;
  transition: border-color 0.12s;
}
.capture-webcam-shutter::after {
  content: "";
  position: absolute;
  inset: 5px;
  border-radius: 50%;
  background: var(--bg-disabled);
  transition: background 0.12s;
}
.capture-webcam-shutter--active::after {
  background: var(--accent-red);
}
.capture-webcam-shutter--active:hover::after {
  opacity: 0.8;
}
.capture-webcam-shutter:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

/* Camera flip button (desktop — overlaid bottom-right of webcam-wrap) ------- */
.capture-webcam-flip {
  position: absolute;
  bottom: var(--sp-md);
  right: var(--sp-md);
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.12s;
  flex-shrink: 0;
}
.capture-webcam-flip:hover { background: rgba(0, 0, 0, 0.65); }
.capture-webcam-flip .icon { color: #fff; }

/* Webcam photo preview + retake ------------------------------------------ */
.capture-webcam-preview {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-md);
}

.capture-webcam-preview-img {
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-md);
  object-fit: cover;
  display: block;
}

.capture-webcam-retake {
  background: transparent;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-full);
  color: var(--label-secondary);
  font: inherit;
  font-size: var(--text-sm);
  padding: var(--sp-xs) var(--sp-lg);
  cursor: pointer;
}
.capture-webcam-retake:hover {
  color: var(--label-primary);
  border-color: var(--label-secondary);
}

.capture-link-form label,
.capture-text-form label {
  font-size: var(--text-xs);
  color: var(--label-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.capture-link-form input,
.capture-text-form input,
.capture-text-content {
  padding: var(--sp-sm);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--label-primary);
  font: inherit;
  outline: none;
}
.capture-link-form input:focus,
.capture-text-form input:focus,
.capture-text-content:focus {
  border-color: var(--bg-secondary);
}

.capture-text-content {
  resize: vertical;
  min-height: 120px;
}

.capture-submit {
  padding: var(--sp-sm);
  border: none;
  border-radius: var(--radius-md);
  background: var(--bg-secondary);
  color: var(--label-secondary);
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
}
.capture-submit:disabled { opacity: 0.4; cursor: not-allowed; }

/* Voice ------------------------------------------------------------------- */

/* Override the shared form layout for voice — needs a tighter gap */
.capture-voice-form {
  gap: var(--sp-sm);
  align-items: center;
}

/* Waveform canvas — fills available width with a fixed height */
.capture-voice-waveform {
  width: 100%;
  height: 160px;
  border-radius: var(--radius-md);
  background: var(--bg-secondary);
  display: block;
  cursor: pointer; /* scrub hint */
}

/* Recording timer */
.capture-voice-timer {
  font-size: var(--text-xl);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
  color: var(--label-primary);
}

/* Controls row — play + record buttons side by side */
.capture-voice-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-sm);
}

/* Round icon button for play/pause */
.capture-voice-play-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1.5px solid var(--border-primary);
  background: var(--bg-primary);
  color: var(--label-primary);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.capture-voice-play-btn:hover {
  background: var(--bg-secondary);
}

/* Big pill record / stop / re-record button */
.capture-voice-record-btn {
  padding: 12px 28px;
  border: none;
  border-radius: var(--radius-full);
  background: #ef4444;
  color: #ffffff;
  font: inherit;
  font-size: var(--text-base);
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s;
}
.capture-voice-record-btn:hover {
  background: #dc2626;
}
/* Pulsing glow while actively recording */
.capture-voice-record-btn--recording {
  animation: voice-record-pulse 1.4s ease-in-out infinite;
}

@keyframes voice-record-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
  50%       { box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); }
}

.capture-voice-hint,
.capture-camera-hint {
  text-align: center;
  font-size: var(--text-xs);
  color: var(--label-secondary);
}

/* Camera file input ------------------------------------------------------- */
.capture-camera-input {
  display: block;
  margin: var(--sp-md) auto;
}

/* Sidebar button variant — used by "+ New Capture" row. ------------------- */
.sidebar-row-button {
  background: transparent;
  border: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  width: 100%;
  text-align: left;
}

/* Link preview card ------------------------------------------------------- */
.capture-link-preview {
  display: flex;
  gap: var(--sp-sm);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-secondary);
  animation: fadeIn 0.15s ease;
}

.capture-link-preview-image-wrap {
  flex-shrink: 0;
  width: 80px;
  height: 80px;
  overflow: hidden;
}

.capture-link-preview-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.capture-link-preview-body {
  flex: 1;
  min-width: 0;
  padding: var(--sp-sm);
  display: flex;
  flex-direction: column;
  gap: 4px;
  justify-content: center;
}

.capture-link-preview-title {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--label-primary);
  margin: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.capture-link-preview-desc {
  font-size: var(--text-xs);
  color: var(--label-secondary);
  margin: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* ─── Keyframe animations ────────────────────────────────────────────── */

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ============================================================================
   Chat feature: thread-view pane (header, messages list, composer).

   Layout: the chat module renders into the main-pane slot in the app shell,
   taking the full available height. The inner content is capped to a
   readable column width and centered.
   ========================================================================= */


   
.chat {
  display: grid;
  /* row 1: header | row 2: messages | row 3: mobile-edit cancel bar (collapses when empty) | row 4: composer */
  grid-template-rows: auto 1fr auto auto;
  /* Inherit height from the .main-pane flex chain rather than setting our
     own dvh value. The shell is sized via --vvh (visualViewport.height) in
     dom.mjs, so this correctly reflects the keyboard-adjusted visible area
     on iOS Safari. */
  height: 100%;
  width: 100%;
  position: relative;
  overflow: hidden;
  background: var(--bg-primary);

}

/* Floating "jump to latest" pill that appears when a new message lands
   while the viewer is scrolled away from the bottom. */
.chat-jump-to-latest {
  position: absolute;
  left: 50%;
  bottom: 72px;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3xs);
  padding: var(--sp-xs) var(--sp-sm);
  background: var(--bg-elevated, var(--bg));
  color: var(--label-primary);
  border: 1px solid var(--border-primary);
  border-radius: 999px;
  font-size: 0.85rem;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  z-index: 2;
}
@media (hover: hover) {
  .chat-jump-to-latest:hover {
    background: var(--bg-hover);
  }
}

/* Floating scroll-to-bottom button — appears when the user scrolls up.
   Right-aligned to the messages column, 56px above the composer. */
.chat-scroll-to-bottom {
  position: absolute;
  right: var(--sp-xl, 24px);
  bottom: 96px;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-primary, var(--bg-secondary));
  color: var(--label-primary);
  border-radius: var(--radius-xs, 8px);
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  z-index: 10;
  padding: 0;
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
.chat-scroll-to-bottom--hidden {
  opacity: 0;
  transform: translateY(4px);
  pointer-events: none;
}
@media (hover: hover) {
  .chat-scroll-to-bottom:hover {
  background: var(--bg-hover, var(--bg-primary));
}
}

/* ── Header ───────────────────────────────────────────────────────────── */

/* The thread header is the standardized ui/page_header component
   (.page-header + slots, shell.css) — back link (desktop) / sidebar
   toggle (mobile) on the left, settings gear (+ group ⋮ menu) on the
   right. Scoped additions: pin it to the .chat grid's first row and keep
   the old .chat-header bottom inset above the messages list (the shared
   .page-header has no bottom padding). */
.chat > .page-header {
  grid-row: 1;
  padding-bottom: var(--sp-md);
}

/* Thread title is a <button> (tap → Chat Settings), not an <a>. Layout
   and typography come from .page-header-title / .page-header-title-link
   (shell.css); this only strips the native button chrome those
   <a>-oriented classes don't cover. */
.chat-header-title-link {
  background: transparent;
  border: none;
  font-family: inherit;
  line-height: inherit;
  padding: 0;
}

/* The settings gear and ⋮ menu buttons get their sizing/color/radius/
   hover from .header-icon-btn; the page-header right slot handles their
   alignment and spacing. */

/* ── Messages ─────────────────────────────────────────────────────────── */

.chat-messages {
  grid-row: 2;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--sp-xl);
  display: flex;
  flex-direction: column;
  gap: var(--sp-xl);
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE/Edge */
  max-width: 800px;
  width: 100%;
  justify-self: center;
}
.chat-messages::-webkit-scrollbar {
  display: none; /* Chrome, Safari, Opera */
}

/* Each message row: avatar column + content stack. The whole row
   reverses for sent messages so the stack hugs the right edge. */
.chat-message-row {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-xs);
  max-width: 70%;
  min-width: 0; /* prevent flex item from overriding max-width with intrinsic size */
}

.chat-message-row.received {
  align-self: flex-start;
}

.chat-message-row.sent {
  align-self: flex-end;
  /* flex-direction: row-reverse; */
}

.chat-message-row.ai-bare {
  align-self: stretch;
  max-width: 100%;
}

/* Desktop (Julian, 2026-10-23): a row never narrows below 375 — with the
   panels open on a thin screen, 70% of the column left bubbles a few
   words wide. min() keeps it inside a column narrower than that; a sent
   row's content keeps to the trailing edge of the wider row. */
@media (min-width: 835px) {
  .chat-message-row {
    min-width: min(375px, 100%);
  }
  .chat-message-row.sent {
    justify-content: flex-end;
  }
}

/* Phones (iOS parity, 2026-10-26): a tap on a bubble opens the action
   sheet (a hold's lift still taps it) and a hold on a reaction pill the
   reaction sheet — Mobile Safari must not also select the words under
   the finger (or, from a held pill, the whole page), raise the loupe, or
   flash the tap wash over the row. Desktop keeps copying from bubbles
   with the mouse. Two things inside the row take selection or the
   callout back: the in-place editor (Edit message mounts it in the
   bubble), which must select and place its caret; and links and images,
   whose hold keeps Safari's preview sheet (Open / Copy Link / Save) —
   without it the lift would click the link instead. */
@media (max-width: 834px) {
  .chat-message-row {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
  }
  .chat-message-row .chat-edit-milkdown {
    -webkit-user-select: text;
    user-select: text;
  }
  .chat-message-row a,
  .chat-message-row img {
    -webkit-touch-callout: default;
  }
}

/* Highlight when the current viewer is @-mentioned: a full-bleed band
   across the whole chat column in the elevated accent, with a solid bar
   in the primary accent on the left edge (Julian, 2026-09-24 — was the
   brand green). Both are the accent tokens, so a personalized accent
   (accent.css) carries through. The band carries the emphasis.
   Geometry: rows are normally capped at 70% width, so the mentioned row
   un-caps to full width and cancels the .chat-messages horizontal
   padding with negative margins (restored as padding so content stays
   aligned with its neighbors). Bubbles keep their own width via
   .chat-message-stack — the row stretching doesn't stretch them. */
.chat-message-row.mentioned-me {
  max-width: none;
  width: calc(100% + 2 * var(--sp-xl));
  align-self: stretch;
  margin-inline: calc(var(--sp-xl) * -1);
  padding-inline: calc(var(--sp-xl) - 3px) var(--sp-xl);
  padding-block: var(--sp-xs);
  border-left: 3px solid var(--accent-blue);
  border-radius: 0;
  background: var(--accent-blue-elevated);
}

/* Sent-side mentions (self-mention edge case) keep right alignment of
   the bubble inside the full-width band. */
.chat-message-row.sent.mentioned-me {
  justify-content: flex-end;
}

/* The row un-capping must not let bubbles grow past their normal
   width — restore the 70% cap at the stack level inside the band. */
.chat-message-row.mentioned-me .chat-message-stack {
  max-width: 70%;
}

/* Bubbles inside the band swap to the elevated fill so they read as a
   surface on top of the accent wash (token themes both modes; no filter —
   filters create a containing block that can mis-anchor bubble popovers). */
.chat-message-row.mentioned-me .chat-message-bubble {
  background: var(--fill-elevated-secondary);
}

.chat-timestamp-header {
  align-self: center;
  font-size: var(--text-xs);
  color: var(--label-secondary);
  padding: var(--sp-sm) 0 var(--sp-3xs);
  text-align: center;
}

/* Fixed-width avatar column. Empty for continuation rows so bubbles
   indent under the group's avatar. */
.chat-message-avatar-slot {
  width: 28px;
  flex-shrink: 0;
  align-self: flex-start;
  display: flex;
  justify-content: center;
}

.chat-message-avatar-ai {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-full);
  background: var(--fg);
  color: var(--bg);
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 600;
}

/* The quote above a reply (iOS ChatReplyQuoteView): an outlined pill
   with no fill of its own, so it reads as a cut-out rather than a card.
   Pill-round at one line, settling at 20px corners when taller. Capped
   narrower than the bubble so it never runs past what it points at. */
.chat-reply-quote {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  max-width: min(100%, 420px);
  margin-bottom: var(--sp-3xs);
  padding: var(--sp-xs) var(--sp-sm);
  border: 0.5px solid var(--border-secondary);
  border-radius: 20px;
  background: none;
  color: var(--label-primary);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.chat-reply-quote:disabled {
  cursor: default;
}
.chat-reply-quote:not(:disabled):hover {
  background: var(--fill-elevated-quaternary);
}
.chat-reply-quote__name {
  font-size: var(--text-xs);
  font-weight: 600;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.chat-reply-quote__text {
  font-size: var(--text-sm);
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
/* Human chats (2026-10-26): the quote leads the run, 12 above the name
   and the bubble (iOS HumanChatLayout.quoteGap); on another member's side
   it stands at 65% so the reply it points at reads first. The AI thread
   keeps the quote under its header at the old 4. */
.chat:not(.chat--ai) .chat-message-stack > .chat-reply-quote {
  margin-bottom: var(--sp-sm);
}
.chat:not(.chat--ai) .chat-message-row.received .chat-message-stack > .chat-reply-quote {
  opacity: 0.65;
}

.chat-reply-quote--deleted .chat-reply-quote__text {
  color: var(--label-secondary);
  font-style: italic;
}

/* A message a quote jumped to flashes briefly once it's on screen. */
.chat-message-row--jumped .chat-message-bubble {
  animation: chatJumpFlash 1.5s ease;
}
@keyframes chatJumpFlash {
  0%, 35% { box-shadow: 0 0 0 3px var(--accent-blue-elevated); }
  100%    { box-shadow: 0 0 0 3px transparent; }
}

/* Stack: tiny timestamp + bubble (and reaction wrapper). */
.chat-message-stack {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: 4px;
}

.chat-message-row.sent .chat-message-stack {
  align-items: flex-end;
}

.chat-message-tiny-time {
  font-size: var(--text-xs);
  color: var(--label-secondary);
  padding: 0 var(--sp-xs);
  /* margin-bottom: var(--sp-3xs); */
}

.chat-message-row.sent .chat-message-tiny-time {
  text-align: right;
}

/* Sender name + tiny time, inline, for the first message of a group (see
   chat.gleam's `header_el`/`show_sender_name`) — both received messages
   and the viewer's own sent messages. The name owns the row's spacing;
   the nested .chat-message-tiny-time keeps its own rules but no longer
   needs its own bottom margin since the row provides it. */
.chat-message-header-row {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3xs);
  margin-bottom: var(--sp-3xs);
}

.chat-message-header-row .chat-message-tiny-time {
  margin-bottom: 0;
  padding: 0;
}

.chat-message-sender-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--label-primary);
  padding-left: var(--sp-xs);
}

/* Bullet separator between the sender name and the tiny time — same
   footprint as .chat-message-tiny-time itself (exact spec, not just
   inherited), so it reads as part of that same tiny-time treatment. */
.chat-message-header-bullet {
  font-size: var(--text-xs);
  color: var(--label-secondary);
  margin-bottom: 0;
  padding: 0;
}

/* "Edited" suffix — same size/colour as the time, italic only */
.chat-message-tiny-time__edited {
  font-style: italic;
  font-size: inherit;
  color: inherit;
}

/* Human chats (iOS HumanChatLayout, Julian 2026-10-23): another member's
   name over a run's first bubble, and the time row under its last — the
   two share footnote 13/18 at label secondary, sit 4px from the bubble,
   and inset 8px from its own edge (leading received, trailing sent — the
   stack's alignment carries the side). The tiny-time/header rules above
   are Please AI threads' now. */
.chat-message-name,
.chat-message-meta {
  padding: 0 var(--sp-xs);
  font-size: var(--text-sm);
  font-weight: 400;
  line-height: 18px;
  color: var(--label-secondary);
}

/* .chat-message-name {
  margin-bottom: var(--sp-3xs);
} */

/* time • Edited • Leave. Reactions follow below it. */
.chat-message-meta {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3xs);
  /* margin-top: var(--sp-3xs); */
}

.chat-message-meta-edited {
  font-style: italic;
}

.chat-message-meta--editing {
  color: var(--label-primary);
  font-style: italic;
}

/* A GIF's credit at the end of the meta row (iOS 2026-10-20): "View at"
   in the row's own face, the domain a link in accent blue at the same
   type. Human chats only — the overlaid caption on the gif itself is off
   there, so the credit shows once. */
.chat-message-meta-gif-domain {
  color: var(--accent-blue);
  font: inherit;
  text-decoration: none;
}
@media (hover: hover) {
  .chat-message-meta-gif-domain:hover {
    text-decoration: underline;
  }
}
.chat:not(.chat--ai) .chat-att-gif-caption {
  display: none;
}

/* A joined collab toDo's Leave: the row's type, in accent red. */
.chat-message-meta-leave {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--accent-red);
  cursor: pointer;
}

@media (hover: hover) {
  .chat-message-meta-leave:hover {
    text-decoration: underline;
  }
}

.chat-message-bubble {
  padding: var(--sp-sm) var(--sp-md);
  border-radius: var(--radius-lg);
  font: var(--type-body);
  overflow-wrap: anywhere; /* break long URLs/tokens at any character; also affects flex min-content */
  min-width: 0;
}

/* AI threads (daily brief, ephemeral, per-item) read a step larger —
   they're prose surfaces, not bubble chatter. Covers both the agent's
   bare messages and the viewer's sent bubbles in the same thread. */
.chat--ai .chat-message-bubble {
  font-size: var(--text-md);
}

/* Continuation bubbles inside a group sit a touch closer to the
   one above. */
.chat-message-row + .chat-message-row .chat-message-bubble {
  margin-top: var(--sp-3xs);
}

/* Words sent with a shared toDo sit in their own bubble under its card,
   4px apart as iOS stacks them, hugging the words rather than the card's
   width; cards stack 4px apart too. */
.chat-att-collab-card + .chat-att-collab-card,
.chat-att-collab-card + .chat-message-bubble {
  margin-top: var(--sp-3xs);
}
.chat-att-collab-card + .chat-message-bubble {
  width: fit-content;
}
.chat-message-row.sent .chat-att-collab-card + .chat-message-bubble {
  margin-left: auto;
}

/* iOS ChatBubble fills: bubbleReceived #E5E5EA / #2C2C2E,
   bubbleSent #FFFFFF / #000000. */
.chat-message-row.received .chat-message-bubble {
  border-top-left-radius: var(--radius-sm);
  background: var(--fill-elevated-quaternary);
  color: var(--label-primary);
}

.chat-message-row.sent .chat-message-bubble {
  border-top-right-radius: var(--radius-sm);
  background: var(--accent-blue-elevated);
  color: var(--accent-blue);
}

/* AI bare: no bubble background; full-width content. Avatar slot
   stays so the row keeps its left-column alignment. */
.chat-message-row.ai-bare .chat-message-stack {
  width: 100%;
}
.chat-message-row.ai-bare .chat-message-bubble {
  padding: 0;
  background: transparent;
  border-radius: 0;
}
.chat-message-row:not(.ai-bare) + .chat-message-row.ai-bare {
  padding-top: var(--sp-md);
}
.chat-message-row.ai-bare .chat-message-text {
  line-height: 1.65;
}
.chat-message-row.ai-bare .chat-message-text p {
  margin: 0 0 var(--sp-sm) 0;
}

.chat-message-ai-bare .chat-message-text ul,
.chat-message-ai-bare .chat-message-text ol,
.chat-message-ai-bare .chat-message-text blockquote,
.chat-message-ai-bare .chat-message-text pre {
  margin-top: -4px;
  margin-right: 0;
  margin-bottom: var(--sp-sm);
  margin-left: 0;
}

/* ── Reactions ───────────────────────────────────────────────────────── */

.chat-message-bubble-wrapper {
  position: relative;
}

.chat-reaction-trigger {
  position: absolute;
  top: -22px;
  /* Anchor to the bubble's LEFT edge so the pill grows RIGHTWARD — the
     open side for received messages (they hug the pane's left). Pinning
     right: -4px grew the pill leftward, so any bubble narrower than the
     pill clipped it against the scroll container's left edge. */
  left: -4px;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-sm);
  border-radius: var(--radius-full);
  background: var(--bg);
  border: 1px solid var(--border-secondary);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  opacity: 0;
  /* Keep the hidden trigger out of tab order and pointer reach so keyboard
     users can't focus an invisible control. */
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.3s ease-in-out;
  color: var(--label-tertiary);
  white-space: nowrap;
}

.reaction-trigger__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  color: inherit;
}

.reaction-trigger__label {
  font-size: 13px;
  line-height: 1;
  color: inherit;
  font-weight: 500;
}

.chat-reaction-trigger:focus-visible {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}
@media (hover: hover) {
  .chat-message-bubble-wrapper:hover .chat-reaction-trigger {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}
}

@media (hover: hover) {
  .chat-reaction-trigger:hover {
  background: var(--bg-subtle);
  color: var(--label-primary);
}
}

/* Sent messages hug the pane's right — mirror the anchor so the pill
   grows LEFTWARD into open space (a left anchor overflowed the right
   edge on bubbles narrower than the pill). */
.chat-message-row.sent .chat-reaction-trigger {
  left: auto;
  right: -4px;
}

/* Hide the trigger whenever the emoji pill is open for this message —
   the pill replaces it rather than overlaying it. */
.chat-message-bubble-wrapper:has(.emoji-picker-pill) .chat-reaction-trigger {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.chat-reaction-picker {
  position: absolute;
  top: -40px;
  left: 0;
  display: flex;
  gap: var(--sp-3xs);
  background: var(--bg);
  border: 1px solid var(--border-secondary);
  border-radius: var(--radius-lg);
  padding: var(--sp-3xs) var(--sp-xs);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
  z-index: 10;
}

.chat-message-row.sent .chat-reaction-picker {
  left: auto;
  right: 0;
}

.chat-reaction-picker-btn {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  font-size: 16px;
  padding: 0;
  transition: background 0.1s ease, transform 0.1s ease;
}

@media (hover: hover) {
  .chat-reaction-picker-btn:hover {
  background: var(--bg-subtle);
  transform: scale(1.2);
}
}

.chat-reaction-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  /* margin-top: var(--sp-3xs); */
}

.chat-message-row.received .chat-reaction-pills {
  justify-content: flex-start;
  padding-left: 8px;
}

.chat-message-row.sent .chat-reaction-pills {
  justify-content: flex-end;
  padding-right: 8px;
}

.chat-reaction-pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xxs);
  padding: var(--size-xxs) var(--size-sm);
  border-color: transparent;
  border-radius: var(--radius-full);
  background: var(--fill-elevated-secondary);
  font-size: var(--text-xs);
  cursor: pointer;
  touch-action: none; /* deliver pointer events immediately — required for long-press */
  /* A held <button> selects the whole page in Mobile Safari unless it
     opts out of selection itself; no loupe, callout or tap wash either —
     at every width, since the hold works wherever the pill does. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.1s ease, border-color 0.1s ease;
}

/* Who reacted (chat.gleam view_reaction_pills): shown after a short
   deliberate hover — the delay is what keeps it from popping on a pointer
   passing over — and at once on keyboard focus. Hides immediately. */
.chat-reaction-names {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 0;
  padding: var(--size-sm) var(--size-md);
  border-radius: 12px;
  background: var(--bg-elevated-primary, var(--bg-primary));
  color: var(--label-primary);
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1.3;
  max-width: 240px;
  width: max-content;
  white-space: normal;
  overflow-wrap: break-word;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.08s ease, visibility 0s linear 0.08s;
  z-index: 10;
  box-shadow: 1px 4px 12px 2px rgba(var(--neutral-black), 0.4);
}

.chat-message-row.sent .chat-reaction-names {
  left: auto;
  right: 0;
}

.chat-reaction-pill:focus-visible .chat-reaction-names {
  opacity: 1;
  visibility: visible;
  transition: none;
}

@media (hover: hover) {
  .chat-reaction-pill:hover .chat-reaction-names {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.12s ease 0.4s, visibility 0s linear 0.4s;
  }
}

.chat-reaction-pill.active {
  background: rgba(var(--color-blue), .2);
  background-blend-mode: var(--blend-mode);
}

@media (hover: hover) {
  .chat-reaction-pill:hover {
  background: var(--fill-elevated-primary);
  border-color: var(--border-secondary);
}
}

@media (hover: hover) {
  .chat-reaction-pill.active:hover {
  background: rgba(var(--color-blue), .4);
  border-color: var(--accent-blue);
}
}

/* BEM-style child spans — `.reaction-pill__emoji` holds the emoji glyph,
   `.reaction-pill__count` holds the numeric count. The pill itself uses
   inline-flex + gap to space them. `tabular-nums` keeps the count from
   jitter-shifting as digits change width. */
.reaction-pill__emoji,
.reaction-pill__count {
  display: inline-flex;
  align-items: center;
  line-height: 1;
}

.reaction-pill__emoji {
  font-size: 16px;
}

.reaction-pill__count {
  font-variant-numeric: tabular-nums;
}

/* ── Composer ─────────────────────────────────────────────────────────── */

.chat-composer {
  grid-row: 4;
  width: 100%;
  max-width: 800px;
  padding: 0 var(--sp-xl) var(--sp-xxl) var(--sp-xl);
  justify-self: center;
  position: relative;
}

/* ── @-mention picker ────────────────────────────────────────────────────── */

.chat-mention-picker {
  position: absolute;
  left: var(--sp-xl);
  right: var(--sp-xl);
  bottom: calc(100% - var(--sp-xxl));
  background: var(--bg-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow);
  max-height: 240px;
  overflow-y: auto;
  z-index: 20;
}

.chat-mention-picker-item {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-md);
  cursor: pointer;
}
@media (hover: hover) {
    .chat-mention-picker-item:hover {
    background: var(--bg-subtle);
  }
}
/* Keyboard-highlighted row (↑/↓ navigation) — elevated secondary fill. */
.chat-mention-picker-item--active {
  background: var(--fill-elevated-secondary);
}
.chat-mention-picker-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.chat-mention-picker-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--label-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.chat-mention-picker-username {
  font-size: var(--text-xs);
  color: var(--label-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* On iOS the home-indicator bar sits below the safe area and overlaps the
   composer. A small bottom margin keeps the form above it. */
.ios .chat-composer {
  margin-bottom: var(--sp-4xs);
}

/* Mobile bottom spacing — two states:
   • Keyboard closed: clear the home indicator (safe-area inset, with a
     sp-md floor for devices without one).
   • Keyboard open (.kb-open, set by the --vvh updater in dom.mjs): the
     keyboard covers the home-indicator zone, but env() does NOT collapse
     — dropping to a slim pad stops the composer floating high above the
     keyboard. */
@media (max-width: 834px) {
  .chat-composer {
    padding-bottom: max(var(--sp-md), env(safe-area-inset-bottom));
  }
  .kb-open .chat-composer {
    padding-bottom: var(--sp-sm);
  }
  .kb-open.ios .chat-composer {
    margin-bottom: 0;
  }
}

/* Inner wrapper — takes up the flex space the textarea used to own */
.composer-inline-input-wrap {
  flex: 1;
  min-width: 0;
  position: relative;
}

/* Override flex: 1 on the textarea so it fills the block wrapper instead */
.composer-inline-input-wrap .chat-composer-input {
  flex: none;
  width: 100%;
}

/* When there's content and the composer is not focused: hide raw markdown
   so only the rendered overlay is visible. Caret-color is also cleared so
   the invisible cursor doesn't flicker when blurred. */
.composer-inline-input-wrap--has-content:not(:focus-within) .chat-composer-input {
  color: transparent;
  caret-color: transparent;
}

/* Rendered markdown overlay — absolutely positioned over the textarea.
   pointer-events: none lets clicks fall through to the textarea, which
   focuses it and triggers :focus-within to switch back to edit mode. */
.composer-inline-preview {
  position: absolute;
  top: 6px;       /* matches textarea padding-top */
  left: 0;
  right: 0;
  pointer-events: none;
  font-size: var(--text-lg);
  line-height: 20px;
  color: var(--label-primary);
  word-break: break-word;
  white-space: pre-wrap;
  overflow: hidden;
  max-height: 250px;
}

/* Strip paragraph margins that marked.parse adds */
.composer-inline-preview p {
  margin: 0;
}

/* Re-enable list bullets/numbers overridden by the base.css `ul, ol { list-style: none }` reset */
.composer-inline-preview ul,
.composer-inline-preview ol {
  padding-left: 1.4em;
}
.composer-inline-preview ul { list-style-type: disc; }
.composer-inline-preview ol { list-style-type: decimal; }

/* Switch to edit mode while the textarea has focus */
.composer-inline-input-wrap:focus-within .composer-inline-preview {
  display: none;
}

/* Unified pill — the form IS the composer box.
   Column layout: [strip?] [input] [toolbar] */
.chat-composer-form {
  display: flex;
  flex-direction: column;
  width: 100%;
  background: var(--bg-primary);
  border: 1px solid var(--border-secondary);
  border-radius: var(--radius-xl);
  transition: border-color 0.15s ease, background 0.15s ease;
  /* No overflow: hidden here — the emoji picker pops above the pill and
     must not be clipped. Corner clipping is handled on inner chips. */
  position: relative;
}

.chat-composer-form.drag-over {
  border-color: var(--bg-secondary);
  background: var(--bg-secondary);
}

/* Emoji picker — anchored above the pill, right-aligned */
.chat-composer-form .emoji-picker {
  position: absolute;
  bottom: calc(100% + var(--sp-sm));
  right: 0;
  width: 50%;
  max-width: none;
}

/* Input row — no background or border, just spacing */
.chat-composer-input-wrap {
  flex: 1;
  position: relative;
  display: flex;
  align-items: flex-end;
  gap: var(--sp-xs);
  padding: var(--sp-sm) var(--sp-sm) 0 var(--sp-sm);
  min-width: 0;
}

.chat-composer-file-input {
  display: none;
}

/* Staged media preview strip — slides in at top of pill when an attachment
   is staged. Horizontal scroll for multiple chips. */
.chat-composer--mediaPreviewStrip {
  display: flex;
  flex-direction: row;
  gap: var(--sp-sm);
  overflow-x: auto;
  padding: var(--sp-sm) var(--sp-sm) 0 var(--sp-sm);
  scrollbar-width: none;
  /* Slide-up entrance animation */
  animation: composerStripIn 180ms ease;
}
.chat-composer--mediaPreviewStrip::-webkit-scrollbar {
  display: none;
}

/* "Replying to: <name> ✕" across the top of the pill (iOS
   ComposerReplyHeader): a header row over a hairline, the ✕ in a small
   filled circle flush with the hairline's end. */
.chat-composer-reply {
  display: flex;
  align-items: center;
  gap: var(--sp-3xs);
  margin: 0 var(--sp-md);
  padding: var(--sp-sm) 0;
  border-bottom: 0.5px solid var(--ui-borders);
  font-size: var(--text-sm);
  min-width: 0;
  animation: composerStripIn 180ms ease;
}
.chat-composer-reply__label {
  color: var(--label-secondary);
  flex-shrink: 0;
}
.chat-composer-reply__name {
  color: var(--label-primary);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.chat-composer-reply__cancel {
  margin-left: auto;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border: none;
  border-radius: 50%;
  background: var(--fill-elevated-secondary);
  color: var(--label-secondary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  cursor: pointer;
}
.chat-composer-reply__cancel:hover {
  color: var(--label-primary);
}

@keyframes composerStripIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Bottom toolbar row — attach | emoji | spacer | send */
.chat-composer-toolbar {
  display: flex;
  flex-direction: row;
  align-items: center;
  padding: var(--sp-xs) var(--sp-xs);
  gap: var(--sp-3xs);
}

.chat-composer-toolbar-spacer {
  flex: 1;
}

/* Attach (+) button — inside the pill, icon-only, no border */
.chat-composer-add {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-full);
  border: none;
  background: transparent;
  color: var(--label-primary);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: color 120ms ease, background 120ms ease;
}
@media (hover: hover) {
  .chat-composer-add:hover {
  background: var(--bg-subtle);
  color: var(--label-primary);
}
}

.chat-composer--mediaPreview {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-primary);
  background: var(--bg-subtle);
  overflow: hidden;
  width: 80px;
  height: 80px;
}

.chat-composer-pending-image-thumb {
  display: block;
  width: 80px;
  height: 80px;
  object-fit: cover;
  border-radius: calc(var(--radius-md) - 1px);
}

/* A staged document (a PDF): no picture to show, so the chip is wider and
   carries the file glyph and the name — the only thing that tells two PDFs
   apart. Height matches the media chips; the ✕ keeps its corner, so the
   name leaves it room. */
.chat-composer--mediaPreview--file {
  width: auto;
  max-width: 200px;
}

.chat-composer-pending-file {
  display: flex;
  align-items: center;
  gap: var(--sp-2xs);
  height: 80px;
  padding: 0 28px 0 var(--sp-sm);
  color: var(--label-primary);
  font-size: var(--text-sm);
}

.chat-composer-pending-file svg {
  flex-shrink: 0;
  color: var(--label-secondary);
}

.chat-composer-pending-file-name {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
  line-height: 1.3;
}

/* Invisible button wrapper around the composer thumbnail — opens lightbox on tap */
.chat-composer-pending-image-preview-btn {
  display: block;
  padding: 0;
  margin: 0;
  border: none;
  background: none;
  cursor: zoom-in;
  width: 100%;
  height: 100%;
  line-height: 0;
}

.chat-composer-pending-image-remove {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-full);
  border: none;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  line-height: 0;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

@media (hover: hover) {
  .chat-composer-pending-image-remove:hover {
  background: rgba(0, 0, 0, 0.75);
}
}

/* Textarea — transparent, no border, fills the pill */
.chat-composer-input {
  flex: 1;
  border: none;
  background: transparent;
  font-family: inherit;
  font-size: var(--text-sm);
  color: var(--label-primary);
  outline: none;
  min-width: 0;
  resize: none;
  max-height: 250px;
  overflow-y: auto;
  line-height: 20px;
  padding: 6px 0;
  margin: 0;
}
.chat-composer-input::placeholder {
  color: var(--label-secondary);
}

/* Emoji button — inside the toolbar row */
.chat-composer-emoji-btn {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--label-primary);
  cursor: pointer;
  padding: 0;
  border-radius: var(--radius-full);
  transition: color 120ms ease, background 120ms ease;
}
@media (hover: hover) {
  .chat-composer-emoji-btn:hover {
  background: var(--bg-subtle);
  color: var(--label-primary);
}
}

/* Send button — inside the toolbar row, right side */
.chat-composer-send {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-full);
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  cursor: pointer;
  transition: background 0.15s ease;
  /* Enabled state */
  background: var(--bg-primary-inverse);
  color: var(--label-inverse-primary);
}

.chat-composer-send:disabled {
  background: var(--fill-tertiary);
  color: var(--label-secondary);
  cursor: default;
}

/* ── Empty / error state (e.g. failed message load) ──────────────────── */

.chat-empty {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--label-secondary);
  font-size: var(--text-sm);
  padding: var(--sp-xl);
}

/* (Header ⋮ dropdown removed — Delete chat lives in the Chat Settings
   sheet's footer, behind a confirmation.) */

/* ── Message attachments ── */

.chat-att {
  border-radius: var(--radius-md);
  overflow: hidden;
  margin-bottom: var(--sp-xs);
}

.chat-att:last-child { margin-bottom: 0; }

/* Image */
.chat-att-image {
  position: relative;
  max-width: 220px;
  overflow: hidden;
}

.chat-att-image-btn {
  display: block;
  padding: 0;
  border: none;
  background: transparent;
  cursor: zoom-in;
  width: 100%;
}

.chat-att-img {
  display: block;
  width: 100%;
  max-width: 100%; /* prevent full-res intrinsic size from breaking out of container */
  height: auto;
  border-radius: var(--radius-md);
}

/* Link unfurl — favicon only. Stream's "image" attachment type resolved to
   a small square icon rather than a real og:image, so it's shown inline
   next to the raw link text instead of as a big media card. */
.chat-att-favicon {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  max-width: 100%;
  text-decoration: none;
}
@media (hover: hover) {
  .chat-att-favicon:hover {
  text-decoration: none;
}
}

.chat-att-favicon-icon {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  object-fit: contain;
  border-radius: var(--radius-sm);
}

.chat-att-favicon-link {
  color: var(--accent-blue);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}
@media (hover: hover) {
  .chat-att-favicon:hover .chat-att-favicon-link {
  text-decoration: underline;
}
}

/* Link unfurl — real og:image preview. The whole card is the image, linked
   to the source; no title/description text (tap the image to navigate). */
.chat-att-image-link {
  display: block;
  max-width: 280px;
}

.chat-att-image-link-img {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-md);
}

/* Link unfurl — animated gif. Same aspect-ratio media treatment as the
   image-link card, plus a "View at: site.domain" caption overlaid at the
   bottom so the source (Tenor, Giphy, etc.) stays visible. */
.chat-att-gif {
  display: block;
  position: relative;
  max-width: 280px;
}

.chat-att-gif-img {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-md);
}

.chat-att-gif-caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: var(--sp-lg) var(--sp-sm) var(--sp-xs);
  background: linear-gradient(to top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  font-size: var(--text-xs);
  pointer-events: none;
}

.chat-att-gif-caption-label {
  font-weight: 400;
  color: var(--label-secondary);
}

.chat-att-gif-caption-domain {
  font-weight: 600;
  color: var(--accent-blue);
}

/* Corner download button shared by image + video attachments. */
.chat-att-download {
  position: absolute;
  top: var(--sp-xs);
  right: var(--sp-xs);
  width: 28px;
  height: 28px;
  border-radius: var(--radius-full);
  border: none;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  opacity: 0;
  transition: opacity 120ms ease;
}

.chat-att-download:focus-visible {
  opacity: 1;
}
@media (hover: hover) {
  .chat-att-image:hover .chat-att-download,
  .chat-att-video:hover .chat-att-download {
  opacity: 1;
}
}

@media (hover: hover) {
  .chat-att-download:hover {
  background: rgba(0, 0, 0, 0.75);
}
}

/* Lightbox overlay for full-size image preview */
.chat-image-preview-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  cursor: zoom-out;
  padding: var(--sp-xl);
}

.chat-image-preview-img {
  max-width: 95%;
  max-height: 95%;
  object-fit: contain;
  border-radius: var(--radius-md);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}

.chat-image-preview-close {
  position: fixed;
  top: var(--sp-md);
  right: var(--sp-md);
  width: 36px;
  height: 36px;
  border-radius: var(--radius-full);
  border: none;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

@media (hover: hover) {
  .chat-image-preview-close:hover {
  background: rgba(255, 255, 255, 0.25);
}
}

/* Video */
.chat-att-video {
  position: relative;
  max-width: 320px;
  overflow: hidden;
}

/* Click-to-preview thumbnail — same shape as .chat-att-image-btn, but with
   a centered play icon signalling it opens the fullscreen video lightbox
   rather than zooming a static image. */
.chat-att-video-btn {
  display: block;
  position: relative;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
  width: 100%;
}

.chat-att-video-thumb {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-md);
  background: #000;
}

.chat-att-video-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 48px;
  height: 48px;
  border-radius: var(--radius-full);
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

.chat-att-placeholder {
  width: 220px;
  height: 160px;
  background: var(--bg-subtle);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--label-tertiary);
}

/* File */
.chat-att-file {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-sm);
  background: var(--bg-subtle);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  font-size: var(--text-xs);
  color: inherit;
  cursor: pointer;
  text-align: left;
  width: 100%;
}

@media (hover: hover) {
  .chat-att-file:hover {
  background: var(--bg-elevated, var(--bg-subtle));
}
}

.chat-att-file-info {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.chat-att-file-name {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-att-file-size {
  color: var(--label-secondary);
}

/* Link preview */
.chat-att-link {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border-primary);
  text-decoration: none;
  color: inherit;
  max-width: 260px;
}
@media (hover: hover) {
  .chat-att-link:hover {
  border-color: var(--border-primary);
  text-decoration: none;
}
}

.chat-att-link-image {
  width: 100%;
  height: 120px;
  object-fit: cover;
}

.chat-att-link-body {
  padding: var(--sp-xs) var(--sp-sm);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.chat-att-link-title {
  font-size: var(--text-sm);
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-att-link-desc {
  font-size: var(--text-xs);
  color: var(--label-tertiary);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Feed content card (Please-specific) */
.chat-att-content {
  display: flex;
  border: 1px solid var(--border-primary);
  text-decoration: none;
  color: inherit;
  max-width: 260px;
  gap: var(--sp-sm);
}
@media (hover: hover) {
  .chat-att-content:hover {
  border-color: var(--border-primary);
  text-decoration: none;
}
}

.chat-att-content-thumb {
  width: 80px;
  height: 80px;
  object-fit: cover;
  flex-shrink: 0;
}

.chat-att-content-thumb-placeholder {
  width: 80px;
  height: 80px;
  background: var(--bg-subtle);
  flex-shrink: 0;
}

.chat-att-content-body {
  padding: var(--sp-xs) var(--sp-xs) var(--sp-xs) 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.chat-att-content-category {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--label-secondary);
}

.chat-att-content-title {
  font-size: var(--text-sm);
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-att-content-likes {
  display: flex;
  align-items: center;
  gap: 3px;
  font-size: 10px;
  color: var(--label-tertiary);
}

/* Message text alongside attachments */
.chat-message-text {
  margin: 0;
}

.chat-message-text:has(+ .chat-att) {
  margin-bottom: var(--sp-xs);
}

/* Links in chat message text use the cobalt accent */

.chat-message-text a {
  color: var(--accent-blue);
  text-decoration: none;
  overflow-wrap: anywhere;
}

@media (hover: hover) {
  .chat-message-text a:hover {
  text-decoration: underline;
}
}

/* ─── Video embed attachment ─────────────────────────────────────── */

/* Outer wrapper: left thread-line, gap, stacked content */
.chat-embed-wrapper {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding-left: calc(8px + var(--sp-xs));
  border-left: 2px solid var(--border);
  margin-bottom: var(--sp-xs);
}

/* When an embed follows message text, give it breathing room */
.chat-message-text + .chat-embed-wrapper {
  margin-top: 12px;
}

/* Brand logo — max-width 72px, display as block so the SVG scales */
.brand-logo {
  display: inline-flex;
  align-items: center;
  line-height: 0;
  margin-bottom: var(--sp-xs);
}

.brand-logo svg {
  width: 100%;
  height: auto;
}

.brand-logo-youtube {
  max-width: 72px;
}

.brand-logo-vimeo {
  max-width: 60px;
}

/* Header row: title + chevron toggle */
.chat-embed-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-xs);
  margin-bottom: 12px;
}

.chat-embed-title {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--accent-blue);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  flex: 1;
  min-width: 0;
}

.chat-embed-toggle {
  flex-shrink: 0;
  background: none;
  border: none;
  padding: 2px;
  cursor: pointer;
  color: var(--label-tertiary);
  display: flex;
  align-items: center;
  border-radius: var(--radius-sm);
}

@media (hover: hover) {
  .chat-embed-toggle:hover {
  color: var(--label-primary);
  background: var(--bg-secondary);
}
}

/* Collapsible area: iframe + link-out */
.chat-embed-collapsible {
  display: flex;
  flex-direction: column;
  gap: 12px;
  overflow: hidden;
  max-width: 420px;
  max-height: 500px;
  transition: max-height 0.3s ease;
}

.chat-embed-collapsible--collapsed {
  max-height: 0;
}

/* Responsive iframe container via aspect-ratio */
.chat-embed-player-wrap {
  position: relative;
  width: 100%;
  max-width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: #000;
}

.chat-embed-iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: none;
  border-radius: var(--radius-md);
}

/* "Open in YouTube" link-out row */
.chat-embed-linkout {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--text-xs);
  color: var(--accent-blue);
  text-decoration: none;
  width: fit-content;
}

@media (hover: hover) {
  .chat-embed-linkout:hover {
  text-decoration: underline;
}
}

/* ============================================================================
   New Chat modal — contact picker overlay
   ========================================================================= */

/* The New Chat picker (#new-chat-modal) is ui/sheet's card (sheet.css):
   the search field and the people list in its stack, the create button
   in its footer. Only the inner content rules below are its own (the
   share picker and Add members share them). */

/* In a sheet the stack holds the 16 inset: the field sits in it, and the
   list fills it (C19) with no cap of its own. */
.sheet .new-chat-search {
  margin-left: 0;
  margin-right: 0;
}
.sheet .new-chat-contacts {
  margin-top: 0;
  min-height: 0;
  max-height: none;
}

/* The footer's primary action at the sheet's 48 (spec §4.4). */
.sheet__footer .new-chat-create {
  min-height: 48px;
  border-radius: var(--radius-full);
}

/* Desktop modal: the picker's 440 footprint instead of the 560 default. */
@media (min-width: 835px) {
  .sheet.new-chat-sheet {
    width: min(440px, 92vw);
  }
}

.new-chat-search {
  margin: 0 var(--sp-lg);
  padding: var(--sp-xs) var(--sp-sm);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  background: var(--bg);
  font-family: inherit;
  font-size: var(--text-lg); /* 16px */
  color: var(--label-primary);
  outline: none;
}
.new-chat-search:focus {
  border-color: var(--bg-secondary);
}
.new-chat-search::placeholder {
  color: var(--label-primary);
}

.new-chat-contacts {
  flex: 1;
  overflow-y: auto;
  list-style: none;
  margin: var(--sp-sm) 0 0;
  padding: 0 var(--sp-sm);
  min-height: 200px;
  max-height: 360px;
}

.new-chat-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-xl);
  color: var(--label-secondary);
  font-size: var(--text-sm);
}

.people-search-error {
  padding: var(--sp-xs) var(--sp-lg) 0;
  color: var(--accent-red);
  font-size: var(--text-sm);
}

.new-chat-contact-row {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: var(--radius-md);
  cursor: pointer;
  user-select: none;
  border: 1px solid transparent;
}
@media (hover: hover) {
  .new-chat-contact-row:hover {
  background: var(--bg-subtle);
}
}

.new-chat-contact-selected {
  background: rgba(var(--accent-rgb, 0, 0, 0), 0.06);
  border-color: rgba(var(--accent-rgb, 0, 0, 0), 0.2);
}
@media (hover: hover) {
  .new-chat-contact-selected:hover {
  background: rgba(var(--accent-rgb, 0, 0, 0), 0.06);
  border-color: rgba(var(--accent-rgb, 0, 0, 0), 0.2);
}
}

.new-chat-contact-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.new-chat-contact-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--label-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.new-chat-contact-username {
  font-size: var(--text-xs);
  color: var(--label-secondary);
}

.new-chat-contact-check {
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--label-primary);
  flex-shrink: 0;
}

.new-chat-create {
  width: 100%;
  padding: var(--sp-xs) var(--sp-md);
  background: var(--accent);
  color: var(--accent-fg);
  border: none;
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
  transition: opacity 120ms ease, background 120ms ease;
}
@media (hover: hover) {
  .new-chat-create:hover:not(:disabled) {
  opacity: 0.9;
}
}
.new-chat-create:disabled {
  background: var(--bg-secondary);
  color: var(--label-tertiary);
  cursor: not-allowed;
}

/* ── Share to chat ── */

.share-chat-spinner {
  font-size: var(--text-xs);
  color: var(--label-tertiary);
  animation: share-chat-pulse 1s ease-in-out infinite;
}

.share-chat-disabled {
  opacity: 0.5;
  pointer-events: none;
  cursor: default;
}

/* The three destination sources share one scroll region. */
/* Share picker — iOS GroupSharePickerView (bug bash 2026-09-29): one
   3-column grid of avatar tiles, group chats then people, a blue check
   badge on each pick. */
.share-grid {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  list-style: none;
  margin: 0;
  padding: var(--sp-sm) var(--sp-lg);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-md) var(--sp-sm);
  align-content: start;
}

.share-cell {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) 0;
  border: none;
  border-radius: var(--radius-md);
  background: none;
  color: var(--label-primary);
  font: inherit;
  cursor: pointer;
}
.share-cell:disabled {
  cursor: default;
  opacity: 0.6;
}
@media (hover: hover) {
  .share-cell:not(:disabled):hover {
    background: var(--bg-subtle);
  }
}

.share-cell-face {
  position: relative;
  width: 56px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* A group without its own image. */
.share-cell-group {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-subtle);
  color: var(--label-secondary);
}

.share-cell-check {
  position: absolute;
  right: -2px;
  bottom: -2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-blue, #0a84ff);
  color: white;
  box-shadow: 0 0 0 2px var(--bg);
}

.share-cell-name {
  max-width: 100%;
  font-size: var(--text-sm);
  line-height: 1.25;
  text-align: center;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Nothing picked (share mode): the Copy link tile, centred. */
.share-empty-bar {
  justify-content: center;
}
.share-bar-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3xs);
  border: none;
  background: none;
  color: var(--label-primary);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}
.share-bar-tile-icon {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-subtle);
}
@media (hover: hover) {
  .share-chat-selected:hover {
  background: rgba(var(--accent-rgb, 0, 0, 0), 0.06);
  border-color: rgba(var(--accent-rgb, 0, 0, 0), 0.2);
}
}

@media (hover: hover) {
  .new-chat-secondary:not(:disabled):hover {
  background: var(--bg-subtle);
}
}
/* A send in flight ("Group collaboration" while its group is made). */
.new-chat-secondary:disabled {
  cursor: default;
  opacity: 0.5;
}

/* ── Typing indicator ── */

.chat-typing {
  min-height: 24px;
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: 0 var(--sp-xl);
  font-size: var(--text-xs);
  color: var(--label-secondary);
}

.chat-typing-dots {
  display: flex;
  gap: 3px;
}

.chat-typing-dot {
  width: 5px;
  height: 5px;
  border-radius: var(--radius-full);
  background: var(--label-tertiary);
  animation: typing-bounce 1.2s ease infinite;
}

.chat-typing-dot:nth-child(2) { animation-delay: 0.2s; }
.chat-typing-dot:nth-child(3) { animation-delay: 0.4s; }

/* ── Markdown in messages ── */

.chat-message-text {
  line-height: 1.5;
}

.chat-message-text p {
  margin: 0 0 var(--sp-xs) 0;
}
.chat-message-text p:last-child {
  margin-bottom: 0;
}

/* Shared with the composer and the in-place editor (2026-10-27): what
   the box shows is what the bubble will draw — strong, code, lists,
   quotes and headings from one rule set. Colours stay per surface
   below. */
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) pre {
  overflow-x: auto;
  margin: var(--sp-xs) 0;
}

:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) code,
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) pre {
  font-family: "SF Mono", "Fira Code", monospace;
  font-size: 0.9em;
  padding: .25em .5em;
  border-radius: var(--radius-sm);
}

:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) pre code {
  background: none;
  padding: 0;
}

:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) strong { font-weight: 600; }
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) em { font-style: italic; }

:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) ul,
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) ol {
  margin: var(--sp-xs) 0;
  /* base.css resets ul/ol to list-style none on every surface. */
  padding-left: var(--sp-lg);
}
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) ul { list-style-type: disc; }
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) ol { list-style-type: decimal; }
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) li { margin: 0; }

:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) blockquote {
  border-left: 3px solid var(--border-primary);
  padding-left: var(--sp-sm);
  color: var(--label-primary);
  margin: var(--sp-xs) 0;
  background: var(--bg-secondary);
}

:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) h1,
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) h2,
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) h3 {
  font-weight: 600;
  margin: var(--sp-xs) 0 var(--sp-3xs);
}
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) h1 { font-size: var(--text-lg); }
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) h2 { font-size: var(--text-base); }
:is(.chat-message-text, .chat-composer-milkdown .ProseMirror, .chat-edit-milkdown .ProseMirror) h3 { font-size: var(--text-sm); }

/* Code colours: the bubble's by side, the editors' on the pill. */
.received .chat-message-text code,
.received .chat-message-text pre {
  color: var(--label-secondary);
  background: var(--bg-primary);
}

.sent .chat-message-text code,
.sent .chat-message-text pre {
  color: var(--label-inverse-primary);
  background: var(--accent-blue);
}

.chat-composer-milkdown .ProseMirror code,
.chat-edit-milkdown .ProseMirror code,
.chat-composer-milkdown .ProseMirror pre,
.chat-edit-milkdown .ProseMirror pre {
  color: var(--code-text);
  background: var(--code-bg);
}

.chat-composer-milkdown .ProseMirror a,
.chat-edit-milkdown .ProseMirror a {
  color: var(--accent-blue);
  text-decoration: none;
  overflow-wrap: anywhere;
}

/* ── Face stack for group chat headers ── */

.chat-face-stack {
  display: flex;
  flex-direction: row;
}

.chat-face-stack .avatar,
.chat-face-stack .avatar-img {
  border: 2px solid var(--bg);
  margin-left: -8px;
}

.chat-face-stack .avatar:first-child,
.chat-face-stack .avatar-img:first-child {
  margin-left: 0;
}

.chat-header-ai-avatar {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-full);
  background: var(--fg);
  color: var(--bg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

/* =============================================================================
   Message action menu
   ============================================================================= */

/* Ellipsis trigger — mirrors the .chat-reaction-trigger layout/reveal pattern */
.chat-message-menu-trigger {
  /* Absolutely positioned: centred on the top-right corner of the bubble */
  position: absolute;
  top: 0;
  right: 0;
  transform: translate(50%, -50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm, 6px);
  background: var(--bg-elevated, var(--bg));
  color: var(--label-secondary);
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(0,0,0,0.10);
  /* Fade in/out */
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s ease, background 0.15s, color 0.15s;
  z-index: 1;
}
@media (hover: hover) {
  .chat-message-menu-trigger:hover {
  background: var(--bg-hover, var(--bg-secondary));
  color: var(--label-primary);
}
}

/* Show trigger on bubble hover — desktop only. Mobile uses the tap action
   sheet instead, so we leave the trigger permanently hidden there. */
@media (min-width: 835px) {
  .chat-message-row.sent .chat-message-bubble-wrapper:focus-within .chat-message-menu-trigger {
    opacity: 1;
    visibility: visible;
  }
@media (hover: hover) {
  .chat-message-row.sent .chat-message-bubble-wrapper:hover .chat-message-menu-trigger {
    opacity: 1;
    visibility: visible;
  }
}
}

/* Dropdown panel — appears above the bubble, right-aligned by default.
   positionMessageMenu() in dom.mjs adds --flip when the top would clip. */
.chat-message-menu-dropdown {
  position: absolute;
  bottom: calc(100% + 4px);
  right: 0;
  min-width: 184px;
  background: var(--bg-primary);
  border: 1px solid var(--border-secondary);
  border-radius: var(--radius-md);
  box-shadow: 0 4px 20px rgba(0,0,0,0.14);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  padding: var(--sp-2xs, 4px) 0;
  z-index: 200;
  overflow: hidden;
}

/* Flip downward when the message is near the top of the chat container */
.chat-message-menu-dropdown--flip {
  bottom: auto;
  top: calc(100% + 4px);
}

/* Bubble-wrapper is the positioning context for the trigger + dropdown */
.chat-message-bubble-wrapper {
  position: relative;
}

.chat-message-menu-item {
  display: flex;
  border-radius: 0;
  align-items: center;
  gap: var(--sp-sm, 10px);
  width: 100%;
  padding: var(--sp-xs, 8px) var(--sp-sm, 12px);
  border: none;
  background: transparent;
  color: var(--label-secondary);
  font-size: var(--text-sm, 0.875rem);
  text-align: left;
  cursor: pointer;
  transition: background 0.2s ease-in-out;
}
@media (hover: hover) {
  .chat-message-menu-item:hover {
  background: var(--bg-secondary);
}
}
.chat-message-menu-item--danger {
  color: var(--accent-red);
}
@media (hover: hover) {
  .chat-message-menu-item--danger:hover {
  background: var(--accent-red-elevated);
}
}

.chat-message-menu-divider {
  height: 1px;
  background: var(--border-primary);
  margin: var(--sp-2xs, 4px) 0;
}

/* =============================================================================
   Emoji-only bubble
   ============================================================================= */

.chat-message-bubble--emoji-only {
  background: transparent;
  border: none;
  padding: 0;
  box-shadow: none;
}

.chat-message-bubble--emoji-only .chat-message-text,
.chat-message-bubble--emoji-only .chat-message-text p {
  font-size: 32px;
  line-height: 1.2;
}

/* A message that is one GIF and nothing else stands bare (2026-10-26):
   the gif's own corners, no bubble fill or padding around it. */
/* Both sides named: the side's own fill rule outweighs a lone class. */
.chat-message-row.received .chat-message-bubble--gif-only,
.chat-message-row.sent .chat-message-bubble--gif-only {
  background: transparent;
  border: none;
  padding: 0;
  box-shadow: none;
}

/* =============================================================================
   Inline edit state
   ============================================================================= */

/* ── Full-width editing on desktop ─────────────────────────────────────────
   When a sent message enters edit mode, expand the row to the full readable
   column width so the textarea has maximum writing space.
   Scoped to desktop — mobile edit state uses the composer + cancel bar.
   ─────────────────────────────────────────────────────────────────────────── */
@media (min-width: 835px) {
  /* Expand the row itself — override max-width: 70% and align-self: flex-end */
  .chat-message-row.sent:has(.chat-message-bubble--editing) {
    max-width: 100%;
    width: 100%;
    align-self: stretch;
  }

  /* Stack grows to fill the row width minus the 28px avatar slot */
  .chat-message-row.sent:has(.chat-message-bubble--editing) .chat-message-stack {
    flex: 1;
    min-width: 0;
    align-items: stretch;
  }

  /* Bubble fills the stack; override the pre-measured min-width inline style */
  .chat-message-row.sent:has(.chat-message-bubble--editing) .chat-message-bubble--editing {
    width: 100%;
    min-width: 0 !important;
  }
}

.chat-message-bubble--editing {
  /* min-width is stamped as an inline style (pre-measured in dom.mjs) to
     match the original message row's width, preventing the editing bubble
     from collapsing narrower than the message being edited. The editor fills
     it via width: 100% and can grow beyond min-width for longer edits. */
  padding: 8px;
  overflow: visible;
  background: transparent;
  box-shadow: none;
  box-sizing: border-box;
}

/* Inline message-edit editor — Milkdown inside the editing bubble.
   Shares the .ProseMirror typography/block rules with the composer (see
   the "Milkdown composer" section below); this block is just the frame. */
.chat-edit-milkdown {
  display: block;
  width: 100%;
  box-sizing: border-box;
  min-height: 2.5em;
  max-height: 40vh;
  overflow-y: auto;
  padding: var(--sp-xs, 8px) var(--sp-sm, 12px);
  border: 1.5px solid var(--accent-blue);
  border-radius: var(--radius-md, 10px);
  background: var(--bg);
  color: var(--label-primary);
  font-size: var(--text-sm, 0.875rem);
  line-height: 1.5;
  cursor: text;
  outline: none;
}

/* Attachment strip — sits snugly below the textarea */
.chat-edit-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs, 8px);
  margin-top: var(--sp-xs, 8px);
  padding: 0 var(--sp-xs, 8px);
}

/* Individual thumbnail wrapper — relative so the remove btn can be absolute */
.chat-edit-att-thumb {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm, 6px);
  overflow: visible; /* let the button bleed outside */
  max-width: 120px;
}

/* Image / video-poster thumbnail */
.chat-edit-att-thumb-img {
  display: block;
  max-width: 120px;
  max-height: 120px;
  width: auto;
  height: auto;
  border-radius: var(--radius-sm, 6px);
  object-fit: cover;
}

/* A new document in an edit's strip: the file glyph and its name on a tile. */
.chat-edit-att-thumb-file {
  display: flex;
  align-items: center;
  gap: var(--sp-2xs);
  max-width: 120px;
  height: 36px;
  padding: 0 var(--sp-xs);
  background: var(--bg-subtle);
  color: var(--label-secondary);
  font-size: var(--text-xs);
}

.chat-edit-att-thumb-file svg {
  flex-shrink: 0;
}

.chat-edit-att-thumb-file-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--label-primary);
}

/* Placeholder for non-visual types (file, link without image, embed) */
.chat-edit-att-thumb-placeholder {
  display: flex;
  align-items: center;
  gap: var(--sp-2xs, 4px);
  padding: var(--sp-xs, 8px);
  background: var(--bg-secondary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-sm, 6px);
  font-size: var(--text-xs, 0.8rem);
  color: var(--label-secondary);
  max-width: 120px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ✕ remove button — absolutely positioned at top-left, half outside */
.chat-edit-att-remove {
  position: absolute;
  top: -8px;
  left: -8px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-full, 999px);
  border: 1.5px solid var(--border-primary);
  background: var(--bg-elevated, var(--bg));
  color: var(--label-secondary);
  cursor: pointer;
  padding: 0;
  box-shadow: 0 1px 3px rgba(0,0,0,0.12);
  transition: background 0.15s, color 0.15s;
}
@media (hover: hover) {
  .chat-edit-att-remove:hover {
  background: var(--accent-red);
  color: #fff;
  border-color: var(--accent-red);
}
}

.chat-edit-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--sp-xs, 8px);
  padding: var(--sp-2xs, 4px) var(--sp-xs, 8px) var(--sp-xs, 8px);
}

.chat-edit-cancel,
.chat-edit-save {
  padding: var(--sp-2xs, 4px) var(--sp-sm, 12px);
  border-radius: var(--radius-sm, 6px);
  font-size: var(--text-xs, 0.8rem);
  font-weight: 500;
  cursor: pointer;
  border: 1px solid var(--border-primary);
  transition: background 0.1s;
}
.chat-edit-cancel {
  background: transparent;
  color: var(--label-secondary);
}
@media (hover: hover) {
  .chat-edit-cancel:hover {
  background: var(--bg-hover, var(--bg-secondary));
}
}
.chat-edit-save {
  background: var(--bg-tertiary);
  color: var(--label-secondary);
}
@media (hover: hover) {
  .chat-edit-save:hover {
  background: var(--bg-primary-inverse);
  color: var(--label-inverse-primary);
}
}

/* ── Deleted message tombstone ── */
.chat-message-deleted {
  padding: var(--sp-xs) 0;
}

.chat-message-deleted-text {
  padding: var(--sp-xs) var(--sp-md);
  font-size: var(--text-base);
  font-style: italic;
  color: var(--label-secondary);
  margin: 0;
  pointer-events: none;
  user-select: none;
}

/* ── (edited) label ── */
.chat-message-edited-label {
  display: block;
  font-size: 0.75rem;
  font-style: italic;
  font-weight: 400;
  color: var(--label-secondary);
  margin-top: 2px;
  padding-left: 2px;
}

/* =============================================================================
   Delete confirmation modal
   ============================================================================= */

.chat-delete-confirm-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 500;
}

.chat-delete-confirm-dialog {
  background: var(--bg-elevated, var(--bg));
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-lg, 14px);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.24);
  padding: var(--sp-lg, 24px);
  max-width: 360px;
  width: calc(100vw - 32px);
}

.chat-delete-confirm-title {
  font-size: var(--text-base, 1rem);
  font-weight: 600;
  color: var(--label-primary);
  margin-bottom: var(--sp-sm, 10px);
}

/* Preview container — scrollable to handle tall images */
.chat-delete-confirm-preview {
  background: var(--bg-secondary, var(--bg));
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-sm, 6px);
  padding: var(--sp-xs, 8px) var(--sp-sm, 12px);
  margin-bottom: var(--sp-md, 16px);
  max-height: 260px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs, 8px);
}

/* Text portion */
.chat-delete-confirm-preview-text {
  font-size: var(--text-sm, 0.875rem);
  color: var(--label-secondary);
  line-height: 1.5;
}

/* Attachment list */
.chat-delete-confirm-preview-attachments {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs, 8px);
}

/* Base for every attachment type in the modal */
.chat-delete-att-preview {
  display: flex;
  align-items: center;
  gap: var(--sp-xs, 8px);
  font-size: var(--text-sm, 0.875rem);
  color: var(--label-secondary);
}

/* Image */
.chat-delete-att-image {
  display: block;
}
.chat-delete-att-img {
  display: block;
  max-width: 100%;
  max-height: 180px;
  width: auto;
  height: auto;
  border-radius: var(--radius-sm, 6px);
  object-fit: cover;
}

/* Video — poster thumbnail or fallback label */
.chat-delete-att-video-label {
  display: flex;
  align-items: center;
  gap: var(--sp-xs, 8px);
  color: var(--label-tertiary);
}

/* File */
.chat-delete-att-file-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Link */
.chat-delete-att-link {
  gap: var(--sp-sm, 10px);
}
.chat-delete-att-link-img {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--radius-xs, 4px);
  flex-shrink: 0;
}
.chat-delete-att-link-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Feed content */
.chat-delete-att-feed {
  gap: var(--sp-sm, 10px);
}
.chat-delete-att-feed-thumb {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--radius-xs, 4px);
  flex-shrink: 0;
}
.chat-delete-att-feed-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Video embed */
.chat-delete-att-embed-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-delete-confirm-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--sp-sm, 10px);
}

.chat-delete-confirm-btn {
  padding: var(--sp-xs, 8px) var(--sp-md, 16px);
  border-radius: var(--radius-sm, 6px);
  font-size: var(--text-sm, 0.875rem);
  font-weight: 500;
  cursor: pointer;
  border: 1px solid var(--border-primary);
  transition: background 0.1s, opacity 0.1s;
}
.chat-delete-confirm-cancel {
  background: transparent;
  color: var(--label-secondary);
}
@media (hover: hover) {
  .chat-delete-confirm-cancel:hover {
  background: var(--bg-hover, var(--bg-secondary));
}
}
.chat-delete-confirm-delete {
  background: var(--text-danger, #e53e3e);
  color: #fff;
  border-color: var(--text-danger, #e53e3e);
}
@media (hover: hover) {
  .chat-delete-confirm-delete:hover {
  opacity: 0.88;
}
}

/* =============================================================================
   Success / info toast
   ============================================================================= */

.chat-toast {
  position: fixed;
  bottom: var(--sp-xl, 24px);
  left: 50%;
  transform: translateX(-50%);
  padding: var(--sp-xs, 8px) var(--sp-md, 16px);
  background: var(--bg-elevated, var(--bg-primary));
  color: var(--label-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-full, 999px);
  font-size: var(--text-sm, 0.875rem);
  white-space: nowrap;
  pointer-events: none;
  z-index: 200;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  animation: chat-toast-in 0.18s ease-out, chat-toast-out 0.3s ease-in 2.2s forwards;
}

/* ============================================================================
   Milkdown rich-text composer
   Milkdown mounts into .chat-composer-milkdown and injects .milkdown >
   .ProseMirror as the actual contenteditable surface.  We style those classes
   to match the previous textarea's look: transparent fill, same font, same
   padding, same scroll cap.
   ========================================================================= */

/* Mount point — flex item that stretches to fill the pill. The type is
   the bubbles' body, 18/25.2 on a 40 line (iOS ChatInputBar's line in the
   56 pill), at every width: the size sits on the mount itself because
   base.css floors the editor and its placeholder at max(16px, 1em)
   !important on phones, and 1em is this. (2026-10-27: the box typed at
   13px under 18px bubbles.) */
.chat-composer-milkdown {
  flex: 1;
  min-width: 0;
  min-height: 40px;
  font-size: 18px;
  cursor: text;
  outline: none; /* suppress native focus ring on the wrapper div */
}
.chat-edit-milkdown {
  font-size: 18px;
}

/* Before ProseMirror has mounted (.milkdown child is absent), show a
   placeholder so the composer looks like a text input. Once the editor
   lands it fills the div and this pseudo-element disappears. */
.chat-composer-milkdown:not(:has(.milkdown))::before,
.chat-edit-milkdown:not(:has(.milkdown))::before {
  content: attr(data-placeholder);
  display: block;
  font-size: 18px;
  line-height: 25.2px;
  padding: 7.4px 0;
  color: var(--label-primary);
  pointer-events: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Milkdown's outer wrapper div */
.chat-composer-milkdown .milkdown,
.chat-edit-milkdown .milkdown {
  all: unset;
  display: block;
  width: 100%;
}

/* ProseMirror's contenteditable div — the actual editable surface */
.chat-composer-milkdown .ProseMirror,
.chat-edit-milkdown .ProseMirror {
  outline: none;
  font-family: inherit;
  font-size: 18px;
  color: var(--label-primary);
  line-height: 25.2px;
  padding: 7.4px 0;
  margin: 0;
  min-height: 40px;
  /* Twelve lines, then it scrolls (iOS ChatInputBar). */
  max-height: calc(12 * 25.2px + 14.8px);
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Empty-state placeholder shown once Milkdown is mounted but the doc is
   empty. The `is-editor-empty` class is applied by our emptyPlaceholderPlugin
   decoration (milkdown_editor.mjs) whenever the doc has a single empty
   paragraph. We use float+height:0 (the Tiptap convention) so the trailing
   <br class="ProseMirror-trailingBreak"> ProseMirror inserts doesn't push
   the pseudo-element onto a second line. */
.chat-composer-milkdown {
  --composer-placeholder: "Write a message...";
}
.chat--ai .chat-composer-milkdown {
  --composer-placeholder: "Ask anything";
}
.chat-composer-milkdown .ProseMirror p.is-editor-empty:first-child::before,
.chat-edit-milkdown .ProseMirror p.is-editor-empty:first-child::before,
.chat-composer-milkdown .ProseMirror:empty::before,
.chat-edit-milkdown .ProseMirror:empty::before {
  content: var(--composer-placeholder, "Write a message...");
  float: left;
  height: 0;
  color: var(--label-primary);
  pointer-events: none;
}

/* ProseMirror wraps bare text in <p> tags — strip the default margin */
.chat-composer-milkdown .ProseMirror p,
.chat-edit-milkdown .ProseMirror p {
  margin: 0;
}

/* Strong, code, links, lists, quotes and headings: the bubble's rules
   ("Markdown in messages" above), shared. */

/* ProseMirror selection highlight */
.chat-composer-milkdown .ProseMirror ::selection,
.chat-edit-milkdown .ProseMirror ::selection {
  background: rgba(var(--color-blue), .3);
  background-blend-mode: var(--blend-mode);
}

/* ─── Reaction detail sheet content ─────────────────────────────────── */

/* Emoji tab bar */
.reaction-sheet__tabs {
  display: flex;
  gap: 12px; /* 12px between tabs */
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  padding-top: var(--sp-sm);
}
.reaction-sheet__tabs::-webkit-scrollbar {
  display: none;
}

.reaction-sheet__tab {
  display: inline-flex;
  align-items: center;
  gap: 2px; /* 2px between emoji and count */
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: var(--radius-sm); /* 8px */
  border: none;
  background: transparent;
  cursor: pointer;
  flex-shrink: 0;
  transition: background 0.1s ease;
}

.reaction-sheet__tab.active {
  background: var(--bg-secondary);
}

.reaction-sheet__tab-emoji {
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
}

.reaction-sheet__tab-count {
  font-size: var(--text-lg); /* 16px */
  color: var(--label-primary);
  font-variant-numeric: tabular-nums;
}

/* The reactors are sheet rows (ui/sheet): a 32 avatar, the name as the
   label, the stack's 24 between them. */

/* ─── Keyframe animations ────────────────────────────────────────────── */

@keyframes share-chat-pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}

@keyframes typing-bounce {
  0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
  30% { opacity: 1; transform: translateY(-3px); }
}

@keyframes chat-toast-in {
  from { opacity: 0; transform: translateX(-50%) translateY(6px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

@keyframes chat-toast-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* ─── Media queries ──────────────────────────────────────────────────── */

@media (max-width: 834px) {
  .chat-composer-form .emoji-picker {
    width: 100%;
    right: 0;
  }

  .chat-scroll-to-bottom {
    bottom: 142px;
  }
}

@media (max-width: 600px) {
  .chat-embed-collapsible {
    max-width: 320px;
  }
    .chat-message-row {
    max-width: 90%;
  }
}

/* =============================================================================
   MOBILE MESSAGE ACTION SHEET
   ============================================================================= */

/* The card is ui/sheet's (sheet.css): no header on the menu page, the quick
   reactions row then the action rows as sheet rows with hairlines between;
   the emoji picker is the sheet's second page (R6), where the sheet turns
   fluid and the page's .sheet__fill child holds the picker. Only the quick
   row and the embedded picker's overrides live here. */

/* ── Quick emoji row ──────────────────────────────────────────────────────── */
.msg-action-sheet__quick-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.msg-action-sheet__quick-emoji {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-size: 24px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  transition: background 0.12s ease;
}

.msg-action-sheet__quick-emoji:active {
  background: var(--bg-secondary);
}

.msg-action-sheet__quick-more {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: var(--label-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-full);
  transition: background 0.12s ease;
}

.msg-action-sheet__quick-more > .icon {
  height: 24px;
  width: 24px;
}

.msg-action-sheet__quick-more:active {
  background: var(--bg-secondary);
}

/* Override the floating-picker defaults when embedded inside a sheet */
.msg-action-sheet .emoji-picker,
.composer-emoji-sheet .emoji-picker {
  position: static;
  width: 100%;
  flex: 1;
  min-height: 0;
  max-height: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  animation: none;
}

/* ── Mobile edit cancel bar ────────────────────────────────────────────────
   Spans the full viewport width between the messages list and the composer.
   Appears only on mobile when a message is being edited (rendered by
   view_mobile_edit_cancel_bar; collapses to nothing when element.none()).
   ─────────────────────────────────────────────────────────────────────── */
.chat-cancel-edit__mobile {
  box-shadow: 0px 2px 4px rgba(0, 0, 0, .08);
  background: var(--bg-tertiary);
  grid-row: 3;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--sp-md);           /* 16px between icon and label */
  width: 100%;
  height: var(--size-3xl);     /* 40px total height */
  padding: 0 var(--sp-xl);
  background: var(--bg-body);
  border: none;
  border-radius: 0;
  color: var(--label-secondary);
  font-size: var(--text-lg);   /* 16px */
  cursor: pointer;
  box-sizing: border-box;
}

.chat-cancel-edit__mobile svg {
  width: var(--size-xl);       /* 24px */
  height: var(--size-xl);
  flex-shrink: 0;
}

.chat-cancel-edit__mobile:active {
  opacity: 0.7;
}

/* "Editing message" tiny-time label variant */
.chat-message-tiny-time--editing {
  color: var(--label-primary);
  font-style: italic;
}

/* ── Mention autocomplete picker ─────────────────────────────────────── */
.chat-mention-picker {
  background: var(--bg-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow);
  max-height: 240px;
  overflow-y: auto;
  margin-bottom: 8px;
}

.chat-mention-picker-item {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-md);
  cursor: pointer;
}

@media (hover: hover) {
  .chat-mention-picker-item:hover {
  background: var(--bg-subtle);
}
}

.chat-mention-picker-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.chat-mention-picker-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--label-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-mention-picker-username {
  font-size: var(--text-xs);
  color: var(--label-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-mention {
  color: var(--accent-blue);
  text-decoration: none;
}
@media (hover: hover) {
  .chat-mention:hover {
  text-decoration: underline;
}
}

/* ── Messages list (/chats, no thread open) ──────────────────────────────
   Shown on both mobile and desktop — desktop still has the sidebar's own
   chat list, but /chats now gets a real page instead of a blank
   placeholder (iOS parity mock).

   STRUCTURE PARITY: this whole block mirrors the Activity page's
   structural system (styles/notifications.css) — container geometry,
   header sizing, chip strip, body gaps, row shell and type scale.
   notifications.css is the source of truth; when a structural value
   changes there, change the mirrored value here too. Each rule notes the
   notifications.css rule it mirrors. */

/* The list itself lives in the sidebar's Chats mode (.sidebar-chats,
   styles/sidebar.css) — there is no standalone Messages page anymore. The
   row shell and type scale below still mirror Activity. */

/* The list's toolbar (2026-10-26 mock): the search pill (search.css's
   .search-pill, 58 tall) with compose ("New Message") beside it as a 58
   disc on desktop. Phones carry compose in the root header, so the disc
   is hidden there and the pill runs full width. */
.chat-list-toolbar {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  flex-shrink: 0;
  padding-bottom: var(--sp-xl);
}

.chat-list-search {
  flex: 1;
  min-width: 0;
}

@media (max-width: 834px) {
  .chat-list-compose {
    display: none;
  }
}

.chat-list-compose {
  flex-shrink: 0;
  width: 58px;
  height: 58px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background: var(--bg-primary);
  box-shadow: inset 0 0 0 1px var(--border-secondary);
  color: var(--label-primary);
  cursor: pointer;
}

@media (hover: hover) {
  .chat-list-compose:hover {
    background: var(--fill-elevated-secondary);
  }
}

/* Mirrors .notifications-empty. */
.chat-list-empty {
  padding: var(--sp-xxl) var(--sp-md);
  text-align: center;
  color: var(--label-secondary);
}

/* List wrapper — mirrors .notifications-list-wrap's structure but NOT its
   overflow: this wrapper must not be a scroll container. It lives inside
   .sidebar-scroll, which owns scrolling, and with the app-wide
   `* { overscroll-behavior: none }` (base.css) Chrome swallows wheel events
   over an overflow:auto element even when it has nothing to scroll — an
   inner `overflow-y: auto` here left the whole list wheel-dead in Chrome
   (Safari chains to the parent scroller regardless). */
.chat-list-rows {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  overflow-x: clip;
}

/* Row shell — mirrors .notification (same padding, gap, hover).
   position: relative anchors the absolute unread dot in the left-padding
   zone, exactly like .notification-badge on Activity rows. */
.chat-list-row {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-md) var(--sp-sm);
  position: relative;
  transition: background 0.1s;
  box-sizing: border-box;
  max-width: 100%;
  text-decoration: none;
  color: inherit;
  border-radius: var(--radius-sm);
}
@media (hover: hover) {
  /* Elevated fill secondary like every list-pane highlight (2026-09-24).
     --bg-subtle measured rgb(245,245,244) on the rgb(242,242,247) canvas
     — barely there — and rgb(23,23,23) on the dark canvas, darker than it. */
  .chat-list-row:hover {
  background: var(--fill-elevated-secondary);
  text-decoration: none;
}
}

/* Avatar slot — mirrors .notification-media's square (82px desktop /
   52px mobile) so Messages rows land at exactly the same height as
   Activity rows. The avatar helper inlines a fixed px size, so the
   container drives the real size via !important — same trick as
   .notification-media .avatar. */
.chat-list-avatar {
  position: relative;
  width: 40px;
  aspect-ratio: 1 / 1;
  flex-shrink: 0;
}

.chat-list-avatar .avatar,
.chat-list-avatar .avatar-img {
  width: 100% !important;
  height: 100% !important;
}

/* Group rows render the channel's custom image or, without one, the
   two-member stack; either fills the slot like a single avatar would. The
   blanket 100% rules above also match the member avatars inside the
   stack, but harmlessly — their percentages resolve against the
   .avatar-stack-member wrapper (66.7% of the slot), which is the size
   they want anyway. */
.chat-list-avatar .avatar-stack {
  width: 100% !important;
  height: 100% !important;
}

/* Unread count on the avatar (Julian, 2026-09-24) — DMs, mentions,
   replies; see chat.row_badge_count. iOS CountBadge geometry: an 18px
   capsule, min-width 18px so one digit is a circle and more grow
   sideways, 0 5px padding, 11px/600 tabular digits, "99+" cap. Colours
   are the rail badge's (.app-rail-badge): accent red, label primary
   inverse. A sibling of the avatar, not inside it, so no avatar clipping
   reaches it; -4px on both edges puts its centre near the circle's 45°
   edge point, overlapping the rim. aria-hidden — the row's
   .chat-list-row-sr text speaks the count. */
.chat-list-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  box-sizing: border-box;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background: var(--accent-red);
  color: var(--label-inverse-primary);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;
}

/* The Please avatar (iOS AIAvatar; Julian 2026-10-27): the hollow base
   sparkle — ai/agent/base/hollow, a 2px stroke drawing — at 20 in
   label-primary, on a disc in the elevated fill, filling the avatar slot.
   Every AI row wears it: Ask Please, the Please chat, chats with the agent. */
.chat-list-ai-avatar {
  width: 100%;
  height: 100%;
  border-radius: var(--radius-full);
  background: var(--fill-elevated-secondary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--label-primary);
}
.chat-list-ai-avatar .icon {
  width: 20px;
  height: 20px;
}

/* Glyphs in the avatar slot render at size 0 (no inline width/height) and
   are sized here — mirrors .notification-media .icon. The Please avatar's
   20 (above) is restated here so it outranks this later rule. */
.chat-list-avatar .icon {
  width: 32px;
  height: 32px;
}
.chat-list-avatar .chat-list-ai-avatar .icon {
  width: 20px;
  height: 20px;
}

/* Mirrors .notification-body (column stack, --sp-3xs gap). */
/* Text clippers inside the row use `overflow: clip`, NOT `hidden`:
   hidden makes the element a scroll container, and under the app-wide
   `* { overscroll-behavior: none }` Chrome latches wheel events onto a
   scroll container whose content overflows (any truncated title/preview)
   instead of chaining them to .sidebar-scroll — leaving scroll stuck
   while the cursor sits over the text. clip clips identically (and
   text-overflow: ellipsis still applies) without creating a scroll
   container. */
.chat-list-row-text {
  flex: 1;
  min-width: 0;
  overflow: clip;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3xs);
}

/* Title + timestamp share one row — mirrors iOS ChatListRow's
   HStack { Text(name); Spacer(); Text(timeLabel) }: title on the leading
   edge, timestamp pushed to the trailing edge. `min-width: 0` on the title
   (not this row) is what lets the title ellipsize instead of the row
   overflowing — the timestamp is fixed-width content and never shrinks. */
.chat-list-row-top {
  display: flex;
  align-items: baseline;
  gap: var(--sp-sm);
}

/* Read/unread is carried by the whole row's typography and colour — there
   is no dot. Read rows keep a primary name over a secondary preview (iOS
   ChatListRow); .chat-list-row--unread lifts the preview to primary below.
   Weights (Julian, 2026-10-23): read 400, the open chat 500, unread 700. */
.chat-list-row-title {
  flex: 1;
  min-width: 0;
  font-size: var(--text-lg);
  font-weight: 400;
  color: var(--label-primary);
  overflow: clip; /* not `hidden` — see .chat-list-row-text */
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-list-row--selected .chat-list-row-title {
  font-weight: 500;
}

.chat-list-row--unread .chat-list-row-title {
  font-weight: 700;
  color: var(--label-primary);
}

/* Spoken-only ", N unread" at the end of the title — the avatar badge's
   count for screen readers (the badge itself is aria-hidden). Absolutely
   positioned, so it takes no room in the title's line or its ellipsis. */
.chat-list-row-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

/* Timestamp — secondary in every state (iOS ChatListRow: no unread variant). */
.chat-list-row-timestamp {
  flex-shrink: 0;
  font-size: var(--text-xs);
  font-weight: 400;
  color: var(--label-secondary);
}

/* Last-message preview line, below the title+timestamp row — mirrors iOS's
   `messagePreview` (single line, lineLimit(1) there / ellipsis here). Only
   rendered by chat.gleam when `last_message_text` is non-empty, mirroring
   iOS's `if !lastMessage.isEmpty`. */
.chat-list-row-subtitle {
  max-width: 80%;
  overflow: clip; /* not `hidden` — see .chat-list-row-text */
  text-overflow: ellipsis;
  color: var(--label-secondary);
  white-space: nowrap;
}

/* "@sender: " prefix on the preview line — a soft blue in every state so
   it recedes behind the title and message instead of competing with them. */
.chat-list-row-sender {
  font-size: var(--text-base);
  color: var(--accent-blue);
}

.chat-list-row-msg {
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

.chat-list-row--unread .chat-list-row-msg {
  color: var(--label-primary);
}

/* ── Ask Please, the list's first row (iOS ChatListView.askPleaseRow,
   2026-10-27) ──────────────────────────────────────────────────────────
   A row in the rows' clothes, seated with the clock to the AI chat list
   overlaid at its trailing edge (iOS: IconButton, the bare 24 glyph on a
   44 target, 8 in from the edge). The row's text stops short of the clock. */
.chat-list-row-seat {
  position: relative;
}

.chat-list-row-seat--ask .chat-list-row {
  padding-right: calc(var(--sp-xs) + 44px + var(--sp-xs));
}

.chat-list-row-trailing {
  position: absolute;
  top: 50%;
  right: var(--sp-xs);
  width: 44px;
  height: 44px;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  color: var(--label-primary);
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) {
  .chat-list-row-trailing:hover {
    background: var(--fill-elevated-secondary);
  }
}
.chat-list-row-trailing:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 1px;
}

/* ── Mobile overrides (≤ 768px) — mirrors the .notifications block ──── */

@media (max-width: 834px) {
  /* Mirrors .notification mobile row padding/gap. */
  .chat-list-row {
    padding: 16px 12px 16px 16px;
    gap: var(--sp-sm);
  }

  /* iOS seats Ask Please 16 above the chats (listRowInsets bottom Gap.md). */
  .chat-list-row-seat--ask {
    margin-bottom: 16px;
  }

  /* Mirrors .notification-media mobile square — aspect-ratio and the
     100%-sized children from the base rules handle the rest. */
  .chat-list-avatar {
    width: 52px;
  }

}

/* =============================================================================
   Received-message hover pill — [Add reaction | hairline | ⋮]
   ============================================================================= */

/* Split variant: the container keeps the pill chrome (position, border,
   reveal transitions from .chat-reaction-trigger); the inner segments own
   their padding and hover states so the hairline reads as a true divider. */
.chat-reaction-trigger--split {
  gap: 0;
  padding: 0;
  overflow: hidden;
}

/* Neutralise the whole-pill hover tint — hover feedback lives per-segment. */
@media (hover: hover) {
  .chat-reaction-trigger--split:hover {
  background: var(--bg-primary);
  color: var(--label-secondary);
}
}

.chat-reaction-trigger__btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-sm);
  border: none;
  background: var(--bg-primary);
  color: inherit;
  cursor: pointer;
  border-radius: 0;
  white-space: nowrap;
  transition: .3s ease all;
}

@media (hover: hover) {
  .chat-reaction-trigger__btn:hover {
  background: var(--bg-secondary);
  color: var(--label-primary);
}
}

.chat-reaction-trigger__divider {
  height: 24px;
  width: 1px;
  background: var(--border-secondary);
}

/* ── Offline / delivery states ─────────────────────────────────────────────
   Chat is server-only: there is no local store behind it, so offline the
   surface can show what was last cached but cannot send, react, or receive.
   These make that legible instead of leaving a live-looking UI that silently
   never updates. */

.chat-offline-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-xs);
    padding: var(--sp-xs) var(--sp-sm);

    background: var(--fill-elevated-secondary);
    color: var(--label-secondary);
    font-size: var(--text-sm);
    line-height: 18px;
}

/* An in-flight message is dimmed — it hasn't landed yet, and shouldn't read
   as delivered.

   Scoped to `.sent` (the viewer's own outgoing messages) on purpose. The AI
   streaming placeholder is also modelled as `Sending` — it has no server id
   yet either, which correctly keeps reactions off it — but it is an INCOMING
   message being typed out, not an unsent one. Dimming it would fade every AI
   response while it streams and snap it to full opacity at the end. */
.chat-message-row.sent.chat-message-row--sending .chat-message-bubble-wrapper {
    opacity: 0.6;
}

/* A failed one is dimmed further and paired with the notice below. Left
   in place deliberately: silently deleting what someone typed because the
   network blinked would be worse than showing that it didn't send. */
.chat-message-row--failed .chat-message-bubble-wrapper {
    opacity: 0.5;
}

.chat-message-failed {
    display: flex;
    align-items: center;
    gap: var(--sp-3xs);
    margin-top: var(--sp-4xs);

    color: var(--accent-red);
    font-size: var(--text-xs);
    line-height: 16px;
}

/* Sent messages are right-aligned, and a failed send is always the viewer's
   own — so the notice hugs the same edge as the bubble it belongs to. */
.chat-message-row.sent .chat-message-failed {
    justify-content: flex-end;
}

.chat-message-failed-retry {
    padding: 0;
    border: none;
    background: none;

    color: var(--accent-blue);
    font: inherit;
    font-size: var(--text-xs);
    font-weight: 600;
    cursor: pointer;
}

@media (hover: hover) {
    .chat-message-failed-retry:hover {
      text-decoration: underline;
  }
}

/* ── New-chat landing (lazy ephemeral AI chat) ────────────────────────────
   Hosted at /chats/new and as the desktop /feed main pane. A hero (pastel
   agent glyph + greeting) centered in the pane, with a single-line
   composer directly beneath it. No channel exists yet — submitting mints
   one and auto-sends the draft. */

.chat-landing {
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xl);
  padding: var(--sp-xl);
  background: var(--bg-primary);
}

.chat-landing .chat-offline-banner {
  position: absolute;
  top: var(--sp-md);
  left: 50%;
  transform: translateX(-50%);
}

.chat-landing-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-md);
}

.chat-landing-glyph .icon {
  background: var(--gradient-pastel);
}

.chat-landing-greeting {
  margin: 0;
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--label-primary);
  text-align: center;
}

.chat-landing-composer {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  width: min(560px, 100%);
  padding: var(--sp-xs) var(--sp-xs) var(--sp-xs) var(--sp-lg);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-full);
  background: var(--bg-secondary);
}

.chat-landing-composer:focus-within {
  border-color: var(--accent-blue);
}

.chat-landing-input {
  flex: 1;
  min-width: 0;
  border: none;
  background: none;
  color: var(--label-primary);
  font: inherit;
  font-size: var(--text-lg);
  line-height: 1.4;
  padding: var(--sp-xs) 0;
}

.chat-landing-input:focus {
  outline: none;
}

.chat-landing-input::placeholder {
  color: var(--label-primary);
}

.chat-landing-send {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--radius-full);
  background: var(--accent-blue);
  color: #ffffff;
  cursor: pointer;
}

.chat-landing-send:disabled {
  opacity: 0.4;
  cursor: default;
}

/* The Messages main pane with no conversation open (desktop /chats, and a
   chat route before its thread allocates). Layout and type are the shared
   .empty-pane (states.css, 2026-09-24). No background of its own: the
   main pane is background primary (Julian, 2026-09-24) and shows through;
   it painted the thread's grey canvas until then. */
.chat-thread-placeholder {
  height: 100%;
}

/* The conversation currently open in the main pane (sidebar chat list).
   Elevated fill secondary, the list pane's one highlight (Julian,
   2026-09-24); was elevated tertiary, fainter than a hovered row. */
.chat-list-row--selected,
.chat-list-row--selected:hover {
  background: var(--fill-elevated-secondary);
}

/* ── Desktop: the thread on the main pane's background primary ────────────
   Julian, 2026-09-24: the desktop main pane is background primary, the
   chat thread included. iOS's thread is the grey canvas with bubbleSent
   #FFFFFF / #000000 — background primary itself, so on a white pane the
   viewer's bubbles vanished. Desktop sent bubbles are elevated accent with
   accent-primary text instead; received keeps iOS's #E5E5EA / #2C2C2E.
   Phones follow iOS and keep the canvas and white bubbles. Last in the
   file on purpose: it overrides the bubble rule above at equal
   specificity by source order. */
@media (min-width: 835px) {
  .chat {
    background: var(--bg-primary);
  }

  .chat-message-row.sent .chat-message-bubble {
    background: var(--accent-blue-elevated);
    color: var(--accent-blue);
  }
}

/* ── Message requests ────────────────────────────────────────────────────── */
/* iOS ChatListView.requestsRow / ChatRequestsView / ChatRequestBar /
   ChatRequestPrivacyNotice parity. A request shows WHO, never WHAT. */

/* "Message requests" entry row atop the chat list (hidden at zero). */
.chat-requests-entry {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  width: 100%;
  padding: var(--sp-sm);
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--label-primary);
  font: inherit;
  font-size: var(--text-base);
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}

.chat-requests-entry:hover {
  background: var(--fill-elevated-secondary);
}

.chat-requests-entry-icon {
  position: relative;
  display: inline-flex;
  color: var(--label-secondary);
}

.chat-requests-entry-badge {
  position: absolute;
  top: -8px;
  right: -8px;
  box-sizing: border-box;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background: var(--accent-blue);
  color: #ffffff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
}

.chat-requests-entry-label {
  flex: 1;
  min-width: 0;
}

.chat-requests-entry-chevron {
  display: inline-flex;
  color: var(--label-tertiary);
}

/* The Requests inbox (sidebar). */
.chat-requests-header {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: 0 var(--sp-xs);
}

.chat-requests-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: none;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--label-primary);
  cursor: pointer;
}

.chat-requests-back:hover {
  background: var(--fill-elevated-secondary);
}

.chat-requests-title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 700;
  color: var(--label-primary);
}

.chat-request-row-main {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
}

.chat-request-row-main > .chat-list-row {
  flex: 1;
  min-width: 0;
}

.chat-request-row-main > .chat-request-actions {
  padding-right: var(--sp-sm);
}

/* Decline (red X) + Accept (green check) circles. */
.chat-request-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  flex-shrink: 0;
}

.chat-request-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  border-radius: var(--radius-full);
  cursor: pointer;
  transition: opacity 0.1s;
}

.chat-request-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

.chat-request-btn--decline {
  background: color-mix(in srgb, var(--accent-red) 14%, transparent);
  color: var(--accent-red);
}

.chat-request-btn--accept {
  background: color-mix(in srgb, var(--accent-green) 14%, transparent);
  color: var(--accent-green);
}

/* Inline decline confirmation — on the row, or in the request bar. */
.chat-request-confirm {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  margin: 0 var(--sp-sm) var(--sp-sm);
  padding: var(--sp-md);
  border-radius: var(--radius-sm);
  background: var(--fill-elevated-secondary);
}

.chat-request-bar .chat-request-confirm {
  flex: 1;
  margin: 0;
}

.chat-request-confirm-title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 700;
  color: var(--label-primary);
}

.chat-request-confirm-body {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 18px;
  color: var(--label-secondary);
}

.chat-request-confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-xs);
  margin-top: var(--sp-xs);
}

.chat-request-confirm-btn {
  padding: var(--sp-xs) var(--sp-md);
  border: none;
  border-radius: var(--radius-full);
  background: var(--bg-primary);
  color: var(--label-primary);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.chat-request-confirm-btn--danger {
  background: var(--accent-red);
  color: #ffffff;
}

/* A request's conversation: notice in place of the timeline, request bar in
   place of the composer. */
.chat.chat--request {
  display: flex;
  flex-direction: column;
}

.chat-request-notice {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xs);
  padding: var(--sp-xl);
  text-align: center;
}

.chat-request-notice-icon {
  display: inline-flex;
  color: var(--label-tertiary);
}

.chat-request-notice-title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--label-secondary);
}

.chat-request-notice-body {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

.chat-request-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  width: 100%;
  max-width: 800px;
  box-sizing: border-box;
  align-self: center;
  padding: var(--sp-sm) var(--sp-xl) var(--sp-xxl);
}

.chat-request-bar-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

.chat-request-error {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-sm);
  background: color-mix(in srgb, var(--accent-red) 12%, transparent);
  color: var(--accent-red);
  font-size: var(--text-sm);
  line-height: 18px;
}

@media (max-width: 834px) {
  .chat-request-bar {
    padding: var(--sp-sm) var(--sp-md)
      max(var(--sp-md), env(safe-area-inset-bottom));
  }
}

/* The Chats toggle's unseen-request mark: a dot, never a count (iOS). Lives
   here with the rest of the request styling; the count badge it modifies is
   in sidebar.css. */
.sidebar-mode-badge.sidebar-mode-badge--dot {
  min-width: 8px;
  width: 8px;
  height: 8px;
  padding: 0;
}

/* Phones (iOS parity round 1, 2026-10-23): pushed chat pages wear the
   phone header (ui/nav_bar) — back to Messages, the name, ⋯ — in place of
   the desktop page header. The AI landing gets one too, pinned on top. */
@media (max-width: 834px) {
  .chat > .page-header {
    display: none;
  }

  .chat > .nav-bar {
    grid-row: 1;
  }

  .chat-landing > .nav-bar {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
  }

  .chat-landing {
    position: relative;
  }
}

/* =============================================================================
   AI CHAT ON PHONES — iOS ChatConversationViewV2, the ephemeral chat
   (2026-10-27, Julian: "parity our AI chat view with iOS"). Everything
   here is ≤834px and scoped to .chat--ai / .chat--eph; the desktop AI
   chat keeps its own design. Numbers are iOS's, cited in the gap map that
   drove this: InlineNavBar 44 / 16, EphemeralHeaderGlow 380×140 blur 48
   breathing 3.6s (1.6s working) with a 14s hue turn, the list inset 32
   with 40 of headroom and a 24 top fade, AIThreadSpacing 16 inside a
   response / 32 between turns / 8 in a prompt burst / 16 before the
   pills / 4 between them, ChatBubble 16 radius at 75% on body 18/25.2,
   ComposerStyle.chat 56 pill with 40 discs and 8 insets, the 56 ✕ and +.
   ============================================================================= */

.chat-eph-glow,
.chat-composer-dismiss,
.chat-composer-fab,
.chat-messages-status {
  display: none;
}

/* Desktop keeps the landing's centred hero + pill; the bar is invisible. */
.chat-landing-bar {
  display: contents;
}

/* ── An AI chat's rows, every width (iOS ChatConversationViewV2; desktop
   drew one as a group chat — names, times, dividers — until 2026-10-27):
   no avatar slot, no dividers, tiny times or name rows; 32 between
   turns, 8 between prompts in a burst, 16 between the parts of one
   response. ── */
.chat--ai .chat-messages {
  gap: 0;
}
.chat--ai .chat-message-row {
  max-width: none;
  gap: 0;
}
.chat--ai .chat-message-row.sent {
  max-width: 75%;
}
.chat--ai .chat-message-avatar-slot {
  display: none;
}
.chat--ai .chat-message-row + .chat-message-row {
  margin-top: var(--sp-xxl);
}
.chat--ai .chat-message-row.sent + .chat-message-row.sent {
  margin-top: var(--sp-xs);
}
.chat--ai .chat-message-row.ai-bare + .chat-message-row.ai-bare {
  margin-top: var(--sp-md);
}
.chat--ai .chat-message-row:not(.ai-bare) + .chat-message-row.ai-bare {
  padding-top: 0;
}
.chat--ai .chat-message-row + .chat-message-row .chat-message-bubble {
  margin-top: 0;
}
.chat--ai .chat-timestamp-header,
.chat--ai .chat-message-tiny-time,
.chat--ai .chat-message-header-row,
.chat--ai .chat-message-name,
.chat--ai .chat-message-sender-name {
  display: none;
}

/* ── The viewer's bubble: body 18/25.2 in 12/16, radius 16 all round,
   solid accent with white copy — the accent's wash with accent copy
   once the viewer chose an accent (iOS sentTone .accent). ── */
.chat--ai .chat-message-row.sent .chat-message-bubble {
  font: var(--type-body);
  padding: var(--sp-sm) var(--sp-md);
  border-radius: 16px;
}
html:not([data-accent]) .chat--ai .chat-message-row.sent .chat-message-bubble {
  background: var(--accent-blue);
  color: var(--label-on-accent);
}

/* ── The agent's bare reply: body 18/25.2, primary; paragraphs 16
   apart; headings 22/27.5 bold, 16 above and 8 below; list items on
   the body line, a list closing 16 above what follows. ── */
.chat--ai .chat-message-row.ai-bare .chat-message-bubble {
  font: var(--type-body);
  color: var(--label-primary);
}
.chat--ai .chat-message-row.ai-bare .chat-message-text {
  line-height: 140%;
}
.chat--ai .chat-message-row.ai-bare .chat-message-text p {
  margin: 0 0 var(--sp-md);
}
.chat--ai .chat-message-row.ai-bare .chat-message-text h1,
.chat--ai .chat-message-row.ai-bare .chat-message-text h2,
.chat--ai .chat-message-row.ai-bare .chat-message-text h3,
.chat--ai .chat-message-row.ai-bare .chat-message-text h4 {
  font-size: 22px;
  font-weight: 700;
  line-height: 27.5px;
  margin: var(--sp-md) 0 var(--sp-xs);
}
.chat--ai .chat-message-row.ai-bare .chat-message-text > :first-child {
  margin-top: 0;
}
.chat--ai .chat-message-row.ai-bare .chat-message-text > :last-child {
  margin-bottom: 0;
}
.chat--ai .chat-message-row.ai-bare .chat-message-text ul,
.chat--ai .chat-message-row.ai-bare .chat-message-text ol {
  margin: 0 0 var(--sp-md);
}
.chat--ai .chat-message-row.ai-bare .chat-message-text li {
  line-height: 25.2px;
}

/* ── The working-status line sits in the timeline, 16 under the newest
   exchange. No typing dots: the agent's activity is the status line. ── */
.chat--ai .chat-messages-status {
  display: block;
  padding-top: var(--sp-md);
}
.chat--ai .chat-messages-status .agent-status-line {
  padding: 0;
}
.chat--ai .chat-composer > .agent-status-line,
.chat--ai .chat-typing,
.chat--ai .chat-scroll-to-bottom,
.chat--ai .chat-jump-to-latest {
  display: none;
}

/* ── The AI chat's chrome, every width (2026-10-27, Julian: "the desktop's
   main pane should be showing the AI chat that mirrors mobile's"): the
   header glow, the list's inset and fade, the composer row — the 56 pill
   with the paperclip disc, the send disc only with text, the ✕ while
   focused and the + while not. Desktop keeps it in a column (below). ── */
/* ── The header glow ── */
.chat--eph .nav-bar {
  position: relative;
  z-index: 2;
}
.chat-eph-glow {
  display: block;
  position: absolute;
  z-index: 1;
  left: 50%;
  top: calc(env(safe-area-inset-top, 0px) - 70px);
  width: 380px;
  height: 140px;
  margin-left: -190px;
  border-radius: 50%;
  background: var(--gradient-pastel);
  filter: blur(48px);
  transform: scale(0.9);
  opacity: 0.4;
  pointer-events: none;
  animation:
    chat-eph-glow-breathe 3.6s ease-in-out infinite alternate,
    chat-eph-glow-hue 14s linear infinite;
}
.chat--eph[data-ai-working] .chat-eph-glow {
  animation-duration: 1.6s, 14s;
}

/* ── The list's phone inset: 32 at the sides, 40 of headroom, a 24
   top fade. The rows' treatment is every width's (below). ── */
.chat--ai .chat-messages {
  max-width: none;
  padding: 40px var(--sp-xxl) var(--sp-xl);
  gap: 0;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 24px);
  mask-image: linear-gradient(to bottom, transparent, #000 24px);
}

/* ── The composer row: [✕ while focused][the pill][+ while not]. iOS:
   16 at the sides, 16 above, 10 below the bar and 8 under it, over the
   home indicator; a scrim edge dissolves content passing under. ── */
.chat--ai .chat-composer {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-xs);
  max-width: none;
  padding: var(--sp-md) var(--sp-md) calc(18px + env(safe-area-inset-bottom, 0px));
  background: var(--bg-primary);
}
.kb-open .chat--ai .chat-composer {
  padding-bottom: 18px;
}
.chat--ai .chat-composer::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -16px;
  height: 16px;
  background: linear-gradient(to bottom, transparent, var(--bg-primary));
  pointer-events: none;
}
.chat--ai .chat-mention-picker {
  left: var(--sp-md);
  right: var(--sp-md);
}

/* The pill: 56 at rest (8 inset, 40 discs), a capsule, one row — the
   paperclip disc, the text, the send disc that mounts only with
   something to send — with a staged strip or a reply bar above. */
.chat--ai .chat-composer-form {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) auto;
  grid-template-areas:
    "strip strip strip"
    "reply reply reply"
    "add input send";
  align-items: end;
  column-gap: var(--sp-xs);
  min-height: 56px;
  padding: var(--sp-xs);
  border: none;
  border-radius: 28px;
  background: var(--bg-secondary);
}
.chat--ai .chat-composer-toolbar {
  display: contents;
}
.chat--ai .chat-composer-toolbar-spacer,
.chat--ai .chat-composer-emoji-btn {
  display: none;
}
.chat--ai .chat-composer--mediaPreviewStrip {
  grid-area: strip;
  padding: var(--sp-xs) var(--sp-xs) 0;
}
.chat--ai .chat-composer-add {
  grid-area: add;
  width: 40px;
  height: 40px;
  background: var(--fill-elevated-secondary);
  color: var(--label-primary);
}
.chat--ai .chat-composer-input-wrap {
  grid-area: input;
  padding: 0;
  align-items: center;
}
.chat--ai .chat-composer-send {
  grid-area: send;
  display: none;
  width: 40px;
  height: 40px;
  background: var(--bg-primary-inverse);
  color: var(--label-inverse-primary);
}
html[data-accent] .chat--ai .chat-composer-send {
  background: var(--accent-blue);
  color: var(--label-on-accent);
}
.chat--ai .chat-composer-form:has(.composer-inline-input-wrap--has-content) .chat-composer-send,
.chat--ai .chat-composer-form:has(.chat-composer--mediaPreviewStrip) .chat-composer-send {
  display: flex;
}
.chat--ai .chat-composer-send:disabled {
  background: var(--fill-tertiary);
  color: var(--label-secondary);
}
/* The text: body 18/25.2 on a 40 line, up to 12 lines — every chat
   composer's since 2026-10-27 (the shared .chat-composer-milkdown rules). */
/* The ✕ (iOS showsKeyboardDismiss) and the + (CaptureFABButton): 56
   discs, 28 glyphs; the ✕ while the pill has focus, the + while not. */
.chat--ai .chat-composer-dismiss {
  flex: none;
  width: 56px;
  height: 56px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--fill-elevated-secondary);
  color: var(--label-primary);
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.chat--ai .chat-composer:focus-within .chat-composer-dismiss {
  display: flex;
}
.chat-composer-fab {
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  background: var(--accent-blue);
  color: var(--label-on-accent);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  -webkit-tap-highlight-color: transparent;
}
.chat--ai .chat-composer-fab,
.chat--eph .chat-composer-fab {
  display: flex;
}
.chat--ai .chat-composer:focus-within .chat-composer-fab,
.chat--eph.chat-landing:focus-within .chat-composer-fab {
  display: none;
}


/* Desktop: the phone's AI chat in a 784 column (720 + the 32 insets),
   the glow behind the page header, which the nav bar replaces there. */
@media (min-width: 835px) {
  .chat--eph > .page-header {
    position: relative;
    z-index: 2;
  }
  .chat-eph-glow {
    top: -70px;
  }
  .chat--ai .chat-messages {
    max-width: 784px;
    margin: 0 auto;
  }
  .chat--ai .chat-composer {
    max-width: 784px;
    margin: 0 auto;
    padding-bottom: var(--sp-lg);
  }
  .chat--ai .chat-composer::before {
    display: none;
  }
}

@media (max-width: 834px) {
}

/* ── The blank chat (/chats/new, and the Messages main pane at rest):
   the chat page's chrome, no hero — every width since 2026-10-27
   (Julian: the desktop pane should mirror mobile's; the hero went).
   Stretched, not centred: a centred nav bar shrink-wraps its grid around
   the title — Back, "AI Chat" and the clock huddled mid-screen. */
.chat--eph.chat-landing {
  position: relative;
  align-items: stretch;
  justify-content: flex-start;
  gap: 0;
  padding: 0;
  overflow: hidden;
}
.chat--eph .chat-landing-hero {
  display: none;
}
.chat-landing-bar {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-xs);
  width: 100%;
  margin-top: auto;
  padding: var(--sp-md) var(--sp-md) calc(18px + env(safe-area-inset-bottom, 0px));
}
.kb-open .chat-landing-bar {
  padding-bottom: 18px;
}
.chat--eph .chat-landing-composer {
  flex: 1 1 auto;
  min-width: 0;
  width: auto;
  max-width: none;
  min-height: 56px;
  padding: var(--sp-xs) var(--sp-xs) var(--sp-xs) var(--sp-md);
  border: none;
  border-radius: 28px;
  background: var(--bg-secondary);
}
.chat--eph .chat-landing-composer:focus-within {
  border-color: transparent;
}
.chat--eph .chat-landing-input {
  font: var(--type-body);
  padding: 7.4px 0;
}
.chat--eph .chat-landing-input::placeholder {
  color: var(--label-primary);
}
.chat--eph .chat-landing-send {
  width: 40px;
  height: 40px;
  background: var(--bg-primary-inverse);
  color: var(--label-inverse-primary);
}
html[data-accent] .chat--eph .chat-landing-send {
  background: var(--accent-blue);
  color: var(--label-on-accent);
}
.chat--eph .chat-landing-composer:has(.chat-landing-input:placeholder-shown) .chat-landing-send {
  display: none;
}

/* Desktop: the page header stands in for the nav bar, the glow behind
   it; the bar in the AI chat's 784 column. */
@media (min-width: 835px) {
  .chat-landing > .page-header {
    position: relative;
    z-index: 2;
  }
  .chat--eph .chat-landing-bar {
    max-width: 784px;
    margin: auto auto 0;
    padding-bottom: var(--sp-lg);
  }
}

@media (max-width: 834px) {
  .chat-landing > .page-header {
    display: none;
  }

  /* ── The AI chat list's phone chrome: the nav bar, the + in a 56
     bottom row. The rows are every width's (below). ── */
  .ai-chats-page > .page-header {
    display: none;
  }
  .ai-chats-bar {
    flex: none;
    display: flex;
    justify-content: flex-end;
    min-height: 56px;
    padding: var(--sp-xs) var(--sp-sm) calc(var(--sp-sm) + env(safe-area-inset-bottom, 0px));
  }
  .ai-chats-plus {
    display: flex;
  }
}

/* ── The AI chat list (iOS AIChatListView; desktop too, 2026-10-27):
   rows 16 apart in 16/4, the 16 sparkle head pastel while unread, the
   title on the body line (bold while unread), time and preview in the
   footnote. Desktop draws it in the main pane under the page header,
   with the header's + for a new chat; the phone's bar carries the +. ── */
.ai-chats-page .chat-list-rows {
  gap: var(--sp-md);
}
.ai-chats-page .chat-list-row.ai-chat-row {
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-3xs) var(--sp-md);
}
.ai-chat-row-head {
  flex: none;
  display: inline-flex;
  width: 16px;
  height: 16px;
  color: var(--label-secondary);
}
.chat-list-row--unread .ai-chat-row-head .icon {
  background: var(--gradient-pastel);
}
.ai-chats-page .chat-list-row-title {
  font: var(--type-body);
}
.ai-chats-page .chat-list-row--unread .chat-list-row-title {
  font-weight: 700;
}
.ai-chats-page .chat-list-row-timestamp,
.ai-chats-page .chat-list-row-subtitle {
  font: var(--type-footnote);
}
.ai-chats-empty {
  padding: var(--sp-xl) var(--sp-md);
  text-align: center;
  font: var(--type-body);
  color: var(--label-secondary);
}
@media (min-width: 835px) {
  .ai-chats-page {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
  }
  .ai-chats-page .messages-page-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: var(--sp-md) var(--sp-xl) var(--sp-xl);
  }
  .ai-chats-page .chat-list-rows {
    max-width: 720px;
    margin: 0 auto;
  }
  .ai-chats-bar {
    display: none;
  }
}

@keyframes chat-eph-glow-breathe {
  from {
    transform: scale(0.9);
    opacity: 0.4;
  }
  to {
    transform: scale(1.08);
    opacity: 0.65;
  }
}

@keyframes chat-eph-glow-hue {
  from {
    filter: blur(48px) hue-rotate(0deg);
  }
  to {
    filter: blur(48px) hue-rotate(360deg);
  }
}

@media (max-width: 834px) and (prefers-reduced-motion: reduce) {
  .chat-eph-glow {
    animation: none;
  }
}

/* Rich chat cards in message bubbles — see ui/chat_card. */
/* ═══════════════════════════════════════════════════════════════════════════
   Rich chat cards (ui/chat_card.gleam) — web port of iOS ChatCardView.
   Two layouts: hero (full-width media over a title footer) and compact
   (64px leading thumbnail). Plus the detail sheet hosted in the shared
   sheet chrome (ui/sheet).
   ═══════════════════════════════════════════════════════════════════════════ */

/* Width-flexible like iOS ChatCardView (maxWidth: .infinity): the card
   fills whatever container hosts it — the full chat column on AI-bare
   rows, the bubble on bubbled rows. No fixed cap. */
.chat-card {
  display: flex;
  width: 100%;
  background: var(--bg-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-lg);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  text-align: left;
  padding: 0;
  font: inherit;
  cursor: pointer;
}

/* Bare cards (no tap behavior) render as a div — keep the default cursor. */
div.chat-card {
  cursor: default;
}

@media (hover: hover) {
  a.chat-card:hover,
  button.chat-card:hover {
    background: var(--bg-subtle);
  }
}

.chat-card--hero {
  flex-direction: column;
}

.chat-card--compact {
  flex-direction: row;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm);
}

/* ── Media ── */

.chat-card__media--hero {
  width: 100%;
  height: 150px;
  flex-shrink: 0;
}

.chat-card__media--compact {
  width: 64px;
  height: 64px;
  flex-shrink: 0;
  border-radius: 10px;
  overflow: hidden;
}

.chat-card__thumb {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Map placeholder — direct map integration is deferred, so the media slot
   is a muted block with a centered accent pin dot (echoes the iOS marker
   composite without tiles). */
.chat-card__map {
  position: relative;
  background: var(--bg-subtle);
}

.chat-card__map-pin {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 22px;
  height: 22px;
  transform: translate(-50%, -50%);
  border-radius: var(--radius-full);
  background: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
}

.chat-card__map-pin::after {
  content: "";
  position: absolute;
  inset: 4px;
  border-radius: var(--radius-full);
  background: var(--accent-blue);
}

/* ── Text + footer ── */

.chat-card__footer {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-md);
}

.chat-card__text {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  flex: 1;
}

.chat-card__title {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--label-primary);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.chat-card__subtitle {
  font-size: var(--text-xs);
  color: var(--label-secondary);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ── Action circle ── */

.chat-card__action {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  border-radius: var(--radius-full);
  background: var(--brand-blue-12);
  color: var(--brand-blue);
  text-decoration: none;
}

@media (hover: hover) {
  .chat-card__action:hover {
    filter: brightness(0.95);
  }
}

/* (The rich card's detail sheet renders in the shared chrome — ui/sheet.) */

/* ============================================================================
   Chat Detail — settings/info page for a single chat thread.

   Layout mirrors the chat module: full-height column, max-width centered.
   ========================================================================= */

/* ── Layout ──────────────────────────────────────────────────────────── */

.chat-detail {
  display: flex;
  flex-direction: column;
  height: 100vh;   /* fallback */
  height: 100dvh;  /* dynamic viewport */
  max-width: 720px;
  margin: 0 auto;
  width: 100%;
  padding: 0 var(--sp-xl);
}

/* ── Header ──────────────────────────────────────────────────────────── */

.chat-detail-back {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  color: var(--label-tertiary);
  text-decoration: none;
  font-size: var(--text-sm);
  padding: var(--sp-3xs) var(--sp-xs);
  border-radius: var(--radius-md);
}
@media (hover: hover) {
  .chat-detail-back:hover {
  background: var(--bg-tertiary);
  color: var(--label-primary);
}
}

/* ── Avatar Section ──────────────────────────────────────────────────── */

.chat-detail-avatar-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--sp-xl) 0;
}

.chat-detail-avatar-wrapper {
  position: relative;
  cursor: pointer;
  border-radius: var(--radius-full);
  /* No overflow: hidden — .chat-detail-avatar-badge is absolutely
     positioned at the wrapper's bottom-right corner and needs to render
     outside the circular avatar's own bounds; hidden overflow clipped it. */
}

.chat-detail-avatar-img {
  width: 80px;
  height: 80px;
  border-radius: var(--radius-full);
  object-fit: cover;
  display: block;
}

.chat-detail-avatar-badge {
  position: absolute;
  bottom: 0;
  right: 0;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  background: var(--bg-secondary);
  color: var(--label-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-xs);
  border: 2px solid var(--bg);
}

.chat-detail-avatar-loading {
  position: absolute;
  bottom: 0;
  right: 0;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  background: var(--bg-tertiary);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--fill-elevated-secondary);
}

.chat-detail-avatar-file {
  display: none;
}

/* ── Name Section ────────────────────────────────────────────────────── */

.chat-detail-name-section {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) 0;
}

.chat-detail-name {
  font-size: var(--text-xl);
  font-weight: 600;
  color: var(--label-primary);
  margin: 0;
}

.chat-detail-name-edit {
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--label-tertiary);
  padding: var(--sp-3xs);
  border-radius: var(--radius-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-base);
  line-height: 1;
}
@media (hover: hover) {
  .chat-detail-name-edit:hover {
  background: var(--bg-tertiary);
  color: var(--label-primary);
}
}

.chat-detail-name-editing {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-xs) 0 var(--sp-md);
}

.chat-detail-name-input {
  padding: var(--sp-xs) var(--sp-sm);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  background: var(--bg);
  font-family: inherit;
  font-size: var(--text-sm);
  color: var(--label-primary);
  outline: none;
  text-align: center;
  width: 240px;
  max-width: 100%;
}
.chat-detail-name-input:focus {
  border-color: var(--bg-secondary);
}

.chat-detail-name-actions {
  display: flex;
  gap: var(--sp-xs);
}

.chat-detail-name-actions button {
  padding: var(--sp-3xs) var(--sp-sm);
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
  border: 1px solid var(--border-primary);
  background: var(--bg);
  color: var(--label-primary);
}
@media (hover: hover) {
  .chat-detail-name-actions button:hover {
  background: var(--bg-tertiary);
}
}

/* ── Members Section ─────────────────────────────────────────────────── */

.chat-detail-members {
  padding: var(--sp-md) 0;
}

.chat-detail-members-header {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding-bottom: var(--sp-sm);
}

.chat-detail-sheet .chat-detail-members-header  .title {
  font-size: var(--text-lg);
  color: var(--label-primary);
}

.chat-detail-member-count {
  font-size: var(--text-base);
  color: var(--label-secondary);
  font-weight: 500;
}

.chat-detail-add-member {
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--label-secondary);
  padding: var(--sp-3xs);
  border-radius: var(--radius-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-lg);
  line-height: 1;
}
@media (hover: hover) {
  .chat-detail-add-member:hover {
  background: var(--bg-tertiary);
  color: var(--label-primary);
}
}

.chat-detail-member-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3xs);
}

.chat-detail-member-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: var(--radius-md);
}
@media (hover: hover) {
  .chat-detail-member-row:hover {
  background: var(--bg-tertiary);
}
}

.chat-detail-member-info,
.chat-detail-member-link {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  text-decoration: none;
  color: inherit;
  min-width: 0;
}
@media (hover: hover) {
  .chat-detail-member-info:hover,
  .chat-detail-member-link:hover {
  text-decoration: none;
}
}

.chat-detail-member-name {
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--accent-blue);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-detail-member-row.you .chat-detail-member-name {
  color: var(--label-primary);
  pointer-events: none;
}


.chat-detail-member-you, .chat-detail-member-row.owner .chat-detail-member-role {
    font-size: var(--text-sm);
    font-weight: 600;
    padding: var(--sp-xs) var(--sp-sm);
    border-radius: var(--radius-full);
    white-space: nowrap;
    flex-shrink: 0;
}

.chat-detail-member-you {
  color: var(--accent-blue);
  background: rgba(var(--color-blue), .1);
  background-blend-mode: var(--blend-mode);
}

.chat-detail-member-row.owner .chat-detail-member-role {
  color: var(--label-primary);
  background: rgba(var(--color-gray-2), .1);
  background-blend-mode: var(--blend-mode);
}

.chat-detail-remove-member {
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--label-secondary);
  padding: var(--sp-3xs);
  border-radius: var(--radius-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-sm);
  line-height: 1;
}
@media (hover: hover) {
  .chat-detail-remove-member:hover {
  background: var(--bg-tertiary);
  color: var(--accent-red);
}
}

.chat-detail-sheet .chat-detail-member-row {
  padding-left: 0;
}

/* ── Actions ─────────────────────────────────────────────────────────── */

/* (The sheet's actions live in its footer (ui/sheet, 48px .button--lg)
   with the semantic .button.destructive / .button.danger variants —
   see styles/button.css and sheet.css.) */

/* ── Confirm Dialog ──────────────────────────────────────────────────── */

.chat-detail-confirm-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Must layer above the settings sheet, which sits at the sheet tier
     (401 — see sheet.css desktop block). */
  z-index: 410;
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  /* Rendered in the settings sheet's layer (`sheet.overlays`), which
     sets `pointer-events: none`. Without re-enabling here, clicks fall
     through to the sheet beneath and the Confirm/Cancel buttons aren't
     even hoverable. */
  pointer-events: auto;
}

.chat-detail-confirm-dialog {
  background: var(--bg-secondary);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: var(--sp-xl);
  width: min(400px, calc(100vw - 48px));
}

.chat-detail-confirm-dialog p {
  margin: 0 0 var(--sp-lg);
  font-size: var(--text-sm);
  color: var(--label-primary);
  line-height: 1.5;
}

.chat-detail-confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-xs);
}

.chat-detail-confirm-cancel {
  padding: var(--sp-xs) var(--sp-md);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--label-primary);
  font-family: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}
@media (hover: hover) {
  .chat-detail-confirm-cancel:hover {
  background: var(--bg-tertiary);
}
}

.chat-detail-confirm-confirm {
  padding: var(--sp-xs) var(--sp-md);
  border: none;
  border-radius: var(--radius-md);
  background: var(--accent-red);
  color: #ffffff;
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
}
@media (hover: hover) {
  .chat-detail-confirm-confirm:hover {
  opacity: 0.9;
}
}

/* ── Sheet chrome ────────────────────────────────────────────────────── */

/* Chat Settings renders in the shared sheet chrome (ui/sheet, sheet.css):
   the phone bottom sheet, the centred modal ≥835px. The header, the ✕,
   the footer and the scroller are the component's; only chat-specific
   sizing and inner layout live here. */

/* Desktop modal: keep the old .chat-detail-modal footprint (480px wide,
   capped height) instead of sheet.css's 560px Medium default. The
   two-class selector outspecifies sheet.css's single-class desktop width
   rule regardless of import order. */
@media (min-width: 835px) {
  .sheet.chat-detail-sheet {
    width: min(480px, 92vw);
    max-height: min(640px, 80vh);
  }
}

/* The settings page's body in the sheet's stack (which scrolls and holds
   the 16 inset); this adds the inner breathing room. */
.chat-detail-body {
  padding: 0 var(--sp-xs);
}

/* Member-count badge inside the sheet. */

.chat-detail-member-role {
  font-size: var(--text-xs);
  color: var(--label-tertiary);
  flex-shrink: 0;
}

.chat-detail-member-remove {
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--label-secondary);
  padding: var(--sp-3xs);
  border-radius: var(--radius-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  flex-shrink: 0;
}
@media (hover: hover) {
  .chat-detail-member-remove:hover {
  background: var(--bg-tertiary);
  color: var(--accent-red);
}
}

.chat-detail-members-section {
  padding: var(--sp-md) 0;
}

.chat-detail-members-header {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding-bottom: var(--sp-sm);
}

.chat-detail-members-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3xs);
}

.chat-detail-name-save,
.chat-detail-name-cancel {
  padding: var(--sp-3xs) var(--sp-sm);
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
  border: 1px solid var(--border-primary);
  background: var(--bg);
  color: var(--label-primary);
}
@media (hover: hover) {
  .chat-detail-name-save:hover,
  .chat-detail-name-cancel:hover {
  background: var(--bg-tertiary);
}
}

/* The thread-header settings gear is a plain page_header.icon_button
   (.header-icon-btn, shell.css); the former .chat-header-actions /
   .chat-header-detail-link chrome was retired with the ui/page_header
   migration. */

/* Block / Unblock on a member row (2026-09-18). A quiet text button beside
   the remove `x`; red while it would BLOCK, secondary while it would unblock. */
.chat-detail-member-block {
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--accent-red);
  font: inherit;
  font-size: var(--text-sm);
  padding: var(--sp-3xs) var(--sp-xs);
  border-radius: var(--radius-md);
  flex-shrink: 0;
}

.chat-detail-member-block--on {
  color: var(--label-secondary);
}

.chat-detail-member-block:disabled {
  opacity: 0.5;
  cursor: default;
}

@media (hover: hover) {
  .chat-detail-member-block:hover {
    background: var(--bg-tertiary);
  }
}

/* Search — the phone's Collections page (search.view) and, on desktop, the
   same field, collections and rows in the list pane (search.view_sidebar,
   round 5; its layout is in sidebar.css under .sidebar-search*). */

/* Page root — mirrors .notifications-page: full-width shell hosting the
   standardized header above the column-constrained body. Only phones show
   it (.main-mobile-only); bg-secondary, like the phone's other pages. */
.search-page {
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg-secondary);
}

/* The page's column, the phone's scope for its gestures and motion; its
   width and padding are the phone block's below. (The old desktop page's
   54vw / 900px column went with that page, round 5.) */
.search {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* ── Header row: pill input ───────────────────────────────────────────── */

.search-header {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  margin-bottom: var(--sp-xl);
}

.search-input {
  flex: 1;
  height: 48px;
  padding: var(--sp-sm) var(--sp-md);
  border: 1px solid transparent;
  border-radius: 9999px;
  background: var(--bg-primary);
  color: var(--label-primary);
  font: inherit;
  outline: none;
  transition: border-color 120ms ease;
}

.search-input::placeholder {
  color: var(--label-primary);
}

.search-input:focus {
  border-color: var(--accent-blue);
}

.feed .search-input {
  color: var(--label-tertiary);
}

/* The field row (search.view_field, iOS SearchFieldRow, round 2
   2026-10-23): the 58px capsule on the phone page and in the desktop list
   pane (round 5). Scoped under .search-pill: .feed-search-pill also wears
   .search-input and keeps the 48px look above. The ✕, the phone's margin
   and the press scale stay in the phone block below; the list pane has no
   ✕ and turns the hairline accent on focus instead (sidebar.css). */
.search-field-row {
  display: flex;
  flex: 1;
  min-width: 0;
  align-items: center;
  gap: 8px;
}

/* 58 tall: 17 above and below a 24 band, 16 at the sides; a 1px inside
   hairline, no shadow, no glass. */
.search-pill {
  display: flex;
  flex: 1;
  min-width: 0;
  align-items: center;
  gap: 8px;
  height: 58px;
  padding: 0 16px;
  border-radius: 9999px;
  background: var(--bg-primary);
  box-shadow: inset 0 0 0 1px var(--border-secondary);
  /* base.css floors phone inputs at max(16px, 1em) !important (no iOS
     zoom-on-focus); 1em is the parent's size, so the pill carries the
     18px body token for the input to inherit. */
  font-size: 18px;
  cursor: text;
}

.search-pill .search-pill-icon {
  display: inline-flex;
  flex: none;
  width: 24px;
  height: 24px;
  color: var(--label-primary);
}

/* iOS body token, placeholder undimmed (textPrimary). */
.search-pill .search-input {
  flex: 1;
  min-width: 0;
  height: 25.2px;
  padding: 0;
  border: none;
  border-radius: 0;
  background: none;
  color: var(--label-primary);
  font-size: 18px;
  font-weight: 400;
  line-height: 25.2px;
}

.search-pill .search-input:focus {
  border-color: transparent;
}

/* The clear chip (SearchBarChrome): a 32px circle laid out as 24 tall,
   overflowing into the pill's padding; 44px of hit area. Gone — no
   width, no gap — while the field is empty. */
.search-pill .search-clear {
  position: relative;
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  margin: -4px 0;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--bg-secondary);
  color: var(--label-primary);
  cursor: pointer;
  transition:
    width 150ms ease-in-out,
    margin-left 150ms ease-in-out,
    opacity 150ms ease-in-out,
    transform 150ms ease-in-out;
}

.search-pill .search-clear::after {
  content: "";
  position: absolute;
  inset: -6px;
}

.search-pill .search-input:placeholder-shown + .search-clear {
  width: 0;
  margin-left: -8px;
  opacity: 0;
  transform: scale(0.8);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .search-pill .search-clear {
    transition: none;
  }
}

/* ── Scrollable body ──────────────────────────────────────────────────── */

.search-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding-bottom: var(--sp-xl);
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.search-body::-webkit-scrollbar {
  display: none;
}

/* ── Results ──────────────────────────────────────────────────────────────
   Results are reminder rows (ui/timeline_list), same as the reminders list —
   see styles/timeline_row.css for the row itself.

   Deliberately NOT .feed-items-wrap: that is a scroll container, and
   .search-body already is one (in the list pane, .sidebar-scroll), so
   reusing it would nest a second scroller inside the first. This is a
   plain column; the scroller around it does the scrolling. */
.search-results {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);

  width: 100%;
  max-width: 900px;
  margin: 0 auto;
  padding-bottom: var(--sp-lg);
}

.search-results .tl-list {
  width: 100%;
}

/* ── The collections grid (iOS BucketGridView, round 2 2026-10-23) ───────
   The phone Search page's entry surface and the desktop list pane's (round
   5), one block at every width: two equal columns 8 apart both ways, so
   the cards are 176.5 wide in the phone's 361 column, 183 in the list
   pane's 374 and 220 in its 448. */
.bucket-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  width: 100%;
}

/* A tinted gradient tile: radius 20, at least 140 tall, no border, shadow
   or hover state (iOS has none, and a fill change would wash out the
   gradient). Overflow stays visible: the deck reaches into the padding. */
.bucket-card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 140px;
  border-radius: 20px;
  background: linear-gradient(
    to bottom right,
    var(--card-grad-from),
    var(--card-grad-to)
  );
  overflow: visible;
}

/* Each tint names its gradient's two stops (round 2's table; teal on the
   fourth at every width). */
.bucket-card.bucket-card-tint-0 {
  --card-grad-from: var(--bucket-grad-blue-from);
  --card-grad-to: var(--bucket-grad-blue-to);
}

.bucket-card.bucket-card-tint-1 {
  --card-grad-from: var(--bucket-grad-orange-from);
  --card-grad-to: var(--bucket-grad-orange-to);
}

.bucket-card.bucket-card-tint-2 {
  --card-grad-from: var(--bucket-grad-red-from);
  --card-grad-to: var(--bucket-grad-red-to);
}

.bucket-card.bucket-card-tint-3 {
  --card-grad-from: var(--bucket-grad-teal-from);
  --card-grad-to: var(--bucket-grad-teal-to);
}

.bucket-card.bucket-card-tint-4 {
  --card-grad-from: var(--bucket-grad-green-from);
  --card-grad-to: var(--bucket-grad-green-to);
}

/* The padding lives on the button so the whole tile is the tap target.
   Pressed it dims, like an iOS plain button; from a keyboard it takes the
   accent ring. */
.bucket-card-main {
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  width: 100%;
  min-height: 140px;
  padding: 16px;
  border: none;
  border-radius: inherit;
  background: none;
  text-align: left;
  cursor: pointer;
  font: inherit;
  color: var(--label-primary);
  transition: opacity 120ms ease;
}

.bucket-card-main:active {
  opacity: 0.7;
}

.bucket-card-main:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

.bucket-card-top {
  display: flex;
  align-items: flex-start;
  gap: 4px;
  width: 100%;
}

.bucket-card-title {
  flex: 1;
  min-width: 0;
  color: var(--label-primary);
  font-size: 18px;
  font-weight: 400;
  line-height: 25.2px;
  overflow-wrap: anywhere;
}

.bucket-card-foot {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  margin-top: auto;
}

.bucket-card-suggested-pill {
  padding: 8px 12px;
  border-radius: 9999px;
  background: var(--fill-elevated-quaternary);
  color: var(--label-primary);
  font-size: 12px;
  font-weight: 400;
  line-height: 16px;
  letter-spacing: 0;
  text-transform: none;
}

.bucket-card-count {
  padding: 0;
  border-radius: 0;
  background: none;
  color: var(--label-secondary);
  font-size: 12px;
  font-weight: 400;
  line-height: 16px;
}

/* The 42px deck (TimelineListRow's thumbnail deck): three cards fanned by
   rotation alone about one centre — the newest on top at +8°, then 0°,
   then −8° — in a 54×48 frame pulled 8 up and 6 right into the card's
   padding. The back two sit at 0.9. */
.bucket-card-deck {
  position: relative;
  flex: none;
  display: block;
  width: 54px;
  height: 48px;
  margin: -8px -6px 0 0;
  padding: 0;
}

.bucket-card-thumb {
  position: absolute;
  top: 3px;
  left: 6px;
  width: 42px;
  height: 42px;
  border-radius: 8px;
  object-fit: cover;
  background: var(--bg-secondary);
  box-shadow: 0 1.5px 6px rgba(0, 0, 0, 0.18);
  transform-origin: center;
  animation: bucket-thumb-in 150ms ease-out;
}

.bucket-card-thumb:nth-child(1) {
  z-index: 3;
  transform: rotate(8deg);
}

.bucket-card-thumb:nth-child(2) {
  z-index: 2;
  opacity: 0.9;
}

.bucket-card-thumb:nth-child(3) {
  z-index: 1;
  opacity: 0.9;
  transform: rotate(-8deg);
}

/* Calendar's badge (CalendarDateBadge): a 42 tile at +8°, weekday in red
   over the day. */
.bucket-card-date-badge {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 42px;
  min-width: 0;
  height: 42px;
  margin: 0;
  padding: 0;
  border-radius: 8.82px;
  background: var(--bg-primary);
  box-shadow: inset 0 0 0 1px var(--ui-borders);
  transform: rotate(8deg);
}

.bucket-card-date-weekday {
  color: var(--calendar-weekday);
  font-size: 9.24px;
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
}

.bucket-card-date-day {
  margin-top: -2.1px;
  color: var(--label-primary);
  font-size: 21.84px;
  font-weight: 400;
  line-height: 1.2;
}

@media (prefers-reduced-motion: reduce) {
  .bucket-card-thumb {
    animation: none;
  }
}

/* ── Browse sections (Today / Yesterday / …) ────────────────────────── */
/* Home's foldable header (styles/section_header.css) and Home's rhythm
   (2026-10-23): header → first card 8px, card → card 4px, last card →
   next header 24px. Typed results keep their flat 12px list. */
.search-results.search-browse {
  gap: var(--sp-xl);
}

/* Header → first card 8px is the folding body's own top padding
   (section_header.css). */
.search-browse-section {
  display: flex;
  flex-direction: column;
}

.search-browse .tl-list {
  gap: var(--sp-3xs);
}

/* ── Phones (≤ 834px): the page's column and the field's phone-only parts */

@media (max-width: 834px) {
  .search {
    width: 100%;
    max-width: 100vw;
    padding: var(--sp-xl) var(--sp-md) var(--sp-sm);
  }

  /* iOS SearchFieldRow / SearchBarChrome (round 2, 2026-10-23): pill
     bottom → first grid card 12 (AppHeaderLayout: 8 + 4). */
  .search-header {
    margin-bottom: 12px;
  }

  /* Pressed, the pill grows to 1.04 (iOS PressableButtonStyle, spring .3
     / .6). Phones only: desktop's field doesn't scale (round 5 spec §3). */
  .search-pill {
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  .search-pill:active {
    transform: scale(1.04);
  }

  /* The ✕ circle (SearchFieldRow): 58px, 8px right of the pill, only
     while the field is focused; the pill's width eases as it arrives. */
  .search-close {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: 0;
    height: 58px;
    margin-left: -8px;
    padding: 0;
    overflow: hidden;
    border: none;
    border-radius: 50%;
    background: var(--bg-primary);
    box-shadow: inset 0 0 0 1px var(--border-secondary);
    color: var(--label-primary);
    opacity: 0;
    transform: scale(0.8);
    pointer-events: none;
    cursor: pointer;
    transition:
      width 200ms ease-in-out,
      margin-left 200ms ease-in-out,
      opacity 200ms ease-in-out,
      transform 200ms ease-in-out;
  }

  .search-field-row:focus-within .search-close {
    width: 58px;
    margin-left: 0;
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }

  /* Presses grow both buttons (iOS scale 1.2, spring .3 / .6). */
  .search-clear:active,
  .search-field-row:focus-within .search-close:active {
    transform: scale(1.2);
  }
}

@media (max-width: 834px) and (prefers-reduced-motion: reduce) {
  .search-pill,
  .search-close {
    transition: none;
  }
}

/* Keep / No thanks inside a suggested collection (iOS
   TimelineListContainerView), in a slot of its own above the content, on
   the phone page and in the list pane: centred, 12 apart. Each surface
   spaces it from the field and the content (the phone block below;
   sidebar.css). */
.search-keep-bar {
  display: flex;
  justify-content: center;
  gap: 12px;
}

.search-keep,
.search-no-thanks {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 8px 12px;
  border: none;
  border-radius: 9999px;
  background: none;
  font-size: 18px;
  font-weight: 500;
  line-height: 25.2px;
  cursor: pointer;
}

.search-keep {
  background: var(--fill-elevated-quaternary);
  color: var(--label-primary);
}

.search-no-thanks {
  color: var(--label-secondary);
}

@media (max-width: 834px) {
  /* Its own slot: 16 under the field (12 + 4), 16 over the content (12 +
     the results' 4). */
  .search-keep-bar {
    margin: 4px 0 12px;
  }

  /* Field → first list row or section header: 16 (8 + 8). */
  .search-results {
    padding-top: 4px;
  }

  /* iOS ListFeed rhythm in a collection: 24px headers, header → first
     row 12, row → row 4, a section's last row → next header 32. The list
     pane keeps Home's 8 / 4 / 24. The list can scroll its last rows up to
     mid-screen (bottom inset: half the screen). */
  .search-results.search-browse {
    gap: 32px;
  }

  .search-results {
    padding-bottom: 50dvh;
  }

  .search-browse .section-header__title {
    font-size: 24px;
    font-weight: 700;
    line-height: 33.6px;
  }

  .search-browse .section-body__content {
    padding-top: 12px;
  }

  /* Leaving a collection: the grid (and the header title, phone_shell)
     fade back in over 0.14s (iOS ToolbarMotion exit). */
  .bucket-grid--enter {
    animation: search-exit-fade 140ms cubic-bezier(0.38, 0.7, 0.125, 1);
  }
}

/* Empty and loading states (iOS CenteredStateView and its spinner), on the
   phone page and in the list pane: the magnifier over the message, 12
   apart, centred; the last results stay up through a refresh
   (.search-results--stale, a plain .search-results column); no count
   footer. Each surface centres the column in its own height (the phone
   block below; sidebar.css's flex: 1). */
.search-state,
.search-spinner {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
}

.search-state-icon {
  display: inline-flex;
  width: 32px;
  height: 32px;
  color: var(--label-tertiary);
}

.search-state-text {
  margin: 0;
  color: var(--label-secondary);
  font-size: 16px;
  font-weight: 600;
  line-height: 20px;
  text-align: center;
}

/* Hidden for 450ms, then an ease-in fade over 0.2s: a search that lands
   sooner never shows it. */
.search-spinner {
  animation: search-spinner-in 200ms ease-in 450ms both;
}

.search-spinner-ring {
  width: 20px;
  height: 20px;
  border: 2px solid var(--label-secondary);
  border-top-color: transparent;
  border-radius: 50%;
  animation: search-spin 800ms linear infinite;
}

@media (max-width: 834px) {
  /* Centred in what's left under the field (header 44 + 10, field 58 +
     12, tab row 86 + 16). */
  .search-state,
  .search-spinner {
    min-height: calc(
      100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 226px
    );
  }

  /* Typed results: one flat list, 4 apart (iOS ListFeed). */
  .search-results .tl-list {
    gap: 4px;
  }
}

@keyframes search-spinner-in {
  from {
    opacity: 0;
  }
}

@keyframes search-spin {
  to {
    transform: rotate(360deg);
  }
}

@keyframes search-exit-fade {
  from {
    opacity: 0;
  }
}

@keyframes bucket-thumb-in {
  from {
    opacity: 0;
  }
}

@media (max-width: 834px) and (prefers-reduced-motion: reduce) {
  .bucket-grid--enter {
    animation: none;
  }
}

/* Monogram avatar — circular badge with a letter on a deterministic
   desaturated background. Sizes are set inline via the `avatar()` helper
   so one CSS class supports arbitrary px sizes. */

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  flex-shrink: 0;
  font-weight: 600;
  font-family: var(--font-sans);
  line-height: 1;
  color: #ffffff;
  user-select: none;
  letter-spacing: 0;
  text-transform: uppercase;
}

/* Muted/desaturated palette — picked so they sit quietly next to the
   monochrome icon set. Tuned for both light and dark modes. */
.avatar-color-1 { background: #8b7aa8; } /* dusty violet */
.avatar-color-2 { background: #7a9a8e; } /* sage */
.avatar-color-3 { background: #b58863; } /* terracotta */
.avatar-color-4 { background: #6e8caf; } /* slate blue */
.avatar-color-5 { background: #c08a6a; } /* warm tan */
.avatar-color-6 { background: #8a9a5b; } /* olive */
.avatar-color-7 { background: #a66e7e; } /* mauve */
.avatar-color-8 { background: #5f8a8b; } /* teal */

[data-theme="dark"] .avatar-color-1 { background: #6a5a82; }
[data-theme="dark"] .avatar-color-2 { background: #5a7568; }
[data-theme="dark"] .avatar-color-3 { background: #8a6547; }
[data-theme="dark"] .avatar-color-4 { background: #526b86; }
[data-theme="dark"] .avatar-color-5 { background: #8f6550; }
[data-theme="dark"] .avatar-color-6 { background: #687543; }
[data-theme="dark"] .avatar-color-7 { background: #7f5361; }
[data-theme="dark"] .avatar-color-8 { background: #476668; }

.avatar-img {
  border-radius: var(--radius-full);
  object-fit: cover;
  flex-shrink: 0;
  position: relative;
  overflow: hidden;
}

/* Only a BROKEN image renders pseudo-elements: its monogram, covering the
   alt text the browser would otherwise print (ui/avatar.gleam). */
.avatar-img::before {
  content: attr(data-monogram);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: inherit;
  color: #ffffff;
}

/* ─── Media queries ──────────────────────────────────────────────────── */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .avatar-color-1 { background: #6a5a82; }
  :root:not([data-theme]) .avatar-color-2 { background: #5a7568; }
  :root:not([data-theme]) .avatar-color-3 { background: #8a6547; }
  :root:not([data-theme]) .avatar-color-4 { background: #526b86; }
  :root:not([data-theme]) .avatar-color-5 { background: #8f6550; }
  :root:not([data-theme]) .avatar-color-6 { background: #687543; }
  :root:not([data-theme]) .avatar-color-7 { background: #7f5361; }
  :root:not([data-theme]) .avatar-color-8 { background: #476668; }
}

.avatar-error {
  position: relative;
  background: var(--bg-secondary);
  color: var(--label-secondary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  flex-shrink: 0;
}

.avatar-error-badge {
  position: absolute;
  bottom: -1px;
  right: -1px;
  background: var(--accent-red, #e53935);
  color: #fff;
  font-size: 8px;
  font-weight: 700;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* ── Group-chat avatar (iOS ChatAvatarStackView parity) ────────────────────
   The uploaded channel image when one is set — alone, a full circle — else
   two members pinned to opposite corners of the slot, each 2/3 of it
   (iOS's 32/48 ratio). See ui/avatar.chat_avatar_stack. */

/* vertical-align: top — an inline-block on the baseline leaves a descender
   gap under it, so a block parent grew ~5px taller than the circle (the
   list's 40px slot measured 40x45, and the row centred the box, not the
   circle). */
.avatar-stack {
  position: relative;
  display: inline-block;
  vertical-align: top;
  flex-shrink: 0;
}

.avatar-stack-member {
  position: absolute;
  width: 66.7%;
  height: 66.7%;
  display: flex;
}

/* The member box drives the real size (the helper's inline px is only a
   fallback/font seed, same convention as .chat-list-avatar's children). */
.avatar-stack-member .avatar,
.avatar-stack-member .avatar-img {
  width: 100% !important;
  height: 100% !important;
}

.avatar-stack-back {
  top: 0;
  left: 0;
}

/* Front avatar punches out of the back one with a surface-colored ring
   (iOS: ringWidth = max(1, size/48) behind a backgroundPrimary circle). */
.avatar-stack-front {
  bottom: 0;
  right: 0;
  border-radius: var(--radius-full);
  background: var(--bg-primary);
  box-shadow: 0 0 0 2px var(--bg-primary);
}

/* A custom image is the slot's only child: the circle carries a neutral
   fill so it reads as an avatar while the fetch is in flight (the member
   stack is never drawn behind it). On error the <img> dispatches the
   failed URL and the caller re-renders the member stack (one-way latch). */
.avatar-stack--custom {
  border-radius: var(--radius-full);
  background: var(--fill-secondary);
}

/* The custom image fills the whole slot. */
.avatar-stack-custom {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-full);
  object-fit: cover;
}

/* Error state + skeleton loaders — shared across all features. */

/* ── Error state ─────────────────────────────────────────────── */

.error-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-3xl) var(--sp-xl);
  text-align: center;
  color: var(--label-secondary);
}
.error-state .icon { color: var(--accent-red); }
.error-state-title {
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--label-primary);
  margin: 0;
}
.error-state-message {
  font-size: var(--text-sm);
  max-width: 320px;
  margin: 0;
}

/* ── Skeleton loaders ────────────────────────────────────────── */

.skeleton-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  break-inside: avoid;
  margin-bottom: var(--sp-sm);
}

.skeleton-media {
  width: 100%;
  aspect-ratio: 4 / 3;
  background: var(--bg-subtle);
  border-radius: var(--radius-lg);
  animation: skeleton-pulse 1.5s ease infinite;
}

.skeleton-hero {
  width: 100%;
  aspect-ratio: 4 / 3;
  background: var(--bg-subtle);
  border-radius: var(--radius-lg);
  animation: skeleton-pulse 1.5s ease infinite;
}

.skeleton-line {
  height: 14px;
  background: var(--bg-subtle);
  border-radius: var(--radius-sm);
  animation: skeleton-pulse 1.5s ease infinite;
}

.skeleton-line-title { width: 70%; }
.skeleton-line-short { width: 40%; }
.skeleton-line-flex { flex: 1; }
.skeleton-line-full { width: 100%; }
.skeleton-line-medium { width: 60%; }

.skeleton-circle {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-full);
  background: var(--bg-subtle);
  animation: skeleton-pulse 1.5s ease infinite;
  flex-shrink: 0;
}

.skeleton-row {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) 0;
}

/* ── Boot screen ─────────────────────────────────────────────── */

.boot {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-lg);
  color: var(--label-primary);
}

.boot-spinner {
  display: flex;
  gap: var(--sp-xs);
}

.boot-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  background: var(--label-tertiary);
  animation: boot-pulse 1.4s ease-in-out infinite both;
}
.boot-dot:nth-child(2) { animation-delay: 0.16s; }
.boot-dot:nth-child(3) { animation-delay: 0.32s; }

/* ─── Keyframe animations ────────────────────────────────────────────── */

@keyframes skeleton-pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 0.8; }
}

@keyframes boot-pulse {
  0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
  40% { opacity: 1; transform: scale(1); }
}

/* ── Drop zone ─────────────────────────────────────────────────────────────
   Drag a photo in from the desktop. Applied to whole surfaces (the New
   Reminder page, a reminder's detail page) rather than a small target, so it
   needs a clearer affordance than the chat composer's background tint: a
   dashed inset outline that reads as "this whole thing will take the file".

   The overlay is a ::after so it can't disturb layout, and it's inert to the
   pointer — a drop zone that swallowed its own drop event would be a poor
   sort of drop zone. */

.nr-root.drag-over::after,
.detail.drag-over::after {
  content: "Drop to attach";
  position: fixed;
  inset: var(--sp-sm);
  z-index: 60;

  display: flex;
  align-items: center;
  justify-content: center;

  border: 2px dashed var(--accent-blue);
  border-radius: var(--radius-lg, 14px);
  background: rgba(var(--color-blue), 0.06);
  color: var(--accent-blue);

  font-size: var(--text-lg);
  font-weight: 600;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .nr-root.drag-over::after,
  .detail.drag-over::after {
    animation: drop-zone-in 0.12s ease-out;
  }
}

@keyframes drop-zone-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* ── Empty main pane (ui/empty_pane) ─────────────────────────────────────
   Nothing selected in the list beside it: a centred glyph over one line of
   text, filling the pane — glyph tertiary, words secondary (iOS
   CenteredStateView). Chat's "Select a conversation" placeholder until
   2026-09-24, now shared with Home (and Settings next).
   No background — the pane's own shows through. */
.empty-pane {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-sm);
  color: var(--label-tertiary);
}

.empty-pane-glyph {
  display: inline-flex;
}

.empty-pane-text {
  margin: 0;
  color: var(--label-secondary);
  font-size: var(--text-lg);
}

/* The way out under the text (the unknown-address page's "Go to Home"). */
.empty-pane-link {
  color: var(--accent-blue);
  font-size: var(--text-md);
  text-decoration: none;
}

.empty-pane-link:hover {
  text-decoration: underline;
}

/* ── Refused-write toast ─────────────────────────────────────────────────
   A write the server refused (a viewer's edit) is rolled back on the device;
   this says so. Same placement as .palette-toast, but it wraps (the copy
   names the toDo) and it stays clickable for its dismiss button. */
.sync-notice {
  position: fixed;
  bottom: var(--sp-xl, 24px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  width: max-content;
  max-width: min(480px, calc(100vw - 32px));
  padding: var(--sp-xs, 8px) var(--sp-md, 16px);
  background: var(--bg-elevated, var(--bg-primary));
  color: var(--label-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-lg, 12px);
  font-size: var(--text-sm, 0.875rem);
  z-index: 300;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

.sync-notice-dismiss {
  flex: none;
  background: none;
  border: none;
  padding: 0 var(--sp-xs, 4px);
  color: var(--label-secondary);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

/* Settings page styles */

/* Every Settings screen's column: content never wider than 600px
   (2026-09-24), centred in whatever the main pane leaves — on desktop
   that's everything right of the rail, since Settings has no list pane. */
.settings {
  box-sizing: border-box;
  width: 100%;
  max-width: calc(600px + 2 * var(--sp-xl));
  margin: 0 auto;
  padding: var(--sp-xl);
}

.settings-header {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  margin-bottom: var(--sp-xl);
}




.settings-title {
  font-size: var(--text-4xl);
  font-weight: 700;
  margin: 0;
}

/* ── Profile header ─────────────────────────────────────────────────── */

.settings-profile {
  display: flex;
  justify-content: center;
  margin-bottom: 0;
}

/* iOS Avatar(size: .lg) = 80pt circle, centred, tap = change photo. */
.settings-profile-avatar {
  position: relative;
  width: 80px;
  height: 80px;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  cursor: pointer;
}

.settings-profile-avatar .avatar,
.settings-profile-avatar img.avatar {
  /* Size only. The old rule also forced display:block, which undid the
     monogram's inline-flex centring and pinned the letter top-left. */
  width: 80px !important;
  height: 80px !important;
  border-radius: 50% !important;
}

/* Camera overlay — hidden on desktop, shown on mobile via media query */
.settings-avatar-overlay {
  display: none;
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.35);
  align-items: center;
  justify-content: center;
  color: #fff;
  pointer-events: none; /* label handles the click */
}

.settings-avatar-input-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ── Form ───────────────────────────────────────────────────────────── */

.settings-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xl); /* 24px */
}

/* ── Row ────────────────────────────────────────────────────────────── */

.settings-row--btn {
  background: none;
  border: none;
  cursor: pointer;
  font: inherit;
  text-align: left;
  padding: 0;
}

.settings-row-input {
  flex: 1;
  height: var(--sp-3xl); /* 40px */
  padding: 0 var(--sp-sm);
  background: var(--bg-secondary);
  border: none;
  border-radius: var(--radius-lg);
  font: inherit;
  font-size: var(--text-lg);
  color: var(--label-primary);
  outline: none;
  min-width: 0;
}

.settings-row-input:focus {
  background: var(--bg-tertiary, var(--bg-secondary));
}

.settings-row-input:disabled {
  color: var(--label-tertiary);
  cursor: default;
}

.settings-row-right {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  margin-left: auto;
  color: var(--label-tertiary);
}

.settings-row-value {
  font-size: var(--text-lg);
  font-weight: 400;
  color: var(--label-secondary);
}

/* ── Privacy / visibility pill toggle ───────────────────────────────── */

.settings-privacy-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  background: transparent;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-full);
  padding: 3px;
  gap: 0;
  cursor: pointer;
  font: inherit;
  transition: background 200ms ease, border-color 200ms ease;
}

/* Disabled (non-interactive) state */
.settings-privacy-toggle:disabled {
  opacity: 0.45;
  cursor: default;
}

/* On state (right / public) */
.settings-privacy-toggle--public {
  background: var(--bg-secondary);
  border-color: transparent;
}

.settings-privacy-toggle--public .settings-privacy-indicator {
  background: var(--bg);
}

/* Sliding indicator — sits behind the option labels (z-index 0).
   Default position: left half (Private active).
   Squish: when arriving at a side, the leading edge moves first and the
   trailing edge follows with a short delay, creating a stretch-then-snap
   feel. Each state declares the transition for arriving at that state. */
.settings-privacy-indicator {
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  right: 50%;
  background: var(--bg-secondary);
  border: 1px solid var(--border-subtle, rgba(0, 0, 0, 0.06));
  border-radius: var(--radius-full);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.10);
  z-index: 0;
  /* Arriving at Private (left): left edge leads, right follows */
  transition:
    left  240ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
    right 240ms cubic-bezier(0.4, 0, 0.2, 1) 50ms;
}

/* Public active: slide indicator to right half */
.settings-privacy-toggle--public .settings-privacy-indicator {
  left: 50%;
  right: 3px;
  /* Arriving at Public (right): right edge leads, left follows */
  transition:
    right 240ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
    left  240ms cubic-bezier(0.4, 0, 0.2, 1) 50ms;
}

.settings-privacy-opt {
  position: relative;
  z-index: 1;
  flex: 1;
  padding: 5px 14px;
  border: none;
  background: transparent;
  border-radius: var(--radius-full);
  font-family: inherit;
  font-size: 14px;
  font-weight: 400;
  color: var(--label-secondary);
  cursor: pointer;
  white-space: nowrap;
  transition: color 180ms ease, font-weight 180ms ease;
}

.settings-privacy-opt--active {
  font-weight: 600;
  color: var(--label-primary);
}

/* ── Error ──────────────────────────────────────────────────────────── */

.settings-error {
  padding: var(--sp-sm);
  margin-bottom: var(--sp-md);
  background: var(--danger-subtle, rgba(220, 38, 38, 0.08));
  color: var(--accent-red);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
}

/* ── Push notifications row ─────────────────────────────────────────── */

.settings-toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-xs) 0;
}

.settings-toggle-label {
  font-size: var(--text-lg);
  font-weight: 400;
  color: var(--label-secondary);
}

.settings-push-status,
.settings-push-denied,
.settings-push-error {
  font-size: var(--text-sm);
  color: var(--label-primary);
}

.settings-push-denied,
.settings-push-error {
  color: var(--accent-red);
}

.settings-push-btn,
.settings-push-disable-btn {
  padding: var(--sp-3xs) var(--sp-sm);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--label-primary);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}

.settings-push-btn:hover,
.settings-push-disable-btn:hover {
  background: var(--bg-subtle);
}

.settings-push-active {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  font-size: var(--text-sm);
  color: var(--label-primary);
}

/* ── Save button ────────────────────────────────────────────────────── */

.settings-save-btn {
  align-self: flex-start;
  padding: var(--sp-xs) var(--sp-md);
  border: none;
  border-radius: var(--radius-md);
  background: var(--bg-secondary);
  color: var(--label-secondary);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
}

.settings-save-btn:hover {
  filter: brightness(0.95);
}

.settings-save-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* ── Divider ────────────────────────────────────────────────────────── */

.settings-divider {
  border: none;
  border-top: 1px solid var(--border-primary);
  margin: var(--sp-md) 0;
}

/* ── Sign-out ───────────────────────────────────────────────────────── */

.settings-signout-btn {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-md);
  border: none;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--label-tertiary);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
}

.settings-signout-btn:hover {
  color: var(--accent-red);
  background: var(--danger-subtle, rgba(220, 38, 38, 0.08));
}

/* ─── Mobile overrides (≤ 768px) ────────────────────────────────────── */

@media (max-width: 834px) {
  /* The avatar stays the same 80px circle on the phone — iOS uses one
     Avatar(size: .lg) at every width. The old 72px rounded-square rule and
     the name/handle/Change Photo rules it sat with are gone (2026-09-23);
     only the tap-to-change camera overlay is mobile-specific. */
  .settings-avatar-overlay {
    display: flex;
  }
}

/* ── Blocked users (/settings/blocked) ───────────────────────────────── */










/* ══════════════════════════════════════════════════════════════════════
   iOS Settings vocabulary (2026-09-23) — the grouped-inset-card language
   of PleaseUI's settingsCard() / SectionHeader / InputCellView, in web
   tokens. Numbers are iOS's: card radius Padding.xl 24, page inset
   Padding.md 16, row 19/12 padding on a 44 (Sizing.xxl) min height, label
   body 18, header subline 15/600, footer footnote 13. Where a token is one
   pixel off (Padding.lg 19 vs --sp-lg 20) the iOS number is hardcoded and
   says so. Rows carry NO leading glyph — iOS root rows do not.
   ══════════════════════════════════════════════════════════════════════ */

/* The settings pages paint the canvas themselves: .main-pane is
   bg-primary (the white sheet) and iOS's settings sit on the canvas. */
.settings-canvas {
  background: var(--canvas-bg);
  height: 100%;
  overflow-y: auto;
  box-sizing: border-box;
}

.settings-section {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs); /* 8px header → card, iOS Gap.xs */
}

/* Headers and footers sit 8px inside the card edge. iOS pads them
   Padding.xl (24) from the SCREEN while settingsCard() pads the cards
   Padding.md (16) from it, so the text lands 24 − 16 = 8 in from the
   card's edge. Web's column edge IS the card edge; until 2026-09-24 this
   was 24px from it — 16px right of iOS on every Settings screen. */
.settings-section-header {
  margin: 0;
  padding: 0 var(--sp-xs); /* 8px = iOS Padding.xl − Padding.md */
  font-size: var(--text-md); /* 15px */
  font-weight: 600;
  line-height: 20px;
  color: var(--label-secondary);
}

.settings-section-footer {
  margin: 0;
  padding: 0 var(--sp-xs); /* 8px, as the header above */
  font-size: var(--text-sm); /* 13px */
  line-height: 18px;
  color: var(--label-secondary);
}

.settings-card {
  display: flex;
  flex-direction: column;
  background: var(--bg-primary);
  border-radius: var(--radius-xl); /* 24px */
  overflow: hidden;
}

.settings-card-row {
  display: flex;
  align-items: center;
  gap: var(--sp-sm); /* 12px, iOS Gap.sm */
  min-height: 44px; /* iOS Sizing.xxl */
  padding: var(--sp-sm) 19px; /* iOS Padding.sm / Padding.lg (19, no token) */
  box-sizing: border-box;
  width: 100%;
  color: var(--label-primary);
  text-decoration: none;
}

/* Inset divider between rows, iOS Divider().padding(.horizontal, lg). */
.settings-card-row + .settings-card-row {
  position: relative;
}
.settings-card-row + .settings-card-row::before {
  content: "";
  position: absolute;
  top: 0;
  left: 19px;
  right: 19px;
  height: 1px;
  background: var(--border-tertiary);
}

/* Connected apps: an intro, then one block per app. Each block is set off
   from the next by a rule and space, and opens with the app's name as its
   heading, so where one app ends and the next begins is plain. */
.connected-apps {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xxl);
}

.connected-apps-intro {
  margin: 0;
  padding: 0 var(--sp-xs);
  font-size: var(--text-md);
  line-height: 1.4;
  color: var(--label-secondary);
}

.connected-app {
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
}

.connected-app + .connected-app {
  padding-top: var(--sp-xxl);
  border-top: 1px solid var(--border-tertiary);
}

.connected-app-header {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0 var(--sp-xs);
}

.connected-app-name {
  margin: 0;
  font-size: var(--text-2xl);
  font-weight: 700;
  line-height: 28px;
  color: var(--label-primary);
}

.connected-app-meta {
  margin: 0;
  font-size: var(--text-base);
  line-height: 20px;
  color: var(--label-secondary);
}

/* A row whose parts stack: a heading, a note, then a control (Connected
   apps' "What it can read"). */
.settings-card-row--stacked {
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-xs);
  padding-top: var(--sp-md);
  padding-bottom: var(--sp-md);
}

.settings-card-row-note {
  margin: 0;
  font-size: var(--text-md);
  line-height: 1.4;
  color: var(--label-secondary);
}

/* A horizontal segmented toggle of equal segments: the sidebar's
   Reminders | Chats pill, on the card's colour. */
.settings-segmented {
  display: flex;
  width: 100%;
  margin-top: var(--sp-xs);
  padding: 3px;
  box-sizing: border-box;
  gap: 2px;
  border-radius: var(--radius-full);
  background: var(--bg-secondary);
}

.settings-segment {
  flex: 1;
  min-width: 0;
  border: none;
  background: none;
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: var(--radius-full);
  font: inherit;
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--label-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}

.settings-segment:disabled {
  cursor: default;
  opacity: 0.6;
}

@media (hover: hover) {
  .settings-segment:not(:disabled):hover {
    color: var(--label-primary);
  }
}

.settings-segment--active,
.settings-segment--active:hover {
  background: var(--bg-primary);
  color: var(--label-primary);
  box-shadow: var(--shadow-sm);
}

/* Connected apps' access: the live list of what the app can do, a tick
   per line. */
.connected-app-can-do {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  font-size: var(--text-md);
  line-height: 1.4;
  color: var(--label-primary);
}
.connected-app-can-do li {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-xs);
}
.connected-app-can-do-tick {
  display: inline-flex;
  flex-shrink: 0;
  margin-top: 2px;
  color: var(--accent-green);
}

/* A label with its note under it (the edit switch, the privacy boxes). */
.connected-app-check-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* "More privacy choices": a fold of checkbox rows. */
.connected-app-privacy {
  gap: 0;
}
.connected-app-privacy-summary {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  min-height: 20px;
  list-style: none;
  cursor: pointer;
}
.connected-app-privacy-summary::-webkit-details-marker {
  display: none;
}
.connected-app-privacy-chevron {
  display: inline-flex;
  color: var(--label-secondary);
  transition: transform 180ms ease;
}
.connected-app-privacy[open] .connected-app-privacy-chevron {
  transform: rotate(180deg);
}

/* One checkbox row: the whole row is the button. */
.connected-app-check {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-sm);
  width: 100%;
  margin-top: var(--sp-sm);
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  text-align: left;
  color: var(--label-primary);
  cursor: pointer;
}
.connected-app-check:disabled {
  cursor: default;
  opacity: 0.6;
}
.connected-app-check-label {
  font-size: var(--text-lg);
  line-height: 22px;
}
.connected-app-check-box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  box-sizing: border-box;
  border: 1.5px solid var(--label-tertiary);
  border-radius: 6px;
  color: #fff;
}
.connected-app-check--on .connected-app-check-box {
  border-color: var(--accent-green);
  background: var(--accent-green);
}

.settings-card-row--btn {
  background: none;
  border: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
@media (hover: hover) {
  .settings-card-row--btn:hover {
    background: var(--fill-elevated-secondary);
  }
}

.settings-card-row-label {
  flex: 1;
  min-width: 0;
  font-size: var(--text-xl); /* 18px, iOS body */
  font-weight: 400;
  line-height: 25px;
  color: var(--label-primary);
}

.settings-card-row-value {
  font-size: var(--text-xl);
  font-weight: 400;
  /* The label's 25px, not the inherited 1.5 (27px at 18px): rows with a
     value were 51px beside 49px label-only rows in the same card. */
  line-height: 25px;
  color: var(--label-secondary);
  text-align: right;
}
.settings-card-row-value--placeholder {
  color: var(--label-tertiary);
}

.settings-card-row-chevron {
  display: inline-flex;
  color: var(--label-secondary);
}

/* Checkmark rows (Theme, Look ahead): the filled check only on the
   selected row, accent blue, 24px frame — iOS InputCell.checkmark. */
.settings-card-row-check {
  display: inline-flex;
  width: 24px;
  height: 24px;
  align-items: center;
  justify-content: center;
  color: var(--accent-blue);
}

/* Native-looking switch, promoted from .integrations-switch and tinted
   green: iOS tints every settings Toggle brandGreen. */
.settings-switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  width: 51px;
  height: 31px;
  padding: 2px;
  border: none;
  border-radius: var(--radius-full);
  background: var(--fill-elevated-secondary);
  cursor: pointer;
  transition: background 180ms ease;
}
.settings-switch:disabled {
  opacity: 0.5;
  cursor: default;
}
.settings-switch--on {
  background: var(--accent-green);
}
.settings-switch-thumb {
  width: 27px;
  height: 27px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
  transform: translateX(0);
  transition: transform 180ms cubic-bezier(0.4, 0, 0.2, 1);
}
.settings-switch--on .settings-switch-thumb {
  transform: translateX(20px);
}

/* Full-width capsule actions — iOS SettingsPillButton (40pt). The label
   is iOS bodyStyle, 18 REGULAR; it was 600 until 2026-09-24. */
.settings-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xs);
  width: 100%;
  min-height: 40px;
  padding: 0 var(--sp-md);
  border: none;
  border-radius: var(--radius-full);
  font: inherit;
  font-size: var(--text-xl);
  font-weight: 400;
  cursor: pointer;
  background: var(--accent-blue-elevated);
  color: var(--accent-blue);
}
.settings-pill--hollow {
  background: transparent;
  border: 1px solid var(--border-secondary);
  color: var(--label-primary);
}
.settings-pill--danger {
  background: var(--accent-red-elevated);
  color: var(--accent-red);
}
.settings-pill:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Sub-screen chrome — iOS SettingsSubscreen: back arrow left, title
   centred, content below on the canvas. */
.settings-subscreen-header {
  display: flex;
  align-items: center;
  margin-bottom: var(--sp-xl);
}
.settings-subscreen-back {
  color: var(--label-primary);
}
.settings-subscreen-title {
  flex: 1;
  margin: 0;
  text-align: center;
  font-size: var(--text-lg); /* 16px */
  font-weight: 600;
}
.settings-subscreen-spacer {
  width: 32px; /* mirrors the back button so the title centres */
}
.settings-subscreen-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md); /* 16px between sub-screen blocks */
}

@media (max-width: 834px) {
  .settings {
    padding: var(--sp-md); /* iOS page inset 16 */
  }
}

/* Root page: 24px between blocks (iOS Gap.xl between settings cards). */
.settings-root-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xl);
}

/* Editable rows: label column then a bare input on the card — no pill,
   iOS InputCell.editable. */
.settings-card-row-label--column {
  flex: 0 0 128px; /* iOS InputCell.labelColumnWidth */
}
.settings-card-row-label--semibold {
  font-weight: 600;
}
.settings-card-row-leading {
  display: inline-flex;
  color: var(--label-primary);
}
.settings-card-row-input {
  flex: 1;
  min-width: 0;
  border: none;
  background: transparent;
  font: inherit;
  font-size: var(--text-xl);
  line-height: 25px; /* one row height with the label-only rows */
  color: var(--label-primary);
  outline: none;
  padding: 0;
}
.settings-card-row-input::placeholder {
  color: var(--label-tertiary);
}

/* Permission rows: "label … STATUS", status bold and secondary — iOS's
   "Allowed" / "Not set" treatment. */
.settings-card-row-status {
  font-size: var(--text-xl);
  line-height: 25px; /* the label's, like .settings-card-row-value */
  font-weight: 600;
  color: var(--label-secondary);
  text-align: right;
}

/* Manage account: iOS's explainer is body text on the canvas, not a card,
   24px above the pill. Its 16 (Padding.md) is from the SCREEN, the cards'
   own inset, so it lines up with the pill below — no inset here. It was
   16px in from the column until 2026-09-24. */
.settings-explainer {
  margin: 0 0 var(--sp-xl);
  padding: 0;
  font-size: var(--text-xl);
  line-height: 25px;
  color: var(--label-primary);
}

/* Confirm modal, prefix "settings-confirm" (ui/confirm_modal). Mirrors
   detail's rules; each feature keeps its own scoped copy by design. */
.settings-confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.5);
}
.settings-confirm-dialog {
  /* Elevated, not primary: since the merge the sign-out and Birthday
     modals sit over the root's cards, and in dark mode --bg-primary is the
     cards' own black. White either way in light. */
  background: var(--bg-elevated-primary);
  border-radius: var(--radius-lg);
  padding: var(--sp-xl);
  max-width: 360px;
  width: 90%;
  box-shadow: var(--shadow-lg, 0 8px 32px rgba(0, 0, 0, 0.2));
}
.settings-confirm-title {
  font-weight: 600;
  font-size: var(--text-lg);
  margin: 0 0 var(--sp-xs);
}
.settings-confirm-body {
  color: var(--label-secondary);
  margin: 0 0 var(--sp-lg);
  font-size: var(--text-md);
}
.settings-confirm-input {
  width: 100%;
  box-sizing: border-box;
  margin: var(--sp-xs) 0 var(--sp-lg);
  padding: var(--sp-xs) var(--sp-sm);
  border: 1px solid var(--border-secondary);
  border-radius: var(--radius-md);
  /* One step above the elevated dialog (same #F2F2F7 as before in light). */
  background: var(--bg-elevated-secondary);
  font: inherit;
  font-size: var(--text-lg);
  color: var(--label-primary);
  outline: none;
}
/* A validation line under the input (Birthday): tuck it up under the field
   and keep clear of the buttons below. */
.settings-confirm-input + .settings-footnote {
  margin: calc(-1 * var(--sp-sm)) 0 var(--sp-md);
}
.settings-confirm-actions {
  display: flex;
  gap: var(--sp-sm);
  justify-content: flex-end;
}
.settings-confirm-btn {
  padding: var(--sp-xs) var(--sp-md);
  border-radius: var(--radius-md);
  border: none;
  font: inherit;
  font-size: var(--text-md);
  font-weight: 500;
  cursor: pointer;
}
.settings-confirm-btn:disabled {
  /* The Settings pills' disabled treatment: a gate that isn't satisfied
     yet (the typed phrase) or a save in flight. */
  opacity: 0.5;
  cursor: default;
}
.settings-confirm-cancel {
  background: var(--fill-elevated-secondary);
  color: var(--label-primary);
}
.settings-confirm-confirm {
  background: var(--accent-blue);
  color: #fff;
}
.settings-confirm-delete {
  background: var(--accent-red);
  color: #fff;
}

/* Settings search — the mock's (2026-09-24) full-width capsule under the
   avatar, as tall as a card row, the magnifier leading at the rows' 19px
   inset. bg-primary, not bg-secondary: the page sits on the canvas, which
   IS the secondary colour. The input itself is bare inside the capsule;
   the capsule shows focus for it. */
.settings-search-field {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  box-sizing: border-box;
  min-height: 48px;
  padding: 0 19px;
  border-radius: var(--radius-full);
  background: var(--bg-primary);
  color: var(--label-primary);
  cursor: text;
}
.settings-search-field:focus-within {
  box-shadow: 0 0 0 2px var(--accent-blue);
}
.settings-search-field .icon {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
}
.settings-search {
  flex: 1;
  min-width: 0;
  height: 48px;
  padding: 0;
  border: none;
  background: transparent;
  font: inherit;
  font-size: var(--text-lg);
  color: var(--label-primary);
  outline: none;
}
.settings-search::placeholder {
  color: var(--label-primary);
}
/* iOS CenteredStateView: 16/600 secondary, centred. */
.settings-search-empty {
  margin: var(--sp-md) 0 0;
  text-align: center;
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--label-secondary);
}

/* Loading / empty / failed copy on a sub-screen: iOS bodyStyle secondary. */
.settings-state {
  margin: 0;
  padding: var(--sp-xl) 0;
  text-align: center;
  font-size: var(--text-xl);
  color: var(--label-secondary);
}

/* Identity rows (Blocked users): name over @username beside a 36px avatar. */
.settings-card-row-identity {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.settings-card-row-identity .settings-card-row-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.settings-card-row-subline {
  font-size: var(--text-md); /* 15px */
  font-weight: 600;
  line-height: 20px;
  color: var(--label-secondary);
}
/* The small in-row pill (Unblock): iOS buttonLabelSmall 14/600 secondary
   on the secondary fill; dims while its request is in flight. */
.settings-pill--small {
  width: auto;
  min-height: 32px;
  padding: 0 var(--sp-md);
  font-size: 14px;
  font-weight: 600;
  color: var(--label-secondary);
  background: var(--bg-secondary);
}

/* ══════════════════════════════════════════════════════════════════════
   Settings › Integrations sub-screens (2026-09-24) — iOS
   GmailIntegrationSettingsView and CalendarSyncSettingsView on the card
   vocabulary above. Replaces integrations.css (the master-detail page,
   2026-09-11 → 2026-09-24) outright; nothing here is scoped to it.
   ══════════════════════════════════════════════════════════════════════ */

/* The Gmail screen's blocks sit 22px apart (iOS Gap.lg — no token; the
   sub-screen body's 16 is Gap.md, which Calendars keeps). */
.settings-gmail {
  display: flex;
  flex-direction: column;
  gap: 22px;
}

/* iOS titleSmall 20/600/25, textPrimary, on the canvas — the value
   statement above everything else. iOS pads it 24 from the screen like a
   section header, so it takes the header's 8 from the card edge and the
   two line up. No 20px type token exists. */
.settings-intro {
  margin: 0;
  padding: 0 var(--sp-xs);
  font-size: 20px;
  font-weight: 600;
  line-height: 25px;
  color: var(--label-primary);
}

/* Label tones for one-line status cards: "Loading…" and empty states in
   secondary, failures in iOS brandRed. */
.settings-card-row-label--secondary {
  color: var(--label-secondary);
}
.settings-card-row-label--danger {
  color: var(--accent-red);
}

/* iOS calloutRegular 16/400/21: the "What to know" rows. */
.settings-card-row-label--callout {
  font-size: var(--text-lg);
  line-height: 21px;
}

/* The account card's summary line: iOS footnote 13/18 secondary, 4px
   under the address (Gap.xxs). */
.settings-card-row-footnote {
  margin-top: var(--sp-3xs);
  font-size: var(--text-sm);
  line-height: 18px;
  color: var(--label-secondary);
}

/* The OAuth-return notice: text and dismiss top-aligned (iOS HStack
   alignment .top) so a two-line message keeps the X on its first line. */
.settings-notice {
  align-items: flex-start;
}
.settings-notice-dismiss {
  display: inline-flex;
  flex-shrink: 0;
  padding: 0;
  border: none;
  background: none;
  color: var(--label-secondary);
  cursor: pointer;
}

/* A pill and its note, 8 apart (Gap.xs) — Connect's failure sits above
   its pill, Disconnect's below. No inset of its own: iOS pads the pills
   16 (Padding.md) from the screen, the same 16 settingsCard() pads the
   cards, so pill and card edges line up — as the column's do here. */
.settings-pill-block {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

/* iOS SettingsPillButton .tinted(accent.base) with a whiteCanvas label:
   Connect / Reconnect Gmail, the one primary action on the screen. It
   wears --accent-blue, which follows the chosen accent (accent.css,
   data-accent). */
.settings-pill--solid {
  background: var(--accent-blue);
  color: #fff;
}

/* iOS footnote 13/18 outside a card: the "unavailable" note, and the
   red failure lines (brandRed). */
.settings-footnote {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 18px;
  color: var(--label-secondary);
}
.settings-footnote--danger {
  color: var(--accent-red);
}

/* The 10px dot before each calendar's name. iOS fills it with the
   calendar's EventKit colour, or textSecondary for one this device doesn't
   have — every calendar, on web, whose wire carries no colour. The 19px
   after it (on top of the row's 12 gap) is the iOS toggle cell's own
   leading padding: iOS lays the dot BESIDE a whole padded cell, so the
   name starts 31 after the dot, not 12. */
.settings-card-row-dot {
  flex-shrink: 0;
  width: 10px;
  height: 10px;
  margin-right: 19px;
  border-radius: 50%;
  background: var(--label-secondary);
}

/* Checkmark rows before the preferences load: iOS disables them; no hover
   wash and no pointer on a row that can't be picked. */
.settings-card-row--btn:disabled {
  background: none;
  cursor: default;
}

/* ── Accent Color additions (Cassie, bf45efa), restyled into the card
   vocabulary at the 2026-09-24 merge: the root row's disc, the preview,
   and the swatch card. Everything else her screens need is above. */

/* Accent colour: the dot is both the row icon and the swatch. */
.settings-accent-dot {
  display: inline-block;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-full);
  background: var(--swatch);
  flex-shrink: 0;
}

.settings-accent--default {
  --swatch: transparent;
  box-shadow: inset 0 0 0 2px var(--border-primary);
}

.settings-accent-preview {
  padding: var(--sp-md);
  border-radius: var(--radius-xl);
  background: var(--accent-blue-elevated);
  color: var(--label-primary);
}

.settings-accent-preview p {
  margin: 0 0 var(--sp-sm);
}

.settings-accent-preview-row {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
}

.settings-accent-badge {
  padding: 2px var(--sp-xs);
  border-radius: var(--radius-full);
  background: var(--accent-blue);
  color: var(--accent-fg-on-blue, #fff);
  font-size: var(--text-sm);
  font-weight: 500;
}

.settings-accent-glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-full);
  background: var(--accent-blue);
  color: #fff;
}

.settings-swatches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-md);
  padding: var(--sp-md) 19px;
}

.settings-swatch {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 0;
  border: none;
  background: none;
  color: var(--label-secondary);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}

.settings-swatch .settings-accent-dot {
  width: 36px;
  height: 36px;
  outline: 2px solid transparent;
  outline-offset: 3px;
}

.settings-swatch--selected .settings-accent-dot {
  outline-color: var(--label-primary);
}

.settings-swatch--selected {
  color: var(--label-primary);
  font-weight: 500;
}

.settings-swatch:disabled {
  cursor: default;
}

/* The Accent Color row's disc sits in the value slot. */
.settings-card-row-value > .settings-accent-dot {
  display: block;
}

/* A value after the label column fills the row, so a trailing chevron
   (Birthday) sits at the far edge; text stays leading-aligned, iOS
   InputCell.standard. */
.settings-card-row-label--column + .settings-card-row-value {
  flex: 1;
  min-width: 0;
  text-align: left;
}

/* ── OAuth consent (/oauth-consent) ──────────────────────────────────── */
/* A page of its own, outside the app shell: one card, centred. Its own
   scroller, the height of what is visible, as .auth-screen (auth.css): an
   agent opens this page in an in-app browser tray, and Allow is at the
   bottom. */

.oauth-consent {
  height: 100vh;
  height: 100dvh;
  overflow-y: auto;
  display: flex;
  padding: var(--sp-xl);
  background: var(--bg-secondary);
}

.oauth-consent-card {
  margin: auto;
  width: 100%;
  max-width: 480px;
  padding: var(--sp-xl);
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.oauth-consent-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-sm);
  text-align: center;
}

.oauth-consent-logo {
  width: 48px;
  height: 48px;
  border-radius: var(--radius-lg);
  object-fit: contain;
}

.oauth-consent-title {
  margin: 0;
  font-size: var(--text-lg);
  color: var(--label-primary);
}

.oauth-consent-text {
  margin: 0;
  color: var(--label-primary);
}

.oauth-consent-note {
  margin: 0;
  color: var(--label-secondary);
}

.oauth-consent-warning {
  margin: 0;
  color: var(--accent-red);
}

/* The edit switch and the privacy fold, set off as a card of their own. */
.oauth-consent-choices {
  display: flex;
  flex-direction: column;
  margin: var(--sp-xs) 0;
  border-radius: var(--radius-lg);
  background: var(--bg-secondary);
  overflow: hidden;
}

/* What was granted, under a rule, just above the buttons. */
.oauth-consent-summary {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-top: var(--sp-sm);
  border-top: 1px solid var(--border-tertiary);
  font-size: var(--text-md);
}

/* Cancel and Allow: side by side, the same size. */
.oauth-consent-buttons {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-sm);
  margin-top: var(--sp-sm);
}

/* Phones (iOS SettingsSubscreen, 2026-10-23): the phone header pins above
   a subpage's scroll; the desktop header and the canvas's top gap go. */
@media (max-width: 834px) {
  .settings-subscreen-header {
    display: none;
  }

  .settings-canvas:has(> .nav-bar--sticky) {
    padding-top: 0;
  }
}

/* Processing badge on feed cards */

.feed-card-processing {
  display: flex;
  align-items: center;
  gap: var(--sp-3xs);
  padding: var(--sp-lg) var(--sp-sm);
  font-size: var(--text-xs);
  color: var(--label-tertiary);
}

.feed-card-processing-spinner {
  width: 12px;
  height: 12px;
  border: 1.5px solid var(--border-primary);
  border-top-color: var(--label-tertiary);
  border-radius: 50%;
  animation: processing-spin 0.8s linear infinite;
}

.feed-card-processing-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--label-tertiary);
  animation: processing-pulse 1.5s ease-in-out infinite;
}

.feed-card-processing-error .feed-card-processing-dot {
  background: var(--accent-red);
  animation: none;
}

.feed-card-processing-error {
  color: var(--accent-red);
}

.feed-card-processing-warning {
  color: #d97706;
}

.feed-card-processing-warning .feed-card-processing-dot {
  background: #d97706;
  animation: none;
}

/* Retry button inside errored processing badge */

.feed-card-retry-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3xs);
  margin-left: auto;
  padding: var(--sp-4xs) var(--sp-xs);
  border: 1px solid currentColor;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--accent-red);
  font-size: var(--text-xs);
  font-family: inherit;
  cursor: pointer;
  line-height: 1;
}

.feed-card-retry-btn:hover {
  opacity: 0.75;
}

.feed-card-retry-btn .icon {
  width: var(--size-sm);
  height: var(--size-sm);
}

/* Pill positioning override for processing/error cards —
   pull the pill out of absolute flow and let the container carry the offset */
.feed-card:has(.feed-card-processing) .feed-badge-pill {
  position: relative;
  top: auto;
  left: auto;
}

.feed-card:has(.feed-card-processing) .feed-card-link {
  padding-top: var(--sp-sm);
  padding-left: var(--sp-sm);
}

/* ─── Keyframe animations ────────────────────────────────────────────── */

@keyframes processing-spin {
  to { transform: rotate(360deg); }
}


@keyframes processing-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

/* client/styles/emoji_picker.css — scoped to the new chat emoji picker. */

/* Compact pill (hover state on bubbles) */
.emoji-picker-pill {
  position: absolute;
  top: -12px;
  /* Left-anchored so the pill grows RIGHTWARD — the open side for
     received messages (they hug the pane's left). Right-anchoring grew
     it leftward, clipping against the scroll container on bubbles
     narrower than the pill (same inversion the reaction trigger had). */
  left: -4px;
  display: flex;
  align-items: center;
  gap: var(--sp-3xs);
  padding: var(--sp-3xs) var(--sp-xs);
  background: var(--bg);
  border: 1px solid var(--border-subtle, var(--border-primary));
  border-radius: var(--radius-full);
  box-shadow: var(--shadow);
  z-index: 10;
  animation: emoji-picker-pill-open 0.12s ease-out;
}

@keyframes emoji-picker-pill-open {
  from { opacity: 0; transform: translateY(-2px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Sent bubbles hug the pane's right — mirror the anchor so the pill
   grows LEFTWARD into open space. */
.chat-message-row.sent .emoji-picker-pill {
  left: auto;
  right: -4px;
}

.emoji-picker-pill-emoji {
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  font-size: 18px;
  padding: 0;
  transition: background 0.1s ease, transform 0.1s ease;
}

@media (hover: hover) {
  .emoji-picker-pill-emoji:hover {
  background: var(--bg-subtle);
  transform: scale(1.15);
}
}

.emoji-picker-overflow {
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  color: var(--label-tertiary);
  border-radius: var(--radius-full);
  cursor: pointer;
  padding: 0;
}

@media (hover: hover) {
  .emoji-picker-overflow:hover {
  background: var(--bg-subtle);
  color: var(--fg);
}
}

/* Drill-down panel — fixed positioning applied inline by view */
.emoji-picker {
  position: fixed;
  width: 336px;
  max-height: 420px;
  display: flex;
  flex-direction: column;
  background: var(--bg-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  z-index: 100;
  overflow: hidden;
  animation: emoji-picker-open 0.12s ease-out;
}

/* Category strip at top */
.emoji-picker-category-strip {
  display: flex;
  justify-content: space-between;
  padding: var(--sp-sm) var(--sp-md);
  gap: var(--sp-xs);
}

.emoji-picker-category-btn {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-3xs);
  background: none;
  border: none;
  color: var(--label-tertiary);
  cursor: pointer;
  border-radius: var(--radius-full);
}

.emoji-picker-category-btn-active {
  color: var(--label-primary);
  background: var(--bg-secondary);
}
@media (hover: hover) {
  .emoji-picker-category-btn:hover {
  color: var(--label-secondary);
  background: var(--bg-secondary);
}
}

.emoji-picker-category-btn:focus-visible {
  outline: 2px dashed var(--label-tertiary);
  outline-offset: 2px;
}

/* Search */
.emoji-picker-search-wrap {
  padding: var(--sp-md) var(--sp-sm);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-xs);
}

.emoji-picker-search {
  flex: 1;
  min-width: 0;
  padding: var(--sp-xs) var(--sp-md);
  background: var(--bg-subtle);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-full);
  font: inherit;
  font-size: var(--text-lg);
  color: var(--label-primary);
}

.emoji-picker-search::placeholder {
  color: var(--label-primary);
}

.emoji-picker-search:focus-visible {
  outline: 2px solid var(--bg-secondary);
  outline-offset: 0;
}

/* Global skin-tone button */
.emoji-picker-tone-global-btn {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  background: none;
  border-radius: var(--radius-sm);
  font-size: 24px;
  cursor: pointer;
  padding: 0;
  color: inherit;
  transition: background 0.1s ease, border-color 0.1s ease;
}

@media (hover: hover) {
  .emoji-picker-tone-global-btn:hover {
  background: var(--bg-subtle);
}
}

.emoji-picker-tone-global-btn--active {
  background: var(--bg-subtle);
  border-color: var(--border-primary);
}
@media (hover: hover) {
  .emoji-picker-tone-global-btn--active:hover {
  background: var(--bg-subtle);
  border-color: var(--border-primary);
}
}

.emoji-picker-tone-global-btn:focus-visible {
  outline: 2px dashed var(--label-tertiary);
  outline-offset: 2px;
}

/* Global tone selector panel — wraps to its own row inside search-wrap */
.emoji-picker-global-tone-panel {
  flex-basis: 100%;
  display: flex;
  gap: var(--sp-3xs);
  padding-top: var(--sp-3xs);
  animation: emoji-picker-tone-open 0.08s ease-out;
}

.emoji-picker-global-tone-swatch {
  flex: 1;
  aspect-ratio: 1 / 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 2px solid transparent;
  background: none;
  border-radius: var(--radius-sm);
  font-size: 20px;
  cursor: pointer;
  padding: 0;
  transition: background 0.1s ease, border-color 0.1s ease;
}

@media (hover: hover) {
  .emoji-picker-global-tone-swatch:hover {
  background: var(--bg-subtle);
}
}

.emoji-picker-global-tone-swatch--active {
  border-color: var(--fg);
}

@media (hover: hover) {
  .emoji-picker-global-tone-swatch--active:hover {
  background: var(--bg-subtle);
}
}

.emoji-picker-global-tone-swatch:focus-visible {
  outline: 2px dashed var(--label-tertiary);
  outline-offset: 2px;
}

/* Scroll region */
.emoji-picker-scroll {
  flex: 1;
  min-height: 0; /* required: without this a flex item won't shrink below its
                    content height, making scrollHeight === clientHeight and
                    disabling programmatic + touch scroll entirely */
  overflow-y: auto;
  scroll-behavior: smooth;
  padding: 0 var(--sp-md) var(--sp-md);
}

.emoji-picker-section {
  padding-top: var(--sp-sm);
}

.emoji-picker-section-header {
  position: sticky;
  top: 0;
  background: var(--bg-primary);
  padding: var(--sp-xs) 0;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--label-secondary);
  margin: 0;
}

.emoji-picker-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--sp-3xs);
}

.emoji-picker-cell {
  aspect-ratio: 1 / 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  font-size: 22px;
  cursor: pointer;
  padding: 0;
}

.emoji-picker-cell:focus-visible {
  background: var(--bg-subtle);
}
@media (hover: hover) {
  .emoji-picker-cell:hover {
  background: var(--bg-subtle);
}
}

.emoji-picker-cell:focus-visible {
  outline: none;
}

/* Empty search state */
.emoji-picker-empty {
  text-align: center;
  color: var(--label-secondary);
  font-size: var(--text-sm);
  padding: var(--sp-xl) var(--sp-md);
}

.emoji-picker-results {
  padding-top: var(--sp-sm);
}

/* Tone popout */
.emoji-picker-tone-popout {
  position: fixed;
  display: flex;
  gap: var(--sp-3xs);
  padding: var(--sp-3xs) var(--sp-xs);
  background: var(--bg-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-full);
  box-shadow: var(--shadow);
  z-index: 110;
  animation: emoji-picker-tone-open 0.08s ease-out;
}

.emoji-picker-tone-btn {
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  font-size: 20px;
  cursor: pointer;
  padding: 0;
}

@media (hover: hover) {
  .emoji-picker-tone-btn:hover {
  background: var(--bg-subtle);
}
}

/* Reduced motion */

/* ─── Keyframe animations ────────────────────────────────────────────── */

@keyframes emoji-picker-pill-open {
  from { opacity: 0; transform: translateY(-2px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes emoji-picker-open {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes emoji-picker-tone-open {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ─── GIF tab button ─────────────────────────────────────────────────── */

/* GIF text button sits in the same strip as emoji category buttons */
.emoji-picker-gif-btn {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 0 var(--sp-3xs);
  min-width: 36px;
}

/* ─── GIF panel ──────────────────────────────────────────────────────── */

/* 2-column masonry-style grid */
.emoji-picker-gif-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-xs);
  padding: var(--sp-xs);
}

.emoji-picker-gif-cell {
  display: block;
  width: 100%;
  padding: 0;
  border: none;
  background: var(--bg-secondary);
  border-radius: var(--radius-md);
  overflow: hidden;
  cursor: pointer;
  transition: opacity 0.1s;
}

@media (hover: hover) {
  .emoji-picker-gif-cell:hover {
  opacity: 0.85;
}
}

.emoji-picker-gif-img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
}

/* While a new search is in flight over existing results, keep the grid
   visible but dimmed — swapping to the spinner collapses the panel. */
.emoji-picker-gif-grid-loading {
  opacity: 0.55;
  transition: opacity 0.15s ease-out;
}

/* Loading spinner (first fetch only — no results to keep on screen).
   Fills the panel's scroll region so entering GIF mode doesn't collapse
   the picker to spinner height. */
.emoji-picker-gif-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 300px;
}

.emoji-picker-gif-spinner {
  width: 24px;
  height: 24px;
  border: 2.5px solid var(--border-primary);
  border-top-color: var(--label-secondary);
  border-radius: 50%;
  animation: emoji-picker-gif-spin 0.7s linear infinite;
}

@keyframes emoji-picker-gif-spin {
  to { transform: rotate(360deg); }
}

/* Empty state — same fill-height as the loading state, so "no results"
   doesn't collapse the panel either. */
.emoji-picker-gif-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 300px;
  color: var(--label-secondary);
  font-size: var(--text-sm);
}

/* Attribution footer — required by Klipy's terms; the .icon mask inherits
   currentColor, so the mark themes with --text-tertiary. */
.emoji-picker-gif-attribution {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-xs) 0;
  border-top: 1px solid var(--border-primary);
  color: var(--label-tertiary);
}

.emoji-picker-gif-attribution .icon {
  width: 76px;
  height: 13px;
}

/* ─── Media queries ──────────────────────────────────────────────────── */

@media (max-width: 834px) {
  .emoji-picker-cell {
    font-size: var(--text-4xl);
  }
}

@media (prefers-reduced-motion: reduce) {
  .emoji-picker-scroll {
    scroll-behavior: auto !important;
  }

  .emoji-picker,
  .emoji-picker-tone-popout,
  .emoji-picker-global-tone-panel,
  .emoji-picker-pill,
  .emoji-picker-pill-emoji,
  .emoji-picker-cell,
  .emoji-picker-category-btn,
  .emoji-picker-tone-global-btn,
  .emoji-picker-global-tone-swatch,
  .emoji-picker-gif-spinner {
    animation: none !important;
    transition: none !important;
  }
}

/* ─── Sheet (ui/sheet, 2026-10-26) ──────────────────────────────────────────
   The one chrome every phone bottom sheet wears — Julian's spec of
   2026-10-26, docs/superpowers/specs/2026-10-26-sheet-chrome-design.md.

     .sheet-layer                 fixed, top 0, height --vvh (the visual
     │                            viewport, dom.mjs), z 401, no pointer events
     ├─ .sheet-overlay            --overlay; a tap dismisses (the controller)
     └─ .sheet-container          padding 0 6 6 6: the card floats off the glass
        └─ .sheet #<id>           radius 32 on top, --sheet-bottom-radius below
           │                      (concentric with the display at the 6 inset,
           │                      or 32); fill --sheet-fill or a material;
           │                      the card's own 42 under everything
           ├─ .sheet__grabber-slot  10 tall, the 60×4 pill on its floor; a button
           ├─ .sheet__header        44 discs leading/trailing, centred titles
           ├─ .sheet__stack         16 / 24 top; 24 between children; the scroller
           │   └─ .sheet__pages     when paged: every page rendered, one shown
           └─ .sheet__footer        24 above (the rhythm), 16 sides, 48 buttons

   Why the layer is anchored at the top with an exact --vvh height and the
   card bottom-aligned inside it: `position: fixed; bottom: 0` anchors to
   the LAYOUT viewport, which iOS Safari does not shrink when its toolbar
   expands, so a bottom-anchored card renders behind the toolbar. The
   visual viewport is the truth, and dom.mjs keeps --vvh equal to it.

   Motion (ffi/sheet_motion.mjs): entrance sheet-in; the controller sets
   the card's height in px while dragging and at a detent, and plays every
   exit (overlay, ✕, Escape, Back, drag, hand-off) before the model closes.

   Desktop (≥ 835px): the same markup presents as today's centred modal —
   R5, untouched: layer and container display: contents, the .85 + 4px blur
   overlay, 560 / 420 panel widths, fade-scale entrance, no grabber.
──────────────────────────────────────────────────────────────────────────── */

.sheet-layer {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--vvh, 100dvh);
  z-index: 401;
  display: flex;
  align-items: flex-end;
  pointer-events: none;
}

.sheet-overlay {
  position: absolute;
  inset: 0;
  background: var(--overlay);
  pointer-events: auto;
  animation: sheet-overlay-in 0.25s ease forwards;
}

.sheet-layer--closing .sheet-overlay {
  animation: sheet-overlay-out 0.28s ease forwards;
}

/* Anything else a caller puts in the layer (`Config.overlays`: a confirm
   over the sheet) takes pointer events again — the layer's `none` is
   inherited, and a confirm nobody can tap is a sheet nobody can leave. */
.sheet-layer > :not(.sheet-container) {
  pointer-events: auto;
}

.sheet-container {
  position: relative;
  width: 100%;
  padding: 0 6px 6px;
  pointer-events: none;
  display: flex;
  flex-direction: column;
}

.sheet {
  position: relative;
  pointer-events: auto;
  width: 100%;
  display: flex;
  flex-direction: column;
  /* The full stop: 12 under the top safe inset, 6 above the glass. */
  max-height: calc(var(--vvh, 100dvh) - env(safe-area-inset-top, 0px) - 18px);
  background: var(--sheet-fill);
  color: var(--label-primary);
  border-radius: var(--sheet-radius) var(--sheet-radius)
    var(--sheet-bottom-radius, var(--sheet-radius))
    var(--sheet-bottom-radius, var(--sheet-radius));
  padding-bottom: 42px;
  overflow: hidden;
  animation: sheet-in 0.3s var(--ease-slide, cubic-bezier(0.32, 0.72, 0, 1)) forwards;
  will-change: transform, height;
}

/* The pages painted --bg-secondary flip the default fill (C11). */
.app-shell--surface-secondary {
  --sheet-fill: var(--bg-primary);
}

/* ── Materials (spec §5.2): layer 1 over layer 2 as two stacked backgrounds
   (the first listed paints on top), both over the backdrop blur. ── */
.sheet--material-ultrathin,
.sheet--material-thin,
.sheet--material-regular,
.sheet--material-thick,
.sheet--material-chrome {
  -webkit-backdrop-filter: blur(var(--material-blur));
  backdrop-filter: blur(var(--material-blur));
}
.sheet--material-ultrathin {
  background:
    linear-gradient(var(--material-ultrathin-1), var(--material-ultrathin-1)),
    linear-gradient(var(--material-ultrathin-2), var(--material-ultrathin-2));
}
.sheet--material-thin {
  background:
    linear-gradient(var(--material-thin-1), var(--material-thin-1)),
    linear-gradient(var(--material-thin-2), var(--material-thin-2));
}
.sheet--material-regular {
  background:
    linear-gradient(var(--material-regular-1), var(--material-regular-1)),
    linear-gradient(var(--material-regular-2), var(--material-regular-2));
}
.sheet--material-thick {
  background:
    linear-gradient(var(--material-thick-1), var(--material-thick-1)),
    linear-gradient(var(--material-thick-2), var(--material-thick-2));
}
.sheet--material-chrome {
  background:
    linear-gradient(var(--material-chrome-1), var(--material-chrome-1)),
    linear-gradient(var(--material-chrome-2), var(--material-chrome-2));
  -webkit-backdrop-filter: blur(var(--material-chrome-blur));
  backdrop-filter: blur(var(--material-chrome-blur));
}

/* ── Grabber: the slot is the button (a labelled control, the 4a ruling);
   the pill is its ::after, 60×4 on the slot's floor. plus-darker is
   WebKit-only; elsewhere the pill paints plain --fill-primary. ── */
.sheet__grabber-slot {
  width: 100%;
  height: 10px;
  flex-shrink: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  cursor: grab;
  touch-action: none;
}
.sheet__grabber-slot:active {
  cursor: grabbing;
}
.sheet__grabber-slot::after {
  content: "";
  display: block;
  width: 60px;
  height: 4px;
  border-radius: 100px;
  background: var(--fill-primary);
  mix-blend-mode: plus-darker;
}
.sheet__grabber-slot:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: -2px;
}

/* ── Header: 16 sides, 0 top, 10 below; 44 discs at the edges; the title
   block centred in the middle column so an odd pair of buttons never
   pushes it off centre. ── */
.sheet__header {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) 44px;
  align-items: center;
  min-height: 44px;
  padding: 0 16px 10px;
  flex-shrink: 0;
  touch-action: none;
}
.sheet__hbtn {
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--sheet-hbtn-fill, var(--fill-secondary));
  color: var(--sheet-hbtn-color, var(--label-primary));
  cursor: pointer;
}
.sheet__hbtn--leading {
  grid-column: 1;
  justify-self: start;
}
.sheet__hbtn--trailing {
  grid-column: 3;
  justify-self: end;
}
.sheet__titles {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  min-width: 0;
  text-align: center;
}
.sheet__title-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-width: 0;
  max-width: 100%;
  color: var(--label-primary);
}
.sheet__title-glyph {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
}
.sheet__title {
  margin: 0;
  font: var(--type-body-bold);
  letter-spacing: var(--tracking-body);
  color: var(--label-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sheet__subtitle {
  font: var(--type-footnote);
  color: var(--label-secondary);
}

/* ── Stack: the only scroller. 24 between EVERY child — row to row, row to
   divider, divider to row. ── */
.sheet__stack {
  padding: 24px 16px 0;
  display: flex;
  flex-direction: column;
  gap: 24px;
  flex: 1 1 auto;
  min-height: 0;
  /* Vertical only. `overflow-y: auto` alone computes overflow-x to auto,
     and a paged sheet's hidden pages (slid to ±100%) made the stack twice
     as wide as its box — on WebKit a sideways finger then scrolled the
     whole sheet's content off its left edge (Julian's staging screenshots,
     2026-10-27). The pages clip their own slide (below); this is the belt. */
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: none;
  scrollbar-width: none;
}
.sheet__stack::-webkit-scrollbar {
  display: none;
}
/* A fluid sheet's main child fills the stack (C19). */
.sheet__fill {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* ── Pages (R6): one card, every page rendered on the same grid cell, the
   shown one in place, the others slid off and hidden once their slide
   ends. The controller animates the stack's height between pages. ── */
.sheet__pages {
  display: grid;
  flex: 1 1 auto;
  min-height: 0;
  /* A page slid off to the side adds nothing to the stack's scrollable
     width: clipped on x only — y stays visible, so the pages' height is
     still what the stack scrolls. */
  overflow-x: clip;
  overflow-y: visible;
}
.sheet__page {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  gap: 24px;
  min-height: 0;
  min-width: 0;
  transition:
    transform 230ms ease,
    opacity 230ms ease,
    visibility 0s 0s;
}
.sheet__page--before,
.sheet__page--after {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition:
    transform 230ms ease,
    opacity 230ms ease,
    visibility 0s 230ms;
}
.sheet__page--before {
  transform: translateX(-100%);
}
.sheet__page--after {
  transform: translateX(100%);
}

/* ── Footer: pinned under the scroller, the rhythm's 24 above it; two
   actions share the row with a 12 gap. The scroll shadow eases in while
   the stack scrolls beneath it. ── */
.sheet__footer {
  flex-shrink: 0;
  display: flex;
  align-items: stretch;
  gap: 12px;
  padding: 24px 16px 0;
  box-shadow: 0 -11px 12px -19px rgba(0, 0, 0, 0);
  transition: box-shadow 0.5s ease;
}
.sheet__footer > * {
  flex: 1 1 0;
  min-width: 0;
}
.sheet__footer--scrolling {
  box-shadow: 0 -12px 24px -12px rgba(0, 0, 0, 0.22);
}

/* ── Rows (spec §4.3): 12 gap; 20 glyph or 32 avatar; title label;
   footnote sublabel 4 below; 24 trailing. No vertical padding of their
   own — the 24 rhythm is the spacing — but a 48px hit area through
   padding the margins take back (C6). Colours per part are overridable
   through the --sheet-row-*-color properties; destructive recolours all. ── */
.sheet-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 12px 0;
  margin: -12px 0;
  border: 0;
  background: transparent;
  text-align: left;
  font: inherit;
  color: var(--sheet-row-color, var(--label-primary));
  cursor: pointer;
  flex-shrink: 0;
}
.sheet-row--static {
  cursor: default;
}
.sheet-row--destructive {
  --sheet-row-color: var(--accent-red);
}
.sheet-row:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: -2px;
  border-radius: var(--radius-sm);
}
.sheet-row__leading {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--sheet-row-leading-color, inherit);
}
.sheet-row__text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.sheet-row__label {
  font: var(--type-body-semibold);
  color: var(--sheet-row-label-color, inherit);
}
.sheet-row__sublabel {
  font: var(--type-footnote);
  color: var(--label-secondary);
}
.sheet-row__trailing {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  color: var(--sheet-row-trailing-color, inherit);
}

/* Phones (iOS parity, 2026-10-26): a hold on a row, a tab or the title
   selects nothing and raises no callout — Mobile Safari selected the
   row's label with the Copy / Look Up callout, and a long hold on the
   reaction sheet took its tabs and rows together; iOS sheets never
   select. The card's fields take selection back (WebKit blocks it inside
   them under a non-selectable ancestor). Desktop's modal keeps the
   mouse's selection. */
@media (max-width: 834px) {
  .sheet {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
  }
  .sheet input,
  .sheet textarea,
  .sheet [contenteditable="true"] {
    -webkit-user-select: text;
    user-select: text;
  }
}
.sheet-divider {
  height: 0.5px;
  flex-shrink: 0;
  background: var(--ui-borders);
}

/* ── Keyframes ── */
@keyframes sheet-in {
  from { transform: translateY(calc(100% + 6px)); }
  to   { transform: translateY(0); }
}
@keyframes sheet-overlay-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes sheet-overlay-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@keyframes sheet-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .sheet {
    animation: sheet-fade-in 0.2s ease forwards;
  }
  .sheet__page,
  .sheet__page--before,
  .sheet__page--after {
    transition: none;
  }
}

/* Desktop entrance: subtle fade + scale (the slide-in stays mobile-only). */
@keyframes sheet-modal-in {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1);    }
}

/* ── Desktop (≥ 835px): today's centred modal, untouched (R5) ── */
@media (min-width: 835px) {
  .sheet-layer,
  .sheet-container {
    display: contents;
  }
  .sheet-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.85);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    z-index: 400;
  }
  .sheet,
  .sheet[class*="sheet--material-"] {
    position: fixed;
    z-index: 401;
    inset: 0;
    margin: auto;
    width: min(92vw, 560px);
    height: fit-content;
    max-height: 80vh;
    border-radius: var(--radius-lg);
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4);
    background: var(--bg-elevated-primary, var(--bg-primary));
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    padding: 0 0 var(--sp-md);
    animation: sheet-modal-in 0.22s ease forwards;
    will-change: auto;
  }
  /* Menu-style sheets read better narrower than the 560 Medium default. */
  .sheet.sheet--menu {
    width: min(92vw, 420px);
  }
  .sheet__grabber-slot {
    display: none;
  }
}

/* The glass surface: tint, material, blur and rim as one class — see
   tokens.css --glass-tint / --glass-rim. */
/* The glass surface — one reusable background for floating chrome (Julian,
   2026-10-27; first user: the link page composer's slab, link_page.css).

   Four things make it: the material pair from tokens.css (the sheet's
   layers, thick by default) over the material blur; the tint gradient
   (--glass-tint) painted over the material, so the body reads darker under
   the top edge and clears toward the foot; and the rim (--glass-rim) — a
   1px light on the top edge, a soft glow along the bottom, a faint ring —
   as inset shadows, so the box and its padding stay exact. Measured from
   the 358×80 composer mock; both themes live in tokens.css.

   Use: add .glass-surface (and a thickness modifier, if not thick) to the
   element and keep its radius and geometry in its own rule. A component
   with a drop shadow of its own composes it after the rim:
     box-shadow: var(--glass-rim), var(--shadow-float);
   Something that cannot take the class (a pseudo-element) writes the same
   three declarations from the tokens. The phone chrome's older glass
   (--glass-fill + blur 12, nav_bar.css) is iOS's below-26 BarChrome and
   stays as it is. */

.glass-surface {
  --glass-layer-1: var(--material-thick-1);
  --glass-layer-2: var(--material-thick-2);
  background:
    var(--glass-tint),
    linear-gradient(var(--glass-layer-1), var(--glass-layer-1)),
    linear-gradient(var(--glass-layer-2), var(--glass-layer-2));
  -webkit-backdrop-filter: blur(var(--material-blur));
  backdrop-filter: blur(var(--material-blur));
  box-shadow: var(--glass-rim);
}

/* Thinner materials under the same tint and rim. */
.glass-surface--regular {
  --glass-layer-1: var(--material-regular-1);
  --glass-layer-2: var(--material-regular-2);
}

.glass-surface--thin {
  --glass-layer-1: var(--material-thin-1);
  --glass-layer-2: var(--material-thin-2);
}

/* ============================================================
   Modal / overlay system — ui/modal.gleam
   ============================================================
   CSS custom properties for runtime tuning (set via inline style):
     --mo   overlay opacity  (default 0.85)
     --mb   backdrop blur    (default 4px)

   Z-index ladder:
     detail confirm overlays  ~200
     this modal system        900
     chat image lightbox     1000
   ============================================================ */

/* ── root container ──────────────────────────────────────── */

.mo-root {
  position: fixed;
  inset: 0;
  z-index: 900;
  display: flex;
  align-items: center;
  justify-content: center;
  /* CSS vars with fallbacks */
  --mo: 0.85;
  --mb: 4px;
}

/* ── backdrop ─────────────────────────────────────────────── */

.mo-backdrop {
  position: absolute;
  inset: 0;
  cursor: pointer;
}

.mo-backdrop-dark {
  background: rgba(0, 0, 0, var(--mo));
  backdrop-filter: blur(var(--mb));
  -webkit-backdrop-filter: blur(var(--mb));
}

/* ── panel ────────────────────────────────────────────────── */

.mo-panel {
  position: relative; /* sits above .mo-backdrop in stacking context */
  /* Elevated surface, not --bg-primary: in dark mode that's pure black,
     and the panel vanished into the 0.85-black backdrop (prod 2026-09-29). */
  background: var(--bg-elevated-primary, var(--bg-primary));
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4);
  overflow: hidden;
  max-height: 92vh;
  display: flex;
  flex-direction: column;
}

.mo-fullscreen {
  width: 100vw;
  height: 100vh;
  max-height: 100vh;
  border-radius: 0;
}

.mo-large {
  width: min(90vw, 1200px);
}

.mo-medium {
  width: min(92vw, 560px);
}

.mo-small {
  width: min(92vw, 380px);
}

/* ── close button ─────────────────────────────────────────── */

.mo-close-btn {
  position: absolute;
  top: var(--sp-sm);
  right: var(--sp-sm);
  z-index: 1;
  width: 32px;
  height: 32px;
  border: none;
  border-radius: var(--radius-full);
  background: rgba(0, 0, 0, 0.35);
  color: #fff;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 120ms ease;
}

.mo-close-btn:hover {
  background: rgba(0, 0, 0, 0.55);
}

/* ── image lightbox ───────────────────────────────────────── */

.mo-lightbox {
  background: #000;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  color: rgba(255, 255, 255, 0.5);
  animation: mo-lb-in 0.3s ease-in-out;
}

@keyframes mo-lb-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes mo-lb-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

.mo-lb-header {
  display: flex;
  align-items: center;
  padding: 12px 8px 8px;
  flex-shrink: 0;
}

.mo-lb-header-spacer { flex: 1; }

.mo-lb-media {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  padding: 0 16px;
}

.mo-lightbox-img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  user-select: none;
  -webkit-user-drag: none;
  border-radius: var(--radius-sm);
}

.mo-lightbox-video {
  background: #000;
}

.mo-lb-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 12px 8px 32px;
  flex-shrink: 0;
}

.mo-lb-btn {
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  cursor: pointer;
  border-radius: var(--radius-full);
  padding: 0;
  color: inherit;
  flex-shrink: 0;
  transition: background 120ms ease;
}
.mo-lb-btn:hover { background: rgba(255, 255, 255, 0.08); }

/* ── mobile adjustments ───────────────────────────────────── */

@media (max-width: 640px) {
  .mo-panel {
    border-radius: var(--radius-md) var(--radius-md) 0 0;
    max-height: 96vh;
  }

  .mo-medium,
  .mo-large {
    width: 100vw;
    align-self: flex-end;
  }
}

/* Global ⌘K overlay — see app/palette. */
/* Command palette (⌘K) — global overlay, see app/palette.gleam.
   Rendered at the session root so it sits above every route; the toast
   (post-create confirmation) renders independently of the overlay. */

.palette-root {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  /* Panel sits in the upper third — reachable without eye travel, and
     stable while the result list below it grows and shrinks. */
  padding: 14vh var(--sp-md) var(--sp-md);
  box-sizing: border-box;
}

.palette-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}

.palette-panel {
  position: relative;
  width: min(560px, 100%);
  max-height: 60vh;
  display: flex;
  flex-direction: column;
  background: var(--bg-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-lg, 12px);
  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.28);
  overflow: hidden;
  animation: palette-in 0.12s ease-out;
}

@keyframes palette-in {
  from {
    opacity: 0;
    transform: translateY(-6px) scale(0.99);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .palette-panel {
    animation: none;
  }
}

.palette-input-row {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-sm) var(--sp-md);
  border-bottom: 1px solid var(--border-tertiary);
  flex-shrink: 0;
}

.palette-input-icon {
  display: inline-flex;
  align-items: center;
  color: var(--label-tertiary);
  flex-shrink: 0;
}

.palette-input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: transparent;
  color: var(--label-primary);
  font: inherit;
  font-size: var(--text-lg);
}

.palette-input::placeholder {
  color: var(--label-primary);
}

.palette-kbd {
  flex-shrink: 0;
  padding: 2px 6px;
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-sm, 6px);
  color: var(--label-tertiary);
  font-size: var(--text-xs, 11px);
  line-height: 1.4;
}

.palette-list {
  overflow-y: auto;
  padding: var(--sp-xs);
  scrollbar-width: thin;
}

.palette-section-title {
  padding: var(--sp-sm) var(--sp-sm) var(--sp-3xs);
  font-size: var(--text-xs, 11px);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--label-secondary);
}

.palette-entry {
  /* Button reset — match .sidebar-row's approach */
  font: inherit;
  border: none;
  background: none;
  cursor: pointer;
  box-sizing: border-box;
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: var(--radius-md);
  color: var(--label-primary);
  text-align: left;
  min-height: 40px;
}

/* Keyboard selection and hover share one look; hover also moves the
   selection (see view_entry's on_mouse_enter), so they never disagree. */
.palette-entry--selected {
  background: var(--fill-elevated-secondary);
}

.palette-entry-icon {
  width: 18px;
  height: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--label-secondary);
}

.palette-entry-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--text-base);
}

.palette-entry-hint {
  flex-shrink: 0;
  color: var(--label-secondary);
  font-size: var(--text-sm);
}

/* ── Post-create toast ──────────────────────────────────────────────────
   Same placement language as .chat-toast, but interactive: it carries the
   "View" link to the created reminder, so pointer-events stay on. */
.palette-toast {
  position: fixed;
  bottom: var(--sp-xl, 24px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-xs, 8px) var(--sp-md, 16px);
  background: var(--bg-elevated, var(--bg-primary));
  color: var(--label-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-full, 999px);
  font-size: var(--text-sm, 0.875rem);
  white-space: nowrap;
  z-index: 300;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  animation: palette-toast-in 0.18s ease-out;
}

@keyframes palette-toast-in {
  from {
    opacity: 0;
    transform: translate(-50%, 6px);
  }
  to {
    opacity: 1;
    transform: translate(-50%, 0);
  }
}

.palette-toast-link {
  color: var(--accent-blue);
  font-weight: 600;
  text-decoration: none;
}

@media (hover: hover) {
  .palette-toast-link:hover {
    text-decoration: underline;
  }
}

/* Mobile: pin the panel to the top with breathing room; 14vh of dead
   space above the keyboard is too much. */
@media (max-width: 834px) {
  .palette-root {
    padding: var(--sp-md);
  }

  .palette-panel {
    max-height: 70vh;
  }
}

/* Desktop home (brief hero + inline switcher) — reuses .palette-entry. */
/* Desktop home (app/home) — greeting, today's brief, and the inline quick
   switcher. Entry rows reuse the ⌘K overlay's .palette-entry styles
   (command_palette.css, imported before this file). */

.home {
  height: 100%;
  min-height: 0;
  overflow-y: auto;
  padding: var(--sp-2xl, 40px) var(--sp-xl);
  box-sizing: border-box;
}

.home-body {
  width: min(740px, 100%);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
}

.home-greeting {
  margin: 0;
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--label-primary);
}

.home-offline {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: var(--radius-md);
  background: var(--bg-secondary);
  color: var(--label-secondary);
  font-size: var(--text-sm);
}

/* ── Switcher column ───────────────────────────────────────────────────
   The switcher alone. Until 2026-09-24 it shared a 7:5 grid with the
   daily brief (.home-columns / .home-aside, split by an 800px container
   query on .home); the brief left this pane that day. */

.home-main {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
  min-width: 0;
}

/* ── Inline switcher ──────────────────────────────────────────────────── */

.home-switcher {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

/* The pill. Same shape as the landing composer; the glow behind it is
   the brief's, blooming on focus. */
.home-switcher-pill-wrap {
  position: relative;
}

.home-switcher-glow {
  inset: -3px;
  border-radius: var(--radius-full);
  opacity: 0;
  transition: opacity 200ms ease;
}

.home-switcher-pill-wrap:focus-within .home-switcher-glow {
  opacity: 0.22;
}

.home-switcher-pill {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-xs) var(--sp-xs) var(--sp-md);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-full);
  background: var(--bg-primary);
}

.home-switcher-pill:focus-within {
  border-color: transparent;
}

.home-switcher-pill .palette-input {
  font-size: var(--text-xl);
  padding: var(--sp-xs) 0;
}

/* The teaching line under the pill: same shortcut, same rows, anywhere. */
.home-switcher-hint {
  margin: calc(-1 * var(--sp-xs)) 0 0;
  padding: 0 var(--sp-md);
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

.home-switcher-hint .palette-kbd {
  margin: 0 2px;
  font-size: var(--text-xs);
  font-family: inherit;
  color: var(--label-secondary);
  background: var(--bg-secondary);
}

/* "Go": runs the selected row, for people who click rather than Enter.
   Mirrors .chat-landing-send. */
.home-switcher-go {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--radius-full);
  background: var(--accent-blue);
  color: #ffffff;
  cursor: pointer;
  padding: 0;
}

@media (hover: hover) {
  .home-switcher-go:hover {
    filter: brightness(1.05);
  }
}

/* The list: the overlay's rows as short cards. Section titles stay for
   scannability. */
.home-switcher-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

.home-switcher-list .palette-section-title {
  padding: var(--sp-sm) var(--sp-xs) var(--sp-3xs);
}

.home-switcher-list .palette-entry {
  gap: var(--sp-sm);
  min-height: 48px;
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: 14px;
  background: var(--bg-secondary);
  font-size: var(--text-lg);
  transition: background-color 120ms ease, box-shadow 120ms ease;
}

.home-switcher-list .palette-entry--selected {
  background: var(--fill-elevated-secondary);
  box-shadow: var(--shadow-sm);
}

/* The primary verb wears the brand green here too (rail parity). */
.home-switcher-list .palette-entry-icon--reminder .icon {
  background: var(--fill-gradient-green);
}

/* Row icons sit in a small white disc on the card. */
.home-switcher-list .palette-entry-icon {
  width: 30px;
  height: 30px;
  border-radius: var(--radius-full);
  background: var(--bg-primary);
  color: var(--label-primary);
}

.home-switcher-list .palette-entry-icon .icon {
  width: 16px;
  height: 16px;
}

/* ── Brief hero ───────────────────────────────────────────────────────── */

.brief-hero {
  position: relative;
}

.brief-hero .brief-card-glow {
  border-radius: 24px;
}

/* A quieter halo than the timeline card's: home is a reading surface,
   and the hero is already the biggest thing on it. No breathing. */
.brief-hero .brief-card-glow--unread {
  animation: none;
  opacity: 0.26;
}

.brief-hero .brief-card-glow--read {
  opacity: 0.1;
}

.brief-hero-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  width: 100%;
  padding: var(--sp-lg);
  border: none;
  border-radius: 20px;
  background: var(--bg-primary);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color 150ms ease;
}

@media (hover: hover) {
  .brief-hero-card:hover {
    background: var(--fill-elevated-secondary);
  }
}

.brief-hero-head {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
}

.brief-hero-glyph {
  font-size: 22px;
  line-height: 1;
}

.brief-hero-title {
  margin: 0;
  flex: 1;
  min-width: 0;
  font-size: var(--text-xl);
  font-weight: 600;
  color: var(--label-primary);
}

.brief-hero-state {
  flex-shrink: 0;
  padding: 2px 8px;
  border-radius: var(--radius-full);
  background: var(--bg-secondary);
  color: var(--label-secondary);
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.brief-hero--unread .brief-hero-state {
  background: var(--accent-blue);
  color: #ffffff;
}

.brief-hero-excerpt {
  margin: 0;
  font-size: var(--text-lg);
  line-height: 1.5;
  color: var(--label-primary);
}

.brief-hero-open {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--accent-blue);
}

/* Read: a single line — reachable all day, no longer the headline. */
.brief-hero--read .brief-hero-card {
  padding: var(--sp-sm) var(--sp-md);
  flex-direction: row;
  align-items: center;
}

.brief-hero--read .brief-hero-head {
  flex: 1;
  min-width: 0;
}

.brief-hero--read .brief-hero-title {
  font-size: var(--text-lg);
}

/* New Reminder creation surface */

/* The per-visit key wrapper (client.view_route): layout-neutral, so
   .nr-root sits in the pane exactly as before. */
.nr-session {
  display: contents;
}

.nr-root {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--bg-body);
  position: relative;
}

/* No top nav on either reminder surface — back lives in the mobile
   accessory bar, and desktop commits via the rail's trailing circle
   (mock parity, matching the detail view). */

/* Full-width hero removed from reminder (container) views — media
   renders in the title-row thumbnail deck instead (iOS parity). */

/* ── Body ──────────────────────────────────────────────────── */

/* No horizontal padding on the container — each child carries its own
   so the hero above can bleed edge-to-edge. */
.nr-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding-top: var(--sp-md);
  padding-bottom: 0;
  gap: var(--sp-xs);
  /* Single page-level scroll container: the reminder card and (on detail)
     the AI chat section below it both flow here — neither owns its own
     independent scrollbox. The extra breathing room between the card and
     the AI chat section comes from .detail-ai-chat's own margin-top, same
     as before this was split into a separate box. */
  overflow-y: auto;
}
@media (max-width: 834px) {
  .nr-body { padding: calc(var(--sp-sm) + var(--sp-xs)) 0 0 0;
  }
}

/* Reminder-editing content (title/due/body/checklist) — its own card.
   height: fit-content sizes it exactly to its content (no max-height
   anywhere in this chain either), so it keeps growing as content (title
   lines, notes, checklist items) is added and never clips or scrolls
   internally. Kept as a dedicated wrapper (rather than styling .nr-body
   directly) so the detail page can render the AI chat section as an
   unboxed sibling below it instead of inside the same box. */
.nr-reminder-card {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: var(--sp-xs);
  height: fit-content;
}

/* Heading block: title row + due pill in one container that owns the
   horizontal/vertical padding and the bottom hairline (replaces the old
   standalone .nr-divider below the pill). */
.reminder-heading-container {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-md); /* 16px title block → due pill */
  padding: 0 var(--sp-xl) var(--sp-md) var(--sp-xl);
  border-bottom: 1px solid var(--border-tertiary);
  flex-shrink: 0;
}

/* Title row: completion circle + textarea + optional thumbnail deck
   (.thumb-stack, styles in detail.css). The deck is pulled OUT OF FLOW
   (absolute, top-right of the heading container) so the row's height
   tracks the title text only — otherwise the 80px deck box forces a
   large gap between a short title and the due pill below. The rotated
   fan overhangs harmlessly. */
.nr-title-row {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-sm);
}

.reminder-heading-container .thumb-stack {
  position: absolute;
  top: 0;
  right: var(--sp-xl);
  margin-left: 0;
}

/* Reserve the deck's width so the title wraps before running under it. */
.reminder-heading-container:has(.thumb-stack) .nr-title {
  margin-right: 96px; /* 80px deck + 16px breathing room */
}

/* Due/reminder pill row — directly below the title block (iOS
   ReminderPill). Pill visuals come from the shared .due-pill component
   (pill.css); spacing/inset come from the heading container. */
/* One pill per alert, then "Add alert" (multi-alert): wrap onto further
   lines rather than overflow a narrow screen. */
.nr-due-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-xs);
}

/* iOS ReminderPill (detail variant): larger than the timeline-row pill —
   15/500/20 label, 12px/10px padding, 8px icon gap. Set/unset fills come
   from the shared .due-pill / .due-pill--empty rules. */
.nr-due-row .due-pill {
  padding: 10px 12px;
  gap: 8px;
}

.nr-due-row .due-pill > .due-pill-label {
  font-size: 15px;
  font-weight: 500;
  line-height: 20px;
}

/* (Hairline below the pill now lives on .reminder-heading-container's
   bottom border.) */

/* Header action strip (reminder_heading `actions`; desktop detail only).
   Starts under the title text: 32 circle slot + 8 (iOS titleTextInset 40;
   mock 40.1). Wraps rather than compresses (iOS FlowRowLayout); the
   trailing slot keeps ≥8 (iOS Spacer(minLength: Gap.xs)). */
.nr-action-strip {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-xs);
  padding-left: calc(var(--size-xxl) + var(--sp-xs));
}
.nr-action-strip-end {
  display: flex;
  flex-shrink: 0;
  margin-left: auto;
}
/* The set state (iOS ReminderPill capsule) sized to the 36px row. */
.nr-action-strip .due-pill {
  height: 36px;
  padding: 0 var(--sp-sm);
  gap: var(--sp-xs);
}
.nr-action-strip .due-pill > .due-pill-label {
  font-size: var(--text-md);
  font-weight: 500;
  line-height: 20px;
}

/* Round header action (ui/round_button) — iOS "label primary on
   background secondary" disc; 36px (mock) with a 22px glyph. */
.nr-round-btn {
  width: 36px;
  height: 36px;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background: var(--bg-secondary); /* mock #F2F1F6 */
  color: var(--label-primary);
  cursor: pointer;
  transition: background 150ms ease;
}
@media (hover: hover) {
  .nr-round-btn:hover {
    background: var(--fill-elevated-secondary);
  }
}
/* base.css's bg-secondary outline vanishes on a bg-secondary disc. */
.nr-round-btn:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

.nr-completion-circle {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  border: 2px solid var(--label-tertiary);
  border-radius: var(--radius-full);
  margin-top: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  cursor: pointer;
  padding: 0;
  font: inherit;
  transition: border-color 150ms ease, background 150ms ease;
}
.nr-completion-circle:hover {
  border-color: var(--label-secondary);
}
/* Completed state: bare checkmark/thick glyph, label-tertiary — the
   button drops its ring and hugs the 24px svg (mock parity). */
.nr-completion-circle--done {
  border: none;
  background: transparent;
  color: var(--label-tertiary);
  padding: 0;
}
.nr-completion-circle--done:hover {
  border-color: transparent;
  color: var(--label-secondary);
}

/* Reminder list/picker section */
.nr-body .detail-reminders-list,
.nr-body .detail-action-remind-wrapper {
  padding: 0 var(--sp-xl);
}

/* The title's type. The phone placeholder span (.nr-title-ph) shares this
   rule, so it reads as the field's own placeholder and follows any change
   made here. */
.nr-title,
.nr-title-ph {
  font-size: var(--text-3xl);
  font-weight: 600;
  font-family: inherit;
  line-height: 1.3;
}

.nr-title {
  flex: 1;
  min-width: 0;
  color: var(--label-primary);
  background: transparent;
  border: none;
  outline: none;
  resize: none;
  padding: 0;
  /* Auto-grow with content, capped at ~3 lines (matches
     .detail-title-editable's treatment and the iOS large title). */
  field-sizing: content;
  min-height: 1.3em;
  max-height: calc(3 * 1.3em);
  overflow: hidden;
}

.nr-title::placeholder { color: var(--label-secondary); }

/* The phone placeholder span (reminder_heading's phone_placeholder): drawn
   only by the phone block below. */
.nr-title-ph { display: none; }

/* Completed state — iOS EditableTitle parity: strikethrough +
   textPrimary → textSecondary. Title-only; due pill, body, and thumbnail
   deck are unaffected. */
.nr-title--done {
  text-decoration: line-through;
  color: var(--label-secondary);
}

/* Milkdown body editor mount. Typography and ProseMirror styling come
   from the shared .detail-body-editor rules (detail.css) — the mount div
   carries both classes.
   Flex sizing: `flex: 1 0 auto` (grow, NEVER shrink, content basis). The
   card is meant to look spacious by default (`.nr-reminder-card`'s
   min-height: 50vh) rather than hug tightly, so any leftover slack should
   read as more room to type — not dead unclickable padding below the
   editor — which is why the editor (not some inert spacer) is the child
   that claims it. Originally this was `flex: 1` (basis 0, shrink 1),
   which let the editor get squeezed toward its min-height once the AI
   chat filled the column; grow:1 + shrink:0 avoids that. */
.nr-body-editor {
  flex: 1 0 auto;
  min-height: 120px;
  /* Full-bleed editable surface: the inset moves INTO the ProseMirror
     node below so the entire remaining area is clickable/editable —
     with padding out here, clicks in the gutter landed on the mount
     div instead of the editor. */
  padding: 0;
  display: flex;
  flex-direction: column;
  cursor: text;
  outline: none;
}

/* Stretch the Milkdown chain to fill the mount (detail.css sets
   display:block — the flex column here wins for both surfaces since the
   mount carries .nr-body-editor on NR and detail alike). */
.nr-body-editor .milkdown {
  display: flex;
  flex-direction: column;
  flex: 1;
  width: 100%;
}

.nr-body-editor .ProseMirror {
  flex: 1;
  width: 100%;
  box-sizing: border-box;
  /* The visual inset the mount used to carry. */
  padding: var(--sp-lg) var(--sp-xl);
}

/* Override the detail editor's placeholder copy for this page. */
.nr-body-editor .ProseMirror p.is-editor-empty:first-child::before,
.nr-body-editor .ProseMirror:empty::before {
  content: "toDo";
}

/* ── Toolbar ───────────────────────────────────────────────── */

/* iOS CaptureBottomBar cluster layout:
     [back circle] [media/formatting capsule] [⋯ circle] [confirm circle]
   Grey circles + capsule on the surface fill; the confirm circle carries
   the green Done gradient. New Reminder's phone bar, on the page's own
   surface; the detail page's phone bar is the formatting bar
   (format_bar.css). */
.nr-toolbar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-md) var(--sp-xl) var(--sp-md);
  padding-bottom: var(--sp-xl);
  flex-shrink: 0;
}

.nr-toolbar-spacer { flex: 1; }

/* Flat icon button, with a transparent fill — inside the flipping capsule
   (New Reminder's phone bar and the desktop rail). */
.nr-toolbar-btn {
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  cursor: pointer;
  border-radius: var(--radius-full);
  color: var(--label-primary);
  padding: 0;
  font: inherit;
  flex-shrink: 0;
  transition: background 120ms ease;
}
.nr-toolbar-btn:hover:not(:disabled) { background: var(--bg-secondary); }
/* Formatting commands the editor FFI can't run yet render disabled. */
.nr-toolbar-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

/* Capsule mode-toggle chevron — narrower hit area, like iOS's 36pt
   capsuleChevronButton next to 44pt icon buttons. */
.nr-toolbar-btn--chevron {
  width: 32px;
}

/* Center capsule — the media-attach strip that flips to formatting. */
.nr-toolbar-capsule {
  min-width: fit-content;
  max-width: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 44px;
  padding: 0 var(--sp-xs);
  border-radius: var(--radius-full);
  background: var(--bg-secondary);
  overflow: hidden;
}

/* Circular grey buttons — leading back and trailing ⋯ (iOS 48pt → 44px). */
.nr-toolbar-circle {
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--radius-full);
  background: var(--bg-secondary);
  color: var(--label-primary);
  cursor: pointer;
  padding: 0;
  font: inherit;
  flex-shrink: 0;
  transition: filter 120ms ease, background 120ms ease;
}
.nr-toolbar-circle:hover:not(:disabled) { filter: brightness(0.96); }

/* The trailing circle renders as an <a> while it's the New Reminder CTA
   (keyboard down) — keep link chrome off the icon. */
a.nr-toolbar-circle,
a.nr-toolbar-circle:hover {
  text-decoration: none;
}

/* Trailing confirm circle — grey disabled + while there's nothing to
   save (iOS editing-empty quaternary fill), green gradient ✓ once
   content exists (same gradient family as the detail header's Done). */
.nr-toolbar-confirm:disabled {
  color: var(--label-tertiary);
  cursor: not-allowed;
}
.nr-toolbar-confirm--active {
  background: var(--fill-gradient);
  color: #ffffff;
}
/* Chained with .nr-toolbar-circle so this beats the grey circles'
   darkening hover rule (same element carries both classes). */
.nr-toolbar-circle.nr-toolbar-confirm--active:hover {
  filter: brightness(1.05);
}

/* ── Accessory bar — responsive split ───────────────────────────────── */
/* ui/accessory_bar renders BOTH the mobile cluster bar (.nr-toolbar) and
   the desktop floating rail (.acc-rail); width media queries gate which
   one shows (same visibility-pin pattern as .page-header).

   Both reminder surfaces (New Reminder + owner detail) render the
   component, so the desktop restructure below applies to every .nr-root. */

/* Mobile bar buttons — mock parity: white chrome with a hairline border
   (was solid bg-secondary fill). */
.nr-toolbar-circle,
.nr-toolbar-capsule {
  background: var(--bg-primary);
  border: 1px solid var(--border-tertiary);
}
/* The active confirm keeps its gradient fill (class order must not
   matter, so re-assert over the border rule above). */
.nr-toolbar-circle.nr-toolbar-confirm--active {
  background: var(--fill-gradient);
  border-color: transparent;
}

.acc-rail {
  display: none;
}

@media (min-width: 835px) {
  .nr-root .nr-toolbar {
    display: none;
  }

  /* Horizontal action bar hovering over the editor column, aligned to
     its right edge: the shared flipping capsule + the standalone ⋯
     circle (mock parity). The base inset aligns with the creation
     surface's centered card (gutter = max(112px, half the space beyond
     960px), plus an inner inset); the detail surface re-anchors 24 from
     its main pane's edge — left of the open AI panel, above the Messages
     pill (see the :has() overrides in detail.css). */
  /* Both surfaces: the card stops var(--sp-xl) above the pane edge
     (.nr-body's bottom margin) — lift the bar so it floats fully inside
     the card instead of straddling its bottom edge. */
  .acc-rail {
    position: absolute;
    right: calc(max(112px, (100% - 960px) / 2) + var(--sp-lg));
    bottom: calc(var(--sp-xl) + var(--sp-lg));
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--sp-lg);
    z-index: 5;
  }

  /* Desktop scale for the shared capsule/circle chrome: a roomier 52px
     bar with generous icon spacing and a floating shadow. */
  .acc-rail .nr-toolbar-capsule {
    max-width: none;
    height: 52px;
    gap: var(--sp-md);
    padding: 0 var(--sp-md);
    box-shadow: var(--shadow);
  }

  .acc-rail .nr-toolbar-btn {
    width: 40px;
    height: 40px;
  }

  .acc-rail .nr-toolbar-circle {
    width: 52px;
    height: 52px;
    box-shadow: var(--shadow);
  }
}

/* Hidden multi-file input behind the accessory bar's folder action. */
.nr-attach-input {
  display: none;
}

/* Primary action in the capture surfaces (Save recording / Use photo) —
   token-based replacement for the old dark-theme .capture-sheet-save-btn. */
.nr-capture-save-btn {
  padding: var(--sp-sm) var(--sp-xl);
  background: var(--fill-gradient);
  color: #ffffff;
  font: inherit;
  font-size: var(--text-base);
  font-weight: 600;
  border: none;
  border-radius: var(--radius-full);
  cursor: pointer;
  transition: filter 120ms ease;
}
.nr-capture-save-btn:hover {
  filter: brightness(1.05);
}

/* (Camera modal visuals come from detail.css's .camera-modal-body /
   .camera-viewfinder and capture.css's .capture-webcam-preview rules.) */

/* (Local media previews render through the shared .media-viewer chrome —
   styles in detail.css — and the shared image lightbox in modal.css.) */

/* ── Voice-memo capture overlay (iOS VoicePickerView parity) ────────── */
/* Fullscreen, PERMANENTLY DARK layer floating over the dimmed/blurred
   page — colors are pinned literals (iOS forces .dark on this modal), so
   the app theme must not leak in. Sits above the sheet layer (z 400/401). */
.voice-overlay {
  position: fixed;
  inset: 0;
  z-index: 500;
  display: flex;
  flex-direction: column;
  background: rgba(20, 20, 22, 0.72);
  -webkit-backdrop-filter: blur(24px);
  backdrop-filter: blur(24px);
}

/* Waveform zone — dotted midline (AudioWaveformLine idle) + timer. */
.voice-overlay__wave {
  flex: 3 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-md);
  padding: 0 var(--sp-md);
}
.voice-overlay__dotline {
  width: 100%;
  border-top: 2px dotted rgba(235, 235, 245, 0.35);
}
.voice-overlay__timer {
  font-variant-numeric: tabular-nums;
  font-size: var(--text-base);
  color: rgba(235, 235, 245, 0.6);
}

.voice-overlay__error {
  margin: 0;
  text-align: center;
  font-size: var(--text-sm);
  color: var(--accent-red);
}

/* Record toggle (VoiceRecordButton): label over a 64px circle. */
.voice-overlay__record {
  flex: 2 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-md);
}
.voice-overlay__record-label {
  font-size: var(--text-base);
  color: rgba(235, 235, 245, 0.6);
}
/* Idle / rerecord: ghost outlined mic, white glyph. */
.voice-record-btn {
  width: 64px;
  height: 64px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background: transparent;
  border: 1px solid rgba(235, 235, 245, 0.3);
  color: #ffffff;
  cursor: pointer;
  padding: 0;
  font: inherit;
}
/* Recording: red fill + detached outer ring (outline-offset = the 4px
   gap iOS draws with the padded overlay circle). */
.voice-record-btn--recording {
  background: var(--accent-red);
  border-color: transparent;
  outline: 1.5px solid var(--accent-red);
  outline-offset: 4px;
}

/* Bottom bar — Cancel / Done wide capsules (56px, fill the row). */
.voice-overlay__actions {
  display: flex;
  gap: var(--sp-md);
  padding: 0 var(--sp-md) calc(var(--sp-md) + env(safe-area-inset-bottom));
}
.voice-capsule {
  flex: 1;
  height: 56px;
  border: none;
  border-radius: var(--radius-full);
  font: inherit;
  font-size: var(--text-base);
  font-weight: 600;
  cursor: pointer;
  background: rgba(118, 118, 128, 0.24);
  color: #ffffff;
}
/* Done — dark-pinned Inverse palette: dim while disabled, white/black
   once a take exists. */
.voice-capsule--done-disabled {
  background: rgba(118, 118, 128, 0.18);
  color: rgba(235, 235, 245, 0.3);
  cursor: not-allowed;
}
.voice-capsule--done {
  background: #ffffff;
  color: #000000;
}
.voice-capsule--done:hover {
  filter: brightness(0.95);
}

/* ── Desktop card shell (New Reminder) ──────────────────────────────── */
/* Mock: grey canvas, centered white content card, rail in the right
   gutter — no top chrome, matching the detail surface. The canvas is the
   shell's --canvas-bg (2026-09-25), so it runs on unbroken from the list
   pane beside it; bg-secondary matched only while the shell painted
   bg-secondary (#1C1C1E vs the canvas's #0C0C0C in dark). */
@media (min-width: 835px) {
  .nr-root {
    background: var(--canvas-bg);
  }

  /* .nr-body itself stays a plain (transparent, unboxed) scroll column,
     centered with gutters wide enough for the rail (112px each side at
     the narrow end) — both the reminder card and, on detail, the AI chat
     section below it line up under this same width. */
  .nr-root .nr-body {
    width: min(960px, calc(100% - 224px));
    margin: 0 auto var(--sp-xl);
  }

  /* Creation surface only (detail brings its own body padding): the top
     breathing room the old floating-header row used to provide, and —
     since there's no AI panel to share the row with — don't let the
     column flex-grow past its reading width (detail.css's
     `.nr-detail-columns > .nr-body { flex: 1 }` would otherwise stretch
     the card full-bleed; the auto margins then re-center it). */
  .nr-root:not(.nr-root--detail) .nr-body {
    padding-top: var(--sp-lg);
    flex: 0 1 auto;
  }

  /* The reminder card becomes the white, rounded box (mock parity) —
     moved off .nr-body so the AI chat section can render below it,
     unboxed, without inheriting the card background. */
  .nr-root .nr-reminder-card {
    background: var(--bg-primary);
    border-radius: var(--radius-xl);
    padding: var(--sp-xl) 0 var(--sp-lg);
  }
}

@media (max-width: 834px) {
  /* The detail page's colour, one property the page and the AI drawer's
     scrim (chat_drawer.css) both read, so the notes end at the docked pill
     in the page's own colour. iOS paints backgroundPrimary under everything
     (spec §4c.1), the nav bar's scrim colour, so no darker band sits under
     the bar. */
  :is(.nr-root--detail, .nr-root--capture) {
    --detail-page-bg: var(--bg-primary);
    background: var(--detail-page-bg);
  }

  /* ── The title block (spec §4c.0, §4c.2, §4c.9) ──────────────────────
     Every rule is scoped .nr-root--detail: /new, share and join draw the
     same heading and keep today's. The heading's 16 gutter is
     detail_header.css's. The title row, then 16, then the phone header
     (detail_header.css), then the hairline 24 under the action row: the
     16 gap and the hairline's own 8. */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container {
    gap: 16px;
    padding-bottom: 0;
    border-bottom: none;
  }

  /* The hairline: in flow, the heading's last box, across its content box
     (x 16 -> 377 at 393). */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container::after {
    content: "";
    flex-shrink: 0;
    height: 0.5px;
    margin-top: 8px;
    background: var(--ui-borders);
  }

  /* The circle: 32 with a 2px ring in UIKit's tertiaryLabel, 4 below the
     title box's top (B + 16). Only the ring's colour is set here, so the
     done state's `border: none` still takes the ring off. */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .nr-completion-circle {
    width: 32px;
    height: 32px;
    margin-top: 4px;
    border-color: var(--label-tertiary-system);
  }

  /* Checked: the glyph fills the same 32 footprint (24 scaled, as on
     desktop), resting in --label-secondary as on iOS. */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .nr-completion-circle--done {
    color: var(--label-secondary);
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .nr-completion-circle--done .icon {
    transform: scale(1.3333);
  }

  /* A tick's celebration (spec §4c.2, round 3a's, C9): while the detail
     root carries data-celebrating the checked glyph wears the accent, and
     so does the ring of a tick taken back inside the 3 s, at once. When the
     attribute goes, both ease back over 0.45 s. The press's spring survives
     the `transition: none` (see the no-preference block below). */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .nr-completion-circle {
    transition:
      color 450ms ease,
      border-color 450ms ease,
      background 150ms ease;
  }

  :is(.nr-root--detail, .nr-root--capture)[data-celebrating] .reminder-heading-container .nr-completion-circle {
    color: var(--accent-blue);
    border-color: var(--accent-blue);
    transition: none;
  }

  /* The title: 32/700/40 at tracking 0 (C21), every line shown (iOS
     never compresses it). iOS's line is 39.68, but Mobile Safari floors a
     fractional line height to the whole px (39.68 lays out at 39), which
     put everything under a two-line title ~1.3pt high; 40 is +0.32 a line
     on both engines (Julian, 2026-10-25). Its colour and the completed
     strike are the base rules'. The placeholder span takes the same type. */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .nr-title,
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .nr-title-ph {
    font-size: 32px;
    font-weight: 700;
    line-height: 40px;
    letter-spacing: 0;
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .nr-title {
    min-height: 40px;
    max-height: none;
    overflow: visible;
  }

  /* The empty title reads "New item" on phones (spec §4b.9), "New toDo" on
     desktop. The textarea's `placeholder` attribute can't be gated on width,
     so on phones it goes transparent (screen readers still hear it) and the
     sibling span right after it draws the phone copy in its place.
     The span overlaps the field's first line, so the row is a grid whose
     second column holds both. A grid, not a positioned row: the thumbnail deck
     is absolute against .reminder-heading-container and must not move. */
  :is(.nr-root--detail, .nr-root--capture) .nr-title::placeholder {
    color: transparent;
  }

  /* The title's text 8 from the circle: x 16 + 32 + 8 = 56. */
  :is(.nr-root--detail, .nr-root--capture) .nr-title-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 8px;
    align-items: start;
  }

  :is(.nr-root--detail, .nr-root--capture) .nr-title-row > .nr-title,
  :is(.nr-root--detail, .nr-root--capture) .nr-title-row > .nr-title-ph {
    grid-column: 2;
    grid-row: 1;
  }

  :is(.nr-root--detail, .nr-root--capture) .nr-title-row > .nr-title {
    justify-self: stretch;
  }

  :is(.nr-root--detail, .nr-root--capture) .nr-title:placeholder-shown + .nr-title-ph {
    display: block;
    justify-self: start;
    max-width: 100%;
    pointer-events: none;
    color: var(--label-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* ── Beside the deck (spec §4c.3; the deck is detail.css's) ──────────
     The title gives up 110 on its right (the 96 frame + 8 + 6), so it ends
     at x <= 267. The alert line and the action row keep the full width and,
     under a short title, ride up beside the deck. The empty title's
     placeholder takes no margin: it sits at the start, as wide as its
     unwrapped copy, which a margin wouldn't hold back, and "New item" ends
     well short of 267. */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container:has(.thumb-stack) .nr-title {
    margin-right: 110px;
  }

  /* The title block and the phone slot are at least 88 tall, so the
     hairline never rides into the deck: the title row is at least its 40
     line (whole px for Safari, above), then the 16 gap, and the slot holds
     the rest. (The action row's
     44 already does, for every seat that draws one.) */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container:has(.thumb-stack) .detail-phone-header {
    min-height: calc(88px - 16px - 40px);
  }

  /* With a pile in the row, the row's top stays at least 88 below the
     title's top, so the faces clear the cards (iOS lets them land under
     the deck): the title row holds 88, less the 16 gap, and less the
     set-alert line and its 8 when there is one. A pile with no deck keeps
     the plain rhythm. */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container:has(.thumb-stack):has(.detail-action-item--pile) .nr-title-row {
    min-height: calc(88px - 16px);
  }

  :is(.nr-root--detail, .nr-root--capture)
    .reminder-heading-container:has(.thumb-stack):has(.detail-action-item--pile):has(.detail-alert-line)
    .nr-title-row {
    min-height: calc(88px - 16px - 24px - 8px);
  }

  .nr-toolbar-capsule {
    height: var(--size-5xl);
  }
  .nr-toolbar-btn, .nr-toolbar-circle {
    height: var(--size-5xl);
    width: var(--size-5xl);
  }
}

/* The circle grows to 1.2 while pressed, on the nav discs' curve (spec
   §4c.2, C10); not under reduced motion, like the card circle. The
   celebration's eases (above) run beside it. */
@media (max-width: 834px) and (prefers-reduced-motion: no-preference) {
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .nr-completion-circle {
    transition:
      color 450ms ease,
      border-color 450ms ease,
      background 150ms ease,
      transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  /* The accent still lands at once, but the press keeps its spring: the
     release that ticks springs back from 1.2 under it, and so does a press
     inside the 3 s. Without this the celebrating rule's `transition: none`
     (0,4,0) beats the rule above and cancels the transform transition, as
     the card circle's tint would without timeline_row.css's own. */
  :is(.nr-root--detail, .nr-root--capture)[data-celebrating] .reminder-heading-container .nr-completion-circle {
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .nr-completion-circle:active {
    transform: scale(1.2);
  }
}


@media (max-width: 488px) {
    .nr-toolbar-circle {
        height: var(--size-4xl);
        width: var(--size-4xl);
    }
    .nr-toolbar-capsule {
        height: var(--size-4xl);
    }

    .nr-toolbar-btn {
      height: var(--size-xxl);
      width: var(--size-xxl);
    }
}

/* ── The capture page on phones (iOS LiveFeedDetailView on a pending item,
   2026-10-27): it shares the detail's phone rules (every `.nr-root--detail`
   phone rule above and in detail.css / detail_header.css reads
   `:is(.nr-root--detail, .nr-root--capture)`), keeps the old phone
   toolbar off, and draws the capture bar (ui/format_bar, format_bar.css)
   at its foot — always: the + at rest, the ✓ while a field has focus. ── */
@media (max-width: 834px) {
  .nr-root--capture .nr-toolbar {
    display: none;
  }
  /* No "Add alert" capsule on iOS's capture header: the row's bell sets
     the alert, and the set-alert line (the phone header's) shows it. The
     desktop keeps its pill. */
  .nr-root--capture .nr-due-row {
    display: none;
  }
  .capture-bar {
    display: contents;
  }
  /* The notes' own padding ends at the bar: the 160 of trailing room iOS
     leaves is the detail's 168 (detail.css), shared. */
}

/* Phone header (iOS InlineNavBar) — ui/nav_bar. */
/* The phone header (ui/nav_bar.gleam) — iOS InlineNavBar, parity round 1
   (2026-10-23). Phone only: hidden at ≥835px, where each page keeps its
   desktop header.

   iOS numbers (InlineNavBar.swift, IconButton.swift, BarChrome.swift):
   a 44pt row, 16pt side padding, the title column kept 44pt clear of each
   edge so it centres; 44pt discs with a 24pt glyph; the back disc's hit
   area widened to 60pt; title bodyBold 18/25.2, letter-spacing 0. */

.nav-bar {
  display: none;
}

@media (max-width: 834px) {
  .nav-bar {
    position: relative;
    z-index: 20;
    isolation: isolate;
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr) 44px;
    align-items: center;
    column-gap: 0;
    /* Every phone header sits at the top of the screen, so it carries the
       status-bar inset itself. */
    min-height: calc(env(safe-area-inset-top) + 44px);
    padding: env(safe-area-inset-top) 16px 0;
    flex-shrink: 0;
  }

  .nav-bar--scrolls-under {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
  }

  /* iOS softScrim(solidInset: 0, blur 16) — scrim-tuner (a): the page's
     colour, solid down to the header's bottom edge, then a gaussian tail
     over the content. The other edges overshoot 3 × blur so their own
     feather lands off-screen. No backdrop blur (iOS pulled overflowBlur,
     2026-08-06). */
  .nav-bar--scrolls-under::before {
    content: "";
    position: absolute;
    z-index: -1;
    top: -48px;
    left: -48px;
    right: -48px;
    bottom: 0;
    background: var(--nav-bar-scrim, var(--canvas-bg));
    filter: blur(16px);
    pointer-events: none;
  }

  /* A toDo scrolls under a primary-background scrim. */
  .nav-bar--scrolls-under {
    --nav-bar-scrim: var(--bg-primary);
  }

  .nav-bar-leading,
  .nav-bar-trailing {
    display: flex;
    align-items: center;
    width: 44px;
    height: 44px;
  }

  .nav-bar-trailing {
    justify-content: flex-end;
  }

  .nav-bar-title {
    min-width: 0;
    display: flex;
    justify-content: center;
    overflow: clip;
  }

  .nav-bar-title-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 18px;
    font-weight: 700;
    line-height: 25.2px;
    letter-spacing: 0;
    color: var(--label-primary);
  }

  .nav-bar-title-button {
    padding: 0;
    border: 0;
    background: none;
    font-family: inherit;
    cursor: pointer;
  }

  /* The glass disc (BarChrome .glass below iOS 26): translucent fill,
     blur, a 1pt inside hairline and the toolbar shadow. Press grows it
     1.2× on the .pressable spring (response .3, damping .6). */
  .nav-bar-disc {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--glass-fill);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    box-shadow:
      inset 0 0 0 1px var(--border-secondary),
      0 4px 8px rgba(0, 0, 0, 0.1);
    color: var(--label-primary);
    text-decoration: none;
    cursor: pointer;
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
    -webkit-tap-highlight-color: transparent;
  }

  .nav-bar-disc:active {
    transform: scale(1.2);
  }

  /* An inert disc (nav_bar.TrailingDisabled) doesn't answer a press, nor
     offer one, and its glyph dims as iOS's .disabled does. */
  .nav-bar-disc[aria-disabled="true"] {
    color: var(--label-tertiary);
    cursor: default;
  }

  .nav-bar-disc[aria-disabled="true"]:active {
    transform: none;
  }

  /* Back's hit area widens to 60pt without moving the layout. */
  .nav-bar-disc--back::after {
    content: "";
    position: absolute;
    inset: -8px;
  }
}

@media (max-width: 834px) and (prefers-reduced-motion: reduce) {
  .nav-bar-disc {
    transition: none;
  }
}

/* A page's own title element riding in the phone header (a chat's name
   button, with its loading skeleton) takes the header's title type. */
@media (max-width: 834px) {
  .nav-bar-title .page-header-title {
    min-width: 0;
    max-width: 100%;
    padding: 0;
    border: 0;
    background: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: inherit;
    font-size: 18px;
    font-weight: 700;
    line-height: 25.2px;
    letter-spacing: 0;
    color: var(--label-primary);
  }

  /* Pinned above a page's own scroll (Settings' subpages): sticky inside
     the scroller, on the page's colour, so nothing shows through. */
  .nav-bar--sticky {
    position: sticky;
    top: 0;
    background: var(--canvas-bg);
  }
}

/* Phone tab row + root pages (iOS RootTabBar / RootHeader). */
/* The phone tab row (ui/tab_bar.gleam) — iOS RootTabBarRow + RootTabBar +
   CaptureFABButton, parity round 1 (2026-10-23). Phone root pages only:
   hidden unless `.app-shell--root` at ≤834px.

   iOS numbers (RootTabBar.swift, RootTabBarRow.swift, iPhone 16 393pt):
   row = 8 sides, 12 above and below, [capsule | 8 | + 56]; capsule 62 tall,
   6/4 padding, four equal 54-tall slots ((barWidth − 12) / 4 = 75.25);
   glyphs 32; pill Fill.Elevated.secondary, one slot wide. Chrome is iOS's
   below-26 glass: material + 1pt inside hairline + 0 4 8 shadow. */

.tab-row {
  display: none;
}

@media (max-width: 834px) {
  .app-shell--root .tab-row {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    isolation: isolate;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 8px calc(12px + env(safe-area-inset-bottom));
    /* iOS ToolbarMotion.reveal: spring(.34, .72). */
    transition:
      transform 340ms cubic-bezier(0.34, 1.3, 0.64, 1),
      opacity 340ms ease;
  }

  /* iOS glassBarScrim(contentTop: 12) = softScrim(solidInset 29, blur 8) —
     scrim-tuner (b): canvas, solid from 29px under the row's top edge to
     the screen bottom, its top edge blurred, so content stays sharp right
     down to the glass. */
  .app-shell--root .tab-row::before {
    content: "";
    position: absolute;
    z-index: -1;
    top: 29px;
    left: -24px;
    right: -24px;
    bottom: -24px;
    background: var(--canvas-bg);
    filter: blur(8px);
    pointer-events: none;
  }

  /* Hidden while the page's search field has focus (iOS RootTabBarPresence),
     never on scroll. */
  .app-shell:has(.search-input:focus) .tab-row {
    transform: translateY(100%);
    opacity: 0;
    pointer-events: none;
  }

  .tab-bar {
    position: relative;
    flex: 1;
    min-width: 0;
    height: 62px;
    box-sizing: border-box;
    padding: 4px 6px;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    border-radius: 999px;
    background: var(--glass-fill);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    box-shadow:
      inset 0 0 0 1px var(--border-secondary),
      0 4px 8px rgba(0, 0, 0, 0.1);
    touch-action: none;
    -webkit-user-select: none;
    user-select: none;
  }

  /* The selection pill: one slot wide, under the selected glyph. Its flight
     between slots (two edge springs) is ffi/tab_bar.mjs; this transition is
     the no-JS fallback. */
  .tab-bar-pill {
    position: absolute;
    top: 4px;
    height: 54px;
    left: calc(6px + var(--tab-index, 0) * (100% - 12px) / 4);
    width: calc((100% - 12px) / 4);
    border-radius: 999px;
    background: var(--fill-elevated-secondary);
    transition: left 300ms cubic-bezier(0.3, 1.25, 0.5, 1);
    pointer-events: none;
  }

  /* Under ffi/tab_bar.mjs the pill's edges are its two springs. */
  .tab-bar[data-motion] .tab-bar-pill {
    left: var(--pill-l);
    width: calc(var(--pill-r) - var(--pill-l));
    opacity: var(--pill-opacity, 1);
    transition: none;
  }

  .tab-bar[data-motion] {
    transform-origin: center;
  }

  /* The lens (iOS TabLens): glass + 18% white, a 1pt rim brightest at the
     top and bottom, and a soft shadow; it rides the pill while moving and
     overhangs the bar when pressed. Glyph magnification is left out. */
  .tab-bar-lens {
    position: absolute;
    opacity: 0;
    border: 1px solid transparent;
    border-radius: 999px;
    box-sizing: border-box;
    background:
      linear-gradient(rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.18))
        padding-box,
      linear-gradient(var(--glass-fill), var(--glass-fill)) padding-box,
      linear-gradient(
          rgba(255, 255, 255, 0.9),
          rgba(255, 255, 255, 0.15),
          rgba(255, 255, 255, 0.6)
        )
        border-box;
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
    pointer-events: none;
  }

  /* The lens's glyph rows (iOS RootTabLensLayer): laid over the slots and
     shown only while ffi/tab_bar.mjs marks the lens live, when the slots'
     own glyphs step aside (their badges stay). The hollow row is cut where
     the lens is (`--lens-hole`, the lens inset 0.5px so the two edges
     overlap instead of leaving a seam); the filled row is windowed to it
     (`--lens-window`). iOS also magnifies under the lens; web doesn't. */
  .tab-bar-glyphs {
    position: absolute;
    inset: 0;
    z-index: 1;
    box-sizing: border-box;
    padding: 4px 6px;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    color: var(--label-primary);
    pointer-events: none;
    visibility: hidden;
  }

  .tab-bar-glyph-cell {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 54px;
  }

  .tab-bar[data-lens-live] .tab-bar-glyphs {
    visibility: visible;
  }

  .tab-bar-glyphs--hollow {
    clip-path: var(--lens-hole, none);
  }

  .tab-bar-glyphs--filled {
    clip-path: var(--lens-window, inset(0 100% 0 0));
  }

  .tab-glyph {
    display: inline-flex;
  }

  .tab-bar[data-lens-live] .tab-bar-slot .tab-glyph {
    visibility: hidden;
  }

  .tab-bar-slot {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 54px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--label-primary);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }

  .tab-badge-anchor {
    position: relative;
    display: inline-flex;
  }

  /* iOS CountBadge: 18 tall, min 18 wide, 5 a side, 11/600 tabular digits;
     pinned 8 right and 8 up of the glyph's top-right corner. brandRed with
     a white label until the viewer picks an accent. iOS cuts its gap out of
     the glyph on glass; here a 1.5px ring stands in. */
  .tab-badge {
    position: absolute;
    top: -8px;
    right: -8px;
    min-width: 18px;
    height: 18px;
    box-sizing: border-box;
    padding: 0 5px;
    border-radius: 9px;
    background: var(--count-badge-fill);
    color: var(--count-badge-label);
    font-size: 11px;
    font-weight: 600;
    line-height: 18px;
    font-variant-numeric: tabular-nums;
    text-align: center;
    box-shadow: 0 0 0 1.5px var(--bg-primary);
  }

  :root[data-accent] .tab-badge {
    background:
      linear-gradient(var(--accent-blue-elevated), var(--accent-blue-elevated)),
      var(--bg-primary);
    color: var(--accent-blue);
  }

  .tab-badge--dot {
    top: -1px;
    right: -1px;
    min-width: 6px;
    width: 6px;
    height: 6px;
    padding: 0;
    border-radius: 50%;
    background: var(--count-badge-fill);
    box-shadow: 0 0 0 1px var(--bg-primary);
  }

  :root[data-accent] .tab-badge--dot {
    background: var(--accent-blue);
  }

  /* The + (iOS CaptureFABButton below iOS 26): a solid accent disc with
     the inverse label, the toolbar shadow, and the 1.2× press. */
  .tab-plus {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--accent-blue);
    color: var(--label-inverse-primary);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    cursor: pointer;
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
    -webkit-tap-highlight-color: transparent;
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }

  .tab-plus:active {
    transform: scale(1.2);
  }
}

@media (max-width: 834px) and (prefers-reduced-motion: reduce) {
  .app-shell--root .tab-row,
  .tab-bar-pill,
  .tab-plus {
    transition: none;
  }
}

/* The + button's hold menu (iOS PlusMenu on AttachmentMenuCard): a card
   anchored 8 from the right and 80 above the bottom safe edge, radius 20,
   12/16 padding; rows an 18 icon, 12 gap, body 18/25.2, 10 above and
   below; 1px dividers centred in 22. A clear catcher behind it closes it;
   the + stays above the catcher. Scales in from .9 at the bottom-right on
   spring(.25, .7). */
.plus-menu-layer {
  display: none;
}

@media (max-width: 834px) {
  .plus-menu-layer {
    display: contents;
  }

  .plus-menu-catcher {
    position: fixed;
    inset: 0;
    z-index: 39;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: default;
  }

  .plus-menu {
    position: fixed;
    right: 8px;
    bottom: calc(env(safe-area-inset-bottom) + 80px);
    z-index: 41;
    display: flex;
    flex-direction: column;
    padding: 12px 16px;
    border-radius: 20px;
    background: var(--bg-primary);
    box-shadow:
      inset 0 0 0 1px var(--border-secondary),
      0 4px 8px rgba(0, 0, 0, 0.1);
    transform-origin: bottom right;
    animation: plus-menu-in 260ms cubic-bezier(0.3, 1.25, 0.5, 1);
  }

  .plus-menu-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 0;
    border: 0;
    background: none;
    color: var(--label-primary);
    font-family: inherit;
    font-size: 18px;
    font-weight: 400;
    line-height: 25.2px;
    text-align: left;
    white-space: nowrap;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  .plus-menu-row:active {
    transform: scale(1.2);
  }

  .plus-menu-divider {
    position: relative;
    height: 22px;
  }

  .plus-menu-divider::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 1px;
    background: var(--border-secondary);
  }
}

@keyframes plus-menu-in {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
}

@media (max-width: 834px) and (prefers-reduced-motion: reduce) {
  .plus-menu {
    animation: plus-menu-fade 160ms ease;
  }

  .plus-menu-row {
    transition: none;
  }
}

@keyframes plus-menu-fade {
  from {
    opacity: 0;
  }
}

/* The card popover (round 3a, 2026-10-24; ui/card_menu.gleam): the + menu's
   card at every width — radius 20, 12/16 padding, rows an 18 icon, 12 gap,
   body 18/25.2, 10 above and below, 1px dividers centred in 22 — placed at
   the pointer by ffi/card_menu.mjs (`--card-menu-left` / `-top`, flipped
   inside the window) and only shown once placed. Scales in from .9 out of
   the corner nearest its anchor. A clear catcher behind it closes it. */
.card-menu-catcher {
  position: fixed;
  inset: 0;
  z-index: 100;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: default;
}

.card-menu {
  position: fixed;
  left: var(--card-menu-left, 0px);
  top: var(--card-menu-top, 0px);
  z-index: 101;
  display: flex;
  flex-direction: column;
  padding: 12px 16px;
  border-radius: 20px;
  background: var(--bg-primary);
  box-shadow:
    inset 0 0 0 1px var(--border-secondary),
    0 4px 8px rgba(0, 0, 0, 0.1);
  transform-origin: var(--card-menu-origin, 0px 0px);
  animation: card-menu-in 260ms cubic-bezier(0.3, 1.25, 0.5, 1);
  outline: none;
}

.card-menu:not([data-placed]) {
  visibility: hidden;
  animation: none;
}

.card-menu-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  border: 0;
  background: none;
  color: var(--label-primary);
  font-family: inherit;
  font-size: 18px;
  font-weight: 400;
  line-height: 25.2px;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.card-menu-row--danger {
  color: var(--accent-red);
}

.card-menu-row:active {
  transform: scale(1.2);
}

.card-menu-row:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
  border-radius: 8px;
}

.card-menu-divider {
  position: relative;
  height: 22px;
}

.card-menu-divider::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: var(--border-secondary);
}

@keyframes card-menu-in {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
}

@media (prefers-reduced-motion: reduce) {
  .card-menu {
    animation: card-menu-fade 160ms ease;
  }

  .card-menu-row {
    transition: none;
  }
}

@keyframes card-menu-fade {
  from {
    opacity: 0;
  }
}

/* The phone shell's root pages (iOS parity round 1, 2026-10-23): one root
   header over Home, Search, Messages and Profile (iOS RootHeader), each
   page scrolling under it and clearing the tab row, on the canvas.

   iOS numbers (AppHeaderLayout, RootHeader, EphemeralHeaderGlow): header
   row 44 under the status bar; header → content 10, then each page's own
   top inset (Home 12); the root header's scrim is the canvas, solid to its
   bottom edge with a 16 gaussian tail. */

.root-header,
.feed-glow {
  display: none;
}

@media (max-width: 834px) {
  .app-shell--root .root-header {
    display: block;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 30;
    isolation: isolate;
  }

  /* iOS softScrim(solidInset: 0, blur 16) — scrim-tuner (a). */
  .app-shell--root .root-header::before {
    content: "";
    position: absolute;
    z-index: -2;
    top: -48px;
    left: -48px;
    right: -48px;
    bottom: 0;
    background: var(--canvas-bg);
    filter: blur(16px);
    pointer-events: none;
  }

  /* Home's breathing glow (iOS HomeHeaderGlowEllipse, 2026-10-14): a
     full-width 170-tall ellipse, its frame top 165 above the header's top
     edge, blurred 16; it rises 24 and back, 3.6s each way. Two copies, as
     on iOS: this one in the header's scrim, `.feed-glow` behind the list. */
  .root-header-glow,
  .app-shell--home-glow .feed-glow {
    position: absolute;
    z-index: -1;
    left: 0;
    right: 0;
    top: calc(env(safe-area-inset-top) - 165px);
    height: 170px;
    border-radius: 50%;
    background: radial-gradient(
      ellipse closest-side,
      var(--home-glow-green) 0%,
      var(--home-glow-blue) 51%,
      var(--home-glow-pink) 100%
    );
    filter: blur(16px);
    pointer-events: none;
  }

  .root-header-glow {
    opacity: 0;
    transition: opacity 180ms cubic-bezier(0.42, 0, 0.58, 1);
  }

  .app-shell--home-glow .root-header-glow {
    opacity: var(--home-glow-opacity);
  }

  .app-shell--home-glow .feed-glow {
    display: block;
    position: fixed;
    z-index: 0;
    opacity: var(--home-glow-opacity);
  }

  .glow-breathe {
    animation: glow-breathe 3.6s cubic-bezier(0.37, 0, 0.63, 1) infinite
      alternate;
  }

  /* Each root page fades in under the header (iOS tabSwitch: easeInOut
     .18), and so does a new title. */
  .app-shell--root .feed-page,
  .app-shell--root .search-page,
  .app-shell--root .messages-page,
  .app-shell--root .settings-canvas,
  .root-header-title {
    animation: phone-tab-in 180ms cubic-bezier(0.42, 0, 0.58, 1);
  }

  .root-header-title {
    display: flex;
    justify-content: center;
    min-width: 0;
  }

  /* Leaving a collection: the remounted title fades back in on the exit
     curve (search.css's grid does too). */
  .root-header-title--enter > * {
    animation: search-exit-fade 140ms cubic-bezier(0.38, 0.7, 0.125, 1);
  }

  /* A page without a header of its own (share, join, email, not found)
     gets the shell's fallback back header above it: the pane stacks them,
     so the page keeps the rest of the height and its bottom stays in
     reach. */
  .main-pane:has(> .nav-bar) {
    display: flex;
    flex-direction: column;
  }

  .main-pane:has(> .nav-bar) > :not(.nav-bar) {
    flex: 1 1 0;
    min-height: 0;
    height: auto;
  }

  /* The pages' own headers give way to the root header. */
  .app-shell--root .feed-page > .page-header,
  .app-shell--root .search-page > .page-header,
  .app-shell--root .settings-header {
    display: none;
  }

  .app-shell--root .main-pane,
  .app-shell--root .feed-page,
  .app-shell--root .search-page,
  .app-shell--root .settings-canvas {
    background: var(--canvas-bg);
  }

  /* Scroll under the header, clear of the tab row. The scroller owns the
     header offset, so the page's own top padding goes. */
  .app-shell--root .feed {
    padding-top: 0;
    padding-bottom: 0;
  }

  .app-shell--root .feed-items-wrap {
    position: relative;
    z-index: 1;
    padding-top: calc(env(safe-area-inset-top) + 44px + 10px + 12px);
    padding-bottom: calc(86px + env(safe-area-inset-bottom) + 16px);
  }

  /* Search scrolls as one piece (iOS round 2): the field rides up with
     the grid or the list and slides under the header's scrim. `.search`
     is the scroller, full height behind the header; the body stops
     scrolling on its own. */
  .app-shell--root .search {
    min-height: 0;
    overflow-y: auto;
    padding-top: calc(env(safe-area-inset-top) + 44px + 10px);
    padding-bottom: calc(86px + env(safe-area-inset-bottom) + 16px);
    scrollbar-width: none;
  }

  .app-shell--root .search::-webkit-scrollbar {
    display: none;
  }

  .app-shell--root .search-body {
    flex: none;
    overflow: visible;
    padding-bottom: 0;
  }

  /* The field row arrives sliding down from the top with a fade, on the
     tab curve (iOS TimelineListContainerView). */
  .app-shell--root .search-field-row {
    animation: search-field-in 180ms cubic-bezier(0.42, 0, 0.58, 1);
  }

  .app-shell--root .settings-canvas {
    padding-top: calc(env(safe-area-inset-top) + 44px + 10px);
    padding-bottom: calc(86px + env(safe-area-inset-bottom) + 16px);
  }

  /* The Messages page (chat.view_messages_page): the list under the root
     header (content top 4, AppHeaderLayout.chatList), or the requests
     inbox under its own back header. */
  .messages-page {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
    background: var(--canvas-bg);
  }

  .messages-page-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .app-shell--root .messages-page-scroll {
    padding-top: calc(env(safe-area-inset-top) + 44px + 10px + 4px);
    padding-bottom: calc(86px + env(safe-area-inset-bottom) + 16px);
  }

  /* The header carries compose and the back; the list's own go. */
  .messages-page .chat-list-compose,
  .messages-page .chat-requests-header {
    display: none;
  }

}

@keyframes glow-breathe {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-24px);
  }
}

@keyframes phone-tab-in {
  from {
    opacity: 0;
  }
}

@keyframes search-field-in {
  from {
    opacity: 0;
    transform: translateY(-12px);
  }
}

@media (max-width: 834px) and (prefers-reduced-motion: reduce) {
  .root-header-title--enter > *,
  .app-shell--root .search-field-row,
  .glow-breathe,
  .app-shell--root .feed-page,
  .app-shell--root .search-page,
  .app-shell--root .messages-page,
  .app-shell--root .settings-canvas,
  .root-header-title {
    animation: none;
  }
}

/* Phone AI drawer on the toDo detail (spec §4a) — app/detail/ai_drawer. */
/* The phone AI drawer (spec §4a) — app/detail/ai_drawer. Every detail page
   an editor opens mounts it; only phones (≤834px) show it.

   A layer fixed to the top of the visual viewport holds a floating card.
   The card rests docked as a pill around the composer, and the controller
   (ffi/chat_drawer.mjs) moves it between docked, half and full. CSS alone
   draws the rest state, docked on a square screen; the controller only adds
   to it, through custom properties and data-* attributes (it never writes
   `style` or `class`). The custom properties inherit, so it writes them on
   the root (`.chat-drawer`) and the card and the scrim beside it read them.

   On the root, from the controller:
     --drawer-h         the card's height (the composer row sets it when unset)
     --drawer-inset     its side inset: 8 docked, 4 half, 0 full
     --drawer-gap       its gap above the layer's bottom: the docked gap, 4, 0
     --drawer-br        its bottom radius: 44, the half radius, 0
     --drawer-hairline  its hairline's strength, 0–1
     --drawer-travel    (h − docked) / (full − docked)
     --drawer-stage     0 docked, 1 half, 2 full
     --drawer-x         the ✕'s opacity, from its room above the row
     --plane-h          the thread plane's height
     --viewport-h       the thread scroller's height (the plane's, keyboard down)
     --viewport-shift   the scroller's drop below the plane's top
     --thread-spacer    the tail spacer's height (the pin)
     --thread-margin    the bottom margin under the thread (half, keyboard)
     data-stop          the resting stop: docked | half | full
     data-settling      while a settle runs
     data-barrier="on"  full's tap-to-dock barrier takes taps
     data-x             "hidden" when the ✕ has faded out, else "shown"
     data-jump="on"     the ↓ pill shows
     data-light         "docked" while the drag's zone counts as docked
     data-glow          "hidden" while the glow's reveal is 0 (no finger
                        down): its loops pause; else "shown"
     data-glow-cadence  idle | working | trough (the ease between the two)
     --drawer-glow-from the glow's breath as the trough starts
   On the composer field, from the controller: data-overflowing past its
   12-line cap (it scrolls by touch then).
   From Gleam: data-signal="on" (unread or working), data-working, data-away
   (with inert and aria-hidden). On <html>: --vvh, --drawer-docked-gap and
   --drawer-half-radius (ffi/dom.mjs), and .kb-open with the keyboard up. */

/* Registered numbers, so they animate on their own and multiply into one
   opacity: the grabber's light (a cross-fade) with its breath, and the
   glow's breath with its reveal. */
@property --chat-drawer-lit {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

@property --chat-drawer-breath {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

@property --chat-drawer-glow-breath {
  syntax: "<number>";
  inherits: false;
  initial-value: 0.9;
}

@media (max-width: 834px) {
  /* ── The layer (§4a.1) ─────────────────────────────────────────────
     Fixed to the top, as tall as the visual viewport: its bottom is the
     glass, or the keyboard's top. Above the page and its nav bar (20),
     under every sheet (401), menu, toast and the lightbox. Only the card
     and the barrier (full's, and at half its strip above the lip) take
     touches. */
  .chat-drawer {
    position: fixed;
    top: 0;
    inset-inline: 0;
    height: var(--vvh, 100dvh);
    z-index: 25;
    pointer-events: none;
  }

  /* Full's barrier over the page behind the card: it paints nothing and
     takes taps only while the controller says so, never while away. */
  .chat-drawer-barrier {
    position: absolute;
    inset: 0;
  }

  .chat-drawer[data-barrier="on"]:not([data-away]) .chat-drawer-barrier {
    pointer-events: auto;
  }

  /* At half the 16 above the lip are the drawer's (§4a.4, §4a.6): the
     barrier shrinks to that strip over the card's top edge, so a tap there
     never reaches the page under it (the controller docks the drawer) and
     a drag from it moves the card. Anything drawn over the layer still
     wins the strip's touches. */
  .chat-drawer[data-stop="half"]:not([data-away]) .chat-drawer-barrier {
    top: auto;
    left: var(--drawer-inset, 8px);
    right: var(--drawer-inset, 8px);
    bottom: calc(
      var(--drawer-h, 88px) + var(--drawer-gap, var(--drawer-docked-gap, 22px))
    );
    height: 16px;
    pointer-events: auto;
    touch-action: none;
  }

  /* The page scrim (§4a.6): the page's own colour from 17 under the card's
     top to the layer's bottom, its top edge blurred (the tab row's recipe),
     so the notes end at the pill instead of running behind it. Gone by
     half. */
  .chat-drawer-scrim {
    position: absolute;
    top: calc(
      100% - var(--drawer-h, 88px) -
        var(--drawer-gap, var(--drawer-docked-gap, 22px)) + 17px
    );
    left: -24px;
    right: -24px;
    bottom: -24px;
    background: var(--detail-page-bg, var(--bg-body));
    filter: blur(8px);
    opacity: calc(1 - var(--drawer-stage, 0));
  }

  /* ── The card (§4a.2, §4a.7) ───────────────────────────────────────
     A clip window over the thread's plane, laid out from its bottom: the
     composer row 16 above the card's bottom (plus the spacer that takes up
     what the shrinking gap gives back), the plane 4 above the row, running
     up past the card's top. Unmeasured, it is the docked card: 12 grabber
     slot + 4 + the row + 16.

     The clip is a clip-path, not overflow: it bounds touches too, and its
     bottom reaches 120 past the card so the seam cover can hang there. */
  .chat-drawer-card {
    position: absolute;
    left: var(--drawer-inset, 8px);
    right: var(--drawer-inset, 8px);
    bottom: var(--drawer-gap, var(--drawer-docked-gap, 22px));
    height: var(--drawer-h, auto);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding: 12px 0
      calc(
        16px + var(--drawer-docked-gap, 22px) -
          var(--drawer-gap, var(--drawer-docked-gap, 22px))
      );
    border-radius: 44px 44px var(--drawer-br, 44px) var(--drawer-br, 44px);
    background: color-mix(in srgb, var(--bg-elevated-secondary) 85%, transparent);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    clip-path: inset(0 0 -120px 0 round 44px 44px 0 0);
    pointer-events: auto;
  }

  /* Docked, the whole card drags (§4a.4); its buttons and field still take
     taps. */
  .chat-drawer:not([data-stop="half"]):not([data-stop="full"]) .chat-drawer-card {
    touch-action: none;
  }

  /* The hairline: 0.5 inside, whole until half, fading to full. */
  .chat-drawer-card::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 4;
    border-radius: inherit;
    box-shadow: inset 0 0 0 0.5px var(--ui-borders);
    opacity: var(--drawer-hairline, 1);
    pointer-events: none;
  }

  /* The seam cover: the surface colour hanging under the card, which only
     matters while the keyboard moves under a full-bleed card. */
  .chat-drawer-seam {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    height: 120px;
    background: var(--bg-elevated-secondary);
    opacity: 0;
    pointer-events: none;
  }

  /* ── The grabber (§4a.8, §4a.16) ───────────────────────────────────
     The card's top band, a button: 16 deep docked (a tap opens it), 42
     raised (a tap docks it, a drag moves it). The light is two capsules
     drawn in its top 12px slot, 4 from the top: idle grey, lit pastel,
     cross-fading. It lights while docked with an unread reply or the agent
     working: Gleam's signal and the controller's zone, both. */
  .chat-drawer-grabber {
    position: absolute;
    z-index: 2;
    top: 0;
    left: 0;
    right: 0;
    height: 16px;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    touch-action: none;
    -webkit-tap-highlight-color: transparent;
  }

  .chat-drawer:is([data-stop="half"], [data-stop="full"]) .chat-drawer-grabber {
    height: 42px;
  }

  /* Keyboard focus in the label colour: the page's own ring
     (`--bg-secondary`) is the card's colour in light and all but that in
     dark, and nearly the glass discs' fill. */
  .chat-drawer-grabber:focus-visible,
  .chat-drawer-close:focus-visible,
  .chat-drawer-dismiss:focus-visible,
  .chat-drawer-jump:focus-visible {
    outline-color: var(--label-primary);
  }

  /* Inside the band, where the card's clip can't cut it. */
  .chat-drawer-grabber:focus-visible {
    outline-offset: -4px;
  }

  .chat-drawer-grabber::before,
  .chat-drawer-grabber::after {
    content: "";
    position: absolute;
    top: 4px;
    left: 0;
    right: 0;
    width: 60px;
    height: 4px;
    margin: 0 auto;
    border-radius: 2px;
    /* iOS: the width on spring(0.45, 0.82), the light on 0.3s ease-in-out. */
    transition:
      width 450ms cubic-bezier(0.3, 1.15, 0.5, 1),
      --chat-drawer-lit 300ms ease-in-out;
  }

  .chat-drawer-grabber::before {
    background: rgb(191 191 191);
    mix-blend-mode: exclusion;
    opacity: calc(1 - var(--chat-drawer-lit));
  }

  .chat-drawer-grabber::after {
    background: var(--gradient-pastel-horizontal);
    opacity: calc(var(--chat-drawer-lit) * var(--chat-drawer-breath));
  }

  /* The light's loops run only while it is lit, and hold while away. The
     hold names the lit rule's attributes too: the `animation` shorthand
     resets the play state, so a less specific hold never applies. */
  .chat-drawer[data-signal="on"][data-light="docked"] .chat-drawer-grabber::after {
    animation:
      chat-drawer-breathe 2.6s ease-in-out infinite alternate,
      chat-drawer-hue 14s linear infinite;
  }

  .chat-drawer[data-signal="on"][data-light="docked"][data-away] .chat-drawer-grabber::after {
    animation-play-state: paused;
  }

  .chat-drawer[data-signal="on"][data-light="docked"] .chat-drawer-grabber::before,
  .chat-drawer[data-signal="on"][data-light="docked"] .chat-drawer-grabber::after {
    width: 72px;
    --chat-drawer-lit: 1;
  }

  /* ── The top glow (§4a.8) ──────────────────────────────────────────
     Raised only: a pastel ellipse sized from the screen, so the card's
     inset doesn't change it, clipped by the card's top radius. Its lower
     40 shows, 56 by full; it fades in from 15% to 35% of the travel and
     breathes 0.6 ↔ 0.9 (3.6s a half-cycle, 1.6s while the agent works),
     turning its hue once every 14s. At rest docked it holds still. */
  .chat-drawer-glow {
    position: absolute;
    top: calc(16px * var(--drawer-travel, 0) - 130px);
    left: 50%;
    width: calc(100vw * 733 / 402);
    height: 170px;
    transform: translateX(-50%);
    border-radius: 50%;
    background: var(--gradient-pastel-horizontal);
    filter: blur(16px);
    opacity: calc(
      clamp(0, (var(--drawer-travel, 0) - 0.15) * 5, 1) *
        var(--chat-drawer-glow-breath)
    );
    animation:
      chat-drawer-glow-breathe 3.6s ease-in-out infinite alternate,
      chat-drawer-glow-hue 14s linear infinite;
    pointer-events: none;
  }

  .chat-drawer[data-glow-cadence="working"] .chat-drawer-glow {
    animation-duration: 1.6s, 14s;
  }

  /* Between cadences the controller holds this for 0.4s: the breath eases
     from where it was (`--drawer-glow-from`, which the controller reads off
     the glow as it switches) to its trough, and the new loop starts there.
     A transition can't do it: removing an animation never transitions from
     its value. */
  .chat-drawer[data-glow-cadence="trough"] .chat-drawer-glow {
    animation:
      chat-drawer-glow-trough 0.4s ease-in-out forwards,
      chat-drawer-glow-hue 14s linear infinite;
  }

  /* The loops hold while the glow doesn't show: its reveal at 0 (the
     controller's data-glow, never under a finger; before the controller
     runs, the docked rest) and away. */
  .chat-drawer:not([data-glow="shown"]) .chat-drawer-glow,
  .chat-drawer[data-away] .chat-drawer-glow {
    animation-play-state: paused;
  }

  /* ── The ✕ (§4a.7) ─────────────────────────────────────────────────
     A 44 glass disc 16 from the card's top and trailing edges (the nav
     bar's disc). Its opacity follows its room above the composer row; once
     it has faded out it leaves the tab order and the accessibility tree.
     Absent at rest docked. It sits in the raised top band, so a drag from
     it moves the card. */
  .chat-drawer-close {
    position: absolute;
    z-index: 3;
    top: 16px;
    right: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--glass-fill);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    box-shadow:
      inset 0 0 0 1px var(--border-secondary),
      0 4px 8px rgba(0, 0, 0, 0.1);
    color: var(--label-primary);
    cursor: pointer;
    opacity: var(--drawer-x, 0);
    visibility: hidden;
    touch-action: none;
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
    -webkit-tap-highlight-color: transparent;
  }

  .chat-drawer[data-x]:not([data-x="hidden"]) .chat-drawer-close {
    visibility: visible;
  }

  .chat-drawer-close:active {
    transform: scale(1.2);
  }

  /* ── The thread (§4a.2, §4a.9, §4a.10) ─────────────────────────────
     The plane: layer-wide at every stop (its margins undo the card's
     inset), 4 above the composer row, its top at full's top edge. It clips
     what overflows it, touches included. Unmeasured it is 0 tall: docked,
     the thread draws nothing anyway. */
  .chat-drawer-plane {
    position: relative;
    flex: none;
    height: var(--plane-h, 0px);
    margin: 0 calc(-1 * var(--drawer-inset, 8px)) 4px;
    overflow: clip;
  }

  /* The scroller, the thread's viewport: its own size whatever the card
     does, dropped to the resting lip by the controller, so a stop change
     never moves a row. The edge fades are a mask on it: clear → opaque
     over 64 at the top, opaque → clear over 24 at the bottom. Whole by 10%
     of the travel. */
  .chat-drawer-thread {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: var(--viewport-h, 100%);
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
    transform: translateY(var(--viewport-shift, 0px));
    opacity: calc(var(--drawer-travel, 0) * 10);
    -webkit-mask-image: linear-gradient(
      to bottom,
      transparent,
      black 64px,
      black calc(100% - 24px),
      transparent
    );
    mask-image: linear-gradient(
      to bottom,
      transparent,
      black 64px,
      black calc(100% - 24px),
      transparent
    );
  }

  .chat-drawer-thread::-webkit-scrollbar {
    display: none;
  }

  /* 64 under the scroller's top (the fade, the pin's clearance), then the
     thread's own 16; 32 from the screen's edges, whatever the card's
     inset. A flex column, so the empty status slot's gaps never collapse
     into the next. */
  .chat-drawer-content {
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    min-height: 100%;
    padding: calc(64px + 16px) 32px calc(16px + var(--thread-margin, 0px));
  }

  /* Each row pays its own gap above (`--gap`, ai_thread_spacing); inside a
     reply the parts are 16 apart: media, toDo cards, rich cards, text. */
  .chat-drawer-msg {
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-width: 0;
    margin-top: var(--gap, 0px);
  }

  /* Prompts trail; their time label sits 8 above what follows. */
  .chat-drawer-msg[data-role="own"] {
    align-items: flex-end;
  }

  .chat-drawer-msg[data-role="member"] {
    align-items: flex-start;
  }

  .chat-drawer-time {
    margin-bottom: -8px;
    font-size: 12px;
    line-height: 16px;
    color: var(--label-primary);
  }

  /* A failed send: "Failed • Try again" in place of the time. */
  .chat-drawer-time--failed {
    font-size: 11px;
  }

  .chat-drawer-send-failed {
    color: var(--accent-red);
  }

  .chat-drawer-time-dot {
    white-space: pre;
    color: var(--label-secondary);
  }

  .chat-drawer-try-again {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    font-weight: 600;
    color: var(--accent-blue);
    cursor: pointer;
  }

  /* The viewer's bubble: 12/16, 16 on every corner, at least 60 clear on
     its leading side. The stock accent fills it with white text (no label
     token is white in both themes); a chosen accent takes its tonal fill
     alone, which composites over the card's surface (iOS's light
     grey-lavender for indigo in dark, not a wash over the page's black). */
  .chat-drawer-bubble {
    box-sizing: border-box;
    max-width: calc(100% - 60px);
    min-width: 0;
    padding: 12px 16px;
    border-radius: 16px;
    background: var(--accent-blue);
    color: #fff;
  }

  :root[data-accent] .chat-drawer-bubble {
    background: var(--accent-blue-elevated);
    color: var(--accent-blue);
  }

  /* Another member's prompt leads, on a plain fill. */
  .chat-drawer-msg[data-role="member"] .chat-drawer-bubble {
    background: var(--fill-elevated-secondary);
    color: var(--label-primary);
  }

  .chat-drawer-bubble[role="button"] {
    cursor: pointer;
  }

  /* Markdown at 18/25.2. Blank lines are block margins: 16 between blocks,
     8 after a heading (the blank line under it collapses) and after a
     lead-in. Every heading level is one 22 bold. */
  .chat-drawer-text {
    min-width: 0;
    font-size: 18px;
    line-height: 25.2px;
    color: var(--label-primary);
    overflow-wrap: anywhere;
  }

  .chat-drawer-bubble .chat-drawer-text {
    color: inherit;
  }

  .chat-drawer-text > * {
    margin: 0;
  }

  .chat-drawer-text > * + * {
    margin-top: 16px;
  }

  .chat-drawer-text :is(h1, h2, h3, h4, h5, h6) {
    font-size: 22px;
    font-weight: 700;
    line-height: 27.5px;
  }

  .chat-drawer-text > :is(h1, h2, h3, h4, h5, h6, .md-lead-in) + * {
    margin-top: 8px;
  }

  .chat-drawer-text :is(ul, ol) {
    padding-left: 24px;
  }

  .chat-drawer-text li + li,
  .chat-drawer-text li > :is(ul, ol) {
    margin-top: 4px;
  }

  .chat-drawer-text li > p {
    margin: 0;
  }

  .chat-drawer-text a {
    color: var(--accent-blue);
  }

  /* Nothing in a reply runs past the thread's width. */
  .chat-drawer-text pre {
    white-space: pre-wrap;
  }

  .chat-drawer-text img {
    max-width: 100%;
  }

  /* None renders today (a table reads as its text, as on iOS); one that
     did would scroll inside its own box. */
  .chat-drawer-text table {
    display: block;
    max-width: 100%;
    overflow-x: auto;
  }

  .chat-drawer-bubble .chat-drawer-text a {
    color: inherit;
    text-decoration: underline;
  }

  /* Embedded toDo cards and rich cards: full width for either sender, the
     toDo cards on bg-primary (Julian, 2026-10-26; the sent-bubble token
     went with the restyle). */
  .chat-drawer-todos,
  .chat-drawer-rich {
    display: flex;
    flex-direction: column;
    gap: 16px;
    align-self: stretch;
  }

  .chat-drawer-todos .tl-row {
    background: var(--bg-primary);
  }

  /* An agent reply's media leads it (iOS ChatAttachmentView): one piece is
     a 260 square, more a two-column grid of 100-tall cells 2 apart, 220
     wide. Thumbnails crop to fill on 12 corners; a fill holds the box until
     the image lands. A video wears a play badge. */
  .chat-drawer-media {
    align-self: flex-start;
  }

  .chat-drawer-media--single {
    width: min(260px, 100%);
  }

  .chat-drawer-media--grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2px;
    width: min(220px, 100%);
    border-radius: 12px;
    overflow: hidden;
  }

  .chat-drawer-media-item {
    position: relative;
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    border-radius: 12px;
    background: var(--fill-tertiary);
    cursor: zoom-in;
  }

  .chat-drawer-media--single .chat-drawer-media-item {
    aspect-ratio: 1;
  }

  .chat-drawer-media--grid .chat-drawer-media-item {
    height: 100px;
  }

  .chat-drawer-media-item[data-media="video"] {
    cursor: pointer;
  }

  .chat-drawer-media-thumb {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 12px;
    object-fit: cover;
  }

  .chat-drawer-media-play {
    position: absolute;
    top: 50%;
    left: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    transform: translate(-50%, -50%);
    pointer-events: none;
  }

  /* The suggestions: 16 under the row before them, 4 apart (Messages keeps
     16), 18 semibold labels, 32 corners, so a one-line pill is a capsule.
     Their tonal fill composites over the card's surface, as the viewer's
     bubble does, not over an opaque page colour. */
  .chat-drawer-pills {
    margin-top: var(--gap, 16px);
  }

  .chat-drawer-pills .suggestion-pills {
    gap: 4px;
    padding-top: 0;
  }

  .chat-drawer-pills .suggestion-pill {
    border-radius: 32px;
    background: var(--accent-blue-elevated);
  }

  .chat-drawer-pills .suggestion-pill-emoji,
  .chat-drawer-pills .suggestion-pill-label {
    font-size: 18px;
    line-height: 25.2px;
  }

  /* The tail: the status slot 48 under the last row (0 tall when idle),
     24, the end anchor, 24, the tail spacer, then the 16 padding. */
  .chat-drawer-status-slot {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    margin-top: 48px;
  }

  .chat-drawer-status-slot .agent-status-line--drawer {
    padding: 0;
  }

  .chat-drawer-sparkle {
    display: flex;
    flex: none;
  }

  .chat-drawer-sparkle .icon {
    background: var(--gradient-pastel);
  }

  .chat-drawer-end {
    flex: none;
    height: 1px;
    margin-top: 24px;
  }

  .chat-drawer-spacer {
    flex: none;
    height: var(--thread-spacer, 0px);
    margin-top: 24px;
  }

  /* A failed load: one line and a bordered Retry, centred. */
  .chat-drawer-load-failed {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    margin-block: auto;
    text-align: center;
  }

  .chat-drawer-load-failed-text {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--label-secondary);
  }

  .chat-drawer-retry {
    padding: 8px 16px;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-full);
    background: none;
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--label-primary);
    cursor: pointer;
  }

  /* ── The ↓ pill (§4a.10) ───────────────────────────────────────────
     A 40 glass disc centred 12 above the composer row. In flow between
     the plane and the row, with margins that take its own height back, so
     it follows the row as it grows and moves nothing. Hidden until the
     controller shows it; it enters at 0.86 with a fade. */
  .chat-drawer-jump {
    position: relative;
    z-index: 1;
    flex: none;
    align-self: center;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin: -52px 0 12px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: color-mix(in srgb, var(--bg-secondary) 60%, transparent);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    box-shadow: 0 2px 16px rgba(0, 0, 0, 0.15);
    color: var(--label-primary);
    cursor: pointer;
    opacity: 0;
    transform: scale(0.86);
    visibility: hidden;
    transition:
      opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
      transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
      visibility 0s linear 300ms;
    -webkit-tap-highlight-color: transparent;
  }

  .chat-drawer[data-jump="on"] .chat-drawer-jump {
    opacity: 1;
    transform: scale(1);
    visibility: visible;
    transition:
      opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
      transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
      visibility 0s;
  }

  .chat-drawer[data-jump="on"] .chat-drawer-jump:active {
    transform: scale(1.04);
  }

  /* ── The composer row (§4a.11) ─────────────────────────────────────
     Bottom-aligned, 8 apart, 12 in from the card's sides: [pill][+],
     [✕][pill] while typing, [pill] with a draft parked. Raised, it is a
     drag handle; dimmed while the chat loads. */
  .chat-drawer-row {
    position: relative;
    z-index: 1;
    flex: none;
    display: flex;
    align-items: flex-end;
    gap: 8px;
    padding: 0 12px;
    touch-action: none;
  }

  .chat-drawer-row[data-loading] {
    opacity: 0.6;
  }

  /* The + (New toDo): the tab row's accent disc, copied (the .tab-plus
     class brings its hold menu along). */
  .chat-drawer-plus {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--accent-blue);
    color: var(--label-inverse-primary);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    text-decoration: none;
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
    -webkit-tap-highlight-color: transparent;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
  }

  .chat-drawer-plus:active {
    transform: scale(1.2);
  }

  /* The ✕ (Dismiss keyboard): a 56 glass disc. */
  .chat-drawer-dismiss {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--glass-fill);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    box-shadow:
      inset 0 0 0 1px var(--border-secondary),
      0 4px 8px rgba(0, 0, 0, 0.1);
    color: var(--label-primary);
    cursor: pointer;
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
    -webkit-tap-highlight-color: transparent;
  }

  .chat-drawer-dismiss:active {
    transform: scale(1.2);
  }

  /* The pill: the tab row's glass, 56 at rest, its corners from dom.mjs
     (28 at rest, easing to 24 as it grows). Staged files take a line of
     their own above the field; the paperclip and the send sit on the
     field's line, 8 from the pill's bottom, at any height. Its 18px is the
     field's: base.css floors phone fields at max(16px, 1em) with
     !important, and that 1em is the parent's size. */
  .chat-drawer-row > .ai-composer-dock {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    column-gap: 8px;
    box-sizing: border-box;
    min-height: 56px;
    padding: 0 8px;
    font-size: 18px;
    border-radius: var(--pill-radius, 28px);
    background: var(--glass-fill);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    box-shadow:
      inset 0 0 0 1px var(--border-secondary),
      0 4px 8px rgba(0, 0, 0, 0.1);
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  .chat-drawer-row > .ai-composer-dock:active {
    transform: scale(1.04);
  }

  .chat-drawer-file-input {
    display: none;
  }

  /* Staged files: a row that scrolls sideways, 10 above the items and 10
     in from the pill's sides, the field straight under it. A scroller
     starts its own touch-action chain: it pans sideways only, so a
     vertical drag on it still moves the drawer. */
  .chat-drawer-staged {
    flex: 0 0 calc(100% + 16px);
    display: flex;
    gap: 8px;
    box-sizing: border-box;
    margin: 0 -8px;
    padding: 10px 10px 0;
    overflow-x: auto;
    scrollbar-width: none;
    touch-action: pan-x;
  }

  .chat-drawer-staged::-webkit-scrollbar {
    display: none;
  }

  .chat-drawer-staged-item {
    position: relative;
    flex: none;
    width: 56px;
    height: 56px;
  }

  .chat-drawer-staged-item--file {
    width: 168px;
  }

  .chat-drawer-staged-thumb {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 10px;
    object-fit: cover;
  }

  .chat-drawer-staged-file {
    display: flex;
    align-items: center;
    gap: 8px;
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    padding: 0 12px;
    border-radius: 10px;
    background: var(--fill-elevated-secondary);
    color: var(--label-primary);
    font-size: var(--text-sm);
  }

  .chat-drawer-staged-file-name {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
  }

  /* Remove: a 22 disc tucked 2 inside the item's corner, a 44 hit area. */
  .chat-drawer-staged-remove {
    position: absolute;
    top: 2px;
    right: 2px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--bg-elevated-primary);
    color: var(--label-primary);
    cursor: pointer;
  }

  .chat-drawer-staged-remove::after {
    content: "";
    position: absolute;
    inset: -11px;
  }

  /* The paperclip: a 40 disc, 8 from the pill's left and bottom and from
     the text; dimmed and inert on an item that isn't a container. */
  .chat-drawer-attach {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin-bottom: 8px;
    border-radius: 50%;
    background: var(--fill-elevated-secondary);
    color: var(--label-primary);
    cursor: pointer;
  }

  .chat-drawer-attach[data-disabled] {
    color: var(--label-tertiary);
    cursor: default;
  }

  /* The field: 18/25.2 (the size from the pill) with 15 above and below,
     55.2 on one line (dom.mjs's grown threshold assumes it). It grows to 12
     lines, then scrolls inside; `autoResizeTextarea` sets its height from
     scrollHeight, padding included, hence border-box. It holds touches for
     the drawer's drag until it overflows; then (the controller's
     data-overflowing) it pans its own text and a drag never starts on it,
     while one from the rest of the pill still moves the drawer. */
  .chat-drawer-field {
    flex: 1;
    min-width: 0;
    box-sizing: border-box;
    max-height: calc(12 * 25.2px + 2 * 15px);
    margin: 0;
    padding: 15px 0;
    border: 0;
    outline: none;
    resize: none;
    background: transparent;
    font-family: inherit;
    font-size: inherit;
    line-height: 25.2px;
    color: var(--label-primary);
    overflow-y: auto;
    touch-action: none;
  }

  .chat-drawer-field[data-overflowing] {
    touch-action: pan-y;
  }

  .chat-drawer-field::placeholder {
    color: var(--label-primary);
    opacity: 1;
  }

  /* Send: mounted only with something to send, scaling in from 0.6. */
  .chat-drawer-send {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin-bottom: 8px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--bg-primary-inverse);
    color: var(--label-inverse-primary);
    cursor: pointer;
    animation: chat-drawer-send-in 0.15s ease-out;
  }

  :root[data-accent] .chat-drawer-send:not(:disabled):not([data-offline]) {
    background: var(--accent-blue);
  }

  /* In flight it stays, disabled. */
  .chat-drawer-send:disabled {
    background: var(--bg-quarternary);
    color: var(--label-quaternary);
    cursor: default;
  }

  /* Offline: an opaque dim disc that does nothing. */
  .chat-drawer-send[data-offline] {
    background: var(--fill-tertiary);
    color: var(--label-secondary);
    cursor: default;
  }

  /* ── Away (§4a.14) ─────────────────────────────────────────────────
     While the formatting bar has the slot, the card slides down out of the
     layer at whatever stop it held (110% of its height and its gap clear
     it at every stop), and the scrim and the seam go with it. No
     transition back: the return is a cut. */
  .chat-drawer[data-away] .chat-drawer-card {
    transform: translateY(
      calc(110% + var(--drawer-gap, var(--drawer-docked-gap, 22px)))
    );
    transition: transform 350ms var(--ease-slide);
  }

  .chat-drawer[data-away] .chat-drawer-scrim,
  .chat-drawer[data-away] .chat-drawer-seam {
    opacity: 0;
  }

  /* ── The keyboard (§4a.12) ─────────────────────────────────────────
     Up, the card is full-bleed at any stop: no side, gap or bottom radius,
     no hairline, the row 8 above the keyboard. Not while away: a card
     sliding away keeps its stop's shape. */
  .kb-open .chat-drawer:not([data-away]) .chat-drawer-card {
    left: 0;
    right: 0;
    bottom: 0;
    padding-bottom: 8px;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
  }

  .kb-open .chat-drawer:not([data-away]) .chat-drawer-card::after {
    opacity: 0;
  }

  .kb-open .chat-drawer:not([data-away]) .chat-drawer-plane {
    margin-inline: 0;
  }

  .kb-open .chat-drawer:not([data-away]) .chat-drawer-scrim {
    top: calc(100% - var(--drawer-h, 80px) + 17px);
  }

  .kb-open .chat-drawer:not([data-away]) .chat-drawer-seam {
    opacity: 1;
  }
}

@keyframes chat-drawer-breathe {
  from {
    --chat-drawer-breath: 1;
  }
  to {
    --chat-drawer-breath: 0.55;
  }
}

@keyframes chat-drawer-hue {
  from {
    filter: hue-rotate(0deg);
  }
  to {
    filter: hue-rotate(360deg);
  }
}

/* From the trough, so a loop after a cadence switch starts where the
   trough left it. */
@keyframes chat-drawer-glow-breathe {
  from {
    --chat-drawer-glow-breath: 0.6;
  }
  to {
    --chat-drawer-glow-breath: 0.9;
  }
}

@keyframes chat-drawer-glow-trough {
  from {
    --chat-drawer-glow-breath: var(--drawer-glow-from, 0.9);
  }
  to {
    --chat-drawer-glow-breath: 0.6;
  }
}

@keyframes chat-drawer-glow-hue {
  from {
    filter: blur(16px) hue-rotate(0deg);
  }
  to {
    filter: blur(16px) hue-rotate(360deg);
  }
}

@keyframes chat-drawer-send-in {
  from {
    opacity: 0;
    transform: scale(0.6);
  }
}

@keyframes chat-drawer-fade-in {
  from {
    opacity: 0;
  }
}

/* Reduced motion (§4a.16): no slide for the handoff; the grabber and the
   glow hold still at their crest with no hue turn; the width change is
   instant; the ↓ pill and the send fade without scaling; presses snap, as
   the tab row's and nav bar's do. */
@media (max-width: 834px) and (prefers-reduced-motion: reduce) {
  .chat-drawer[data-away] .chat-drawer-card {
    transition: none;
  }

  .chat-drawer-grabber::before,
  .chat-drawer-grabber::after {
    transition: --chat-drawer-lit 300ms ease-in-out;
  }

  .chat-drawer[data-signal="on"][data-light="docked"] .chat-drawer-grabber::after {
    animation: none;
  }

  .chat-drawer-glow,
  .chat-drawer[data-glow-cadence] .chat-drawer-glow {
    animation: none;
    --chat-drawer-glow-breath: 0.9;
  }

  .chat-drawer-jump,
  .chat-drawer[data-jump="on"] .chat-drawer-jump,
  .chat-drawer[data-jump="on"] .chat-drawer-jump:active {
    transform: none;
  }

  .chat-drawer-send {
    animation: chat-drawer-fade-in 0.15s ease-out;
  }

  .chat-drawer-close,
  .chat-drawer-plus,
  .chat-drawer-dismiss,
  .chat-drawer-row > .ai-composer-dock {
    transition: none;
  }
}

@media (min-width: 835px) {
  .chat-drawer {
    display: none;
  }
}

/* Phone formatting bar on the toDo detail (spec §4b) — ui/format_bar. */
/* The phone formatting bar (spec §4b) — ui/format_bar. The detail page draws
   it while the title, the notes or the bar itself has focus; only phones
   (≤834px) show it, the desktop rail being the wide layouts' bar.

   The DOM contract (ui/format_bar's header): .fmt-bar[data-mode][data-pins]
   > .fmt-capsule > (.fmt-pins > .fmt-tool.fmt-pin ×2) + .fmt-strip-wrap >
   .fmt-strip > .fmt-tool[data-idx] > .fmt-wash > .icon; then .fmt-done.
   The strip controller (ffi/format_strip.mjs) writes `--fmt-pitch` (iOS's
   sizing rule on the bar's width: 50.67 at 393) and `data-pins-fit` on the
   bar, then `data-pitched`, which lets the capsule and the pins morph.
   Every slot is one pitch wide; the fallback is the 393 pitch, so a bar
   the controller hasn't measured yet is still laid out. On the strip it
   writes the trailing fade (`--fmt-edge-alpha`, `--fmt-ramp`, `--fmt-tail`,
   and `data-fade="none"` where the edge is clear) and scrolls it to reveal
   what is lit. */

.fmt-bar {
  display: none;
}

@keyframes fmt-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@media (max-width: 834px) {
  /* ── The row (§4b.1, §4b.2) ────────────────────────────────────────
     72 tall: the 56 capsule with 8 above and below, 8 at the sides, 8
     between the capsule and the ✓. In the page's flow at the end of
     .nr-root, so it rides the keyboard and the notes end at its top; the
     8 below is its only gap to the keys. In the page's colour, as nothing
     passes behind it. Above the AI drawer's layer (fixed, z 25), so a card
     still sliding away goes under it: both are children of .nr-root, so
     no stacking context can hold one without the other. It fades in;
     leaving is a cut. */
  .fmt-bar {
    display: flex;
    align-items: center;
    flex: none;
    box-sizing: border-box;
    height: 72px;
    padding: 8px;
    gap: 8px;
    background: var(--detail-page-bg);
    position: relative;
    z-index: 26;
    animation: fmt-in 350ms;
  }

  /* ── The capsule (§4b.2) ───────────────────────────────────────────
     The nav bar's glass disc recipe (nav_bar.css), 56 tall and fully
     round. It fills the room with the notes focused, and hugs its three
     tools with the title focused, at the notes' pitch, so they sit at one
     spacing in both. A hop between the two eases (the morphs, below). */
  .fmt-capsule {
    display: flex;
    flex: none;
    box-sizing: border-box;
    width: calc(100% - 64px);
    height: 56px;
    padding-inline: 4.5px;
    border-radius: 28px;
    overflow: hidden;
    background: var(--glass-fill);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    box-shadow:
      inset 0 0 0 1px var(--border-secondary),
      0 4px 8px rgba(0, 0, 0, 0.1);
  }

  .fmt-bar[data-mode="title"] .fmt-capsule {
    width: calc(3 * var(--fmt-pitch, 50.6667px) + 9px);
  }

  /* ── Undo and Redo (§4b.4) ─────────────────────────────────────────
     Pinned before the strip, outside its scroll: 0 wide until there is
     history, then two slots, over the capsule's 320ms. A bar too narrow
     for four slots drops them. */
  .fmt-pins {
    display: flex;
    flex: none;
    width: 0;
    overflow: hidden;
    opacity: 0;
  }

  .fmt-bar[data-pins="1"]:not([data-pins-fit="0"]) .fmt-pins {
    width: calc(2 * var(--fmt-pitch, 50.6667px));
    opacity: 1;
  }

  /* The morphs. Only once the controller has written the pitch
     (data-pitched): a bar first styles with the 393 fallback, and at any
     other width the measured pitch must land at once, not ease the title
     capsule or latched pins from the 393 sizes; the way in is the fade
     alone (§4b.1 Motion). From then on a mode hop or the pins' latch eases
     over 320ms, out with no overshoot. */
  .fmt-bar[data-pitched] .fmt-capsule {
    transition: width 320ms cubic-bezier(0.2, 0.9, 0.3, 1);
  }

  .fmt-bar[data-pitched] .fmt-pins {
    transition:
      width 320ms cubic-bezier(0.2, 0.9, 0.3, 1),
      opacity 320ms cubic-bezier(0.2, 0.9, 0.3, 1);
  }

  /* ── The strip (§4b.5, §4b.7) ──────────────────────────────────────
     The rest of the capsule, scrolled sideways a slot at a time. Its
     swipe never chains into the page's or the browser's. Each entry into
     the notes mounts a fresh strip, at slot 0, which cross-fades in. */
  .fmt-strip-wrap {
    flex: 1;
    min-width: 0;
    display: flex;
  }

  .fmt-strip {
    flex: 1;
    display: flex;
    overflow-x: auto;
    scrollbar-width: none;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    animation: fmt-in 200ms;
  }

  /* The trailing fade, on the scroller only (the pins and the glass never
     fade): opaque, a ramp 0.91·pitch wide, then a 0.14·pitch tail at the
     edge alpha, which runs from 0 a slot or more from the end of travel
     to 1 at it. No leading fade. */
  .fmt-strip:not([data-fade="none"]) {
    mask-image: linear-gradient(to right, #000 calc(100% - var(--fmt-ramp) - var(--fmt-tail)),
      rgb(0 0 0 / var(--fmt-edge-alpha)) calc(100% - var(--fmt-tail)), rgb(0 0 0 / var(--fmt-edge-alpha)));
  }

  .fmt-strip::-webkit-scrollbar {
    display: none;
  }

  /* ── A tool (§4b.5, §4b.6) ─────────────────────────────────────────
     The whole pitch × 56 slot is the hit target; the glyph sits in the
     40px wash at its centre. Tints, never opacity: dimmed tools in the
     tertiary label, dimmed Undo and Redo in the secondary (they are
     expected there). */
  .fmt-tool {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--fmt-pitch, 50.6667px);
    height: 56px;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--label-primary);
    scroll-snap-align: start;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }

  .fmt-tool:disabled {
    color: var(--label-tertiary);
    cursor: default;
  }

  .fmt-pin:disabled {
    color: var(--label-secondary);
  }

  /* The ring goes on the wash, not the slot (§4b.14). */
  .fmt-tool:focus-visible {
    outline: none;
  }

  /* The 40px circle: the lit fill, and the press's 1.2× on the round-1
     stand-in for spring 0.3 / 0.6 (nav_bar.css). */
  .fmt-wash {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  .fmt-tool--lit .fmt-wash {
    background: var(--accent-blue-elevated);
    color: var(--accent-blue);
  }

  .fmt-tool:not(:disabled):active .fmt-wash {
    transform: scale(1.2);
  }

  .fmt-tool:focus-visible .fmt-wash {
    outline: 2px solid var(--label-primary);
    outline-offset: 2px;
  }

  /* ── The ✓ (§4b.2) ─────────────────────────────────────────────────
     tab_bar.css's .tab-plus: a solid accent disc with the inverse label,
     the toolbar shadow and the 1.2× press. It holds the trailing edge in
     both modes and stays put while the capsule's width moves; with the
     title focused the open space sits before it. */
  .fmt-done {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    margin-inline-start: auto;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--accent-blue);
    color: var(--label-inverse-primary);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    cursor: pointer;
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
    -webkit-tap-highlight-color: transparent;
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }

  .fmt-done:active {
    transform: scale(1.2);
  }

  /* base.css's ring is --bg-secondary, close to the page behind the disc. */
  .fmt-done:focus-visible {
    outline-color: var(--label-primary);
  }
}

/* Reduced motion (§4b.14): the bar and a fresh strip appear without a
   fade, the capsule and the pins change width at once, and presses snap,
   as the tab row's and the nav bar's do. (The controller's reveal jumps.) */
@media (max-width: 834px) and (prefers-reduced-motion: reduce) {
  .fmt-bar,
  .fmt-strip {
    animation: none;
  }

  .fmt-bar[data-pitched] .fmt-capsule,
  .fmt-bar[data-pitched] .fmt-pins,
  .fmt-wash,
  .fmt-done {
    transition: none;
  }
}

/* Phone toDo detail header (spec §4c): the Alerts sheet, the action row. */
/* The phone toDo detail's header (spec §4c): the set-alert line and the
   action row (app/detail/phone_header), and the Alerts sheet's own content
   (app/detail/alerts_sheet; its chrome is ui/sheet, sheet.css). */

/* Both headers are always in the DOM; the width picks one. The desktop
   strip leaves the phone's layout and accessibility tree at <=834px, and
   the phone header leaves the desktop's at >=835px. */
@media (max-width: 834px) {
  :is(.nr-root--detail, .nr-root--capture) .nr-action-strip {
    display: none;
  }
}
@media (min-width: 835px) {
  .detail-phone-header {
    display: none;
  }
}

/* A row glyph never gives up width: beside the AI glyph, the working
   status truncates first. */
.detail-action-glyph {
  flex: none;
}

@media (max-width: 834px) {
  /* The heading's 16 gutter. With the slot's 40 inset (circle 32 + gap 8)
     the slot's content box runs x 56 -> 377 at 393. */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container {
    padding-inline: 16px;
  }

  /* The set-alert line, then the action row 8 under it (or first, with no
     line). */
  :is(.nr-root--detail, .nr-root--capture) .detail-phone-header {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    margin-left: 40px;
  }

  /* §4c.4: the angled bell in a 24 box, 2, then the day and time. No
     capsule, no fill. */
  :is(.nr-root--detail, .nr-root--capture) .detail-alert-line {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    height: 24px;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--label-secondary);
    font: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-alert-glyph {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    color: var(--label-secondary);
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-alert-label {
    font-size: 15px;
    font-weight: 600;
    line-height: 20px;
    color: var(--label-secondary);
    white-space: nowrap;
  }

  /* Keyboard rings in the accent: base.css's --bg-secondary ring all but
     vanishes on the phone page's --bg-primary (1.23:1 dark, 1.12:1 light). */
  :is(.nr-root--detail, .nr-root--capture) .detail-alert-line:focus-visible,
  :is(.nr-root--detail, .nr-root--capture) .detail-action-item:focus-visible,
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .nr-completion-circle:focus-visible,
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container .thumb-stack:focus-visible,
  :is(.nr-root--detail, .nr-root--capture) .file-chip:focus-visible {
    outline: 2px solid var(--accent-blue);
    outline-offset: 2px;
  }

  /* §4c.5: 44 tall, items 8 apart, outset 8 each side so the items' own 8
     padding lands the first glyph box on x 56 and the pile's last face on
     x 377. */
  :is(.nr-root--detail, .nr-root--capture) .detail-action-row {
    display: flex;
    align-items: center;
    align-self: stretch;
    gap: 8px;
    height: 44px;
    margin-inline: -8px;
  }

  /* A plain button (no press grow): 44 tall, at least 44 wide, content
     hugging the leading padding; a count 2 after its glyph. */
  :is(.nr-root--detail, .nr-root--capture) .detail-action-item {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: flex-start;
    gap: 2px;
    height: 44px;
    min-width: 44px;
    padding: 0 8px;
    border: none;
    background: transparent;
    color: var(--label-secondary);
    font: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }

  /* The AI item gives way to a long working status (min-width stays 44, so
     the idle glyph keeps its 44 target). */
  :is(.nr-root--detail, .nr-root--capture) .detail-action-item[data-item="ai"] {
    flex-shrink: 1;
  }

  /* The spacer before the pile, which hugs its trailing padding. */
  :is(.nr-root--detail, .nr-root--capture) .detail-action-item--pile {
    justify-content: flex-end;
    margin-left: auto;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-action-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    color: var(--label-secondary);
  }

  /* An inert item (the plane until the create acks) looks it, as iOS's
     .disabled does: the glyph in the tertiary label, and no pointer. */
  :is(.nr-root--detail, .nr-root--capture) .detail-action-item[aria-disabled="true"],
  :is(.nr-root--detail, .nr-root--capture) .detail-action-item[aria-disabled="true"] .detail-action-glyph {
    color: var(--label-tertiary);
    cursor: default;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-action-count {
    font-size: 15px;
    font-weight: 600;
    line-height: 20px;
    color: var(--action-count);
    white-space: nowrap;
  }

  /* §4c.7: the AI glyph lit by unread replies or a working agent. The
     detail's only gradient glyph; cards stay grey. */
  :is(.nr-root--detail, .nr-root--capture) .detail-action-item--lit .detail-action-glyph .icon {
    background: var(--gradient-pastel);
  }

  /* The working status at iOS's footnote size, on the row's one line. The
     type lives on the text span; its ink stays the shimmer's
     --label-secondary (agent_status.css). */
  :is(.nr-root--detail, .nr-root--capture) .agent-status-line--row {
    padding: 0;
  }

  :is(.nr-root--detail, .nr-root--capture) .agent-status-line--row .agent-status-line-text {
    font-size: 13px;
    line-height: 18px;
    letter-spacing: 0;
  }

  /* §4c.8: 32 faces overlapped by half, ringed in the page's colour. */
  :is(.nr-root--detail, .nr-root--capture) .detail-action-row .shared-pile {
    --pile-ring: var(--detail-page-bg);
    --pile-overlap: 16px;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-action-row .shared-pile-avatar {
    position: relative;
    container-type: inline-size;
  }

  /* "+N" on the trailing face past three people: a black 70% disc over the
     blurred face, white 600 digits at 0.45 x 32 = 14.4, inset 10% each
     side, scaling with the face down to 40%. */
  :is(.nr-root--detail, .nr-root--capture) .detail-action-row .shared-pile-cover {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 10%;
    overflow: hidden;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.7);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
    color: #fff;
    font-size: clamp(5.76px, 45cqi, 14.4px);
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
  }
}

/* The alert line grows to 1.2 on press, like the circle and the nav discs
   (nav_bar.css). The action row's items don't grow. */
@media (max-width: 834px) and (prefers-reduced-motion: no-preference) {
  :is(.nr-root--detail, .nr-root--capture) .detail-alert-line {
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-alert-line:active {
    transform: scale(1.2);
  }
}

/* Below 360 the row tightens whether or not a status shows: gaps and
   padding to 4, the outset with them (so the glyphs still start at the
   gutter's 56 and the pile still ends at its edge), the faces overlapped
   by 20. */
@media (max-width: 359px) {
  :is(.nr-root--detail, .nr-root--capture) .detail-action-row {
    gap: 4px;
    margin-inline: -4px;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-action-item {
    padding-inline: 4px;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-action-row .shared-pile {
    --pile-overlap: 20px;
  }
}

/* The pencil disc closing each row. --bg-secondary because the phone sheet
   paints --bg-primary, where iOS's backgroundPrimary disc would vanish. */
.alerts-sheet__edit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-full);
  background: var(--bg-secondary);
  color: var(--label-primary);
}

