/* 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-xxxl: 44px;
  --size-3xl: 40px;
  --size-4xl: 48px;
  --size-5xl: 56px;

  /* The top inset (2026-10-27). `--safe-top` is the status bar the page
     is under: 59–62 in the iOS home-screen app, 0 wherever the browser
     owns the status bar (Safari, Chrome, Brave and Firefox in-browser;
     Android PWAs — Chrome draws edge-to-edge at the bottom only) and
     where env() is unknown. `--phone-top-inset` is what the phone chrome
     pads by: the status bar, or the floor where the header would
     otherwise run flush against the top — Brave on Android ran the 44
     discs against its toolbar. The floor is 24 (Julian, 2026-11-02; 32
     from 2026-10-28, the Pro Max status bar's 62 before that). iOS
     floors at 0 (below): Safari paints its 59–62 status region in the
     page's colour, so the row already sits under it as in the app.
     Sheets measure from `--safe-top` (the status bar); the headers and
     the paddings keyed to them from `--phone-top-inset`. */
  --safe-top: env(safe-area-inset-top, 0px);
  --phone-top-floor: 24px;
  --phone-top-inset: max(var(--safe-top), var(--phone-top-floor));

  /* The bottom inset `--safe-bottom` is the home indicator's 34 in the iOS
     home-screen app and 0 in a browser tab, where the browser's own bar
     owns the edge — so the sign-up and sign-in screens' pills, drawn 20.5
     over iOS's 34 in the mocks, sat 20.5 off the glass in Safari.
     `--phone-bottom-inset` floors it at the home indicator, so the pills
     sit where the mocks put them everywhere. */
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --phone-bottom-floor: 34px;
  --phone-bottom-inset: max(var(--safe-bottom), var(--phone-bottom-floor));

  /* With the keyboard up (html.kb-open), every composer's bottom edge
     clears the keyboard's top (the visual viewport's bottom, --vvh) by
     this much: the formatting and capture bars, the AI drawer's row, the
     chats' pills (Julian, 2026-11-05: was 8 on the bars and the drawer,
     18 on the chats). One number with ffi/keyboard_gap.mjs, which the
     drawer's geometry reads. */
  --kb-gap: 10px;

  /* 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 --background-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(--background-elevated-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%);


  --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;
}

/* iOS (`html.ios`, ffi/dom.mjs): no floor — see the top inset above. */
html.ios {
  --phone-top-floor: 0px;
}


/* ─── 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);


  /* Background */
   --background-primary: #FFFFFF;
   --background-secondary: #F2F2F7;
   --background-tertiary: #FFFFFF;

   --background-elevated-primary: var(--background-primary);
   --background-elevated-secondary: var(--background-secondary);
   --background-elevated-tertiary: var(--background-tertiary);

   /* Inverse: the dark theme's backgrounds, for surfaces that flip (2026-11-06). */
   --background-primary-inverse: #000000;
   --background-secondary-inverse: #1C1C1E;
   --background-tertiary-inverse: #2C2C2E;
   --background-elevated-primary-inverse: #1C1C1E;
   --background-elevated-secondary-inverse: #2C2C2E;
   --background-elevated-tertiary-inverse: #3A3A3C;

  /* 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);
  /* Ultra thick (2026-10-29, the desktop Messages panel): a step past
     thick, as Apple's ladder goes. Picked on the ladder, not measured —
     Figma has no ultra-thick swatch yet. */
  --material-ultrathick-1:         rgba(255, 255, 255, 0.92);
  --material-ultrathick-2:         rgba(255, 255, 255, 0.50);
  --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, under one recipe in both themes, measured from Julian's
     glass mock (2026-10-28 evening, images at 2x): a FLAT body (light 247,
     3% black; dark 26, 10% white), a darker band only under the top edge
     (8 levels below the body, fading into it over 24px), a white glow at
     the foot (light 248 → 255 over ~9px; dark 27 → 50), a hairline one
     device pixel wide (light: 20% black; dark: a 33% white light on the
     top edge, a 6% ring) and a soft wide drop shadow (--glass-shadow).
     --glass-tint is the band and body, the first background layer over
     the material pair; --glass-rim the rim's inset shadows, composed
     before the shadow: `box-shadow: var(--glass-rim), var(--glass-shadow)`.
     History: the first cut graduated the tint over the whole box and in
     light cleared it to 0 with a 90% white foot glow and a 35% white ring,
     so composers dissolved into the white page; the next cut the foot with
     shade and a 12% black 1px ring, which read as slate. */
  --glass-tint:                    linear-gradient(180deg, rgba(0, 0, 0, 0.065) 0px, rgba(0, 0, 0, 0.03) 24px, rgba(0, 0, 0, 0.03) 100%);
  --glass-rim:                     inset 0 0.5px 0 rgba(255, 255, 255, 0.90), inset 0 -9px 9px -4px rgba(255, 255, 255, 0.90), inset 0 0 0 0.5px rgba(0, 0, 0, 0.20);
  /* The glass's own drop shadow, soft and wide (the mock: ~2% fading over
     15pt); --shadow-float stays the sheets' and the rest of the chrome's. */
  --glass-shadow:                  0 6px 16px rgba(0, 0, 0, 0.06);
  --glass-shadow-disc:             0 4px 8px rgba(0, 0, 0, 0.10);
  /* A disc's rim (2026-10-27): the top light and the ring, without the
     slab's 6px bottom glow, which paints a crescent along a circle's foot
     and squashes it to an oval. */
  --glass-rim-disc:                inset 0 1px 0 rgba(255, 255, 255, 0.95), 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:              var(--accent-red);
  --count-badge-label:             #FFFFFF;

  --home-glow-green:               #62D475;
  --home-glow-blue:                #8EC3F8;
  --home-glow-pink:                #EABDEF;
  --home-glow-opacity:             1;

   --fill-primary:      rgba(120, 120, 120, 0.20);
   --fill-secondary:    rgba(120, 120, 128, 0.16);
   --fill-tertiary:     rgba(118, 118, 128, 0.12);
   --fill-quaternary:   rgba(116, 116, 128, 0.08);
   --fill-destructive:  rgba(255, 56, 60, 0.14);

   --fill-vibrant-primary:    #CCCCCC;
   --fill-vibrant-secondary:  #E0E0E0;
   --fill-vibrant-tertiary:   #EDEDED;

   /* Fills / UI */
   --fill-tabbar-selected:  #0088FF;
   --fill-segmented-selected:  #FFFFFF; 

   --fill-inverse-primary:      rgba(120, 120, 120, 0.36);
   --fill-inverse-secondary:    rgba(120, 120, 128, 0.32);
   --fill-inverse-tertiary:     rgba(118, 118, 128, 0.24);
   --fill-inverse-quaternary:   rgba(116, 116, 128, 0.18);
   --fill-inverse-destructive:  rgba(255, 66, 69, 0.14);   
  

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


  /* Label (Figma variables, 2026-11-06) — named as Figma names them:
     Label, Label / Vibrant, Label / Inverse (the other mode's ramp) and
     Label / Inverse / Vibrant. The base ladder is UIKit's translucent one
     (3C3C43 / EBEBF5 at an alpha, so it takes the surface's tint); Vibrant
     is the opaque ladder for copy over materials and controls. Figma's
     "Controls" sub-ladders are Vibrant minus quaternary, so they are not
     tokens here — read --label-vibrant-* / --label-inverse-vibrant-*. */
  --label-primary:            #000000;
  --label-secondary:          rgba(60, 60, 67, 0.60);
  --label-tertiary:           rgba(60, 60, 67, 0.30);
  --label-quaternary:         rgba(60, 60, 67, 0.18);

  --label-vibrant-primary:    #1A1A1A;
  --label-vibrant-secondary:  #727272;
  --label-vibrant-tertiary:   #BFBFBF;
  --label-vibrant-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:  rgba(235, 235, 245, 0.70);
  --label-inverse-tertiary:   rgba(235, 235, 245, 0.30);
  --label-inverse-quaternary: rgba(235, 235, 245, 0.16);

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

  /* 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. */
  --file-size-text:           var(--label-tertiary);

  /* Border (Figma variables, 2026-11-06): Primary, Secondary, Vibrant.
     The old --border, --border-strong and --border-tertiary are retired
     (primary, vibrant and secondary took their callers);
     test/border_tokens_test.mjs fails any new use. */
  --border-primary:   #C6C6C8;
  --border-secondary: rgba(0, 0, 0, 0.12);
  --border-vibrant:   #E6E6E6;

  /* Accent (Figma variables, 2026-11-06): raw values — light here, dark in
     the two dark scopes. --accent-blue is the app's accent, which accent.css
     re-points when the user picks one. Accents / Elevated is the accent at
     10% (red at 14%): the wash under accent-coloured ink. Accents / AX are
     the accessible inks the user-accent picker binds in place of the
     native hue (accent.css); the stock ink is the AX blue, since the
     native 0088FF is 3.5:1 on white. */
  --accent-red:      #FF383C;
  --accent-orange:   #FF8D28;
  --accent-yellow:   #FFCC00;
  --accent-green:    #34C759;
  --accent-mint:     #00C8B3;
  --accent-teal:     #00C3D0;
  --accent-cyan:     #00C0E8;
  --accent-blue:     var(--accent-ax-blue); /* native #0088FF */
  --accent-indigo:   #6155F5;
  --accent-purple:   #CB30E0;
  --accent-pink:     #FF2D55;
  --accent-brown:    #AC7F5E;
  /* iOS Messages' green, the Share Item sheet's Messages disc (2026-11-03);
     the app's own green in both themes. */
  --messages-green: #34C759;

  --accent-red-elevated:      rgba(255, 56, 60, 0.14);
  --accent-orange-elevated:   rgba(255, 141, 40, 0.10);
  --accent-yellow-elevated:   rgba(255, 204, 0, 0.10);
  --accent-green-elevated:    rgba(52, 199, 89, 0.10);
  --accent-mint-elevated:     rgba(0, 200, 179, 0.10);
  --accent-teal-elevated:     rgba(0, 195, 208, 0.10);
  --accent-cyan-elevated:     rgba(0, 192, 232, 0.10);
  --accent-blue-elevated:     rgba(0, 102, 235, 0.10);
  --accent-indigo-elevated:   rgba(97, 85, 245, 0.10);
  --accent-purple-elevated:   rgba(203, 48, 224, 0.10);
  --accent-pink-elevated:     rgba(255, 45, 85, 0.10);
  --accent-brown-elevated:    rgba(172, 127, 94, 0.10);

  --accent-ax-orange: #783D02;
  --accent-ax-yellow: #5E4B07;
  --accent-ax-blue:   #0066EB;
  --accent-ax-indigo: #481ADA;
  --accent-ax-pink:   #99062B;

  /* Accents / Custom (Figma, 2026-11-06): the list pastels and their
     elevated washes. The home grid still paints a list's wire hex
     directly (app/buckets list_colors); these are for chrome that tints
     by name. */
  --custom-red:      #F6B4BE;
  --custom-yellow:   #F6E7B4;
  --custom-orange:   #FFE9B3;
  --custom-green:    #9EDB9D;
  --custom-mint:     #BAEBE7;
  --custom-blue:     #A0D0FF;
  --custom-indigo:   #C4CFF7;
  --custom-purple:   #DAB0E9;
  --custom-pink:     #F1CDDF;
  --custom-brown:    #E1D8CF;
  --custom-slate:    #DADADA;
  --custom-red-elevated:      rgba(221, 72, 111, 0.10);
  --custom-yellow-elevated:   rgba(160, 131, 0, 0.10);
  --custom-orange-elevated:   rgba(166, 129, 0, 0.10);
  --custom-green-elevated:    rgba(0, 163, 33, 0.10);
  --custom-mint-elevated:     rgba(7, 154, 148, 0.10);
  --custom-blue-elevated:     rgba(0, 138, 230, 0.10);
  --custom-indigo-elevated:   rgba(102, 122, 235, 0.10);
  --custom-purple-elevated:   rgba(184, 79, 218, 0.10);
  --custom-pink-elevated:     rgba(203, 84, 155, 0.10);
  --custom-brown-elevated:    rgba(163, 126, 88, 0.10);
  --custom-slate-elevated:    rgba(134, 134, 134, 0.10);

  /* Text selection (2026-11-01): one opaque highlight + the label it
     carries, every selected run in the app (base.css ::selection). Held
     by test/selection_tokens_test.mjs to WCAG AAA for the text — 7:1
     against the highlight — which with black text caps the highlight's
     own contrast against the page at 3:1 (both bounds meet at luminance
     0.30), so this is the stock ink (AX blue 0,102,235) at 68.3% over
     white: text 7.00:1, page 3.00:1. accent.css re-points it per accent. */
  --selection-bg: #5197F1;
  --selection-fg: var(--label-primary);

  /* 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:             var(--accent-ax-blue);
  --swipe-share-wash:        rgba(0, 102, 235, 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:        var(--accent-ax-blue);
  --brand-blue-12:     rgba(0, 102, 235, 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);
  /* A floating panel over the page (the desktop Messages panel, mock
     2026-10-29): a wide, soft drop. */
  --shadow-panel: 0 8px 40px rgba(0, 0, 0, 0.12);
  /* 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);

  color-scheme: light;
}


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

[data-theme="dark"] {

  --blend-mode:      plus-lighter;

  --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);


     /* Background */
   --background-primary: #000000;
   --background-secondary: #1C1C1E;
   --background-tertiary: #2C2C2E;

   --background-elevated-primary: #1C1C1E;
   --background-elevated-secondary: #2C2C2E;
   --background-elevated-tertiary: #3A3A3C;

   /* Inverse: the light theme's backgrounds, for surfaces that flip (2026-11-06). */
   --background-primary-inverse: #FFFFFF;
   --background-secondary-inverse: #F2F2F7;
   --background-tertiary-inverse: #FFFFFF;
   --background-elevated-primary-inverse: #FFFFFF;
   --background-elevated-secondary-inverse: #F2F2F7;
   --background-elevated-tertiary-inverse: #FFFFFF;

  --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-ultrathick-1:         rgba(58, 58, 60, 0.50);
  --material-ultrathick-2:         rgba(28, 28, 30, 0.94);
  --material-chrome-1:             rgba(72, 72, 76, 0.40);
  --material-chrome-2:             rgba(28, 28, 28, 0.90);
  --material-chrome-blur:          100px;

  --glass-tint:                    linear-gradient(180deg, rgba(255, 255, 255, 0.07) 0px, rgba(255, 255, 255, 0.10) 24px, rgba(255, 255, 255, 0.10) 100%);
  --glass-rim:                     inset 0 1px 0 rgba(255, 255, 255, 0.33), inset 0 -10px 10px -4px rgba(255, 255, 255, 0.10), inset 0 0 0 0.5px rgba(255, 255, 255, 0.06);
  --glass-shadow:                  0 6px 16px rgba(0, 0, 0, 0.50);
  --glass-shadow-disc:             0 4px 8px rgba(0, 0, 0, 0.10);
  --glass-rim-disc:                inset 0 1px 0 rgba(255, 255, 255, 0.22), 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-primary:      rgba(120, 120, 128, 0.36);
   --fill-secondary:    rgba(120, 120, 128, 0.32);
   --fill-tertiary:     rgba(118, 118, 128, 0.24);
   --fill-quaternary:   rgba(118, 118, 128, 0.18);
   --fill-destructive:  rgba(255, 66, 69, 0.14);

   --fill-vibrant-primary:    #333333;
   --fill-vibrant-secondary:  #1F1F1F;
   --fill-vibrant-tertiary:   #121212;


   /* Fills / UI */
   --fill-tabbar-selected:  #FFFFFF; 
   --fill-segmented-selected:  rgba(255, 255, 255, 0.27); 
  
   --fill-inverse-primary:      rgba(120, 120, 128, 0.20);
   --fill-inverse-secondary:    rgba(120, 120, 128, 0.16);
   --fill-inverse-tertiary:     rgba(118, 118, 128, 0.12);
   --fill-inverse-quaternary:   rgba(118, 118, 128, 0.08);
   --fill-inverse-destructive:  rgba(255, 56, 60, 0.14);


  /* Label (Figma variables, 2026-11-06) — named as Figma names them:
     Label, Label / Vibrant, Label / Inverse (the other mode's ramp) and
     Label / Inverse / Vibrant. The base ladder is UIKit's translucent one
     (3C3C43 / EBEBF5 at an alpha, so it takes the surface's tint); Vibrant
     is the opaque ladder for copy over materials and controls. Figma's
     "Controls" sub-ladders are Vibrant minus quaternary, so they are not
     tokens here — read --label-vibrant-* / --label-inverse-vibrant-*. */
  --label-primary:            #FFFFFF;
  --label-secondary:          rgba(235, 235, 245, 0.70);
  --label-tertiary:           rgba(235, 235, 245, 0.30);
  --label-quaternary:         rgba(235, 235, 245, 0.16);

  --label-vibrant-primary:    #F5F5F5;
  --label-vibrant-secondary:  #8A8A8A;
  --label-vibrant-tertiary:   #404040;
  --label-vibrant-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:  rgba(60, 60, 67, 0.60);
  --label-inverse-tertiary:   rgba(60, 60, 67, 0.30);
  --label-inverse-quaternary: rgba(60, 60, 67, 0.18);

  --label-inverse-vibrant-primary:    #1A1A1A;
  --label-inverse-vibrant-secondary:  #727272;
  --label-inverse-vibrant-tertiary:   #BFBFBF;
  --label-inverse-vibrant-quaternary: #D9D9D9;

  /* 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: back on tertiary now that its dark value is
     the translucent ladder (2026-11-06), per the 2026-10-25 note. */
  --file-size-text:           var(--label-tertiary);

  /* Border (Figma variables, 2026-11-06): Primary, Secondary, Vibrant. */
  --border-primary:   #38383A;
  --border-secondary: rgba(255, 255, 255, 0.17);
  --border-vibrant:   #1A1A1A;

  /* Accent (Figma variables, 2026-11-06): the dark values — see light. */
  --accent-red:      #FF4245;
  --accent-orange:   #FF9230;
  --accent-yellow:   #FFD600;
  --accent-green:    #30D158;
  --accent-mint:     #00DAC3;
  --accent-teal:     #00D2E0;
  --accent-cyan:     #3CD3FE;
  --accent-blue:     var(--accent-ax-blue); /* native #0091FF */
  --accent-indigo:   #6D7CFF;
  --accent-purple:   #DB34F2;
  --accent-pink:     #FF375F;
  --accent-brown:    #B78A66;
  /* iOS Messages' green, the Share Item sheet's Messages disc (2026-11-03);
     the app's own green in both themes. */
  --messages-green: #34C759;

  --accent-red-elevated:      rgba(255, 66, 69, 0.14);
  --accent-orange-elevated:   rgba(255, 146, 48, 0.10);
  --accent-yellow-elevated:   rgba(255, 214, 0, 0.10);
  --accent-green-elevated:    rgba(48, 209, 88, 0.10);
  --accent-mint-elevated:     rgba(0, 218, 195, 0.10);
  --accent-teal-elevated:     rgba(0, 210, 224, 0.10);
  --accent-cyan-elevated:     rgba(60, 211, 254, 0.10);
  --accent-blue-elevated:     rgba(0, 145, 255, 0.10);
  --accent-indigo-elevated:   rgba(109, 124, 255, 0.10);
  --accent-purple-elevated:   rgba(219, 52, 242, 0.10);
  --accent-pink-elevated:     rgba(255, 55, 95, 0.10);
  --accent-brown-elevated:    rgba(183, 138, 102, 0.10);

  --accent-ax-orange: #FF9230;
  --accent-ax-yellow: #FFD600;
  --accent-ax-blue:   #0091FF;
  --accent-ax-indigo: #B5A7FF;
  --accent-ax-pink:   #FF90AC;

  /* Accents / Custom (Figma, 2026-11-06): the list pastels and their
     elevated washes. The home grid still paints a list's wire hex
     directly (app/buckets list_colors); these are for chrome that tints
     by name. */
  --custom-red:      #FFB4C0;
  --custom-yellow:   #FEE798;
  --custom-orange:   #FFE6A7;
  --custom-green:    #87E687;
  --custom-mint:     #A7F4EE;
  --custom-blue:     #A7D3FE;
  --custom-indigo:   #C6D2FF;
  --custom-purple:   #E9A9FF;
  --custom-pink:     #FFCBE5;
  --custom-brown:    #E8DACD;
  --custom-slate:    #DDDDDD;
  --custom-red-elevated:      rgba(255, 108, 141, 0.10);
  --custom-yellow-elevated:   rgba(196, 161, 0, 0.10);
  --custom-orange-elevated:   rgba(202, 158, 0, 0.10);
  --custom-green-elevated:    rgba(3, 199, 45, 0.10);
  --custom-mint-elevated:     rgba(3, 189, 182, 0.10);
  --custom-blue-elevated:     rgba(75, 170, 254, 0.10);
  --custom-indigo-elevated:   rgba(135, 157, 254, 0.10);
  --custom-purple-elevated:   rgba(217, 112, 252, 0.10);
  --custom-pink-elevated:     rgba(238, 115, 186, 0.10);
  --custom-brown-elevated:    rgba(194, 157, 117, 0.10);
  --custom-slate-elevated:    rgba(164, 164, 164, 0.10);

  /* Text selection: the dark blue (0,145,255) at 62.5% over black — white
     text 7.01:1, highlight 3.00:1 against the page (the AAA cap, see light). */
  --selection-bg: #005B9F;
  --selection-fg: var(--label-primary);

  
  /* 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:             var(--accent-ax-blue);
  --swipe-share-wash:        rgba(0, 145, 255, 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:        var(--accent-ax-blue);
  --brand-blue-12:     rgba(0, 145, 255, 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);
  --shadow-panel: 0 0 0 1px rgba(255, 255, 255, 0.08), 0 8px 40px 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);

  color-scheme: dark;
}


/* ─── OS dark preference — catch-all when no data-theme is set ──────────────
   The [data-theme="dark"] block above, verbatim (regenerated 2026-11-06;
   the old copy had drifted and still carried the retired --bg-* names).
   Change the dark block and mirror it here. ─────────────────────────── */

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

    --blend-mode:      plus-lighter;

    --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);


       /* Background */
     --background-primary: #000000;
     --background-secondary: #1C1C1E;
     --background-tertiary: #2C2C2E;

     --background-elevated-primary: #1C1C1E;
     --background-elevated-secondary: #2C2C2E;
     --background-elevated-tertiary: #3A3A3C;

     /* Inverse: the light theme's backgrounds, for surfaces that flip (2026-11-06). */
     --background-primary-inverse: #FFFFFF;
     --background-secondary-inverse: #F2F2F7;
     --background-tertiary-inverse: #FFFFFF;
     --background-elevated-primary-inverse: #FFFFFF;
     --background-elevated-secondary-inverse: #F2F2F7;
     --background-elevated-tertiary-inverse: #FFFFFF;

    --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-ultrathick-1:         rgba(58, 58, 60, 0.50);
    --material-ultrathick-2:         rgba(28, 28, 30, 0.94);
    --material-chrome-1:             rgba(72, 72, 76, 0.40);
    --material-chrome-2:             rgba(28, 28, 28, 0.90);
    --material-chrome-blur:          100px;

    --glass-tint:                    linear-gradient(180deg, rgba(255, 255, 255, 0.07) 0px, rgba(255, 255, 255, 0.10) 24px, rgba(255, 255, 255, 0.10) 100%);
    --glass-rim:                     inset 0 1px 0 rgba(255, 255, 255, 0.33), inset 0 -10px 10px -4px rgba(255, 255, 255, 0.10), inset 0 0 0 0.5px rgba(255, 255, 255, 0.06);
    --glass-shadow:                  0 6px 16px rgba(0, 0, 0, 0.50);
    --glass-shadow-disc:             0 4px 8px rgba(0, 0, 0, 0.10);
    --glass-rim-disc:                inset 0 1px 0 rgba(255, 255, 255, 0.22), 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-primary:      rgba(120, 120, 128, 0.36);
     --fill-secondary:    rgba(120, 120, 128, 0.32);
     --fill-tertiary:     rgba(118, 118, 128, 0.24);
     --fill-quaternary:   rgba(118, 118, 128, 0.18);
     --fill-destructive:  rgba(255, 66, 69, 0.14);

     --fill-vibrant-primary:    #333333;
     --fill-vibrant-secondary:  #1F1F1F;
     --fill-vibrant-tertiary:   #121212;


     /* Fills / UI */
     --fill-tabbar-selected:  #FFFFFF; 
     --fill-segmented-selected:  rgba(255, 255, 255, 0.27); 
  
     --fill-inverse-primary:      rgba(120, 120, 128, 0.20);
     --fill-inverse-secondary:    rgba(120, 120, 128, 0.16);
     --fill-inverse-tertiary:     rgba(118, 118, 128, 0.12);
     --fill-inverse-quaternary:   rgba(118, 118, 128, 0.08);
     --fill-inverse-destructive:  rgba(255, 56, 60, 0.14);


    /* Label (Figma variables, 2026-11-06) — named as Figma names them:
       Label, Label / Vibrant, Label / Inverse (the other mode's ramp) and
       Label / Inverse / Vibrant. The base ladder is UIKit's translucent one
       (3C3C43 / EBEBF5 at an alpha, so it takes the surface's tint); Vibrant
       is the opaque ladder for copy over materials and controls. Figma's
       "Controls" sub-ladders are Vibrant minus quaternary, so they are not
       tokens here — read --label-vibrant-* / --label-inverse-vibrant-*. */
    --label-primary:            #FFFFFF;
    --label-secondary:          rgba(235, 235, 245, 0.70);
    --label-tertiary:           rgba(235, 235, 245, 0.30);
    --label-quaternary:         rgba(235, 235, 245, 0.16);

    --label-vibrant-primary:    #F5F5F5;
    --label-vibrant-secondary:  #8A8A8A;
    --label-vibrant-tertiary:   #404040;
    --label-vibrant-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:  rgba(60, 60, 67, 0.60);
    --label-inverse-tertiary:   rgba(60, 60, 67, 0.30);
    --label-inverse-quaternary: rgba(60, 60, 67, 0.18);

    --label-inverse-vibrant-primary:    #1A1A1A;
    --label-inverse-vibrant-secondary:  #727272;
    --label-inverse-vibrant-tertiary:   #BFBFBF;
    --label-inverse-vibrant-quaternary: #D9D9D9;

    /* 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: back on tertiary now that its dark value is
       the translucent ladder (2026-11-06), per the 2026-10-25 note. */
    --file-size-text:           var(--label-tertiary);

    /* Border (Figma variables, 2026-11-06): Primary, Secondary, Vibrant. */
    --border-primary:   #38383A;
    --border-secondary: rgba(255, 255, 255, 0.17);
    --border-vibrant:   #1A1A1A;

    /* Accent (Figma variables, 2026-11-06): the dark values — see light. */
    --accent-red:      #FF4245;
    --accent-orange:   #FF9230;
    --accent-yellow:   #FFD600;
    --accent-green:    #30D158;
    --accent-mint:     #00DAC3;
    --accent-teal:     #00D2E0;
    --accent-cyan:     #3CD3FE;
    --accent-blue:     var(--accent-ax-blue); /* native #0091FF */
    --accent-indigo:   #6D7CFF;
    --accent-purple:   #DB34F2;
    --accent-pink:     #FF375F;
    --accent-brown:    #B78A66;
    /* iOS Messages' green, the Share Item sheet's Messages disc (2026-11-03);
       the app's own green in both themes. */
    --messages-green: #34C759;

    --accent-red-elevated:      rgba(255, 66, 69, 0.14);
    --accent-orange-elevated:   rgba(255, 146, 48, 0.10);
    --accent-yellow-elevated:   rgba(255, 214, 0, 0.10);
    --accent-green-elevated:    rgba(48, 209, 88, 0.10);
    --accent-mint-elevated:     rgba(0, 218, 195, 0.10);
    --accent-teal-elevated:     rgba(0, 210, 224, 0.10);
    --accent-cyan-elevated:     rgba(60, 211, 254, 0.10);
    --accent-blue-elevated:     rgba(0, 145, 255, 0.10);
    --accent-indigo-elevated:   rgba(109, 124, 255, 0.10);
    --accent-purple-elevated:   rgba(219, 52, 242, 0.10);
    --accent-pink-elevated:     rgba(255, 55, 95, 0.10);
    --accent-brown-elevated:    rgba(183, 138, 102, 0.10);

    --accent-ax-orange: #FF9230;
    --accent-ax-yellow: #FFD600;
    --accent-ax-blue:   #0091FF;
    --accent-ax-indigo: #B5A7FF;
    --accent-ax-pink:   #FF90AC;

    /* Accents / Custom (Figma, 2026-11-06): the list pastels and their
       elevated washes. The home grid still paints a list's wire hex
       directly (app/buckets list_colors); these are for chrome that tints
       by name. */
    --custom-red:      #FFB4C0;
    --custom-yellow:   #FEE798;
    --custom-orange:   #FFE6A7;
    --custom-green:    #87E687;
    --custom-mint:     #A7F4EE;
    --custom-blue:     #A7D3FE;
    --custom-indigo:   #C6D2FF;
    --custom-purple:   #E9A9FF;
    --custom-pink:     #FFCBE5;
    --custom-brown:    #E8DACD;
    --custom-slate:    #DDDDDD;
    --custom-red-elevated:      rgba(255, 108, 141, 0.10);
    --custom-yellow-elevated:   rgba(196, 161, 0, 0.10);
    --custom-orange-elevated:   rgba(202, 158, 0, 0.10);
    --custom-green-elevated:    rgba(3, 199, 45, 0.10);
    --custom-mint-elevated:     rgba(3, 189, 182, 0.10);
    --custom-blue-elevated:     rgba(75, 170, 254, 0.10);
    --custom-indigo-elevated:   rgba(135, 157, 254, 0.10);
    --custom-purple-elevated:   rgba(217, 112, 252, 0.10);
    --custom-pink-elevated:     rgba(238, 115, 186, 0.10);
    --custom-brown-elevated:    rgba(194, 157, 117, 0.10);
    --custom-slate-elevated:    rgba(164, 164, 164, 0.10);

    /* Text selection: the dark blue (0,145,255) at 62.5% over black — white
       text 7.01:1, highlight 3.00:1 against the page (the AAA cap, see light). */
    --selection-bg: #005B9F;
    --selection-fg: var(--label-primary);

  
    /* 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:             var(--accent-ax-blue);
    --swipe-share-wash:        rgba(0, 145, 255, 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:        var(--accent-ax-blue);
    --brand-blue-12:     rgba(0, 145, 255, 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);
    --shadow-panel: 0 0 0 1px rgba(255, 255, 255, 0.08), 0 8px 40px 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);

    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%
   elevated wash, and the text-selection highlight --selection-bg (the
   accent over the page at the alpha that keeps the selected label at WCAG
   AAA 7:1 — test/selection_tokens_test.mjs). No attribute = the stock
   accent from tokens.css.

   The native accents (tokens.css --accent-*) are not readable as ink —
   orange, yellow, blue, indigo and pink bind the Accents / AX colours
   (--accent-ax-*, 2026-11-06; blue is the stock ink too); green and
   purple keep iOS's PleaseUI Colors.xcassets inks, which have no AX row
   yet.
   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. Pink is bound here but not yet offered
   by the picker (model.Accent / the PINK wire value). */

:root[data-accent="orange"],
.settings-accent--orange {
  --accent-blue: var(--accent-ax-orange);
  --accent-blue-elevated: rgba(120, 61, 2, 0.10);
  --selection-bg: #B18F6D;
  --swatch: var(--accent-ax-orange);
}

:root[data-theme="dark"][data-accent="orange"],
:root[data-theme="dark"] .settings-accent--orange {
  --accent-blue: var(--accent-ax-orange);
  --accent-blue-elevated: rgba(255, 146, 48, 0.10);
  --selection-bg: #834B19;
  --swatch: var(--accent-ax-orange);
}

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

:root[data-accent="yellow"],
.settings-accent--yellow {
  --accent-blue: var(--accent-ax-yellow);
  --accent-blue-elevated: rgba(94, 75, 7, 0.10);
  --selection-bg: #A0956C;
  --swatch: var(--accent-ax-yellow);
}

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

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

:root[data-accent="green"],
.settings-accent--green {
  --accent-blue: rgb(18, 89, 36);
  --accent-blue-elevated: rgba(18, 89, 36, 0.10);
  --selection-bg: #759F80;
  --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);
  --selection-bg: #18662B;
  --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);
    --selection-bg: #18662B;
    --swatch: rgb(48, 209, 88);
  }
}

:root[data-accent="blue"],
.settings-accent--blue {
  --accent-blue: var(--accent-ax-blue);
  --accent-blue-elevated: rgba(0, 102, 235, 0.10);
  --selection-bg: #5197F1;
  --swatch: var(--accent-ax-blue);
}

:root[data-theme="dark"][data-accent="blue"],
:root[data-theme="dark"] .settings-accent--blue {
  --accent-blue: var(--accent-ax-blue);
  --accent-blue-elevated: rgba(0, 145, 255, 0.10);
  --selection-bg: #005B9F;
  --swatch: var(--accent-ax-blue);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-accent="blue"],
  :root:not([data-theme]) .settings-accent--blue {
    --accent-blue: var(--accent-ax-blue);
    --accent-blue-elevated: rgba(0, 145, 255, 0.10);
    --selection-bg: #005B9F;
    --swatch: var(--accent-ax-blue);
  }
}

:root[data-accent="indigo"],
.settings-accent--indigo {
  --accent-blue: var(--accent-ax-indigo);
  --accent-blue-elevated: rgba(72, 26, 218, 0.10);
  --selection-bg: #9E85EB;
  --swatch: var(--accent-ax-indigo);
}

:root[data-theme="dark"][data-accent="indigo"],
:root[data-theme="dark"] .settings-accent--indigo {
  --accent-blue: var(--accent-ax-indigo);
  --accent-blue-elevated: rgba(181, 167, 255, 0.10);
  --selection-bg: #5A537F;
  --swatch: var(--accent-ax-indigo);
}

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

:root[data-accent="purple"],
.settings-accent--purple {
  --accent-blue: rgb(132, 1, 148);
  --accent-blue-elevated: rgba(132, 1, 148, 0.10);
  --selection-bg: #C07DC8;
  --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);
  --selection-bg: #7A4581;
  --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);
    --selection-bg: #7A4581;
    --swatch: rgb(242, 137, 255);
  }
}

:root[data-accent="pink"],
.settings-accent--pink {
  --accent-blue: var(--accent-ax-pink);
  --accent-blue-elevated: rgba(153, 6, 43, 0.10);
  --selection-bg: #CB8092;
  --swatch: var(--accent-ax-pink);
}

:root[data-theme="dark"][data-accent="pink"],
:root[data-theme="dark"] .settings-accent--pink {
  --accent-blue: var(--accent-ax-pink);
  --accent-blue-elevated: rgba(255, 144, 172, 0.10);
  --selection-bg: #804857;
  --swatch: var(--accent-ax-pink);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-accent="pink"],
  :root:not([data-theme]) .settings-accent--pink {
    --accent-blue: var(--accent-ax-pink);
    --accent-blue-elevated: rgba(255, 144, 172, 0.10);
    --selection-bg: #804857;
    --swatch: var(--accent-ax-pink);
  }
}

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

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

/* Text selection, everywhere (2026-11-01): the notes, the title, the chat
   composer and bubbles, lists, sheets — one highlight and one label from
   tokens.css / accent.css, held to WCAG AAA by
   test/selection_tokens_test.mjs. Opaque on purpose: a translucent wash
   reads differently over every surface and left the notes' at 1.14:1. The
   colour is forced to --selection-fg so secondary / special runs clear 7:1
   too. No other stylesheet styles ::selection.

   Engines (measured 2026-11-01): Chromium and Firefox paint the tokens as
   written. macOS Safari paints them in light, but in dark re-alphas any
   opaque ::selection to 0.8 (RenderTheme), so the highlight sits at ~2.2:1
   against the page there; the text still clears 7:1. iOS Safari paints its
   own system tint (~20% blue) over a touch selection whatever this says —
   no CSS reaches it. */
::selection {
  background-color: var(--selection-bg);
  color: var(--selection-fg);
}

/* A picture inside a selected run: Chromium tints a selected image with
   the highlight at high alpha, and an opaque one turns it into a flat
   block, so images and video take a 30% wash instead. */
img::selection,
video::selection {
  background-color: color-mix(in srgb, var(--selection-bg) 30%, transparent);
}

/* 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, and no scroll chaining out
   of a scroller that hit its edge — what makes the app feel like a fixed
   shell rather than a page. Every scroller (overflow auto | scroll)
   declares `overscroll-behavior: none` in its own rule, html/body below
   included; test/overscroll_test.mjs holds the line. Never on `*`
   (2026-10-28, Julian: "mouseOver to scroll is incredibly difficult" on
   the desktop Home sidebar): an overflow: hidden box — a row's title, a
   section's clip, a card's rounded corners — is a scroll container too,
   always at its boundary, and `none` on it stopped a wheel or a finger
   that started over it from reaching the scroller behind. */

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(--background-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-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;
}

/* The iOS menu pop: springs and the .pop entrance — see ui/motion. */
/* Motion: the iOS menu pop (2026-10-30), measured off Julian's 60 fps
   recording of the paperclip menu; a corner pivot since 2026-11-02
   (Julian's call, off the Pop Pivot Compare render). The menu is a frame
   morph out of what opened it: it starts as a blob a fifth of its size
   PIVOTED on the point it was opened from — the pointer, or the corner
   nearest the button (iOS `.scale(anchor: .bottomTrailing)`) — and grows
   to its own size about that point, width and height each on their own
   spring; the launched vertical spring (it shoots past and eases back, as
   iOS's does) reads as a small overshoot past full size. The shape goes
   from an ellipse to the element's own corners, the content from blurred
   to sharp, the whole thing from clear to opaque. Leaving, it folds back
   toward the same pivot, blurs and fades in ~80 ms (ffi/pop.mjs plays
   that on a ghost once Lustre has removed the element).

   Use: `ui/motion.pop(motion.menu)` on the element (the class `pop` plus
   any knob that differs from the defaults below, as inline custom
   properties), or the class alone with knobs set from a stylesheet. Every
   knob is a `--pop-*` custom property:

     --pop-corner          the element's own corner radius, at rest: set it
                           here instead of border-radius (see pop-round)
     --pop-anchor-x / -y   the pivot: the point in the element's own box it
                           unfolds from, one length or percentage each,
                           with its unit (50% 50%, the default, is its
                           centre). The FFI that places the element sets
                           these on an ancestor (ffi/menu_place.mjs); they
                           reach the element, not past it.
     --pop-from-x / -y     its starting size as a fraction of its own
     --pop-start-x / -y    the same, set by the FFI that places it on an
                           ancestor (ffi/menu_place.mjs placeMenuOver: the
                           trigger's box over the menu's, so it starts AS
                           the button it covers); --pop-from-* on the
                           element itself still wins
     --pop-width / -ms     the width's spring (a linear() easing) and how
                           long it runs; -height, -move-x, -move-y alike
     --pop-round / -ms     the starting corner radius, one length or
                           percentage (50%: an ellipse), and how long the
                           corners take to reach --pop-corner
     --pop-blur / -ms      the starting blur, px (CSS px are iOS points)
     --pop-fade-ms         the fade in
     --pop-exit            `off` to vanish at once
     --pop-exit-ms / -scale / -blur   the way out

   A `.pop` element carries `scale` and `translate`, so it is a stacking
   context and the containing block of its fixed descendants. Its springs
   animate registered custom properties, which browsers run on the main
   thread (the fade and blur can stay on the compositor), so a long task
   during an entrance stalls the growth for that long. */

@property --pop-sx {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

@property --pop-sy {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

/* Travel: 0 at the anchor, 1 at home. */
@property --pop-mx {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

@property --pop-my {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

/* The corner radius in flight. WebKit 26 (Safari 26; 18.6 was fine) never
   plays a border-radius keyframe on an element that also animates custom
   properties once any var() declaration precedes its radius in the
   cascade, so the morph animates this registered property and
   border-radius only ever reads it. */
@property --pop-r {
  syntax: "<length-percentage>";
  inherits: false;
  initial-value: 0px;
}

/* springs:begin — generated by scripts/springs.mjs, do not edit */
:root {
  /* spring(duration: 0.36, bounce: 0.25) */
  --spring-pop-width: linear(0, 0.0139, 0.0508, 0.1046, 0.1698, 0.2421, 0.318, 0.3946, 0.4697, 0.5416, 0.6093, 0.6717, 0.7286, 0.7796, 0.8247, 0.8642, 0.8982, 0.927, 0.9512, 0.9712, 0.9873, 1, 1.0098, 1.0171, 1.0223, 1.0257, 1.0276, 1.0283, 1.0282, 1.0273, 1.0259, 1.0241, 1.0221, 1.0199, 1.0178, 1.0156, 1.0136, 1.0116, 1.0098, 1.0081, 1.0066, 1.0053, 1.0041, 1.0031, 1.0023, 1.0016, 1);
  --spring-pop-width-ms: 459ms;
  /* spring(duration: 0.3, bounce: 0.21) */
  --spring-pop-height: linear(0, 0.0188, 0.0675, 0.136, 0.2165, 0.3026, 0.3899, 0.4748, 0.5551, 0.629, 0.6957, 0.7549, 0.8064, 0.8506, 0.888, 0.919, 0.9443, 0.9646, 0.9806, 0.9928, 1.0019, 1.0085, 1.0129, 1.0156, 1.017, 1.0175, 1.0171, 1.0163, 1.0151, 1.0137, 1.0122, 1.0107, 1.0092, 1.0078, 1.0065, 1.0053, 1.0043, 1.0034, 1.0026, 1.002, 1.0014, 1);
  --spring-pop-height-ms: 401ms;
  /* spring(duration: 0.33, bounce: 0.24) */
  --spring-pop-move-x: linear(0, 0.0162, 0.0586, 0.1196, 0.1924, 0.2721, 0.3543, 0.436, 0.5146, 0.5887, 0.657, 0.7188, 0.7739, 0.8222, 0.864, 0.8995, 0.9293, 0.9539, 0.9737, 0.9894, 1.0016, 1.0106, 1.0171, 1.0214, 1.024, 1.0252, 1.0253, 1.0246, 1.0234, 1.0217, 1.0198, 1.0177, 1.0156, 1.0136, 1.0116, 1.0098, 1.0081, 1.0066, 1.0053, 1.0041, 1.0031, 1.0023, 1.0015, 1);
  --spring-pop-move-x-ms: 426ms;
  /* spring(duration: 0.39, bounce: 0.36), initial velocity 25.5 */
  --spring-pop-move-y: linear(0, 0.239, 0.4513, 0.6371, 0.7969, 0.9319, 1.0435, 1.1335, 1.2039, 1.2566, 1.2937, 1.3173, 1.3292, 1.3314, 1.3256, 1.3134, 1.2962, 1.2755, 1.2522, 1.2275, 1.2021, 1.1768, 1.1521, 1.1284, 1.1062, 1.0857, 1.067, 1.0501, 1.0353, 1.0224, 1.0113, 1.002, 0.9944, 0.9884, 0.9837, 0.9802, 0.9779, 0.9764, 0.9758, 0.9759, 0.9765, 0.9775, 0.9788, 0.9804, 0.9822, 0.984, 0.9859, 0.9877, 0.9895, 0.9912, 0.9928, 0.9942, 0.9955, 0.9967, 0.9978, 0.9987, 0.9994, 1.0001, 1.0006, 1.001, 1.0013, 1.0015, 1.0017, 1.0017, 1.0018, 1.0018, 1.0017, 1.0016, 1.0015, 1.0014, 1.0013, 1.0011, 1);
  --spring-pop-move-y-ms: 712ms;
}
/* springs:end */

.pop {
  --pop-corner: 0px;
  --pop-from-x: var(--pop-start-x, 0.2);
  --pop-from-y: var(--pop-start-y, 0.2);
  --pop-width: var(--spring-pop-width);
  --pop-width-ms: var(--spring-pop-width-ms);
  --pop-height: var(--spring-pop-height);
  --pop-height-ms: var(--spring-pop-height-ms);
  --pop-move-x: var(--spring-pop-move-x);
  --pop-move-x-ms: var(--spring-pop-move-x-ms);
  --pop-move-y: var(--spring-pop-move-y);
  --pop-move-y-ms: var(--spring-pop-move-y-ms);
  --pop-round: 50%;
  --pop-round-ms: 280ms;
  --pop-blur: 6px;
  --pop-blur-ms: 230ms;
  --pop-fade-ms: 150ms;
  --pop-exit: on;
  --pop-exit-ms: 80ms;
  --pop-exit-scale: 0.6;
  --pop-exit-blur: 5px;

  --pop-r: var(--pop-corner);
  border-radius: var(--pop-r);
  /* The pivot. No travel: the pivot is where it is at rest; the launched
     vertical spring (--pop-my overshoots to ~1.33) survives as an 8% swell
     past full size, in both axes, so the menu still "shoots past". */
  transform-origin: var(--pop-anchor-x, 50%) var(--pop-anchor-y, 50%);
  scale:
    calc(var(--pop-sx) * max(1, 1 + (var(--pop-my) - 1) * 0.08))
    calc(var(--pop-sy) * max(1, 1 + (var(--pop-my) - 1) * 0.08));
  animation:
    pop-width var(--pop-width-ms) var(--pop-width) backwards,
    pop-height var(--pop-height-ms) var(--pop-height) backwards,
    pop-move-x var(--pop-move-x-ms) var(--pop-move-x) backwards,
    pop-move-y var(--pop-move-y-ms) var(--pop-move-y) backwards,
    pop-round var(--pop-round-ms) cubic-bezier(0.45, 0, 0.55, 1) backwards,
    pop-blur var(--pop-blur-ms) cubic-bezier(0.45, 0, 0.55, 1) backwards,
    pop-fade var(--pop-fade-ms) linear backwards;
}

/* An anchor (or a start size) set on an ancestor reaches the pop but not
   a pop inside it. */
.pop > * {
  --pop-anchor-x: initial;
  --pop-anchor-y: initial;
  --pop-start-x: initial;
  --pop-start-y: initial;
}

@keyframes pop-width {
  from {
    --pop-sx: var(--pop-from-x);
  }
}

@keyframes pop-height {
  from {
    --pop-sy: var(--pop-from-y);
  }
}

@keyframes pop-move-x {
  from {
    --pop-mx: 0;
  }
}

@keyframes pop-move-y {
  from {
    --pop-my: 0;
  }
}

/* The ellipse holds a while before it squares up (still pill-ended at
   167 ms in the recording, square-cornered by ~260 ms). It ends on the
   underlying --pop-r: the element's --pop-corner. */
@keyframes pop-round {
  from {
    --pop-r: var(--pop-round);
  }
  45% {
    --pop-r: calc(var(--pop-round) * 0.85);
  }
}

/* So does the blur: soft through ~170 ms, sharp by ~210 ms. */
@keyframes pop-blur {
  from {
    filter: blur(var(--pop-blur));
  }
  45% {
    filter: blur(calc(var(--pop-blur) * 0.9));
  }
}

@keyframes pop-fade {
  from {
    opacity: 0;
  }
}

/* Without linear(), the springs fall back to one overshooting curve. The
   whole animation is restated with literal curves, so a spring knob an
   instance sets inline (a linear() it can't play) can't reach it. */
@supports not (animation-timing-function: linear(0, 1)) {
  .pop {
    animation:
      pop-width var(--pop-width-ms) cubic-bezier(0.3, 1.25, 0.5, 1) backwards,
      pop-height var(--pop-height-ms) cubic-bezier(0.3, 1.25, 0.5, 1) backwards,
      pop-move-x var(--pop-move-x-ms) cubic-bezier(0.3, 1.25, 0.5, 1) backwards,
      pop-move-y var(--pop-move-y-ms) cubic-bezier(0.3, 1.25, 0.5, 1) backwards,
      pop-round var(--pop-round-ms) cubic-bezier(0.45, 0, 0.55, 1) backwards,
      pop-blur var(--pop-blur-ms) cubic-bezier(0.45, 0, 0.55, 1) backwards,
      pop-fade var(--pop-fade-ms) linear backwards;
  }
}

/* Reduced motion: a plain fade in (and out, ffi/pop.mjs). */
@media (prefers-reduced-motion: reduce) {
  .pop {
    animation: pop-fade 160ms ease backwards;
  }
}

.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-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(--background-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 {
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
}

.button.pill {
    border-radius: var(--radius-full, 100px);
}

.button.xl, .button.xxl {
    padding: var(--sp-sm, 16px) var(--sp-xl, 24px);
    font-size: var(--text-xl, 18px);
}

.button.header-control {
    height: var(--size-xxxl, 44px);
    width: var(--size-xxxl, 44px);
}

.button.xl {
    height: var(--size-4xl, 48px);
}

.button.xxl {
    height: var(--size-5xl, 56px);
}

.button.fullWidth {
    width: 100%;
}

.button.destructive, .button.danger {
    color: var(--accent-red);
    background: var(--fill-vibrant-secondary);
}

.button.secondary {
    color: var(--label-primary);
    background: var(--fill-vibrant-secondary);
}

/* ── 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);
}


.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(--background-secondary);
  color: var(--label-primary);
}

.detail-confirm-cancel:hover {
  background: var(--background-secondary);
}

.detail-confirm-delete {
  background: var(--accent-red);
  color: white;
}

.detail-confirm-delete:hover {
  opacity: 0.9;
}
/* 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;
  overscroll-behavior: none;
  display: flex;
  background: var(--background-secondary);
  padding: var(--sp-xl);
}

.auth-card {
  margin: auto;
  width: 100%;
  max-width: 440px;
  padding: var(--sp-xxl);
  background: var(--background-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);
}

/* Sign-up's heading (2026-10-28): the title with a line of copy under it,
   the pair keeping the title's 24 above the phone row. */
.auth-heading .auth-title {
  margin-bottom: var(--sp-xs);
}

.auth-lead {
  margin: 0 0 var(--sp-xl);
  font-size: var(--text-md);
  line-height: 1.4;
  text-align: center;
  color: var(--label-secondary);
  text-wrap: balance;
}

.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(--background-primary);
  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(--background-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(--background-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(--background-primary-inverse);
  color: var(--label-inverse-primary);
}

.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(--background-primary);
  color: var(--label-primary);
  font-family: var(--font-sans);
  width: 100%;
  box-sizing: border-box;
}
.auth-phone-input:focus { border-color: var(--background-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(--background-primary);
  cursor: pointer;
  font: inherit;
  white-space: nowrap;
}

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

.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(--background-primary);
  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;
  overscroll-behavior: none;
  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(--background-secondary);
}

.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; }

/* "Sent to" and the number, one line under "Enter the code" (Julian's
   mocks, 2026-10-30; the number had its own line under the lead). */
.auth-otp-subtitle {
  margin: 0 0 var(--sp-xl);
}

/* Over the boxes, so a tap on them lands in the field. */
.auth-otp-hidden-input {
  position: absolute;
  z-index: 1;
  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(--background-primary);
}
.auth-otp-box-active { border-color: var(--background-secondary); }

.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:not(:disabled) { text-decoration: underline; }
/* "Resend code in Ns": dimmed while the cooldown runs. */
.auth-resend-btn:disabled {
  color: var(--label-tertiary);
  cursor: default;
}

/* 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(--background-secondary);
}

.auth-interest-chip.selected {
  background: var(--background-primary-inverse);
  color: var(--label-inverse-primary);
  border-color: var(--background-primary-inverse);
}

.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(--background-secondary);
}

/* 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 name step (iOS's NameEntryView, 2026-10-29), in the card: "Your
   Name" with its line under it, the "Full Name" label over the field, and
   a plain-text "I already have an account" under Continue. Phones lay it
   out the landing's way (the block at the end). */
.auth-name-title {
  margin: 0;
}

.auth-name-subtitle {
  margin: 0;
  font-size: var(--text-md);
  line-height: 1.4;
  color: var(--label-secondary);
  text-wrap: balance;
}

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

.auth-name-label {
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

.auth-have-account {
  align-self: center;
  padding: var(--sp-3xs) 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--label-secondary);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

@media (hover: hover) {
  .auth-have-account:hover {
    color: var(--label-primary);
  }
}

/* "Creating your account…" (iOS's CreatingAccountView, 2026-10-29), in the
   card: a spinner over the line, or the warning sign, the title and the
   message over Try again and a plain-text Start over, all centred. Phones
   lay it out the landing's way (the block at the end). */
.auth-creating {
  align-items: center;
  text-align: center;
}

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

.auth-creating-title {
  margin: 0;
}

.auth-creating-art {
  display: inline-flex;
  color: var(--label-secondary);
}

/* iOS's large ProgressView, as a ring: 36 across. The landings' spinner
   (Google or Apple starting) is the same ring. */
.auth-creating-spinner,
.sign-up-actions-spinner {
  box-sizing: border-box;
  width: 36px;
  height: 36px;
  border: 3px solid var(--label-tertiary);
  border-top-color: transparent;
  border-radius: 50%;
  animation: auth-creating-spin 800ms linear infinite;
}

.auth-creating-message {
  margin: 0;
  font-size: var(--text-md);
  line-height: 1.4;
  color: var(--label-secondary);
  text-wrap: balance;
}

.auth-creating .auth-submit {
  align-self: stretch;
}

@keyframes auth-creating-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Under reduced motion the ring turns slowly rather than not at all: a
   still ring reads as stuck. */
@media (prefers-reduced-motion: reduce) {
  .auth-creating-spinner,
  .sign-up-actions-spinner {
    animation-duration: 2400ms;
  }
}

/* ── The sign-up landing (Julian's 1440 mock, 2026-10-28; app/auth
   view_sign_up_landing). Its own scroller like .auth-screen (html/body
   never scroll). The bar: the wordmark at 35 and Login, 24 down and 64 in.
   The carousel: 640 wide, each slide 432 tall, scroll-snapped, the
   scrollbar hidden; a pill per slide 28 under it, the title 28 under those, the
   three 392×48 pills 28 under the title with 10 between. Phones: 16 gutters,
   a 300 slide, a 32 title, full-width buttons. ── */
.sign-up {
  height: 100vh;
  height: 100dvh;
  overflow-y: auto;
  overscroll-behavior: none;
  display: flex;
  flex-direction: column;
  background: var(--background-secondary);
  color: var(--label-primary);
}

.sign-up-bar {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 24px 64px;
}

.sign-up-wordmark {
  display: inline-flex;
  color: var(--label-primary);
}

.sign-up-login {
  font-size: var(--text-xl);
  font-weight: 600;
  line-height: 1.25;
  color: var(--accent-blue);
  text-decoration: none;
}

.sign-up-main {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 16px 48px;
}

.sign-up-carousel {
  width: 100%;
  max-width: 640px;
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}

.sign-up-carousel::-webkit-scrollbar {
  display: none;
}

.sign-up-slide {
  flex: 0 0 100%;
  height: 432px;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The art sizes itself against the slide (cq units), and the art's
     behind-fades (z-index -1) stay inside the slide, over the page. */
  container-type: size;
  isolation: isolate;
}

/* The slides' art (Julian's "Carousel - <n>" set, 2026-10-28: seven PNGs
   at 2x, 786×844 for a 393×422 slot, alpha), one per slide in index order,
   at /icons/sign-up/slide-<n>.png on .sign-up-slide-art--<n>. The box IS
   the picture — contained in its slide: at its own size on the 640×432
   desktop slide, scaled to the phone's — so the fades below, in percent of
   the box, land on the art wherever it is drawn. Slide 4 is a px off 393
   wide; slide 2 is its own canvas (below). */
.sign-up-slide-art {
  --sign-up-art-w: 393;
  --sign-up-art-h: 422;
  position: relative;
  width: min(
    100cqw,
    100cqh * var(--sign-up-art-w) / var(--sign-up-art-h)
  );
  aspect-ratio: var(--sign-up-art-w) / var(--sign-up-art-h);
  background: center / 100% 100% no-repeat;
}

/* A slide in pieces: each layer is the box, one PNG each, exported on
   the slide's own canvas so they register without offsets; in tree order
   the later piece paints over the earlier one and over its scrim. */
.sign-up-slide-layer {
  position: absolute;
  inset: 0;
  background: center / 100% 100% no-repeat;
  pointer-events: none;
}

.sign-up-slide-art--1 {
  background-image: url("/icons/sign-up/slide-1.png");
}

/* Slide 2 in two pieces (Julian's "Slide 2 - Phone" / "- Camera"
   exports, 2026-10-30, on a 424×449 canvas at 2x): the phone with the
   + menu, the camera over it. */
.sign-up-slide-art--2 {
  --sign-up-art-w: 424;
  --sign-up-art-h: 449;
}

.sign-up-slide-layer--2-phone {
  background-image: url("/icons/sign-up/slide-2-phone.png");
}

.sign-up-slide-layer--2-camera {
  background-image: url("/icons/sign-up/slide-2-camera.png");
}

.sign-up-slide-art--3 {
  background-image: url("/icons/sign-up/slide-3.png");
}

.sign-up-slide-art--4 {
  --sign-up-art-w: 392;
  background-image: url("/icons/sign-up/slide-4.png");
}

/* The phone's scrim (Julian's Figma frame, 2026-10-30, measured from
   his screenshot: 424×449, the L of a blurred rectangle along the
   phone's top and left edges, under the camera). Each band's soft edge
   bows on Julian's ellipse — "the ellipses define the bow of each
   band's soft edge, like the other slides' bands": the wide one,
   565×143.5 centred 27 above the frame's top at x 226, bows the top
   band 14 deeper in the middle than at the phone's corners; the tall
   one, 147×709 centred 190 down at x 40, bows the left band 11 further
   in at its middle than at the foot. The measured falloff (solid for 13
   from the phone's edge, half-way at 46, gone by 86) rides the ellipse:
   the half-way level IS the ellipse's edge (the wide one's foot is 45
   down in the middle), so each gradient's shape is that ellipse scaled
   to where the band is gone, and the stops are the same curve in
   percent of it. Both are the page's own colour, so they follow the
   theme; the camera layer, after this one, paints over them. */
.sign-up-slide-layer--2-phone::before,
.sign-up-slide-layer--2-phone::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* Top: the wide ellipse, ×1.575 (445×113 at 226, −27). */
.sign-up-slide-layer--2-phone::before {
  background: radial-gradient(
    ellipse 104.95% 25.17% at 53.3% -6.01%,
    var(--sign-up-fade) 35.4%,
    color-mix(in srgb, var(--sign-up-fade) 95%, transparent) 38.9%,
    color-mix(in srgb, var(--sign-up-fade) 92%, transparent) 42.5%,
    color-mix(in srgb, var(--sign-up-fade) 88%, transparent) 45.1%,
    color-mix(in srgb, var(--sign-up-fade) 83%, transparent) 48.7%,
    color-mix(in srgb, var(--sign-up-fade) 78%, transparent) 52.2%,
    color-mix(in srgb, var(--sign-up-fade) 71%, transparent) 55.8%,
    color-mix(in srgb, var(--sign-up-fade) 64%, transparent) 58.4%,
    color-mix(in srgb, var(--sign-up-fade) 56%, transparent) 61.9%,
    color-mix(in srgb, var(--sign-up-fade) 48%, transparent) 65.5%,
    color-mix(in srgb, var(--sign-up-fade) 40%, transparent) 69%,
    color-mix(in srgb, var(--sign-up-fade) 32%, transparent) 72.6%,
    color-mix(in srgb, var(--sign-up-fade) 22%, transparent) 77%,
    color-mix(in srgb, var(--sign-up-fade) 16%, transparent) 80.5%,
    color-mix(in srgb, var(--sign-up-fade) 12%, transparent) 84.1%,
    color-mix(in srgb, var(--sign-up-fade) 8%, transparent) 87.6%,
    color-mix(in srgb, var(--sign-up-fade) 4%, transparent) 92%,
    color-mix(in srgb, var(--sign-up-fade) 2%, transparent) 95.6%,
    transparent
  );
}

/* Left: the tall ellipse, ×1.446 (106.6×512.6 at 40.4, 190). */
.sign-up-slide-layer--2-phone::after {
  background: radial-gradient(
    ellipse 25.14% 114.17% at 9.53% 42.32%,
    var(--sign-up-fade) 31.5%,
    color-mix(in srgb, var(--sign-up-fade) 95%, transparent) 35.3%,
    color-mix(in srgb, var(--sign-up-fade) 92%, transparent) 39%,
    color-mix(in srgb, var(--sign-up-fade) 88%, transparent) 41.8%,
    color-mix(in srgb, var(--sign-up-fade) 83%, transparent) 45.6%,
    color-mix(in srgb, var(--sign-up-fade) 78%, transparent) 49.3%,
    color-mix(in srgb, var(--sign-up-fade) 71%, transparent) 53%,
    color-mix(in srgb, var(--sign-up-fade) 64%, transparent) 55.9%,
    color-mix(in srgb, var(--sign-up-fade) 56%, transparent) 59.6%,
    color-mix(in srgb, var(--sign-up-fade) 48%, transparent) 63.4%,
    color-mix(in srgb, var(--sign-up-fade) 40%, transparent) 67.1%,
    color-mix(in srgb, var(--sign-up-fade) 32%, transparent) 70.9%,
    color-mix(in srgb, var(--sign-up-fade) 22%, transparent) 75.6%,
    color-mix(in srgb, var(--sign-up-fade) 16%, transparent) 79.3%,
    color-mix(in srgb, var(--sign-up-fade) 12%, transparent) 83.1%,
    color-mix(in srgb, var(--sign-up-fade) 8%, transparent) 86.9%,
    color-mix(in srgb, var(--sign-up-fade) 4%, transparent) 91.6%,
    color-mix(in srgb, var(--sign-up-fade) 2%, transparent) 95.3%,
    transparent
  );
}

.sign-up-slide-art--5 {
  background-image: url("/icons/sign-up/slide-5.png");
}

.sign-up-slide-art--6 {
  background-image: url("/icons/sign-up/slide-6.png");
}

.sign-up-slide-art--7 {
  background-image: url("/icons/sign-up/slide-7.png");
}

/* The art's fades, from Julian's scrim frames (2026-10-30, five 393×422
   frames measured off his screenshots, one or two ellipses each): the
   slides' edges melt into the page, the band's soft edge bowing on the
   ellipse as on slide 2's L. The half-way level IS the ellipse's edge,
   so each gradient is that ellipse scaled to where the band is gone, and
   the stops are the measured falloff in percent of it. Three bands
   recur:
   - top (3, 4, 6): the ellipse 565 (3) or 586 (4, 6) wide, 147 tall,
     centred 13 below the frame's top — solid to 45, half-way at 86,
     gone by 123;
   - bottom (1, 4, 6): the same ellipse centred 36 below the foot — 97%
     at the foot, half-way at 34, gone by 72;
   - Collections' bottom (5): centred 4 below the foot — solid to 33,
     half-way at 66, gone by 104.
   Under each bottom band a solid foot of 4 art px, melting over 8: the
   ellipse's bow leaves the foot's sides short of solid and the phones'
   bezels showed through there (measured 2026-10-30). Each band is the
   page's own colour, so it follows the theme. ::after sits over the art;
   ::before at z-index -1 behind it, where only the art's clear pixels
   show it (Share's top, behind its phone). Slide 2 has its own L on its
   phone layer; slide 7 has none. */
.sign-up-slide-art {
  --sign-up-fade: var(--background-secondary);
  --sign-up-fade-top: radial-gradient(
    ellipse 107.57% 26.07% at 50% 3.08%,
    var(--sign-up-fade) 29.1%,
    color-mix(in srgb, var(--sign-up-fade) 98%, transparent) 32.7%,
    color-mix(in srgb, var(--sign-up-fade) 97%, transparent) 36.4%,
    color-mix(in srgb, var(--sign-up-fade) 95%, transparent) 40.0%,
    color-mix(in srgb, var(--sign-up-fade) 91%, transparent) 43.6%,
    color-mix(in srgb, var(--sign-up-fade) 87%, transparent) 47.3%,
    color-mix(in srgb, var(--sign-up-fade) 82%, transparent) 50.9%,
    color-mix(in srgb, var(--sign-up-fade) 76%, transparent) 54.5%,
    color-mix(in srgb, var(--sign-up-fade) 69%, transparent) 58.2%,
    color-mix(in srgb, var(--sign-up-fade) 61%, transparent) 61.8%,
    color-mix(in srgb, var(--sign-up-fade) 52%, transparent) 65.5%,
    color-mix(in srgb, var(--sign-up-fade) 43%, transparent) 69.1%,
    color-mix(in srgb, var(--sign-up-fade) 35%, transparent) 72.7%,
    color-mix(in srgb, var(--sign-up-fade) 28%, transparent) 76.4%,
    color-mix(in srgb, var(--sign-up-fade) 21%, transparent) 80.0%,
    color-mix(in srgb, var(--sign-up-fade) 15%, transparent) 83.6%,
    color-mix(in srgb, var(--sign-up-fade) 10%, transparent) 87.3%,
    color-mix(in srgb, var(--sign-up-fade) 7%, transparent) 90.9%,
    color-mix(in srgb, var(--sign-up-fade) 4%, transparent) 94.5%,
    color-mix(in srgb, var(--sign-up-fade) 1%, transparent) 98.2%,
    transparent
  );
  --sign-up-fade-top-wide: radial-gradient(
    ellipse 111.57% 26.07% at 50% 3.08%,
    var(--sign-up-fade) 29.1%,
    color-mix(in srgb, var(--sign-up-fade) 98%, transparent) 32.7%,
    color-mix(in srgb, var(--sign-up-fade) 97%, transparent) 36.4%,
    color-mix(in srgb, var(--sign-up-fade) 95%, transparent) 40.0%,
    color-mix(in srgb, var(--sign-up-fade) 91%, transparent) 43.6%,
    color-mix(in srgb, var(--sign-up-fade) 87%, transparent) 47.3%,
    color-mix(in srgb, var(--sign-up-fade) 82%, transparent) 50.9%,
    color-mix(in srgb, var(--sign-up-fade) 76%, transparent) 54.5%,
    color-mix(in srgb, var(--sign-up-fade) 69%, transparent) 58.2%,
    color-mix(in srgb, var(--sign-up-fade) 61%, transparent) 61.8%,
    color-mix(in srgb, var(--sign-up-fade) 52%, transparent) 65.5%,
    color-mix(in srgb, var(--sign-up-fade) 43%, transparent) 69.1%,
    color-mix(in srgb, var(--sign-up-fade) 35%, transparent) 72.7%,
    color-mix(in srgb, var(--sign-up-fade) 28%, transparent) 76.4%,
    color-mix(in srgb, var(--sign-up-fade) 21%, transparent) 80.0%,
    color-mix(in srgb, var(--sign-up-fade) 15%, transparent) 83.6%,
    color-mix(in srgb, var(--sign-up-fade) 10%, transparent) 87.3%,
    color-mix(in srgb, var(--sign-up-fade) 7%, transparent) 90.9%,
    color-mix(in srgb, var(--sign-up-fade) 4%, transparent) 94.5%,
    color-mix(in srgb, var(--sign-up-fade) 1%, transparent) 98.2%,
    transparent
  );
  --sign-up-fade-bottom: radial-gradient(
    ellipse 106.10% 25.71% at 50% 108.65%,
    color-mix(in srgb, var(--sign-up-fade) 97%, transparent) 36.4%,
    color-mix(in srgb, var(--sign-up-fade) 94%, transparent) 40.1%,
    color-mix(in srgb, var(--sign-up-fade) 90%, transparent) 43.8%,
    color-mix(in srgb, var(--sign-up-fade) 85%, transparent) 47.5%,
    color-mix(in srgb, var(--sign-up-fade) 80%, transparent) 51.2%,
    color-mix(in srgb, var(--sign-up-fade) 73%, transparent) 54.8%,
    color-mix(in srgb, var(--sign-up-fade) 66%, transparent) 58.5%,
    color-mix(in srgb, var(--sign-up-fade) 58%, transparent) 62.2%,
    color-mix(in srgb, var(--sign-up-fade) 46%, transparent) 65.9%,
    color-mix(in srgb, var(--sign-up-fade) 40%, transparent) 69.6%,
    color-mix(in srgb, var(--sign-up-fade) 32%, transparent) 73.3%,
    color-mix(in srgb, var(--sign-up-fade) 25%, transparent) 77.0%,
    color-mix(in srgb, var(--sign-up-fade) 19%, transparent) 80.6%,
    color-mix(in srgb, var(--sign-up-fade) 13%, transparent) 84.3%,
    color-mix(in srgb, var(--sign-up-fade) 9%, transparent) 88.0%,
    color-mix(in srgb, var(--sign-up-fade) 5%, transparent) 91.7%,
    color-mix(in srgb, var(--sign-up-fade) 2%, transparent) 95.4%,
    transparent
  );
  --sign-up-fade-bottom-wide: radial-gradient(
    ellipse 110.05% 25.71% at 50% 108.65%,
    color-mix(in srgb, var(--sign-up-fade) 97%, transparent) 36.4%,
    color-mix(in srgb, var(--sign-up-fade) 94%, transparent) 40.1%,
    color-mix(in srgb, var(--sign-up-fade) 90%, transparent) 43.8%,
    color-mix(in srgb, var(--sign-up-fade) 85%, transparent) 47.5%,
    color-mix(in srgb, var(--sign-up-fade) 80%, transparent) 51.2%,
    color-mix(in srgb, var(--sign-up-fade) 73%, transparent) 54.8%,
    color-mix(in srgb, var(--sign-up-fade) 66%, transparent) 58.5%,
    color-mix(in srgb, var(--sign-up-fade) 58%, transparent) 62.2%,
    color-mix(in srgb, var(--sign-up-fade) 46%, transparent) 65.9%,
    color-mix(in srgb, var(--sign-up-fade) 40%, transparent) 69.6%,
    color-mix(in srgb, var(--sign-up-fade) 32%, transparent) 73.3%,
    color-mix(in srgb, var(--sign-up-fade) 25%, transparent) 77.0%,
    color-mix(in srgb, var(--sign-up-fade) 19%, transparent) 80.6%,
    color-mix(in srgb, var(--sign-up-fade) 13%, transparent) 84.3%,
    color-mix(in srgb, var(--sign-up-fade) 9%, transparent) 88.0%,
    color-mix(in srgb, var(--sign-up-fade) 5%, transparent) 91.7%,
    color-mix(in srgb, var(--sign-up-fade) 2%, transparent) 95.4%,
    transparent
  );
  --sign-up-fade-collections: radial-gradient(
    ellipse 106.24% 25.57% at 50% 100.92%,
    var(--sign-up-fade) 34.2%,
    color-mix(in srgb, var(--sign-up-fade) 95%, transparent) 37.9%,
    color-mix(in srgb, var(--sign-up-fade) 91%, transparent) 41.6%,
    color-mix(in srgb, var(--sign-up-fade) 87%, transparent) 45.3%,
    color-mix(in srgb, var(--sign-up-fade) 82%, transparent) 49.0%,
    color-mix(in srgb, var(--sign-up-fade) 76%, transparent) 52.7%,
    color-mix(in srgb, var(--sign-up-fade) 69%, transparent) 56.4%,
    color-mix(in srgb, var(--sign-up-fade) 61%, transparent) 60.1%,
    color-mix(in srgb, var(--sign-up-fade) 52%, transparent) 63.9%,
    color-mix(in srgb, var(--sign-up-fade) 43%, transparent) 67.6%,
    color-mix(in srgb, var(--sign-up-fade) 35%, transparent) 71.3%,
    color-mix(in srgb, var(--sign-up-fade) 27%, transparent) 75.0%,
    color-mix(in srgb, var(--sign-up-fade) 21%, transparent) 78.7%,
    color-mix(in srgb, var(--sign-up-fade) 15%, transparent) 82.4%,
    color-mix(in srgb, var(--sign-up-fade) 10%, transparent) 86.1%,
    color-mix(in srgb, var(--sign-up-fade) 7%, transparent) 89.8%,
    color-mix(in srgb, var(--sign-up-fade) 3%, transparent) 93.5%,
    color-mix(in srgb, var(--sign-up-fade) 1%, transparent) 97.2%,
    transparent
  );
  --sign-up-fade-foot: linear-gradient(
      to top,
      var(--sign-up-fade) calc(100% * 4 / var(--sign-up-art-h)),
      transparent calc(100% * 12 / var(--sign-up-art-h))
    );
}

.sign-up-slide-art--1::after,
.sign-up-slide-art--3::after,
.sign-up-slide-art--4::before,
.sign-up-slide-art--4::after,
.sign-up-slide-art--5::after,
.sign-up-slide-art--6::before,
.sign-up-slide-art--6::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Top: over the suitcase's icons and the cup's phone; behind Share's. */
.sign-up-slide-art--3::after {
  background: var(--sign-up-fade-top);
}

.sign-up-slide-art--4::before,
.sign-up-slide-art--6::before {
  background: var(--sign-up-fade-top-wide);
}

.sign-up-slide-art--6::before {
  z-index: -1;
}

/* Bottom: over the phones of Home, the cup, Collections and Share. */
.sign-up-slide-art--1::after {
  background: var(--sign-up-fade-foot), var(--sign-up-fade-bottom);
}

.sign-up-slide-art--4::after,
.sign-up-slide-art--6::after {
  background: var(--sign-up-fade-foot), var(--sign-up-fade-bottom-wide);
}

.sign-up-slide-art--5::after {
  background: var(--sign-up-fade-foot), var(--sign-up-fade-collections);
}

/* The pills follow Apple's timed dotnav (the "Get the highlights" gallery
   on apple.com/macbook-pro/proled-xdr-display, measured 2026-10-28): 8px
   dots at a 24 pitch, the current one widening to 48 over 250ms linear,
   and a fill that grows across it from a hidden 15% nub to 115% over the
   6150ms beat, linear, driving the slide's turn. Their track is white at
   36% on black; ours is the fill token on the page, and the fill is the
   brand gradient, mapped to the pill's full width so it never squashes. */
.sign-up-pills {
  --sign-up-pill-w: 48px;
  display: flex;
  align-items: center;
  /* The buttons' side padding makes the 24 pitch (8 + 16). */
  gap: 0;
  /* 44 tall around the 8 dots: 18 over and under, so the row keeps the
     dots where the 8-tall row had them (the margins give the 18 back). */
  height: 44px;
  margin-top: 10px;
}

/* A pill is a button (Julian, 2026-10-28: a tap goes to that slide) with
   a 44 × 24 hit area around its dot, which is the span inside. */
.sign-up-pill-button {
  display: inline-flex;
  align-items: center;
  padding: 18px 8px;
  margin: 0;
  border: 0;
  background: none;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.sign-up-pill {
  position: relative;
  display: block;
  width: 8px;
  height: 8px;
  border-radius: 10px;
  background: var(--fill-vibrant-primary);
  overflow: hidden;
  transition: width 250ms linear;
}

.sign-up-pill--active {
  width: var(--sign-up-pill-w);
}

/* The fill box starts 15% left of the pill (the hidden nub) and grows to
   115%, so its right edge crosses the pill over the beat. The gradient
   is the pill's width and is anchored to the PILL's left edge, 15% into
   the box — anchored to the box it ran out 7px short, and the fill never
   reached the end (Julian, 2026-10-28). */
.sign-up-pill--active::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 0;
  min-width: 15%;
  margin-left: -15%;
  border-radius: 10px;
  background: var(--gradient-pastel-horizontal) calc(var(--sign-up-pill-w) * 0.15) 0 / var(--sign-up-pill-w) 8px no-repeat;
  animation: sign-up-progress 6150ms linear forwards;
}

@keyframes sign-up-progress {
  from {
    width: 0;
  }
  to {
    width: 115%;
  }
}

/* A new beat on the same pill (a tap on the current one, Julian,
   2026-10-29) must start the fill over, but an element that keeps its class
   keeps its running animation. So odd generations wear these identical
   keyframes under another name (auth.gleam), and a changed animation-name
   starts from the beginning. The pill itself is untouched, so its widening
   transition stays. */
.sign-up-pill--beat-b::after {
  animation-name: sign-up-progress-b;
}

@keyframes sign-up-progress-b {
  from {
    width: 0;
  }
  to {
    width: 115%;
  }
}

/* Autoplay paused (auth.gleam `autoplay`, Julian, 2026-10-29): no beat,
   so no fill to run — the current pill is simply full, as under reduced
   motion below. */
.sign-up-pills--paused .sign-up-pill--active::after {
  animation: none;
  width: 115%;
}

/* No autoplay under reduced motion (carousel.mjs), so no fill to run: the
   current pill is simply full. */
@media (prefers-reduced-motion: reduce) {
  .sign-up-pill {
    transition: none;
  }

  .sign-up-pill--active::after {
    animation: none;
    width: 115%;
  }
}

/* The title stack: the leaving and arriving titles share one grid cell,
   so the block is as tall as the taller of the two. On a turn the leaving
   title scrolls 16 and fades out first (260ms); only then does the next
   scroll 16 in from the other side and fade in (420ms, delayed by the
   leaving's whole run, so the two never share the cell — Julian,
   2026-10-28: "time the outgoing title to not collide with the incoming").
   Downward for next, upward for previous. Reduced motion keeps the fades
   alone, in the same order. */
.sign-up-title-stack {
  display: grid;
  /* 28 under the dots: the pills' buttons already hold 18. */
  padding: 0 16px;
}

.sign-up-title {
  grid-area: 1 / 1;
  margin: 0;
  font: 700 40px/1.2 var(--font-sans);
  letter-spacing: var(--tracking-body);
  text-align: center;
  color: var(--label-primary);
  text-wrap: balance;
}

.sign-up-title--in-down,
.sign-up-title--in-up {
  animation: 420ms cubic-bezier(0.2, 0.8, 0.2, 1) 260ms both;
}

.sign-up-title--out-down,
.sign-up-title--out-up {
  animation: 260ms ease-in both;
  pointer-events: none;
}

.sign-up-title--in-down {
  animation-name: sign-up-title-in-down;
}

.sign-up-title--out-down {
  animation-name: sign-up-title-out-down;
}

.sign-up-title--in-up {
  animation-name: sign-up-title-in-up;
}

.sign-up-title--out-up {
  animation-name: sign-up-title-out-up;
}

@keyframes sign-up-title-in-down {
  from {
    opacity: 0;
    transform: translateY(-16px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes sign-up-title-out-down {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: translateY(16px);
  }
}

@keyframes sign-up-title-in-up {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes sign-up-title-out-up {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: translateY(-16px);
  }
}

@keyframes sign-up-title-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes sign-up-title-fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sign-up-title--in-down,
  .sign-up-title--in-up {
    animation-name: sign-up-title-fade-in;
  }

  .sign-up-title--out-down,
  .sign-up-title--out-up {
    animation-name: sign-up-title-fade-out;
  }
}

.sign-up-actions {
  width: 100%;
  max-width: 392px;
  margin-top: 28px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* Google or Apple starting (iOS SignInView's isLoading, 2026-10-29): the
   ring where the pills are, the pills kept unseen so nothing moves. */
.sign-up-actions--pending {
  position: relative;
}

.sign-up-actions--pending .sign-up-cta {
  visibility: hidden;
}

.sign-up-actions-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -18px 0 0 -18px;
}

/* A failed Google or Apple sign-in, under the pills (a cancel says
   nothing). */
.sign-up-error {
  width: 100%;
  max-width: 392px;
  margin-top: 12px;
  text-wrap: balance;
}

.sign-up-cta {
  height: 48px;
  margin: 0;
  padding: 0 var(--sp-lg);
  border: 0;
  border-radius: var(--radius-full);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: var(--background-primary);
  color: var(--label-primary);
  font-family: inherit;
  font-size: var(--text-2xl);
  font-weight: 500;
  line-height: 1;
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.sign-up-cta--phone .icon {
  color: var(--accent-blue);
}

/* Apple's mark on its own ink: black on light, white on dark. */
.sign-up-cta--apple {
  background: var(--label-primary);
  color: var(--background-primary);
}

.sign-up-cta-icon,
.sign-up-cta .icon {
  width: 24px;
  height: 24px;
}

/* Phones: no bar, and the carousel takes what the stack leaves: the
   pills 16 under it, the title 28 under those, the three pills 24 under
   the title with 8 between, 20.5 over the foot plus the home indicator's
   inset. A short screen scrolls. The title and the pills are sign-in's
   393 mock (Julian, 2026-10-30: one framework for both doors — 600
   33/41, 310-wide pills at 19/400 with 22 marks), and the carousel is
   79.22vw wide (Julian, 2026-10-30: carousels and hero art on phones),
   311 at 393, instead of the 10-28 mock's full bleed. */
@media (max-width: 834px) {
  .sign-up {
    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(--background-secondary);
    background-repeat: no-repeat;
  }

  .sign-up-bar {
    display: none;
  }

  /* The screen's height, not more: a failed Google or Apple sign-in's line
     under the pills (2026-10-29) takes its room from the art, which gives
     way down to its floor (iOS's VStack does the same); past that floor a
     short screen scrolls. */
  .sign-up-main {
    height: 100dvh;
    min-height: 100dvh;
    padding: 0;
    padding-bottom: calc(20.5px + var(--phone-bottom-inset));
  }

  /* The art at 79.22vw, under the status bar; the slot takes what the
     stack leaves, the art anchored to its foot (Julian, 2026-10-30: it
     sat at the top before), so it stays by the pills. Its floor is 240 or
     34% of the screen, the smaller (2026-11-06): an SE in a browser tab
     (375×561) left Continue with Apple under the toolbar at a flat 240,
     and the art fits whatever slot it gets (.sign-up-slide-art). */
  .sign-up-carousel {
    flex: 1 1 auto;
    min-height: min(240px, 34dvh);
    max-width: none;
    width: 100dvw;
    margin: 0 auto;
  }

  .sign-up-slide {
    height: auto;
    align-items: flex-end;
  }

  .sign-up-pills {
    /* 16 under the art, less the buttons' 18. */
    margin-top: -2px;
  }

  .sign-up-title {
    font: 600 33px/41px var(--font-sans);
  }

  .sign-up-actions {
    padding: 0 16px;
    gap: 8px;
  }

  .sign-up-cta {
    font-size: 19px;
    font-weight: 400;
  }

  .sign-up-cta-icon,
  .sign-up-cta .icon {
    width: 22px;
    height: 22px;
  }
}


/* ── Desktop (Julian's mocks, 2026-10-29; app/auth view_sign_up_landing,
   view_sign_in_landing and view_card), measured off their 1440×1024
   frames. The page on the canvas grey, the bar as above with Login at 20.
   No carousel on desktop: `/sign-up` shows one still picture, the app on a
   laptop and a phone (desktop-hero.png, 640×449 from 80 down), `/sign-in`
   the icon cluster (hero.png, 430×456 from 73 down); iOS's large
   title, 34/44 at 600, on two lines; the three 392×48 pills 8 apart from
   685, their labels 19 at 500 and their marks 22.
   The phone step, both doors, drops the card: the bar's flag pill where
   Login was; the title and a 60-tall glass row (the flag, 88 wide, and
   the number at 32) where the picture was; Google and Apple exactly where
   the landing's last two pills sit, under a 21 line of copy. Enter sends
   the code (Continue stays in the form, unseen). Phones see none of it. ── */
.sign-up-hero,
.auth-step-title,
.auth-flag-pill,
.auth-country-chevron,
.auth-alt {
  display: none;
}

/* The row and its picker: the form's own gap between them. */
.auth-phone-field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

/* The auth glow (2026-10-29): Home's header glow (phone_shell.css: the
   green, blue and pink ellipse, full width), breathing as Home's does
   (its glow-breathe, 3.6 s up 24 and back). Since 2026-10-30 it is its
   own element, `.auth-glow` (app/auth glow, first in every auth screen
   and in priming), not a pseudo-element in the background: it layers OVER
   the art like a scrim (Julian: "that way we can adjust which layers it
   obscures or not"). `--auth-glow-z` is that dial, per screen or per
   theme: 2 (the default) sits over the art and the copy, -1 behind
   everything. The chrome — the bar, the phone headers — is always above
   it. Each width gives the ellipse its place. */
.sign-up,
.auth-screen,
.priming {
  position: relative;
  isolation: isolate;
}

.auth-glow {
  --auth-glow-z: 2;
  position: absolute;
  z-index: var(--auth-glow-z);
  left: 0;
  right: 0;
  top: calc(var(--phone-top-inset) - 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%
  );
  opacity: var(--home-glow-opacity);
  filter: blur(16px);
  pointer-events: none;
  animation: glow-breathe 3.6s cubic-bezier(0.37, 0, 0.63, 1) infinite alternate;
}

.sign-up-bar,
.auth-nav-bar {
  position: relative;
  z-index: 3;
}

@media (prefers-reduced-motion: reduce) {
  .auth-glow {
    animation: none;
  }
}

@media (min-width: 835px) {
  .sign-up,
  .auth-screen--phone,
  .auth-screen--otp {
    background: var(--background-secondary);
  }

  /* The glow on desktop too (Julian, 2026-10-29: "add our breathing
     gradient for desktop views as well"), under the bar: the screen's
     width, 240 tall, its foot 44 down, blurred 24 — the phones' soft band
     at 1440. */
  .auth-glow {
    top: -196px;
    height: 240px;
    filter: blur(24px);
  }

  .sign-up-bar {
    padding: 18px 64px 30px;
  }

  /* The phone step's bar keeps the 84 its flag pill gave it (the pill
     went 2026-10-29: the row carries the flag), so its column stays where
     the mock put it. */
  .auth-screen--phone .sign-up-bar {
    min-height: 84px;
  }

  .sign-up-login {
    font-size: 20px;
  }

  .sign-up-main {
    padding-top: 70px;
  }

  .sign-up-carousel,
  .sign-up-pills,
  .sign-up-title-stack {
    display: none;
  }

  .sign-up-hero {
    display: flex;
    flex-direction: column;
    align-items: center;
  }

  .sign-up-hero-art {
    width: 380px;
    height: 362px;
    background: url("/icons/sign-up/hero.png") center / contain no-repeat;
  }

  /* `/sign-up`'s devices (desktop-hero.png, 1280×898 at 2x: the laptop's
     base 600 wide as in the mock): 80 down (the main's 70 less 74, its top
     14 of air under the bar), 45 over the title, so the title and the
     pills sit where the cluster's do. */
  .sign-up-hero--devices {
    margin-top: -74px;
  }

  .sign-up-hero--devices .sign-up-hero-art {
    width: 640px;
    height: 449px;
    background-image: url("/icons/sign-up/desktop-hero.png");
  }

  .sign-up-hero--devices .sign-up-hero-title {
    margin-top: 45px;
  }

  /* `/sign-in`'s cluster, Julian's export (471×500, transparent, the ink
     403×382 at 34/94): at the mock's 366 ink wide the box is 430×456, 73
     down (the main's 70 less 81), 45 over the title, so the title and the
     pills sit where sign-up's do. */
  .sign-up--login .sign-up-hero {
    margin-top: -81px;
  }

  .sign-up--login .sign-up-hero-art {
    width: 430px;
    height: 456px;
  }

  .sign-up--login .sign-up-hero-title {
    margin-top: 45px;
  }

  .sign-up-hero-title,
  .auth-screen--phone .auth-step-title {
    margin: 0;
    font: 600 34px/41px var(--font-sans);
    text-align: center;
    color: var(--label-primary);
  }

  .sign-up-hero-title {
    margin-top: 58px;
    line-height: 44px;
  }

  .sign-up-actions {
    margin-top: 24px;
    gap: 8px;
  }

  .sign-up-cta {
    font-size: 19px;
  }

  .sign-up-cta-icon,
  .sign-up-cta .icon {
    width: 22px;
    height: 22px;
  }

  /* The phone step: no card, the bar over the column. */
  .auth-screen--phone {
    flex-direction: column;
    padding: 0;
  }

  .auth-screen--phone .auth-card {
    margin: 0 auto;
    padding: 70px 0 48px;
    max-width: 464px;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  .auth-screen--phone .auth-logo,
  .auth-screen--phone .auth-heading,
  .auth-screen--phone .auth-country-code,
  .auth-screen--phone .auth-country-caret {
    display: none;
  }

  .auth-screen--phone .auth-form {
    gap: 0;
  }

  .auth-screen--phone .auth-step-title {
    display: block;
    margin-top: 150px;
  }

  /* A Google or Apple sign-up waiting on the number (iOS's
     socialSubtitle, 2026-10-29). No mock: the alt caption's 21, in the
     secondary ink, 8 under the title. */
  .auth-screen--phone .auth-phone-subtitle {
    margin: 8px 0 0;
    font: 400 21px/1.25 var(--font-sans);
    color: var(--label-secondary);
  }

  .auth-screen--phone .auth-phone-field {
    position: relative;
    margin-top: 26px;
  }

  .auth-screen--phone .auth-phone-row {
    gap: 10px;
  }

  /* The row's pills: glass.css's recipe, thin, written out (phones write
     their own pill). */
  .auth-screen--phone .auth-country-button,
  .auth-screen--phone .auth-phone-input {
    border: 0;
    border-radius: var(--radius-full);
    background:
      var(--glass-tint),
      linear-gradient(var(--material-thin-1), var(--material-thin-1)),
      linear-gradient(var(--material-thin-2), var(--material-thin-2));
    -webkit-backdrop-filter: blur(var(--material-blur));
    backdrop-filter: blur(var(--material-blur));
    box-shadow: var(--glass-rim), var(--glass-shadow);
    color: var(--label-primary);
  }

  .auth-screen--phone .auth-phone-input:focus {
    box-shadow: var(--glass-rim), 0 0 0 2px var(--accent-blue), var(--glass-shadow);
  }

  .auth-screen--phone .auth-country-button {
    flex-shrink: 0;
    width: 88px;
    height: 60px;
    padding: 0;
    justify-content: center;
    gap: 8px;
  }

  .auth-screen--phone .auth-country-flag {
    font-size: 32px;
    line-height: 1;
  }

  .auth-country-chevron {
    color: var(--label-secondary);
  }

  .auth-screen--phone .auth-country-chevron {
    display: inline-flex;
  }

  .auth-screen--phone .auth-country-chevron .icon {
    width: 20px;
    height: 20px;
  }

  .auth-screen--phone .auth-phone-input {
    height: 60px;
    padding: 0 16px;
    font-size: 32px;
    text-align: left;
    letter-spacing: normal;
  }

  /* Drops over what follows, from the row's foot. */
  .auth-screen--phone .auth-country-picker {
    position: absolute;
    top: calc(100% + 8px);
    left: 0;
    right: 0;
    margin: 0;
    box-shadow: var(--shadow);
  }

  .auth-screen--phone .auth-error {
    margin-top: var(--sp-md);
  }

  /* Continue, the row's width, 24 under it (Julian's mock, 2026-11-05:
     the step gave no sign it could go on — Continue was in the form,
     unseen, for Enter): the phones' 48 pill, dimmed until the number is
     whole, then white with the label in the accent. */
  .auth-screen--phone .auth-submit {
    width: 100%;
    height: 48px;
    margin: 24px 0 0;
    padding: 0;
    border-radius: var(--radius-full);
    background: var(--fill-quaternary);
    color: var(--label-tertiary);
    font: 400 19px/1 var(--font-sans);
  }

  .auth-screen--phone .auth-submit:not(:disabled) {
    background: var(--background-primary);
    color: var(--accent-blue);
    box-shadow: var(--shadow);
  }

  /* Google and Apple stay on the landing's last two pills: the 252 the
     row's foot gave them, less Continue's 72. */
  .auth-screen--phone .auth-alt {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-top: 180px;
  }

  .auth-alt-caption {
    margin: 0;
    font-size: 21px;
    line-height: 1.25;
    /* The secondary ink (Julian, 2026-10-29): the tertiary all but went
       in dark mode. */
    color: var(--label-secondary);
  }

  .auth-alt .sign-up-actions {
    max-width: 392px;
    margin-top: 32px;
  }

  /* ── The code step (Julian's mock, 2026-11-05) on the phone step's
     column, no card: the bar over it, "Enter the code we sent to" and the
     number as two title lines where the phone step's title sits, the six
     boxes phones wear (iOS's OTPDigitBoxView: 55×64, 12 round, on glass)
     24 under them, Resend as a plain link 32 under the boxes — "Signing
     in…" in its place while the code is checked — and Back under that.
     The card's bordered 48×56 boxes went with the card. ── */
  .auth-screen--otp {
    flex-direction: column;
    padding: 0;
  }

  .auth-screen--otp .sign-up-bar {
    min-height: 84px;
  }

  .auth-screen--otp .auth-card {
    margin: 0 auto;
    padding: 70px 0 48px;
    max-width: 464px;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  .auth-screen--otp .auth-logo,
  .auth-screen--otp .auth-otp-lead {
    display: none;
  }

  .auth-screen--otp .auth-form {
    gap: 0;
  }

  .auth-screen--otp .auth-otp-title,
  .auth-screen--otp .auth-otp-subtitle {
    margin: 0;
    font: 600 34px/41px var(--font-sans);
    text-align: center;
    color: var(--label-primary);
  }

  .auth-screen--otp .auth-otp-title {
    margin-top: 150px;
  }

  .auth-screen--otp .auth-otp-wrapper {
    margin-top: 24px;
  }

  .auth-screen--otp .auth-otp-display {
    gap: 8px;
  }

  .auth-screen--otp .auth-otp-box {
    width: 55px;
    height: 64px;
    border: 0;
    border-radius: 12px;
    background:
      var(--glass-tint),
      linear-gradient(var(--material-thin-1), var(--material-thin-1)),
      linear-gradient(var(--material-thin-2), var(--material-thin-2));
    -webkit-backdrop-filter: blur(var(--material-blur));
    backdrop-filter: blur(var(--material-blur));
    box-shadow: var(--glass-rim), var(--glass-shadow);
    color: var(--label-primary);
    font: 600 26px/1 var(--font-sans);
    font-variant-numeric: tabular-nums;
  }

  .auth-screen--otp .auth-otp-box-active {
    box-shadow: var(--glass-rim), 0 0 0 1px var(--label-tertiary), var(--glass-shadow);
  }

  .auth-screen--otp .auth-otp-hidden-input:focus-visible + .auth-otp-display .auth-otp-box-active {
    box-shadow: var(--glass-rim), 0 0 0 2px var(--accent-blue), var(--glass-shadow);
  }

  .auth-screen--otp .auth-error {
    margin-top: var(--sp-md);
  }

  .auth-screen--otp .auth-resend {
    margin: 32px 0 0;
    font: 400 17px/22px var(--font-sans);
    color: var(--label-tertiary);
  }

  .auth-screen--otp .auth-resend-btn {
    color: var(--accent-blue);
  }

  .auth-screen--otp .auth-resend-btn:disabled {
    color: var(--label-tertiary);
  }

  .auth-screen--otp .auth-back {
    justify-content: center;
    margin: 16px 0 0;
    font: 400 15px/20px var(--font-sans);
    color: var(--label-tertiary);
  }

  /* ── The name, email and new-password steps (2026-11-06) on the phone
     step's column, no card — desktop was the last width on the old card:
     the bar over it, the title where the phone step's sits (34 on 41,
     150 under the bar), the line 8 under it in the alt caption's 21, the
     labelled field (iOS's FormField: the 13 label, a 48 field on glass)
     26 under, Continue the column's width 24 under the field — dimmed
     until it can go on, then white with the label in the accent, as the
     phone step's — and "I already have an account" (or "Forgot
     password?") as the ghost pill 8 under it. ── */
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) {
    flex-direction: column;
    padding: 0;
    background: var(--background-secondary);
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .sign-up-bar {
    min-height: 84px;
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-card {
    margin: 0 auto;
    padding: 70px 0 48px;
    max-width: 464px;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-logo {
    display: none;
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-form {
    gap: 0;
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-title {
    margin: 150px 0 0;
    font: 600 34px/41px var(--font-sans);
    text-align: center;
    color: var(--label-primary);
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-subtitle {
    margin: 8px 0 0;
    font: 400 21px/1.25 var(--font-sans);
    text-align: center;
    color: var(--label-secondary);
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-field,
  :is(.auth-screen--email, .auth-screen--new-password) .auth-fields {
    margin-top: 26px;
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-field {
    display: flex;
    flex-direction: column;
    gap: 5px;
  }

  :is(.auth-screen--email, .auth-screen--new-password) .auth-fields .auth-name-field {
    margin-top: 0;
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-label {
    font: 400 13px/16px var(--font-sans);
    color: var(--label-secondary);
  }

  /* The field on the row's glass (the phone step's recipe), 48 tall. */
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-input {
    height: 48px;
    padding: 0 16px;
    border: 0;
    border-radius: 14px;
    background:
      var(--glass-tint),
      linear-gradient(var(--material-thin-1), var(--material-thin-1)),
      linear-gradient(var(--material-thin-2), var(--material-thin-2));
    -webkit-backdrop-filter: blur(var(--material-blur));
    backdrop-filter: blur(var(--material-blur));
    box-shadow: var(--glass-rim), var(--glass-shadow);
    font: 400 17px/22px var(--font-sans);
    color: var(--label-primary);
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-input:focus {
    outline: none;
    box-shadow: var(--glass-rim), 0 0 0 2px var(--accent-blue), var(--glass-shadow);
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-error {
    margin-top: var(--sp-md);
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-submit {
    width: 100%;
    height: 48px;
    margin: 24px 0 0;
    padding: 0;
    border-radius: var(--radius-full);
    background: var(--fill-quaternary);
    color: var(--label-tertiary);
    font: 400 19px/1 var(--font-sans);
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-submit:not(:disabled) {
    background: var(--background-primary);
    color: var(--accent-blue);
    box-shadow: var(--shadow);
  }

  /* iOS's ghost PillButton: the pill's box, the label in the secondary ink. */
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-have-account {
    width: 100%;
    height: 48px;
    margin: 8px 0 0;
    padding: 0;
    border: 0;
    border-radius: var(--radius-full);
    background: none;
    font: 400 17px/22px var(--font-sans);
    color: var(--label-secondary);
  }
}

/* ── Phones (Julian's 393 mock, 2026-10-29), measured off its 2x frames.
   `/sign-up` keeps its carousel (the block above). `/sign-in`'s landing
   (.sign-up--login) is still: the icon cluster (394×418), "Click below to
   / login to Please." at 33/41 600 and the three pills 310 wide, 8 apart,
   labels 19 — the stack anchored to the foot, 20.5 over the home
   indicator's inset, the picture taking what is left above it.
   The phone step, both doors, drops the card: the phone header (back to
   the landing, the flag pill trailing), a box per digit under it (the
   input unseen over them, as the code step does), the title where the
   landing's sits and Continue where its Apple pill sits. The step is the
   visual viewport's height (--vvh, dom.mjs), so with the keyboard up its
   gaps give way and Continue stays in reach. ── */
@media (max-width: 834px) {
  /* The canvas under the header glow (.auth-glow, above: full width, 170
     tall, its foot 5 under the top inset, blurred 16). */
  .sign-up--login,
  .auth-screen--phone,
  .auth-screen--otp,
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password),
  .auth-screen--creating {
    background: var(--background-secondary);
  }

  .sign-up--login .sign-up-main {
    padding-bottom: var(--phone-bottom-inset);
  }

  .sign-up--login .sign-up-hero {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    /* Edge to edge: the main has no side gutters on a phone (the pills
       keep their own 16), so the full width is the screen's. It took back
       16 a side it no longer had, and that overhang made the page pan
       sideways by 16 (Julian, Brave, 2026-11-05). */
    width: 100%;
    margin: 0;
    padding-top: var(--phone-top-inset);
    /* Down to the art's own floor when something under the pills needs
       the room (the main is the screen's height). */
    min-height: 0;
  }

  /* Julian's export (471×500, transparent, the cluster's ink 403×382 at
     34/94): at the mock's 337.5 ink wide the box is 394×418, its foot at
     481, edge to edge on a 393 phone. */
  .sign-up--login .sign-up-hero-art {
    flex: 0 1 418px;
    min-height: 160px;
    /* 79.22vw (Julian, 2026-10-30), 311 at 393; the mock drew it 394. */
    width: 79.22vw;
    background: url("/icons/sign-up/hero.png") center bottom / contain no-repeat;
  }

  /* The mock breaks the step's title "Enter your phone / number": no
     balancing. */
  .sign-up--login .sign-up-hero-title,
  .auth-screen--phone .auth-step-title {
    margin: 0;
    font: 600 33px/41px var(--font-sans);
    text-align: center;
    color: var(--label-primary);
  }

  /* The steps' titles at 34 on 120% (Julian's mocks, 2026-10-30), 80 under
     the header; their input 24 under the text. */
  .auth-screen--phone .auth-step-title,
  .auth-screen--otp .auth-otp-title {
    font: 600 34px/1.2 var(--font-sans);
  }

  .sign-up--login .sign-up-hero-title {
    margin-top: 22.5px;
  }

  /* The pills as sign-up's (Julian, 2026-10-30: full width, 16 in from
     the edges, in the block above); only the gap to the title is its own. */
  .sign-up--login .sign-up-actions {
    margin-top: 52px;
    gap: 8px;
  }

  .sign-up--login .sign-up-cta {
    font-size: 19px;
    font-weight: 400;
  }

  /* The phone, code, name and creating-account steps: the visible
     height, a column under the header (the last has none). */
  .auth-screen--phone,
  .auth-screen--otp,
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password),
  .auth-screen--creating {
    height: var(--vvh, 100dvh);
    flex-direction: column;
    padding: 0;
  }

  .auth-screen--phone .auth-card,
  .auth-screen--otp .auth-card,
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-card,
  .auth-screen--creating .auth-card {
    flex: 1 1 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    margin: 0;
    max-width: none;
    padding: 0 16px calc(20.5px + var(--phone-bottom-inset));
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  .auth-screen--phone .auth-logo,
  .auth-screen--phone .auth-heading,
  .auth-screen--otp .auth-logo,
  .auth-screen--otp .auth-back,
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-logo,
  .auth-screen--creating .auth-logo {
    display: none;
  }

  /* The phone step (Julian's 402 mocks, 2026-10-29, second round): the
     title 75 under the header, the number in one glass pill 32 under it,
     Continue at the foot (the landing's Apple pill's line). With the
     keyboard up (--vvh) the space over Continue goes first, then the gap
     over the title, so Continue rides just over the keyboard. */
  .auth-screen--phone .auth-form {
    flex: 1 1 0;
    min-height: 0;
    display: grid;
    grid-template-rows: minmax(16px, 80px) auto auto auto auto minmax(16px, 1fr) auto;
    gap: 0;
  }

  .auth-screen--phone .auth-step-title {
    display: block;
    grid-row: 2;
  }

  /* A Google or Apple sign-up waiting on the number (iOS's
     socialSubtitle, 2026-10-29): the name step's subtitle, under the
     title. */
  .auth-screen--phone .auth-phone-subtitle {
    grid-row: 3;
    margin: 6px 0 0;
    font: 400 17px/22px var(--font-sans);
    text-align: center;
    text-wrap: balance;
    color: var(--label-secondary);
  }

  /* The number: the mock's 48-tall glass pill, 20 in from the screen's
     edges (the card's 16, plus 4); the flag lives in the header. */
  .auth-screen--phone .auth-phone-field {
    grid-row: 4;
    position: relative;
    margin: 24px 4px 0;
  }

  .auth-screen--phone .auth-error {
    grid-row: 5;
    align-self: start;
    margin-top: 12px;
  }

  .auth-screen--phone .auth-country-button {
    display: none;
  }

  /* glass.css's recipe, thin, written out as desktop's row is. 22 at
     regular (iOS title2), so iOS doesn't zoom on focus. */
  .auth-screen--phone .auth-phone-input {
    width: 100%;
    height: 48px;
    padding: 0 16px;
    border: 0;
    border-radius: var(--radius-full);
    background:
      var(--glass-tint),
      linear-gradient(var(--material-thin-1), var(--material-thin-1)),
      linear-gradient(var(--material-thin-2), var(--material-thin-2));
    -webkit-backdrop-filter: blur(var(--material-blur));
    backdrop-filter: blur(var(--material-blur));
    box-shadow: var(--glass-rim), var(--glass-shadow);
    color: var(--label-primary);
    font: 400 22px/1 var(--font-sans);
    letter-spacing: normal;
    text-align: left;
  }

  .auth-screen--phone .auth-phone-input::placeholder {
    color: var(--label-tertiary);
  }

  .auth-screen--phone .auth-phone-input:focus {
    outline: none;
    box-shadow: var(--glass-rim), 0 0 0 2px var(--accent-blue), var(--glass-shadow);
  }

  /* The code step's six boxes (iOS's OTPDigitBoxView: 64 tall, 12 round). */
  .auth-screen--otp .auth-otp-box {
    flex: 1 1 0;
    min-width: 0;
    max-width: 44px;
    width: auto;
    height: 64px;
    margin: 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--border-secondary);
    border-radius: 12px;
    background: color-mix(in srgb, var(--fill-quaternary) 25%, transparent);
    color: var(--label-primary);
    font: 600 26px/1 var(--font-sans);
    font-variant-numeric: tabular-nums;
  }

  .auth-screen--otp .auth-otp-box-active {
    border-color: var(--label-tertiary);
  }

  .auth-screen--otp .auth-otp-hidden-input:focus-visible + .auth-otp-display .auth-otp-box-active {
    border-color: var(--accent-blue);
  }

  /* Drops over what follows, from the pill's foot. */
  .auth-screen--phone .auth-country-picker {
    position: absolute;
    top: calc(100% + 8px);
    left: 0;
    right: 0;
    z-index: 2;
    margin: 0;
    box-shadow: var(--shadow);
  }

  /* iOS's CapsuleButton (filledDark): 310×48, the fill and label dimmed
     while the number (the name) isn't whole. */
  .auth-screen--phone .auth-submit,
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-submit,
  .auth-screen--creating .auth-submit {
    grid-row: 6;
    justify-self: center;
    width: 100%;
    max-width: 310px;
    height: 48px;
    margin: 0;
    padding: 0;
    border-radius: var(--radius-full);
    font-size: 19px;
    font-weight: 400;
  }

  .auth-screen--phone .auth-submit:disabled,
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-submit:disabled {
    background: var(--fill-quaternary);
    color: var(--label-tertiary);
  }

  /* The foot's track; the mock's Continue here is 286 wide. */
  .auth-screen--phone .auth-submit {
    grid-row: 7;
    max-width: 286px;
  }

  /* The name step (iOS's NameEntryView, 2026-10-29) on the landing's
     frame: "Your Name" where the landings' titles sit (501.5 at the 393
     mock), its line and the "Full Name" field under it, Continue and "I
     already have an account" where the landing's last two pills sit (693.5
     and 749.5), 20.5 over the inset. The copy and the field sit CENTRED
     between the header and the buttons (Julian, 2026-11-05: they hung at
     the foot before, a screen of nothing above them): the two flexible
     tracks share the room, and both give way — to an error, or to the
     keyboard (--vvh) — so Continue stays in reach. The field is the
     pills' 310, as iOS's inset FormField is. */
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-form {
    flex: 1 1 0;
    min-height: 0;
    display: grid;
    grid-template-rows: minmax(16px, 1fr) auto auto auto auto minmax(16px, 1fr) auto auto;
    gap: 0;
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-title {
    grid-row: 2;
    margin: 0;
    font: 600 33px/41px var(--font-sans);
    text-align: center;
    color: var(--label-primary);
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-subtitle {
    grid-row: 3;
    margin: 6px 0 0;
    font: 400 17px/22px var(--font-sans);
    text-align: center;
    color: var(--label-secondary);
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-field {
    grid-row: 4;
    justify-self: center;
    width: 100%;
    max-width: 310px;
    margin-top: 16px;
    gap: 5px;
    /* The field's 17: base.css floors a phone's inputs at max(16px, 1em),
       the em this box's. */
    font-size: 17px;
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-label {
    font: 400 13px/16px var(--font-sans);
    color: var(--label-secondary);
  }

  /* iOS's body 17, over the 16 under which iOS zooms on focus. */
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-input {
    height: 48px;
    padding: 0 16px;
    border: 1px solid var(--border-secondary);
    border-radius: 14px;
    background: var(--background-primary);
    font: 400 17px/22px var(--font-sans);
    color: var(--label-primary);
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-name-input:focus {
    border-color: var(--label-tertiary);
  }

  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-error {
    grid-row: 5;
    align-self: start;
    margin: 8px 0 0;
  }

  /* Continue on the foot's track, under the centred block's room. */
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-submit {
    grid-row: 7;
  }

  /* iOS's ghost PillButton: the pill's box, the label in the secondary
     ink. */
  :is(.auth-screen--name, .auth-screen--email, .auth-screen--new-password) .auth-have-account {
    grid-row: 8;
    justify-self: center;
    width: 100%;
    max-width: 310px;
    height: 48px;
    margin-top: 8px;
    padding: 0;
    border-radius: var(--radius-full);
    font: 400 17px/22px var(--font-sans);
  }

  /* "Creating your account…" (iOS's CreatingAccountView, 2026-10-29) on
     the landing's frame, no header (iOS gives it no back). The copy's top
     line sits where the landings' titles sit (501.5 at the 393 mock), the
     spinner or the warning sign 16 over it. After a failure the message
     follows the title, and Try again and Start over sit where the name
     step's Continue and "I already have an account" sit (693.5 and 749.5)
     — iOS's order, the plain-text one last. The foot keeps the buttons'
     104 whether they show or not, and the copy's track runs to 192 (the
     name step's title top to Continue) so the line holds still from
     spinner to failure; a longer copy climbs into the space above, 16
     kept over Try again. */
  .auth-screen--creating .auth-form {
    flex: 1 1 0;
    min-height: 0;
    display: grid;
    grid-template-rows: minmax(16px, 1fr) auto minmax(auto, 192px) 48px 56px;
    justify-items: stretch;
    gap: 0;
  }

  .auth-screen--creating .auth-creating-art {
    grid-row: 2;
    justify-self: center;
    margin-bottom: 16px;
  }

  .auth-screen--creating .auth-creating-copy {
    grid-row: 3;
    align-self: start;
    gap: 0;
    padding-bottom: 16px;
  }

  .auth-screen--creating .auth-creating-title {
    margin: 0;
    font: 600 33px/41px var(--font-sans);
    text-align: center;
    color: var(--label-primary);
  }

  .auth-screen--creating .auth-creating-message {
    margin: 8px 0 0;
    font: 400 17px/22px var(--font-sans);
    text-align: center;
    color: var(--label-secondary);
  }

  .auth-screen--creating .auth-submit {
    grid-row: 4;
    align-self: auto;
  }

  /* iOS's ghost PillButton, as on the name step. */
  .auth-screen--creating .auth-start-over {
    grid-row: 5;
    justify-self: center;
    align-self: end;
    width: 100%;
    max-width: 310px;
    height: 48px;
    margin: 0;
    padding: 0;
    border-radius: var(--radius-full);
    font: 400 17px/22px var(--font-sans);
  }

  /* The code step (iOS's OTPVerificationView, 2026-10-29) on the phone
     step's frame, so nothing moves between the two: the header (back to a
     fresh phone step), the six boxes where the number's sat (136 under
     the header), Clerk's message under them, "Enter the code sent to" and
     the number where the phone step's title sits, and Resend — iOS's ghost
     PillButton, plain text — where its Continue sits (749.5 at the 393
     mock), 20.5 over the inset. The message has a track of its own, so
     it never runs into the title. With the keyboard up (--vvh) the space
     under the boxes goes first, then the other two share what is left. */
  .auth-screen--otp .auth-form {
    flex: 1 1 0;
    min-height: 0;
    display: grid;
    grid-template-rows: minmax(16px, 80px) auto auto auto auto minmax(16px, 1fr) auto;
    gap: 0;
  }

  .auth-screen--otp .auth-otp-title {
    grid-row: 2;
    margin: 0;
    text-align: center;
    color: var(--label-primary);
  }

  /* Desktop's " we sent to": phones say "Sent to" under the title. */
  .auth-screen--otp .auth-otp-title-tail {
    display: none;
  }

  /* "Sent to" and the number: 20 on 120% in the secondary ink, 6 under
     the title. */
  .auth-screen--otp .auth-otp-subtitle {
    grid-row: 3;
    margin: 6px 0 0;
    font: 400 20px/1.2 var(--font-sans);
    text-align: center;
    color: var(--label-secondary);
  }

  .auth-screen--otp .auth-otp-wrapper {
    grid-row: 4;
    margin-top: 24px;
  }

  .auth-screen--otp .auth-otp-display {
    gap: 8px;
    width: 100%;
  }

  /* The mock's six: 55 wide (what a 402 fits with 16 insets and 8
     between; a 393 shares the gap), 64 tall, 12 round, on the glass the
     phone step's pill wears. */
  .auth-screen--otp .auth-otp-box {
    max-width: 55px;
    border: 0;
    background:
      var(--glass-tint),
      linear-gradient(var(--material-thin-1), var(--material-thin-1)),
      linear-gradient(var(--material-thin-2), var(--material-thin-2));
    -webkit-backdrop-filter: blur(var(--material-blur));
    backdrop-filter: blur(var(--material-blur));
    box-shadow: var(--glass-rim), var(--glass-shadow);
  }

  .auth-screen--otp .auth-otp-box-active {
    box-shadow: var(--glass-rim), 0 0 0 1px var(--label-tertiary), var(--glass-shadow);
  }

  .auth-screen--otp .auth-otp-hidden-input:focus-visible + .auth-otp-display .auth-otp-box-active {
    box-shadow: var(--glass-rim), 0 0 0 2px var(--accent-blue), var(--glass-shadow);
  }

  /* 16, so iOS doesn't zoom on focus. */
  .auth-screen--otp .auth-otp-hidden-input {
    font-size: 16px;
  }

  .auth-screen--otp .auth-error {
    grid-row: 5;
    align-self: start;
    margin-top: 12px;
  }

  .auth-screen--otp .auth-resend {
    grid-row: 7;
    justify-self: center;
    width: 100%;
    max-width: 310px;
    margin: 0;
  }

  .auth-screen--otp .auth-resend-btn {
    width: 100%;
    height: 48px;
    border-radius: var(--radius-full);
    font: 400 17px/22px var(--font-sans);
    color: var(--label-secondary);
    -webkit-tap-highlight-color: transparent;
  }

  /* Counting down: the tertiary ink, as iOS dims it. */
  .auth-screen--otp .auth-resend-btn:disabled {
    color: var(--label-tertiary);
    cursor: default;
    text-decoration: none;
  }

  /* "Signing in…" on Resend's line, in its pill's 48. */
  .auth-screen--otp .auth-signing-in {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 48px;
    font: 400 17px/22px var(--font-sans);
    color: var(--label-tertiary);
  }

  /* The mock's header: 40 discs, and the flag pill 88×40, in the discs'
     tinted glass with a hairline ring and no drop shadow. */
  /* Scoped by the bar: glass.css loads after this sheet, and the glass
     surface's own shadow would win at one class. */
  .auth-nav-bar .nav-bar-disc {
    width: 40px;
  }

  .auth-nav-bar .nav-bar-disc,
  .auth-nav-bar .auth-flag-pill--phone {
    height: 40px;
    box-shadow: var(--glass-rim-disc), 0 0 0 0.5px var(--border-secondary);
  }

  .auth-flag-pill--phone {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 88px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-full);
    --glass-tint-color: var(--background-primary);
    color: var(--label-primary);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }

  .auth-flag-pill--phone .auth-country-flag {
    font-size: 32px;
    line-height: 1;
  }

  .auth-flag-pill--phone .icon {
    color: var(--label-secondary);
  }
}

/* Sign-up's phone canvas is plain (its 10-28 wash went 2026-10-30 when
   the glow became the element above; the glow is the only colour). */
@media (max-width: 834px) {
  .sign-up:not(.sign-up--login) {
    background: var(--background-secondary);
  }
}

/* ── Email + password (iOS's EmailEntryView / NewPasswordView, 2026-10-31).
   The email and new-password steps wear the name step's clothes (the
   `:is(.auth-screen--name, …)` rules above): the same title, line, field
   and ghost button. Two labelled fields stack inside the one field slot. */
.auth-fields {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

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

@media (max-width: 834px) {
  :is(.auth-screen--email, .auth-screen--new-password) .auth-fields {
    gap: 12px;
  }

  :is(.auth-screen--email, .auth-screen--new-password) .auth-field-stack {
    gap: 5px;
  }
}

/* The permission screens on phones (iOS PermissionPriming) — app/priming. */
/* The permission screens on phones (iOS's PermissionStepView, 2026-10-29;
   the soft gate of Julian's mocks, 2026-11-05; app/priming), on the
   landing's frame: the canvas under iOS's header glow, the art at the top,
   the 33/41 title and its line in the secondary label over the one accent
   pill, and "I'll do this later" under the pill. Measured off the 393×852
   mocks: the pill at 714 and the link's 19.5 line 16 under it, 20.5 over
   the home indicator's inset; the line of copy 84 over the pill, the title
   over it (539 with a line of each, 498 with a two-line title), so the
   copy holds its place whatever the picture; its track runs to 248 (the
   pill's top up), and a longer copy climbs into the art's space. Shown on
   phones only, but laid out at any width: a window widened mid-queue keeps
   a working screen. */
.priming {
  position: relative;
  isolation: isolate;
  box-sizing: border-box;
  height: var(--vvh, 100dvh);
  overflow: hidden;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto 48px 35.5px;
  justify-items: center;
  padding: 0 16px calc(20.5px + var(--phone-bottom-inset));
  background: var(--background-secondary);
}

/* Home's glow is the landing's `.auth-glow` element (auth.css; app/auth
   glow, drawn first here too), over the art. */

/* The art's slot: edge to edge (the 16 gutters taken back), everything
   above the copy less 8 — the copy hangs from the pill (its own row, 84
   over it), so the slot is whatever room the window leaves. The pictures
   (Julian's Permissions assets, 2026-11-05, 2× over the 393 frame, in
   icons/priming) stack in it as iOS's ZStack of scaledToFit layers, placed
   off the mocks: the phones' bezels sit at 86, so calendar's frame starts
   at 67 and location's at 63; the Reminders icon's first dot at 190, its
   frame at 60; email's card title at 166, its frame at 62. Notifications
   runs full-bleed from the top of the screen (iOS's startsAtTopOfScreen),
   its bell's foot at 490, in the air over the copy. */
.priming-art {
  grid-row: 1;
  position: relative;
  align-self: stretch;
  justify-self: stretch;
  min-height: 0;
  margin: 0 -16px 8px;
}

.priming-art-layer {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  aspect-ratio: 393 / 464;
  background: center top / 100% auto no-repeat;
}

.priming-art-layer--notifications {
  aspect-ratio: 393 / 526;
  background-image: url("/icons/priming/notifications.png");
}

.priming-art-layer--calendar-screen {
  top: 67px;
  background-image: url("/icons/priming/calendar-screen.png");
}

.priming-art-layer--calendar-icon {
  top: 67px;
  background-image: url("/icons/priming/calendar-icon-light.png");
}

.priming-art-layer--reminders-icon {
  top: 60px;
  aspect-ratio: 393 / 463;
  background-image: url("/icons/priming/reminders-icon-light.png");
}

.priming-art-layer--reminders-wave {
  top: 60px;
  aspect-ratio: 393 / 463;
  background-image: url("/icons/priming/reminders-wave.png");
}

.priming-art-layer--email {
  top: 62px;
  aspect-ratio: 393 / 463;
  background-image: url("/icons/priming/email.png");
}

.priming-art-layer--location {
  top: 63px;
  background-image: url("/icons/priming/location.png");
}

/* The app icons' dark cuts under the dark theme, set or the OS's
   (tokens.css's three tiers). */
[data-theme="dark"] .priming-art-layer--calendar-icon {
  background-image: url("/icons/priming/calendar-icon-dark.png");
}

[data-theme="dark"] .priming-art-layer--reminders-icon {
  background-image: url("/icons/priming/reminders-icon-dark.png");
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .priming-art-layer--calendar-icon {
    background-image: url("/icons/priming/calendar-icon-dark.png");
  }

  :root:not([data-theme]) .priming-art-layer--reminders-icon {
    background-image: url("/icons/priming/reminders-icon-dark.png");
  }
}

/* Phones (2026-11-06, Julian: "serious collision on iPhone Pro Maxes"):
   a screen's pictures scale together to fit the slot, the smaller of its
   width (393) and its height, centred. The height to fit is where the
   lowest picture's pixels end (--fit, in frame points: a frame's top plus
   its drawn foot — notifications' bell at 494, calendar's and location's
   phones at their frames' feet, email's card at 476), not the frame,
   whose foot is often clear. On the 393×852 mock every screen fits as
   drawn, so nothing moves there; a browser tab's toolbar or a wider phone
   used to keep the pictures at the width's scale while the copy rose into
   them. Each layer names its frame's top and height; the scale reads the
   slot through container units. */
@media (max-width: 834px) {
  .priming-art {
    --fit: 531;
    container-type: size;
  }

  .priming-art--notifications {
    --fit: 494;
  }

  .priming-art--reminders {
    --fit: 420;
  }

  .priming-art--email {
    --fit: 476;
  }

  .priming-art--location {
    --fit: 527;
  }

  .priming-art-layer {
    --frame-top: 0;
    --frame-h: 464;
    --k: min(calc(100cqw / 393), calc(100cqh / var(--fit)));
    top: calc(var(--k) * var(--frame-top));
    right: auto;
    left: 50%;
    width: calc(var(--k) * 393);
    aspect-ratio: 393 / var(--frame-h);
    translate: -50% 0;
  }

  .priming-art-layer--notifications {
    --frame-h: 526;
  }

  .priming-art-layer--calendar-screen,
  .priming-art-layer--calendar-icon {
    --frame-top: 67;
  }

  .priming-art-layer--reminders-icon,
  .priming-art-layer--reminders-wave {
    --frame-top: 60;
    --frame-h: 463;
  }

  .priming-art-layer--email {
    --frame-top: 62;
    --frame-h: 463;
  }

  .priming-art-layer--location {
    --frame-top: 63;
  }
}

/* Calendar's and location's phone, cut off at its foot, dissolves into
   the page under Julian's scrim (Figma "Calendar Sync", 2026-11-06,
   measured off the 393×464 frame: an ellipse 564 wide and 153.5 tall,
   centred 447.5 down). Flatter here than the mock's outline (Julian, same
   day: the mock's read too elliptical, and in dark the phone's edges bled
   through its sides): 180% of the frame wide, centred 105% down, the
   mock's height — so the horizon runs nearly straight across the frame
   and the page's colour holds its full width. In the frame's proportions,
   so a narrower phone and the desktop canvases scale it with the picture;
   the copy stands above it (its own layer). */
.priming-art-layer--fade::after {
  content: "";
  position: absolute;
  top: 105%;
  left: 50%;
  width: 180%;
  height: 33.08%;
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side, var(--background-secondary) 0%, var(--background-secondary) 45%, transparent 100%);
  pointer-events: none;
}

.priming-copy {
  grid-row: 2;
  position: relative;
  z-index: 1;
  align-self: end;
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 100%;
  padding-bottom: 84px;
  text-align: center;
}

.priming-title {
  margin: 0;
  font: 600 33px/41px var(--font-sans);
  color: var(--label-primary);
  text-wrap: balance;
}

.priming-message {
  margin: 0;
  font: 400 17px/22px var(--font-sans);
  color: var(--label-secondary);
  text-wrap: balance;
}

/* iOS's CapsuleButton, filled with the accent: 310×48, the label at the
   landing pills' 19. While the browser asks, a spinner stands in for the
   label (iOS's isLoading) and the button takes no second tap. */
.priming-button {
  grid-row: 3;
  position: relative;
  width: 100%;
  max-width: 310px;
  height: 48px;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: var(--accent-blue);
  color: var(--label-on-accent);
  font: 400 19px/1 var(--font-sans);
  cursor: pointer;
}

.priming-button:disabled {
  cursor: default;
}

.priming-button--requesting .priming-button-label {
  visibility: hidden;
}

.priming-button-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  box-sizing: border-box;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  border: 2px solid var(--label-on-accent);
  border-top-color: transparent;
  border-radius: 50%;
  animation: priming-spin 800ms linear infinite;
}

/* "I'll do this later" (iOS's plain 15/19.5 in the secondary label), on
   its line 16 under the pill; off while the browser asks. */
.priming-later {
  grid-row: 4;
  align-self: end;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: 400 15px/19.5px var(--font-sans);
  color: var(--label-secondary);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.priming-later:disabled {
  cursor: default;
}

/* Desktop (2026-11-06): the screen on the sign-up's column (auth.css's
   phone step, ≥835) instead of the phones' frame — the bar's wordmark
   over a 464 column, the picture, the title at the column's 34/41, the
   line 8 under it in the alt caption's 21, the phones' 310 pill, and
   "I'll do this later" 16 under that. The air between them is the
   window's, not fixed (Julian's Reminders mock, 1440×1024, later the
   same day: "use a ratio so things don't seem so tight"): the picture 71
   under the bar, the title 79 under the picture, the pill 40 under the
   line — 6.9%, 7.7% and 3.9% of the 1024 — and 118 left under the link.
   A short window spends its air first, then shrinks the picture (its
   layers scale with it, as on a narrow phone), never the copy. */
@media (min-width: 835px) {
  .priming {
    --air-art: calc(var(--vvh, 100dvh) * 0.069);
    --air-title: calc(var(--vvh, 100dvh) * 0.077);
    --air-pill: calc(var(--vvh, 100dvh) * 0.039);
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 0 0 48px;
    overflow: auto;
    overscroll-behavior: none;
  }

  .priming .sign-up-bar {
    position: relative;
    z-index: 1;
    box-sizing: border-box;
    width: 100%;
    min-height: 84px;
    padding: 18px 64px 30px;
  }

  /* Julian's desktop exports (2026-11-06; the -desktop.png files in
     icons/priming):
     each picture cut to its own content at 2×, about 400 tall — the
     laptop-and-phone notifications scene, calendar's phone with its "Tue
     1" on a canvas 2 wider and 14 taller (the tile overhangs the phone's
     foot), email's card with the Mail icon, location's phone. The slot is
     the screen's own frame (--frame-w × --frame-h, at 1×), its layers
     painted from its top-left corner; a short window shrinks the slot
     and its layers with it. */
  .priming-art {
    --frame-w: 393;
    --frame-h: 464;
  }

  /* The picture's room: the window less the bar (84), the copy (41 + 8 +
     two lines of 21/1.25), the pill (48), the link (16 + 20), the foot's
     48 and the three airs — 318 and 18.5% of the window. */
  .priming-art,
  .priming--top .priming-art {
    flex: 0 0 auto;
    align-self: center;
    width: auto;
    height: min(calc(var(--frame-h) * 1px), calc(var(--vvh, 100dvh) * 0.815 - 318px));
    min-height: 240px;
    aspect-ratio: var(--frame-w) / var(--frame-h);
    margin: var(--air-art) 0 0;
    overflow: hidden;
  }

  .priming-art--notifications {
    --frame-w: 581;
    --frame-h: 400;
  }

  .priming-art--calendar {
    --frame-w: 314;
    --frame-h: 414;
  }

  .priming-art--email {
    --frame-w: 402.5;
    --frame-h: 427.5;
  }

  .priming-art--location {
    --frame-w: 339;
    --frame-h: 400;
  }

  .priming-art .priming-art-layer {
    inset: 0;
    aspect-ratio: auto;
    background-position: left top;
    background-size: 100% auto;
  }

  .priming-art-layer--notifications {
    background-image: url("/icons/priming/notifications-desktop.png");
  }

  /* The screen (312 wide) on the icon's 314 canvas, both from the corner. */
  .priming-art .priming-art-layer--calendar-screen {
    background-image: url("/icons/priming/calendar-screen-desktop.png");
    background-size: 99.363% auto;
  }

  .priming-art-layer--calendar-icon {
    background-image: url("/icons/priming/calendar-icon-light-desktop.png");
  }

  .priming-art-layer--email {
    background-image: url("/icons/priming/email-desktop.png");
  }

  .priming-art-layer--location {
    background-image: url("/icons/priming/location-desktop.png");
  }

  [data-theme="dark"] .priming-art-layer--calendar-icon {
    background-image: url("/icons/priming/calendar-icon-dark-desktop.png");
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme]) .priming-art-layer--calendar-icon {
      background-image: url("/icons/priming/calendar-icon-dark-desktop.png");
    }
  }

  .priming-copy {
    align-self: center;
    width: 464px;
    margin-top: var(--air-title);
    padding-bottom: 0;
    gap: 8px;
  }

  .priming-title {
    font: 600 34px/41px var(--font-sans);
  }

  .priming-message {
    font: 400 21px/1.25 var(--font-sans);
  }

  .priming-button {
    flex-shrink: 0;
    align-self: center;
    margin-top: var(--air-pill);
  }

  .priming-later {
    flex-shrink: 0;
    align-self: center;
    margin-top: 16px;
  }
}

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

/* A still ring reads as stuck: under reduced motion it turns slowly. */
@media (prefers-reduced-motion: reduce) {
  .priming-button-spinner {
    animation-duration: 2400ms;
  }
}

/* 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. --background-secondary, not
     --background-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 --background-secondary 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(--background-secondary);
  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(--background-secondary);
  color: var(--label-primary);
  text-decoration: none;
}

/* Quick fade-down on press for tactile feedback. */
.header-icon-btn:active {
  background: var(--background-secondary);
  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(--background-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 --background-secondary 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-secondary);
  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 --background-secondary 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(--background-secondary);
}

.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-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-secondary);
    text-decoration: none;
    transition: .3s ease-in-out;
  }
}
.sidebar-row-active {
  background: var(--fill-secondary);
  font-weight: 500;
  box-shadow: none;
}
@media (hover: hover) {
    .sidebar-row-active:hover {
    background: var(--fill-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;
  overscroll-behavior: none;
  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(--background-primary-inverse);
  color: var(--label-inverse-primary);
  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(--background-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(--background-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 widget space, first in the pane's body (Julian, 2026-10-28: "same
   index on the view as mobile"): the stack on the pane's 16 inset (the
   timeline's 8 over the pane's own 8, the rows' edge), 24 to the first
   section header — the timeline's 12 gap and the header's 12 top margin,
   the pane's own card → header rhythm and the phone's widget → Today 24
   alike. The faces are the phone's (widget_card.css); the swipes are
   the phone's too — a mouse never swipes (ffi/update_stack.mjs), so the
   pane's stack reads and opens, and does not read away. */
.sidebar-widget {
  padding: 0;
}

/* 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: var(--sp-xxl);
    /* 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);
  /* The cards' fades dissolve into the pane's canvas — the shell's
     --background-secondary showing through (search.css .list-card-fade). */
  --list-canvas: var(--background-secondary);
}

.sidebar-search:has(> .list-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 .list-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 .list-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 44px disc centres its 24px glyph 10 in, so −10 puts the glyph's
   left edge on the cards' 16px edge. */
.sidebar-list-back {
  margin-left: -10px;
}

/* The list header's buttons (Julian, 2026-10-27): the phone header's disc
   — 44 round, the tinted glass surface in the page's colour (glass.css),
   the rim, a 24 glyph in label primary; hover tints the glass a step
   toward the ink. 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,
.sidebar-list-action {
  flex: none;
  font: inherit;
  border: none;
  cursor: pointer;
  padding: 0;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  --glass-tint-color: var(--background-primary);
  color: var(--label-primary);
  text-decoration: none;
}

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

/* Search's discs before the collapse button (the sort "…" and the
   list-plus on the grid, a list's "…" inside one; iOS PR 1260), 8 apart.
   The title takes the slack so the discs cluster on the right. */
.sidebar-list-header > .sidebar-list-title,
.sidebar-list-header > .sidebar-list-heading {
  flex: 1 1 auto;
}

.sidebar-list-action,
.sidebar-list-action + .sidebar-list-header-button {
  margin-left: 8px;
}

@media (hover: hover) {
  .sidebar-list-header-button:hover,
  .sidebar-list-back:hover,
  .sidebar-list-action:hover {
    --glass-tint-color: color-mix(in srgb, var(--background-primary), var(--label-primary) 8%);
  }
}

/* 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) is the same disc
   with the 24 sparkle; .sidebar-list-ai only names it. The list's first
   row is the other door (iOS keeps both). */

.sidebar-list-header-button:focus-visible,
.sidebar-list-back:focus-visible,
.sidebar-list-action: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 --background-secondary 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: 44px;
  height: 44px;
  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-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(--background-elevated-primary);
}

/* Settings: the viewer's avatar at the rail's 40px (2026-10-26, as the
   phone tab bar and iOS). In full colour at rest (Julian, 2026-11-01; it
   was dimmed to 65% and drained to a quarter saturation until current).
   The current section wears a 2px inset ring in background primary,
   drawn on an overlay of the anchor so it sits over a photo too (an
   inset shadow on an <img> paints under the picture). The disc
   hover/current fill behind it is hidden by the avatar itself. */
.app-rail-button--avatar::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
}
.app-rail-button--avatar[aria-current="page"]::after {
  box-shadow: inset 0 0 0 2px var(--background-primary);
}

/* 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 circle is the tinted glass surface
   in the user's accent (glass.css; accent.css retints --accent-blue —
   Julian, 2026-10-27; was a solid disc). */
.app-rail-slot {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.app-rail-new {
  font: inherit;
  border: none;
  padding: 0;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  --glass-tint-color: var(--accent-blue);
  --glass-tint-strength: 85%;
  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(--background-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;
  overscroll-behavior: none;
  /* 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(--background-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(--background-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(--label-primary);
}
@media (hover: hover) {
  .feed-card-complete-btn--done:hover {
  color: var(--label-primary);
}
}

/* 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(--label-primary);
}
@media (hover: hover) {
  .feed-card-bookmark-btn--active:hover {
  color: var(--label-primary);
}
}
.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: color-mix(in srgb, var(--background-primary) 70%, transparent);
  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(--background-secondary);
    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(--background-tertiary);
  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-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(--background-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-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, no shadow (the new designs, 2026-11-01). 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);
}
.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(--background-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(--background-tertiary);
  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(--background-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.

   The Comments sheet's classes below (.comments-sheet, .comments-row,
   .comments-composer, .comments-empty, .detail-confirm) are shared by a
   LIST's Comments sheet (app/list_comments, 2026-11-05), which is the
   toDo's sheet over the list's comments — one chrome, not a fork. */

/* 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(--background-tertiary);
  color: var(--label-primary);
  font: inherit;
  font-size: var(--text-md);
  cursor: pointer;
  flex-shrink: 0;
}
.collab-pill--danger {
  background: color-mix(in srgb, var(--accent-red) 12%, transparent);
  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);
}

.collab-pill--primary {
  background: var(--accent-blue);
  color: var(--background-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(--background-secondary);
}

/* ── Likes and comments (iOS `TodoActionRow`, `CommentsSheet`) ─────────── */

/* The heart and the bubble outside the phone row: the desktop strip and a
   read-only seat's page. The phone row's item, at any width. */
.detail-social {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.detail-social .detail-action-item {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 2px;
  height: 44px;
  min-width: 44px;
  padding: 0 8px;
  border: none;
  background: transparent;
  color: var(--label-secondary);
  font: inherit;
  cursor: pointer;
}

.detail-social .detail-action-item:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

.detail-social .detail-action-item[aria-disabled="true"] {
  color: var(--label-tertiary);
  cursor: default;
}

.detail-social .detail-action-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
}

.detail-social .detail-action-count {
  font-size: 15px;
  font-weight: 600;
  line-height: 20px;
  color: var(--action-count);
  white-space: nowrap;
}

/* The viewer's own like: the star's lit treatment. */
.detail-social .detail-action-item--lit .detail-action-glyph .icon {
  background: var(--gradient-pastel);
}

/* The viewer face's pair (a read-only seat, a visitor: no strip), under
   the heading on desktop. */
.detail-viewer-social {
  padding: 4px 0 8px;
}

.detail-viewer-social:empty {
  display: none;
}

/* The phone's header carries its own heart and bubble (detail_header.css). */
@media (max-width: 834px) {
  .reminder-heading-container .detail-social,
  .detail-viewer-social {
    display: none;
  }
}

/* The faces on the sheets' rows and the composer: sized here, not inline
   (ui/avatar.avatar_url_styled; Julian, 2026-11-04). The monogram's
   font-size is the helper's 45% of the circle. */
.comments-composer > .avatar,
.comments-row > .avatar,
.liker-row > .avatar {
  width: 32px;
  height: 32px;
  font-size: 14px;
}

/* The thread and the likers run under the stuck composer / the card's
   foot, padded by the footer's measured height plus this clearance
   (sheet.css --sheet-foot-clear), so nothing meets the composer's top. */
.comments-sheet,
.likes-sheet {
  --sheet-foot-clear: 12px;
}

/* The fluid fill is the SCROLLER here, not the stack: the scrim is a mask
   over the fill's box (sheet.css), and a mask clips to the box it is on —
   a fill that shrank to the stack while its thread overflowed it would
   scroll its box away and leave the thread unpainted. Scrolling inside
   the fill keeps the box, and the fade, where they are. */
.comments-sheet .sheet__fill,
.likes-sheet .sheet__fill {
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
}

.comments-sheet .sheet__fill::-webkit-scrollbar,
.likes-sheet .sheet__fill::-webkit-scrollbar {
  display: none;
}

.comments-list {
  display: flex;
  flex-direction: column;
  padding-bottom: 12px;
}

.comments-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 16px;
  /* iOS's 24 continuous corner, as the toDo card's: seen when the row
     wears a fill (a phone's swipe). */
  border-radius: 24px;
}

.comments-row--pending {
  opacity: 0.5;
}

.comments-row__text {
  flex: 1;
  min-width: 0;
}

.comments-row__meta {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 13px;
  line-height: 18px;
}

.comments-row__name {
  overflow: hidden;
  color: var(--label-secondary);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.comments-row__when {
  flex: none;
  color: var(--label-tertiary);
}

.comments-row__body {
  margin: 2px 0 0;
  color: var(--label-primary);
  font-size: 17px;
  line-height: 24px;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

/* A phone's row swipes (ffi/card_swipe.mjs; the toDo cards' strip in
   timeline_row.css): the card moves over the sheet's fill and the Delete
   circle grows in behind it. The card paints the fill so the strip stays
   hidden at rest. */
@media (max-width: 834px) {
  .comments-sheet .swipe-wrap {
    position: relative;
    touch-action: pan-y;
  }

  .comments-sheet .swipe-wrap > .swipe-card {
    position: relative;
    z-index: 1;
    background: var(--background-secondary);
    /* The vibrant tertiary fill eases in as the row moves and back out as
       it settles home (Julian, 2026-11-04); --comment-swipe-fill to retune. */
    transition: background-color 160ms ease;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
  }

  .comments-sheet .swipe-wrap[data-swipe] > .swipe-card {
    transform: translateX(var(--swipe-x, 0px));
    background: var(--comment-swipe-fill, var(--fill-tertiary));
  }
}

/* On hover or focus where a mouse can get there; always where it can't. */
.comments-row__delete {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--label-tertiary);
  cursor: pointer;
}

@media (hover: hover) {
  .comments-row__delete {
    opacity: 0;
  }

  .comments-row:hover .comments-row__delete,
  .comments-row__delete:focus-visible {
    opacity: 1;
  }
}

.comments-empty {
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-height: 200px;
  padding: 24px;
  color: var(--label-tertiary);
  text-align: center;
}

.comments-empty__title {
  margin: 8px 0 0;
  color: var(--label-primary);
  font-size: 17px;
  font-weight: 600;
}

.comments-empty__detail {
  margin: 0;
  color: var(--label-secondary);
  font-size: 15px;
}

.comments-composer {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 8px 16px;
}

.comments-composer > .avatar,
.comments-composer > :first-child {
  flex: none;
  margin-bottom: 6px;
}

.comments-composer__field {
  display: flex;
  flex: 1;
  align-items: flex-end;
  min-width: 0;
  min-height: 44px;
  padding: 4px 6px 4px 16px;
  border-radius: 22px;
  background: var(--background-secondary);
}

.comments-composer__input {
  flex: 1;
  min-width: 0;
  max-height: 120px;
  padding: 8px 0;
  border: none;
  background: transparent;
  color: var(--label-primary);
  font: inherit;
  font-size: 17px;
  line-height: 20px;
  resize: none;
  outline: none;
  field-sizing: content;
}

.comments-composer__send {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  border-radius: 50%;
  background: var(--label-primary);
  color: var(--background-primary);
  cursor: pointer;
}

.comments-composer__send:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Over the sheet: at desktop width its card sits at 401, above the page
   confirm's 200, which would draw behind it. */
.sheet-layer > .detail-confirm-overlay {
  z-index: 402;
}

.sheet-layer > .comments-toast {
  position: fixed;
  bottom: 96px;
  z-index: 402;
}

/* The desktop strip's own height, so it doesn't push the page down. */
.reminder-heading-container .detail-social .detail-action-item {
  height: 36px;
}

/* ── The split heart (2026-11-04) ───────────────────────────────────────
   With a count the like item is a GROUP of two buttons — the glyph (the
   like toggle) and the count (the Likes sheet) — each its own target, the
   item's padding shared between them so the pair measures as the one
   button did. The phone row's own dress is in detail_header.css. */
.detail-social .detail-action-item--split {
  gap: 0;
  padding: 0;
}

.detail-social .detail-action-item__glyph,
.detail-social .detail-action-item__count {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  height: 100%;
  padding: 0;
  border: none;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.detail-social .detail-action-item__glyph {
  padding: 0 1px 0 8px;
}

.detail-social .detail-action-item__count {
  padding: 0 8px 0 1px;
}

.detail-social .detail-action-item__glyph:focus-visible,
.detail-social .detail-action-item__count:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: -2px;
  border-radius: var(--radius-full);
}

.detail-social .detail-action-item--split[aria-disabled="true"] .detail-action-item__glyph,
.detail-social .detail-action-item--split[aria-disabled="true"] .detail-action-item__count {
  color: var(--label-tertiary);
  cursor: default;
}

/* ── The Likes sheet (2026-11-04) ───────────────────────────────────────
   Who liked, one row each: the comment row's 32 avatar, the name, when
   under it — the comment row's bones without a body or a bin. The empty
   state is the comments'. */
.likes-list {
  display: flex;
  flex-direction: column;
  padding-bottom: 12px;
}

.liker-row {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 56px;
  padding: 8px 16px;
}

.liker-row__text {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.liker-row__name {
  overflow: hidden;
  color: var(--label-primary);
  font-size: 17px;
  font-weight: 600;
  line-height: 22px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.liker-row__when {
  color: var(--label-tertiary);
  font-size: 13px;
  line-height: 18px;
}

/* ── The list Collaborators sheet (list sharing, 2026-11-06) ──────────────
   The reminder sheet's rows and pills, plus a capsule under the roster:
   the owner's Share (the invite link to the system share sheet, the
   clipboard where there is none), a member's Leave List. */
.list-collab__bar {
  display: flex;
  justify-content: center;
  padding: 16px 24px 8px;
}
.list-collab__capsule {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 44px;
  padding: 0 22px;
  border: 0;
  border-radius: 9999px;
  font: 600 15px/20px var(--font-sans);
  cursor: pointer;
}
.list-collab__capsule--share {
  background: var(--accent-blue);
  color: var(--label-on-accent);
}
.list-collab__capsule--leave {
  background: color-mix(in srgb, var(--accent-red) 12%, transparent);
  color: var(--accent-red);
}
.list-collab__capsule:disabled {
  opacity: 0.5;
  cursor: default;
}

/* 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. On the
   phone timeline's top and, since 2026-10-28, first in the desktop list
   pane (sidebar.css .sidebar-widget): the same card at the column's
   width, so nothing here needs a breakpoint. */

/* Styling hooks (2026-11-04). Every card names what it leads with in
   data-kind (ui/widget_card.update_kind):
     brief  today's Daily Brief lead card (the band, "View your brief")
     tray   the stack's floor (the band, "Ask Please")
     todo   an update about a toDo: its own item's card, or one the agent posted
     email  an email the agent pointed at (a please.ai/email link, or a mail draft)
     place  a place card (a map)
     link   a web page card
     card   any other rich card (a call or text card, say)
     text   narration alone, no card
   and, on an update, data-source: what produced it — agent, daily_brief,
   item_chat, alert, event or ephemeral_chat. Select with e.g.
   .widget-card[data-kind="email"] or .widget-card[data-source="alert"]. */

.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);
  /* The card fill the toDo cards wear (--background-tertiary, 2026-11-04): white in
     light, iOS's #2C2C2E in dark, where --background-primary's black vanished
     into the body. */
  background: var(--background-tertiary);
  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;
  }
}

/* An update face's pieces (iOS AgentUpdateHeroCard, 2026-11-04): a toDo
   card or a rich card leads, the body under it, the suggestion pill, the
   button — 16 apart on iOS; the card's 8 gap plus each piece's own room
   here. The toDo card is the timeline's own row on the card's fill with
   the chat's hairline; the rich card the chat's compact card. */
.widget-card__lead .tl-row {
  background: var(--background-tertiary);
  border: 1px solid var(--border-secondary);
}
.widget-card__lead .chat-card {
  width: 100%;
}
.widget-card__pill .suggestion-pills {
  padding-top: 0;
}
.widget-card__body {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  white-space: pre-line;
}

/* "🌅 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 under base.css's old
   `* { overscroll-behavior: none }` a finger on the title, the band or
   these two lines scrolled nothing (measured 2026-10-27; the rule went
   2026-10-28 — overscroll_test.mjs). `clip` stays: it is the lighter box. */
.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(--background-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-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
   at rest; a swipe pins it, carried toward the incoming face's — 2026-11-05),
   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%;
  /* A flex item of the column scroller (.feed-items-wrap): never shrunk.
     With the default shrink a feed taller than the screen squeezed the
     row back to its min-content — the front face's own height — the
     moment a swipe pinned it taller, so the card coming back hung below
     the row over the rows under it until it dropped (2026-11-05). */
  flex: none;
  /* 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;
}

/* "Weather ›" (iOS WeatherPill): the fallback band's one live control, a
   translucent white capsule at the trailing edge, footnote medium in
   white with the chevron. Only while the browser would still ask for the
   location. */
.widget-band__pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3xs);
  flex-shrink: 0;
  margin-left: auto;
  padding: var(--sp-3xs) var(--sp-3xs) var(--sp-3xs) var(--sp-sm);
  border: 0;
  border-radius: var(--radius-full);
  background: rgb(255 255 255 / 0.2);
  color: var(--sky-ink);
  font: var(--type-footnote);
  font-weight: 500;
  cursor: pointer;
  transition: transform 120ms ease, background-color 120ms ease;
}

.widget-band__pill:hover {
  background: rgb(255 255 255 / 0.28);
}

.widget-band__pill:active {
  transform: scale(1.02);
}

.widget-band__pill:focus-visible {
  outline: 2px solid var(--sky-ink);
  outline-offset: 2px;
}

.widget-band__pill svg {
  color: currentColor;
}

/* The weather band and its data source's credit. The band reads as one
   image, so the credit link is its sibling: a quiet line under the band's
   trailing edge, in the card's own ink (Open-Meteo's CC BY 4.0 asks for
   the credit with a link wherever the data shows). */
.widget-band-frame {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--sp-4xs);
  width: 100%;
}

.widget-band__credit {
  font-size: var(--text-xs);
  line-height: 14px;
  color: var(--label-secondary);
  text-decoration: none;
}

.widget-band__credit:hover,
.widget-band__credit:focus-visible {
  color: var(--label-primary);
  text-decoration: underline;
}

/* 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;
  overscroll-behavior: none;
  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-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(--background-secondary);
  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;
}

/* 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;
  border-radius: var(--radius-2xl);
  padding: var(--sp-xl);
  max-width: 390px;
  width: 90%;
}
.detail-confirm-title {
  font: var( --type-body-semibold);
  margin: 0 0 var(--sp-xs);
  color: var(--label-primary);
}
.detail-confirm-body {
  font: var( --type-body);
  color: var(--label-primary);
  margin: 0 0 var(--sp-lg);
}

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

.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). The logged-out link page wraps
   the same card in .link-page-card, not .share-page (2026-10-28). */
.share-page .md-task-drag,
.public-item-page .md-task-drag,
.link-page-card .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: color-mix(in srgb, var(--accent-blue) 40%, transparent);
  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;
  overscroll-behavior: none;
  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; }
/* Inline code: the accent pattern the app's picks wear — the user's
   accent as ink on its 10% elevated wash, as the chat composer's
   (chat.css; Julian, 2026-11-06). A block keeps the neutral fill below,
   its code plain ink. */
.detail-body-editor .ProseMirror code {
  font-family: monospace;
  font-size: 0.9em;
  color: var(--accent-blue);
  background: var(--accent-blue-elevated);
  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(--background-secondary, rgba(0,0,0,0.08));
  border-radius: var(--radius-sm, 4px);
  padding: var(--sp-xs) var(--sp-sm);
  overflow-x: auto;
  overscroll-behavior: none;
  font-family: monospace;
  font-size: 0.9em;
  margin: var(--sp-xs) 0;
}
.detail-body-editor .ProseMirror pre code {
  color: inherit;
  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-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;
  overscroll-behavior: none;
  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(--background-secondary));
  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(--background-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(--background-secondary);
}

.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(--background-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(--background-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(--background-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(--background-primary);
}
.detail-rail-btn-ai:hover {
  /* color: var(--background-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(--background-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(--background-secondary);
  border-radius: var(--radius-lg);
}

.detail-player-btn {
  width: 48px;
  height: 48px;
  border-radius: var(--radius-full);
  background: var(--background-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(--background-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);
}

/* ── 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(--background-secondary);
  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(--background-elevated-primary);
  }
}

.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(--background-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-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: var(--var-bg-secondary);
}

.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-vibrant-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-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;
  /* `anywhere`, not `break-word`: a word too long for the line counts as
     breakable when the bubble sizes itself too, so the viewer's bubble (a
     fit-content flex item) keeps its 85% instead of growing to the word. */
  overflow-wrap: anywhere;
}

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

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

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

/* ── 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(--background-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(--background-secondary);
}
.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(--background-primary);
  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(--background-primary);
}

.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(--background-primary);
}

.detail-reminder-custom-btn {
  padding: var(--sp-3xs) var(--sp-sm);
  border: none;
  border-radius: var(--radius-md);
  background: var(--background-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(--background-secondary);
  font-size: var(--text-sm);
  font-family: inherit;
  color: var(--label-primary);
  cursor: pointer;
}
.detail-reminder-chip:hover {
  background: var(--background-secondary);
  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(--background-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(--background-secondary);
}
.detail-reminder-chip-menu-item-danger {
  color: var(--accent-red);
}
.detail-reminder-chip-menu-item-danger:hover {
  background: var(--danger-subtle, var(--background-secondary));
}

/* ── 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(--background-elevated-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-text-content {
    padding-left: var(--sp-xl);
    padding-right: var(--sp-xl);
  }

}

@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(--background-secondary); /* base background — flips with theme */
  border-radius: 16px;
  background: var(--background-secondary);
  /* No shadow (Julian, 2026-11-01: the new designs remove them from
     every thumbnail stack); the 2px base-coloured border is the gap. */
}

/* 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(--background-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-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;
}

/* ─── 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. */
/* The field as iOS draws it: a 44 pill on the page's own colour, no
   stroke, the magnifier leading the placeholder (2026-11-02). The label
   wraps both so the glyph is part of the hit area. In dark it lifts a tier
   off the sheet, as the New Message capsule does (new_message.css), not
   sink into it (2026-11-06; it read the deprecated --bg, white in dark
   while that alias was missing there). */
.share-picker .share-search {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 44px;
  border-radius: var(--radius-full);
  background: var(--background-primary);
}
[data-theme="dark"] .share-picker .share-search {
  background: var(--background-elevated-tertiary);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .share-picker .share-search {
    background: var(--background-elevated-tertiary);
  }
}
.share-picker .share-search__icon {
  position: absolute;
  left: 16px;
  display: flex;
  color: var(--label-primary);
  pointer-events: none;
}
.share-picker .new-chat-search {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  min-height: 44px;
  padding: 0 16px 0 44px;
  border: none;
  border-radius: var(--radius-full);
  background: transparent;
  font: var(--type-body);
}
.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;
}
/* ── The Share Item sheet's stuck footer (Julian's 393 artboard, 2026-11-03;
   the spec's "Footer (stuck)"). The chrome positions it over the grid and
   masks the grid's foot over its top 40 (sheet.css .sheet--stuck); these
   rules are the 242 block's own geometry: the 40 cap is the footer's top
   padding; the toggle row's centre at 60 (32 side inset); the hairline at
   92 (24 inset); 64 discs on an 86 pitch from 20 in, their tops at 110;
   the labels' centres at 190; the chrome's bottom inset below. ── */
.share-sheet .sheet__footer {
  display: block;
  padding: 40px 0 0;
  gap: 0;
}
.share-sheet__toggle-row {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 40px;
  margin: 0 32px;
}
/* Two rows (the 402 artboards, 2026-11-04): 48 centre to centre. */
.share-sheet__toggle-row + .share-sheet__toggle-row {
  margin-top: 8px;
}

/* The turning subtitle (view_share.subtitle_stack): the sign-up carousel's
   title motion (auth.css .sign-up-title--*) at the footnote's scale — the
   leaving words scroll 10 and fade out over 260, the next scroll in over
   420 on the same curve, 260 behind. Both sit on one grid cell so the
   header never moves. */
.share-sheet__subtitle-stack {
  display: grid;
  justify-items: center;
}
.share-sheet__subtitle {
  grid-area: 1 / 1;
  white-space: nowrap;
}
.share-sheet__subtitle--in-down,
.share-sheet__subtitle--in-up {
  animation: 420ms cubic-bezier(0.2, 0.8, 0.2, 1) 260ms both;
}
.share-sheet__subtitle--out-down,
.share-sheet__subtitle--out-up {
  animation: 260ms ease-in both;
  pointer-events: none;
}
.share-sheet__subtitle--in-down {
  animation-name: share-subtitle-in-down;
}
.share-sheet__subtitle--out-down {
  animation-name: share-subtitle-out-down;
}
.share-sheet__subtitle--in-up {
  animation-name: share-subtitle-in-up;
}
.share-sheet__subtitle--out-up {
  animation-name: share-subtitle-out-up;
}
@keyframes share-subtitle-in-down {
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: none; }
}
@keyframes share-subtitle-out-down {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: translateY(10px); }
}
@keyframes share-subtitle-in-up {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}
@keyframes share-subtitle-out-up {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: translateY(-10px); }
}
@media (prefers-reduced-motion: reduce) {
  .share-sheet__subtitle--in-down,
  .share-sheet__subtitle--in-up {
    animation-name: sign-up-title-fade-in;
  }
  .share-sheet__subtitle--out-down,
  .share-sheet__subtitle--out-up {
    animation-name: sign-up-title-fade-out;
  }
}
.share-sheet__toggle-glyph {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--label-primary);
}
.share-sheet__toggle-label {
  flex: 1 1 auto;
  min-width: 0;
  font: var(--type-body);
  color: var(--label-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.share-sheet__divider {
  height: 0.5px;
  margin: 12px 24px 0;
  background: var(--ui-borders);
}
/* No toggle row (a seat that cannot invite): the hairline still sits at
   92, where the row would have ended. */
.share-sheet__toggle-row + .share-sheet__divider {
  margin-top: 12px;
}
.share-sheet .sheet__footer > .share-sheet__divider:first-child {
  margin-top: 52px;
}
.share-sheet__strip {
  display: flex;
  gap: 22px;
  margin: 0;
  padding: 17px 20px 0;
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  align-items: center;
  justify-content: center;
}
.share-sheet__strip::-webkit-scrollbar {
  display: none;
}
.share-sheet__action {
  flex: 0 0 auto;
  width: 64px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.share-sheet__disc {
  width: 64px;
  height: 64px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  display: grid;
  place-items: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.share-sheet__disc--link {
  background: var(--accent-blue);
  color: #fff;
}
.share-sheet__disc--messages {
  background: var(--messages-green);
  color: #fff;
}
.share-sheet__disc--share {
  background: var(--background-secondary);
  color: var(--label-primary);
}
.share-sheet__disc-label {
  display: block;
  width: 64px;
  margin-top: 8px;
  font: var(--type-footnote);
  color: var(--label-primary);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The send row once a target is picked: the chrome's 48 buttons, sharing
   the row with a 12 gap, 24 under the hairline. */
.share-sheet__send {
  display: flex;
  gap: 12px;
  padding: 24px 16px 0;
}
.share-sheet__send > * {
  flex: 1 1 0;
  min-width: 0;
}
.share-picker .share-picker__primary {
  background: var(--background-primary-inverse);
  color: var(--label-inverse-primary);
}
.share-picker .share-picker__primary:disabled {
  background: var(--background-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 gutter padding — the detail column
   provides its own spacing. Since the Messages composer became a layer
   floating over its list (chat.css, 2026-10-27: absolute, pinned to the
   pane's foot, a gradient scrim behind it), the panel takes it back into
   its column's flow: here the composer follows the messages in the one
   page scroll, with nothing scrolling under it, so neither the float nor
   the scrim applies. It stays positioned (relative) — the typing row
   floats above it. */
.detail-ai-chat .chat-composer {
  position: relative;
  inset: auto;
  z-index: auto;
  padding: 0;
  max-width: none;
  background: 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-secondary); /* mock .06 */
  border-radius: var(--radius-full);
  background: var(--background-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;
}

/* The panel glow's breaths and its working mix (below, the panel
   section): registered numbers, so they animate and transition on their
   own and multiply into one opacity. */
@property --detail-ai-glow-idle {
  syntax: "<number>";
  inherits: false;
  initial-value: 0.6;
}

@property --detail-ai-glow-busy {
  syntax: "<number>";
  inherits: false;
  initial-value: 0.6;
}

@property --detail-ai-glow-working {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

@keyframes detail-ai-glow-idle {
  from {
    --detail-ai-glow-idle: 0.6;
  }
  to {
    --detail-ai-glow-idle: 0.9;
  }
}

@keyframes detail-ai-glow-busy {
  from {
    --detail-ai-glow-busy: 0.6;
  }
  to {
    --detail-ai-glow-busy: 0.9;
  }
}

@keyframes detail-ai-glow-hue {
  from {
    filter: hue-rotate(0deg);
  }
  to {
    filter: hue-rotate(360deg);
  }
}

@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;
}

/* The header's end: the Please AI button, then ⋯ (mock 2026-10-29). */
.detail-header-end {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
}


.detail-ai-entry,
.detail-more-entry {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  cursor: pointer;
  transition: color 150ms ease;
}

.detail-more-entry {
  border-radius: var(--radius-full);
  color: var(--label-vibrant-primary);
}

@media (hover: hover) {
  .detail-more-entry:hover:not(:disabled) {
    color: var(--label-primary);
  }
}

.detail-more-entry[aria-expanded="true"] {
  color: var(--label-primary);
}

.detail-more-entry:disabled {
  opacity: 0.4;
  cursor: default;
}

.detail-more-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, so its breath
   runs at half (its crest the 0.45 it held when it stood still). */
[data-theme="dark"]
  .detail-ai-entry:not([aria-expanded="true"]):not(.detail-ai-entry--working)
  .detail-ai-entry-disc,
[data-theme="dark"] .detail-ai-panel-glow {
  --detail-ai-glow-strength: 0.5;
}

@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,
  :root:not([data-theme]) .detail-ai-panel-glow {
    --detail-ai-glow-strength: 0.5;
  }
}

@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(--background-primary);
  }

  /* Breathing room around the editor now that the top nav is gone. The
     bottom is reserved for the Messages pill (58, bottom-right), so
     scrolled to the end, a long note's last line clears it (prod QA
     2026-10-01: it sat on it). .nr-body's own bottom margin matches its 24
     inset, so this is its 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;
  }

  /* ── 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 (once the accessory rail's 330 + 24 each side) down to
     the 300 floor. It resolves where it's used (the panel's width, the
     pill's inset below), each against the same pane-wide box, so both
     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 */
    /* `clip`, not `hidden`: no scroll container, so focusing the composer
       (or scrollIntoView) can never scroll the panel and carry the glow
       off its foot. The chat scrolls inside its own messages list. */
    overflow: clip;
    background: var(--background-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 */
    gap: 0; /* the thread's body alone, or the offline notice over it */
    /* 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;
  }

  /* Offline, the notice heads the panel inside its gutters: 24 from the
     top as from the sides. The thread's list under it brings its own 24
     of headroom, so nothing between them. The loading and failed rows
     start 24 down too. */
  .detail-ai-panel .detail-ai-chat-offline {
    margin: var(--sp-xl) 0 0;
  }

  .detail-ai-panel .detail-ai-chat-status {
    padding-top: var(--sp-xl);
  }

  .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;
    overscroll-behavior: none;
    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-vibrant-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-vibrant-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(--background-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);
  }

  /* On a toDo that isn't a container the paperclip can't attach (inert,
     its input disabled): dimmed, as the drawer's is. */
  .detail-ai-panel .chat-composer-add[data-disabled] {
    color: var(--label-tertiary);
    cursor: default;
  }

  .detail-ai-panel .chat-composer-send {
    width: 24px;
    height: 24px;
  }

  .detail-ai-panel .chat-composer-send:disabled {
    background: var(--background-secondary); /* mock #F2F2F4 */
    color: var(--label-tertiary); /* mock #C4C3C8 */
  }

  /* The breathing glow under the composer — the phone drawer's
     (chat_drawer.css §4a.8), as iOS's AI glow breathes
     (EphemeralHeaderGlow): the brand pastel run, breathing 0.6 ↔ 0.9 on a
     3.6s half-cycle, 1.6s while the agent works (the panel's
     data-working, the header button's halo's signal), its hue turning once
     every 14s on a loop no cadence change restarts. Both breaths always
     run, and the working state cross-fades between them over 0.4s: the
     cadence changes without a snap and without the drawer's controller
     (iOS settles to the trough first; a CSS loop can't start from where
     another one was). The mask is the mock's falloff: full at the foot,
     gone 96 up. Dark halves it (above, with the entry disc's dark rules).
     Each var() has a fallback, so a browser without @property draws a
     valid opacity (its keyframes then step rather than ease). */
  .detail-ai-panel-glow {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 96px;
    pointer-events: none;
    background: var(--gradient-pastel-horizontal);
    opacity: calc(
      var(--detail-ai-glow-strength, 1) *
        (
          var(--detail-ai-glow-idle, 0.9) *
            (1 - var(--detail-ai-glow-working, 0)) +
            var(--detail-ai-glow-busy, 0.9) * var(--detail-ai-glow-working, 0)
        )
    );
    transition: --detail-ai-glow-working 0.4s ease-in-out;
    animation:
      detail-ai-glow-idle 3.6s ease-in-out infinite alternate,
      detail-ai-glow-busy 1.6s ease-in-out infinite alternate,
      detail-ai-glow-hue 14s linear infinite;
    -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
    );
  }

  .detail-ai-panel[data-working] .detail-ai-panel-glow {
    --detail-ai-glow-working: 1;
  }

  /* Closed, the loops hold. */
  .detail-ai-panel[hidden] .detail-ai-panel-glow {
    animation-play-state: paused;
  }

  /* Reduced motion: still, at the crest, with no hue turn. */
  @media (prefers-reduced-motion: reduce) {
    .detail-ai-panel-glow {
      animation: none;
      transition: none;
      --detail-ai-glow-idle: 0.9;
      --detail-ai-glow-busy: 0.9;
    }
  }

  /* ── Messages pill ────────────────────────────────────────────────────
     24 from the main pane's bottom-right corner (the pane edge moves left
     by the AI panel's live, drag-resizable, clamped width while it is
     open). */
  .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(--background-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(--background-secondary);
    }

    .detail-messages-pill:hover .shared-pile {
      --pile-ring: var(--background-secondary);
    }
  }

  .detail-messages-pill:focus-visible {
    outline: 2px solid var(--accent-blue);
    outline-offset: 2px;
  }
}

/* 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. */

/* 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(var(--phone-top-inset) + 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-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;
  }  /* ── 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);
  }

  /* ── 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 --background-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(--background-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;
  }
}

/* ── The viewer face (iOS LiveFeedDetailView non-owner layout, 2026-08-28
   mock; web 2026-11-02) ─────────────────────────────────────────────────
   A seat that can't edit gets the owner's page with the editing chrome
   off (`.nr-root--detail[data-viewer]`, app/detail): the notes rendered
   read-only in the editor's place, and the viewer bar — one 56 glass pill
   trailing in the bottom slot (Join, or Save a copy), the drawer's docked
   geometry — over a scrim in the page's colour so the notes end at it. */
.nr-root--detail[data-viewer] .nr-body-editor--readonly {
  flex: 1 0 auto;
  min-height: 120px;
  padding: var(--sp-lg) var(--sp-xl);
  box-sizing: border-box;
}

/* No completion circle on the viewer face (iOS's non-owner layout draws
   the title alone); the title keeps the row's start. */
.nr-root--detail[data-viewer] .nr-completion-circle {
  display: none;
}

/* Read-only task rows have no drag handle (iOS: static rows). */
.nr-root--detail[data-viewer] .md-task-drag {
  display: none;
}

.viewer-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 25;
  display: flex;
  justify-content: flex-end;
  padding: 12px 16px calc(var(--phone-bottom-inset, 0px) + 22px);
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    transparent,
    var(--detail-page-bg, var(--background-primary)) 40%
  );
}

.viewer-bar-pill {
  pointer-events: auto;
  height: 56px;
  padding: 0 var(--sp-xl);
  border: none;
  border-radius: var(--radius-full);
  font: inherit;
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--label-primary);
  cursor: pointer;
  transition: transform 150ms ease;
}

.viewer-bar-pill:active {
  transform: scale(1.02);
}

.viewer-bar-pill:disabled {
  opacity: 0.6;
  cursor: default;
}

/* The notes end above the bar. */
.nr-root--detail[data-viewer] .nr-body {
  padding-bottom: calc(var(--phone-bottom-inset, 0px) + 112px);
}

@media (min-width: 835px) {
  /* Desktop: laid over the page (not the window, so the sidebar is never
     under it), 24 from the corner as the Messages pill is — and beside
     that pill (220 wide) when a shared toDo draws it, never under it. */
  .viewer-bar {
    position: absolute;
    padding: 12px var(--sp-xl) var(--sp-xl);
    background: none;
  }

  .nr-root--detail:has(.detail-messages-pill) .viewer-bar {
    padding-right: calc(220px + var(--sp-xl) + 12px);
  }
}

/* ── The public item page's hero and meta row (app/public_item: the
   logged-out /item/<id> deep link) — kept when the non-owner detail layout
   went (2026-11-02). ─────────────────────────────────────────────── */
.detail-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--sp-md);
}

.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);
}

.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;
}

@media (max-width: 834px) {
  .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;
  }

}

/* ════════════════════════════════════════════════════════════════════════
   The media viewer (ui/media_viewer) — every full-screen media preview:
   the toDo detail's pages (images, videos, voice notes), the chat's sent
   media and the composer's staged attachment, the New toDo's local
   previews (still its own composition over these pieces). One chrome
   since 2026-11-05 (the detail's of 11-04): the
   forced-dark shell, the ✕ and ⋯ glass discs, the keyed page slide, the
   dots and desktop chevrons, the voice playbar. Moved here from detail.css
   when the chat became its third host. z 950: above the modal system
   (900), the sheets (401) and the chat drawer; the ⋯ popover stacks
   inside the shell.
   ════════════════════════════════════════════════════════════════════════ */

.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;
}

/* ── the header row (iOS MediaViewer.headerRow, the .header layout;
   2026-11-04): two 44 glass discs over the page, 16 in from the edges,
   no title. The discs are the nav bar's (.nav-bar-disc's glass, on the
   viewer's black), and they sit where the nav bar's do: the top inset
   (--phone-top-inset, the status bar or its floor) above them, so the
   ✕ opens over the back disc and the ⋯ over the ⋯. iOS's 10 under the
   status bar left them 10 lower than the detail's in Safari and 14
   higher on Android (Julian, 2026-11-05). 10 below. ── */

.media-viewer-top {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  padding: var(--phone-top-inset) 16px 10px;
  pointer-events: none;
}

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

.media-viewer-disc {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  --glass-tint-color: var(--background-primary);
  color: var(--label-primary);
  cursor: pointer;
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
  transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.media-viewer-disc:active {
  transform: scale(1.2);
}

/* A host's toast, drawn over the viewer: 24 above the home indicator. Two
   classes deep so the host's own rule (.chat-toast, imported later) never
   takes its place back. */
.media-viewer .media-viewer-toast {
  position: fixed;
  bottom: calc(var(--safe-bottom) + 24px);
  z-index: 3;
}

/* A disc-sized gap where the ⋯ would be (a preview with no menu). */
.media-viewer-top-ghost {
  width: 44px;
  height: 44px;
  flex-shrink: 0;
}

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

.media-viewer-image {
  width: 100%;
  max-height: 100%;
  object-fit: contain;
}
/* An image page runs edge to edge (iOS): no gutter on its centre. */
.media-viewer-center:has(> .media-viewer-image) {
  padding: 0;
}

/* 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;
  overscroll-behavior: none;
}

.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;
}

/* 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(--background-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(--background-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(--background-primary-inverse);
  color: var(--label-inverse-primary);
  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(--background-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));
}

/* ── The activity row under the title (Julian's mock, 2026-10-28;
   share_view.view_metrics): likes and comments, then the views at the far
   end (the mock's eye, as words; its AI glyph and plane not drawn). Each
   pair is a 44 ghost button — 12 of side padding, the first flush left —
   with no gap between buttons; the 24 glyph in label secondary, the count
   2 after it at 15 semibold in label tertiary (#727272 in light). The row
   starts on the title text's left edge: the circle's 32 plus the title
   row's gap (new_reminder.css, 12 — Julian's 40 counted it as 8). ── */
.share-metrics {
  display: flex;
  align-items: center;
  gap: 0;
  padding: 0 var(--sp-xl) 0 calc(var(--sp-xl) + 32px + var(--sp-sm));
}

.share-metric {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  height: 44px;
  margin: 0;
  padding: 0 12px;
  border: 0;
  background: none;
  color: var(--label-secondary);
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.share-metric:first-child {
  padding-left: 0;
}

.share-metric-count {
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 20px;
  color: var(--label-tertiary);
}

/* "1,378 views": pushed to the row's end, the counts' type a step fainter
   (the mock's eye and number, #BFBFBF in light). Text, not a button. */
.share-metric-views {
  margin-left: auto;
  padding-left: 12px;
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 20px;
  color: var(--label-vibrant-tertiary);
  white-space: nowrap;
}


/* 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. */

/* 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(--background-secondary);
  color: var(--label-primary);
}

/* ── List invite — /join/list/<token> (list sharing, 2026-11-06) ──────────

   app/join_list_view. The card keeps the toDo invite's .nr-reminder-card
   root, so link_page.css's card rules (the flex, the phone scroller, the
   room under the last line) apply unchanged; inside it is a list's anatomy,
   not a reminder's — no heading container, body or deck — so the card lays
   out its own three pieces: the swatch, the title block, the meta line.
   Both mounts share these; the signed-in one sits in .detail-content
   (detail.css) and the signed-out one in .link-page-card. */

.join-list-card {
  gap: var(--sp-md);
  padding: 0 var(--sp-xl) var(--sp-md);
}

/* In the link page the reminder's heading container carried 32/24/16 of
   its own; the list card restates the inset. On a phone the card is the
   page's scroller and brings its top padding (link_page.css). */
.link-page-card > .join-list-card {
  padding: 32px 24px 24px;
}

/* The list's face: its colour under its emoji — the Lists grid's card
   (search.css .list-card) at the card's width and a shorter height. A list
   with no colour of its own wears the grid's first tint. Decoration, so no
   pointer and no selection. */
.join-list-swatch {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 160px;
  border-radius: 24px;
  background: linear-gradient(
    to bottom right,
    var(--bucket-grad-blue-from),
    var(--bucket-grad-blue-to)
  );
  user-select: none;
  -webkit-user-select: none;
  pointer-events: none;
}

/* A list wearing its own colour: the hex at full strength, flat, and in
   dark mode a 22% wash over the page — the grid's .list-card--own rule,
   restated for this element because the custom property is the card's. */
.join-list-swatch--own {
  background: var(--list-color);
}

[data-theme="dark"] .join-list-swatch--own {
  background: color-mix(in srgb, var(--list-color) 22%, transparent);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .join-list-swatch--own {
    background: color-mix(in srgb, var(--list-color) 22%, transparent);
  }
}

/* An emoji glyph renders a touch wider than its point size (the grid's
   116 in a 144 box; 72 in 160 here, the card being a page, not a tile). */
.join-list-emoji {
  font-size: 72px;
  line-height: 1;
}

.join-list-heading {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

/* The mock's title weight for a page that is the whole card: the signed-in
   share page's 28, a size up on a phone (below), as the toDo invite's. */
.join-list-title {
  margin: 0;
  font-size: 28px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--label-primary);
  overflow-wrap: anywhere;
}

/* "12 items · Shared with 3 people · view only" — the kicker's colour and
   size, wrapping onto a second line on a narrow card rather than clipping. */
.join-list-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-xs);
  margin: 0;
  color: var(--label-secondary);
  font-size: var(--text-sm);
  line-height: 1.3;
}

.join-list-meta-sep {
  color: var(--label-tertiary);
}

/* The seat's limit as a small pill, so it reads as a property of the link
   and not one more count. */
.join-list-view-only {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--sp-xs);
  border-radius: var(--radius-full);
  background: var(--fill-tertiary);
  color: var(--label-secondary);
  font-size: var(--text-xs);
  font-weight: 500;
  line-height: 1.3;
}

/* The refusal under the Join button: iOS's alert as two lines — the title,
   then the line — in .people-search-error's red (chat.css). */
.join-list-error {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3xs);
}

.join-list-error-title {
  font-weight: 600;
}

/* A dead list link: a title over the toDo invite's one sentence, the pair
   centred in the card as that sentence is alone. The sentence keeps
   .share-unavailable-text's side padding and gives up its top to the
   title. */
.join-unavailable-title {
  margin: 0;
  padding: var(--sp-xl) var(--sp-xl) 0;
  text-align: center;
  font-weight: 600;
  color: var(--label-primary);
}

.join-list-unavailable .share-unavailable-text {
  padding-top: var(--sp-xs);
}

@media (max-width: 834px) {
  /* The mock's phone title, as link_page.css sizes the toDo's. */
  .join-list-title {
    font-size: var(--text-4xl);
  }

  .join-list-swatch {
    height: 144px;
  }
}


/* 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;
  overscroll-behavior: none;
  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(--background-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;
}

/* The pair as a button (it opens "Never miss a thing", 2026-10-28): the
   browser's button face, font and centring undone. */
.link-page-sender--opens {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  -webkit-tap-highlight-color: transparent;
}

.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;
  min-height: 32vh;
  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(--background-primary);
  border-radius: 50px;
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.link-page-card .reminder-heading-container {
  padding: 32px 24px 16px 24px;
  border: none;
}

/* With the activity row (share.css), 8 under the title and the hairline
   under the row instead: the row's own bottom edge, 16 under its buttons,
   the title's width. Only an invite whose counts were served draws the
   row; without one the card keeps the title's 16 and its hairline
   (below). */
.link-page-card .reminder-heading-container:has(+ .share-metrics) {
  padding-bottom: 8px;
}

.link-page-card .share-metrics {
  margin: 0 24px;
  padding: 0 0 16px calc(32px + var(--sp-sm));
  border-bottom: 1px solid var(--border-secondary);
}

.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;
}

/* The activity row draws the hairline when there is one (above). */
.link-page-card .reminder-heading-container:has(+ .share-metrics) .nr-title-row::after {
  content: none;
}

.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;
  /* `auto`, not `none`: this scroller sits inside the page's (.link-page,
     which declares none and stops the bounce), and on a phone the card
     outruns the viewport, so once the body reaches its end — or at once,
     when it fits — a finger or wheel over it must chain to the page to
     reach the composer at the card's foot (2026-10-28). */
  overscroll-behavior: auto;
}

/* 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, transparent 0%, var(--background-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(--glass-shadow);
}

/* 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).

   It is the AI drawer's lit grabber, always on (Julian, 2026-11-05): the
   same two loops chat_drawer.css runs while a new insight waits — the
   breath 1 ↔ 0.55 over 2.6s each way, the hue turning once every 14s —
   on the registered --chat-drawer-breath, so the numbers stay one set.
   Reduced motion holds it at its crest, as the drawer's does. */
.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);
  opacity: var(--chat-drawer-breath);
  animation:
    chat-drawer-breathe 2.6s ease-in-out infinite alternate,
    chat-drawer-hue 14s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .link-page-composer-accent {
    animation: none;
    --chat-drawer-breath: 1;
  }
}

.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-secondary);
  /* Its own fill now the slab is translucent: the mock's white pill on the
     frosted slab, the card's colour. */
  background: var(--background-primary);
  color: var(--label-primary);
  text-decoration: none;
  /* A button since 2026-10-28 (it opens the Ask Please prompt): the
     browser's button face, font and centring undone. */
  margin: 0;
  font: inherit;
  text-align: left;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  -webkit-tap-highlight-color: transparent;
}

.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-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;
  color: var(--label-primary);
}

/* ── Sign-up prompts (Julian's mocks, 2026-10-28) ────────────────────── */

/* The hero graphics. "Never miss a thing" shows the sender's avatar at 88
   (Julian's 369×383 artboard, 2026-10-28) with the app icon as a 32 badge
   on the avatar's own ::after, flush with its bottom-right corner — so the
   frame must not clip (the photo keeps its own circle). The other two
   prompts carry Julian's renders in a 132 slot. */
.sheet-hero__avatar {
  position: relative;
  width: 88px;
  height: 88px;
}

.sheet-hero__avatar > .avatar {
  position: relative;
  width: 88px !important;
  height: 88px !important;
  overflow: visible;
}

.sheet-hero__avatar > .avatar::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  width: 32px;
  height: 32px;
  background: url("/icons/link-page/app-icon.png") center / contain no-repeat;
}

.sheet-hero__mark {
  width: 88px;
  height: 88px;
}

.sheet-hero__art {
  width: 132px;
  height: 132px;
  background: center / contain no-repeat;
}

/* Julian's renders (2026-10-28), 528px — 4× the slot, so a 3× phone is
   crisp and the slot can grow to 176 without a re-export. Decorative:
   the title says what the prompt is. */
.sheet-hero__art--ask {
  background-image: url("/icons/link-page/ask.png");
}

.sheet-hero__art--comments {
  background-image: url("/icons/link-page/comments.png");
}

/* ── 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 .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);
  }

  /* Seamless (Julian's mock, 2026-10-28): the page never scrolls. The
     card fills what the bar leaves, edge to edge, rounded at the top only
     and with no shadow against the page it spans; its content — the title
     row, the deck, the body, an invite's store row — is the page's one
     scroller, clipped at the card's rounded top as it scrolls under the
     bar, and the composer stays pinned at the card's foot. */
  .link-page {
    overflow: hidden;
  }

  .link-page-card {
    max-width: none;
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
    max-height: none;
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    box-shadow: none;
    /* The top inset is the scroller's, below, so the content scrolls all
       the way to the card's edge rather than stopping under a band. */
    padding-top: 0;
  }

  .link-page-card > .nr-reminder-card {
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
    max-height: none;
    padding-top: var(--sp-xl);
    /* The room under the last line (the desktop body's 180) is the
       scroller's here, so an invite's store row sits over it, not under. */
    padding-bottom: 180px;
    overflow-y: auto;
    /* contain, not none: the bounce stays inside the card, as the app's
       detail page bounces, and never reaches the page (there is nothing
       behind it to scroll). */
    overscroll-behavior: contain;
  }

  .link-page .share-body {
    height: auto;
    padding-bottom: 0;
    overflow: visible;
  }

  /* 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(--background-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;
  overscroll-behavior: none;
  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(--background-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);
  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(--background-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(--background-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(--background-primary);
  color: var(--label-primary);
}
.capture-btn-cancel:hover {
  background: var(--background-secondary);
}
.capture-btn-save {
  background: var(--background-secondary);
  color: var(--label-primary);
  border-color: var(--border-primary);
}
.capture-btn-save:hover:not(:disabled) {
  background: var(--background-secondary);
}
.capture-btn-save-disabled {
  background: var(--background-secondary);
  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-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(--background-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(--background-tertiary);
  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(--background-primary);
  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(--background-secondary);
}

.capture-text-content {
  resize: vertical;
  min-height: 120px;
}

.capture-submit {
  padding: var(--sp-sm);
  border: none;
  border-radius: var(--radius-md);
  background: var(--background-secondary);
  color: var(--label-secondary);
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
}
.capture-submit:disabled { opacity: 0.4; cursor: not-allowed; }

/* (The voice recorder left this file for voice_capture.css, 2026-10-28.) */

.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(--background-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(--background-primary);
  /* The scrim tail (Julian, 2026-10-27): the composer is a layer over
     the list, its container a gradient from clear at its top edge to the
     page's colour this far down, where the pill starts; a human chat's
     header is the same layer flipped (below). The list's paddings follow
     the layers' heights (--chat-foot-h / --chat-head-h, dom.mjs
     watchChatChrome) less this tail, plus 8: the rows end under a tail's
     clear foot, never under the bar or the pill. */
  --chat-scrim: 24px;
}

/* 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(--background-elevated-primary);
  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(--background-secondary);
  }
}

/* Floating scroll-to-bottom button — appears when the user scrolls up.
   Centred on the messages column (translateX(-50%): with left: 50% alone
   its left edge sat on the centre line, 20 right of it), 8 above the
   composer's layer; on a phone 12 above the pill (below). */
.chat-scroll-to-bottom {
  position: absolute;
  left: 50%;
  bottom: calc(var(--chat-foot-h, 104px) + var(--sp-xs));
  z-index: 6;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--label-primary);
  border-radius: var(--radius-full, 100px);
  cursor: pointer;
  z-index: 10;
  padding: 0;
  opacity: 1;
  transform: translate(-50%, 0);
  transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
.chat-scroll-to-bottom--hidden {
  opacity: 0;
  transform: translate(-50%, 4px);
  pointer-events: none;
}
@media (hover: hover) {
    .chat-scroll-to-bottom:hover {
    background: var(--background-secondary);
  }
}

/* ── 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);
}

/* A human chat's header (Julian, 2026-10-27): a layer over the list like
   the composer, the scrim flipped — the page's colour down to the bar's
   content, then the tail (`--chat-scrim`) dissolving the rows that scroll
   up under it. The list runs to the page's top and pads its head by the
   layer's height (--chat-head-h, dom.mjs) less the tail, plus 8. The AI
   chat keeps its own chrome: its list fades under the bar on its mask. */
.chat.chat--human > .page-header,
.chat.chat--human > .nav-bar {
  grid-row: auto;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 5;
  padding-bottom: var(--chat-scrim);
  background: linear-gradient(
    to top,
    transparent,
    var(--background-primary) var(--chat-scrim)
  );
}
.chat.chat--human > .chat-messages {
  grid-row: 1 / 5;
  padding-top: calc(var(--chat-head-h, 92px) - var(--chat-scrim) + var(--sp-xs));
  /* A row scrolled into view lands below the bar, not under it. */
  scroll-padding-top: var(--chat-head-h, 92px);
}

/* 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 {
  /* Down to the page's foot: the composer floats over the last rows. */
  grid-row: 2 / 5;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: none;
  overflow-x: hidden;
  padding: var(--sp-xl);
  padding-bottom: calc(var(--chat-foot-h, 164px) - var(--chat-scrim) + var(--sp-xl));
  /* A row scrolled into view lands above the composer's layer, not under it. */
  scroll-padding-bottom: var(--chat-foot-h, 164px);
  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-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(--background-primary-inverse);
  color: var(--label-inverse-primary);
  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-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-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;
}

/* ── A desktop message's action pill (Julian's mock, 2026-10-31) ──────
   Over the message — its quote or name, or its bubble when it runs on —
   its leading edge on the message's (the trailing edge on the viewer's
   own): three quick reactions, a hairline, the emoji sheet, Reply, More,
   on glass. Taller than the --sp-xl gap between rows, it covers the foot
   of the message above while it shows. It fades in as the
   pointer comes onto the message, and stays while the pointer is on it
   (it is the row's own) or keyboard focus is in the message; hidden while
   its More menu is up, the menu taking its place. dom.mjs's
   placeActionPill moves it (--pill-dx/-dy) down from under the floating
   header and in from a narrow column's edge. Phones have no hover: the
   bubble opens the action sheet. */
.chat-message-stack {
  position: relative;
}

.chat-message-actions {
  position: absolute;
  bottom: calc(100% - 2px);
  left: 0;
  translate: var(--pill-dx, 0px) var(--pill-dy, 0px);
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 6px 8px;
  border-radius: var(--radius-full);
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition:
    opacity 0.3s ease-in-out,
    visibility 0s linear 0.3s;
}

.chat-message-row.sent .chat-message-actions {
  left: auto;
  right: 0;
}

@media (min-width: 835px) and (hover: hover) {
  .chat-message-row:hover .chat-message-actions:not([data-menu-open]) {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition:
      opacity 0.3s ease-in-out,
      visibility 0s;
  }
}

/* The keyboard's way in: focus on anything in the message (a link, a
   reaction, the pill's own buttons) shows it; a closed More menu hands
   focus back to More through data-focus-return (dom.mjs). */
@media (min-width: 835px) {
  .chat-message-row:has(:focus-visible) .chat-message-actions:not([data-menu-open]),
  .chat-message-actions[data-focus-return] {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition:
      opacity 0.3s ease-in-out,
      visibility 0s;
  }
}

@media (max-width: 834px) {
  .chat-message-actions {
    display: none;
  }
}

.chat-message-actions__emoji,
.chat-message-actions__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: none;
  color: var(--label-secondary);
  cursor: pointer;
  transition:
    background 0.15s ease,
    transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.chat-message-actions__emoji {
  font-size: 26px;
  line-height: 1;
}

@media (hover: hover) {
  .chat-message-actions__emoji:hover,
  .chat-message-actions__btn:hover {
    background: var(--fill-vibrant-tertiary);
    color: var(--label-primary);
  }
}

.chat-message-actions__emoji:active {
  transform: scale(1.2);
}

.chat-message-actions__emoji:focus-visible,
.chat-message-actions__btn:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

.chat-message-actions__divider {
  flex: none;
  width: 1px;
  height: 30px;
  margin: 0 6px;
  background: var(--border-secondary);
}

/* A fade isn't motion: the pill keeps it under reduced motion, and with
   it the 0.3s its hidden state waits — the moment focus takes to move
   from one of the message's controls to another (Shift+Tab onto More),
   when neither holds it and the pill would otherwise lose its focusability
   and drop focus to <body>. The presses' scale goes. */
@media (prefers-reduced-motion: reduce) {
  .chat-message-actions__emoji,
  .chat-message-actions__btn {
    transition: none;
  }
}

.chat-reaction-picker {
  position: absolute;
  top: -40px;
  left: 0;
  display: flex;
  gap: var(--sp-3xs);
  background: var(--background-primary);
  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(--background-secondary);
  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-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(--background-elevated-primary, var(--background-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: color-mix(in srgb, var(--accent-blue) 20%, transparent);
  mix-blend-mode: var(--blend-mode);
}

@media (hover: hover) {
  .chat-reaction-pill:hover {
  background: var(--fill-primary);
  border-color: var(--border-secondary);
}
}

@media (hover: hover) {
  .chat-reaction-pill.active:hover {
  background: color-mix(in srgb, var(--accent-blue) 40%, transparent);
  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 ─────────────────────────────────────────────────────────── */

/* The composer: a layer over the list (Julian, 2026-10-27), full width
   with the 800 column's insets, its container the scrim — clear at the
   top edge, the page's colour from the tail down (`--chat-scrim`,
   .chat). The list scrolls under the pill's glass. */
.chat-composer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 5;
  padding: var(--chat-scrim) max(var(--sp-xl), calc((100% - 752px) / 2)) var(--sp-xxl);
  background: linear-gradient(
    to bottom,
    transparent,
    var(--background-primary) var(--chat-scrim)
  );
}

/* ── @-mention picker ────────────────────────────────────────────────────── */

.chat-mention-picker {
  position: absolute;
  left: var(--sp-xl);
  right: var(--sp-xl);
  bottom: calc(100% - var(--sp-xxl));
  background: var(--background-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow);
  max-height: 240px;
  overflow-y: auto;
  overscroll-behavior: none;
  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(--background-secondary);
  }
}
/* Keyboard-highlighted row (↑/↓ navigation) — elevated secondary fill. */
.chat-mention-picker-item--active {
  background: var(--fill-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;
}

/* No bottom margin on the layer, iOS included (2026-10-28). `.ios
   .chat-composer { margin-bottom: 2px }` predated the layer — it kept the
   old bordered form off the home indicator — and once the composer became
   a layer over the list (2026-10-27) it opened a 2px seam under the
   layer's scrim where the rows scrolling beneath showed through, on every
   iOS browser (Julian's Safari and Brave shots, staging). The layer's own
   bottom padding carries the home-indicator clearance (env()). */

/* 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(--kb-gap);
  }
}

/* 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]
   The glass surface (glass.css; Julian, 2026-10-27 — was a bordered fill):
   the thick material over the blur, the tint, the rim, then the float
   shadow, as the link page's slab and the AI drawer's card. */
.chat-composer-form {
  display: flex;
  flex-direction: column;
  width: 100%;
  border: none;
  border-radius: var(--radius-xl);
  box-shadow: var(--glass-rim), var(--glass-shadow);
  transition: box-shadow 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;
}

/* A file dragged over it: the accent rings the glass. */
.chat-composer-form.drag-over {
  box-shadow: var(--glass-rim), 0 0 0 2px var(--accent-blue), var(--glass-shadow);
}

/* Emoji picker — anchored above the pill, right-aligned */
.chat-composer-form .emoji-picker {
  position: absolute;
  bottom: calc(100% + var(--sp-sm));
  left: 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-md) 0 var(--sp-md);
  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;
  overscroll-behavior: none;
  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-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);
}

/* The draft's link preview — the reply bar's row, with the page's image,
   title and host (iOS ComposerLinkPreviewHeader). */
.chat-composer-link {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  margin: 0 var(--sp-md);
  padding: var(--sp-sm) 0;
  border-bottom: 0.5px solid var(--ui-borders);
  min-width: 0;
  animation: composerStripIn 180ms ease;
}
.chat-composer-link__thumb {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 8px;
  overflow: hidden;
  /* main's link preview landed with the retired elevated fill (2026-11-06);
     the refactor's --fill-secondary is the same 0.16 grey. */
  background: var(--fill-secondary);
  color: var(--label-secondary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.chat-composer-link__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.chat-composer-link__spinner {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid var(--ui-borders);
  border-top-color: var(--label-secondary);
  animation: composerLinkSpin 0.8s linear infinite;
}
@keyframes composerLinkSpin {
  to { transform: rotate(360deg); }
}
.chat-composer-link__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  font-size: var(--text-sm);
}
.chat-composer-link__title {
  color: var(--label-primary);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.chat-composer-link__host {
  color: var(--label-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@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;
  padding: 0;
  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(--background-secondary);
  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(--background-secondary);
  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-3xs);
  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;
  overscroll-behavior: none;
  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(--background-secondary);
  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(--background-primary-inverse);
  color: var(--label-inverse-primary);
}

.chat-composer-send:disabled {
  background: var(--fill-vibrant-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);
}
}

/* 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(--background-secondary);
  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(--background-secondary);
  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(--background-elevated-primary);
}
}

.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;
}

.chat-att-link-host {
  font-size: var(--text-xs);
  color: var(--label-tertiary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 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(--background-secondary);
  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-primary);
  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(--background-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(--background-primary);
  font-family: inherit;
  font-size: var(--text-lg); /* 16px */
  color: var(--label-primary);
  outline: none;
}
.new-chat-search:focus {
  border-color: var(--background-secondary);
}
.new-chat-search::placeholder {
  color: var(--label-primary);
}

.new-chat-contacts {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: none;
  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(--background-secondary);
}
}

.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(--background-primary-inverse);
  color: var(--label-inverse-primary);
  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(--background-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;
  overscroll-behavior: none;
  scrollbar-width: none;
  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-lg) var(--sp-sm);
  align-content: start;
}
.share-grid::-webkit-scrollbar {
  display: none;
}

.share-cell {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-sm);
  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;
}
/* Edit mode keeps a chat the server would refuse in place, dimmed
   (Julian, 2026-11-03); the title says why. */
.share-cell--dimmed,
.share-cell--dimmed:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
@media (hover: hover) {
  .share-cell:not(:disabled):hover {
    background: var(--background-secondary);
  }
}

/* The face: 74 — Julian's 393 artboard for the Share Item sheet
   (2026-11-03): 74 avatars on a 120 column pitch, 124 row to row, the
   name 16 under (view_share.avatar_px). */
.share-cell-face {
  position: relative;
  width: 74px;
  height: 74px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* A group without its own image. */
.share-cell-group {
  width: 74px;
  height: 74px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--background-secondary);
  color: var(--label-secondary);
}

.share-cell-check {
  position: absolute;
  right: 2px;
  bottom: 2px;
  width: 28px;
  height: 28px;
  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(--background-primary);
}

/* iOS names: semibold, a step under the sheet's title (18 semibold). */
.share-cell-name {
  max-width: 100%;
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.25;
  text-align: center;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* The Share Item sheet's grid (Julian's 393 artboard, 2026-11-03): three
   120 columns across the stack's 361, rows 124 apart — the 74 face, 16 to
   a one-line regular name, 12 between rows. */
.share-sheet .share-grid {
  padding: 8px 0 0;
  column-gap: 0;
  row-gap: 12px;
}
.share-sheet .share-cell {
  gap: 16px;
  padding: 0;
}
.share-sheet .share-cell-name {
  display: block;
  font: var(--type-body);
  line-height: 22px;
  white-space: nowrap;
  text-overflow: ellipsis;
  -webkit-line-clamp: unset;
}

/* Nothing picked (share mode): the Copy link tile, centred. */
.share-empty-bar {
  justify-content: center;
}
/* iOS's bottom tiles: a 56 disc, the label in the names' type; the link
   glyph in the accent blue on its tinted disc. */
.share-bar-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-xs);
  border: none;
  background: none;
  color: var(--label-primary);
  font: inherit;
  font-size: var(--text-lg);
  font-weight: 600;
  cursor: pointer;
}
.share-bar-tile-icon {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent-blue);
  background: var(--accent-blue-elevated);
}
@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(--background-secondary);
}
}
/* A send in flight ("Group collaboration" while its group is made). */
.new-chat-secondary:disabled {
  cursor: default;
  opacity: 0.5;
}

/* ── Typing indicator ── */

/* Nothing until someone types (Julian, 2026-10-27): 0 tall and clear, so
   the composer layer's scrim tail meets the pill; the modifier opens it to
   its 24 on a transition, and closes it back. One line, trimmed. */
.chat-typing {
  height: 0;
  opacity: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: 0 var(--sp-xl);
  font-size: var(--text-xs);
  color: var(--label-secondary);
  white-space: nowrap;
  transition: height 0.2s ease, opacity 0.2s ease;
  position: absolute;
  top: 0;
  left: 0;
}
.chat-typing--on {
  height: 24px;
  opacity: 1;
}
.chat-typing-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.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;
  overscroll-behavior: none;
  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(--background-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' inline code the accent
   pattern the app's picks wear — the user's accent as ink on its 10%
   elevated wash (the alert chips, the calendar's day; Julian, 2026-11-06)
   — so a span reads as marked on the composer's glass and on the in-place
   editor's box alike, in whichever accent is set (accent.css re-points
   both tokens). A block (pre) keeps a translucent fill: a slab of accent
   would shout, and the code inside it is plain ink. (The --code-text /
   --code-bg pair the editors read before was dropped with the bubble
   rework of 2026-10-26, leaving them unstyled until the fill of
   2026-11-06, which this accent pattern replaces.) */
.received .chat-message-text code,
.received .chat-message-text pre {
  color: var(--label-secondary);
  background: var(--background-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 {
  color: var(--accent-blue);
  background: var(--accent-blue-elevated);
}

.chat-composer-milkdown .ProseMirror pre,
.chat-edit-milkdown .ProseMirror pre {
  color: var(--label-primary);
  background: var(--fill-secondary);
}

.chat-composer-milkdown .ProseMirror pre code,
.chat-edit-milkdown .ProseMirror pre code {
  color: inherit;
}

.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(--background-primary);
  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(--background-primary-inverse);
  color: var(--label-inverse-primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

/* =============================================================================
   Message action menu
   ============================================================================= */

/* A desktop message's More menu (chat.view_message_menu; Julian's mock,
   2026-10-31): the attach menu's card — glass, radius 20, 12/16 padding,
   rows a 20 icon, 12 gap, body 18/25.2, 10 above and below, 1px dividers
   centred in 22 — at least 280 wide. Drawn with the app's overlays and
   placed where the pill was by dom.positionMessageMenu (`--chat-menu-left`
   / `-top`, on its layer), only shown once placed; pops out of the pill's
   More (ui/motion). Its catcher is the sidebar's .user-menu-backdrop. */
.chat-message-menu {
  position: fixed;
  left: var(--chat-menu-left, 0px);
  top: var(--chat-menu-top, 0px);
  z-index: 200;
  display: flex;
  flex-direction: column;
  min-width: 280px;
  /* A window shorter than the menu (a small laptop, zoomed) scrolls it
     rather than cut its last rows off. */
  max-height: calc(100dvh - 2 * var(--sp-xs));
  overflow-y: auto;
  overscroll-behavior: none;
  padding: 12px 16px;
  /* Its corners: the pop (styles/motion.css) morphs into them. */
  --pop-corner: 20px;
  outline: none;
  user-select: none;
  -webkit-user-select: none;
}

.chat-message-menu > * {
  flex-shrink: 0;
}

.chat-message-menu:not([data-placed]) {
  visibility: hidden;
  animation: none;
}

.chat-message-menu-item {
  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);
}

.chat-message-menu-item > .icon {
  flex: none;
}

.chat-message-menu-item:active {
  transform: scale(1.2);
}

.chat-message-menu-item:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
  border-radius: 8px;
}

.chat-message-menu-item--danger {
  color: var(--accent-red);
}

/* Create toDo's add/circle/filled/lg, in the viewer's accent (blue unless
   they chose another) — as the phone sheet's row. */
.chat-message-menu-item__todo {
  display: inline-flex;
  color: var(--accent-blue);
}

.chat-message-menu-divider {
  position: relative;
  flex: none;
  height: 22px;
}

.chat-message-menu-divider::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: var(--border-secondary);
}

@media (prefers-reduced-motion: reduce) {
  .chat-message-menu-item {
    transition: none;
  }
}

/* =============================================================================
   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;
  overscroll-behavior: none;
  padding: var(--sp-xs, 8px) var(--sp-sm, 12px);
  border: 1.5px solid var(--accent-blue);
  border-radius: var(--radius-md, 10px);
  background: var(--background-primary);
  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-3xs);
  max-width: 120px;
  height: 36px;
  padding: 0 var(--sp-xs);
  background: var(--background-secondary);
  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-3xs);
  padding: var(--sp-xs, 8px);
  background: var(--background-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(--background-elevated-primary);
  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-3xs) var(--sp-xs, 8px) var(--sp-xs, 8px);
}

.chat-edit-cancel,
.chat-edit-save {
  padding: var(--sp-3xs) 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(--background-secondary);
}
}
.chat-edit-save {
  background: var(--background-tertiary);
  color: var(--label-secondary);
}
@media (hover: hover) {
  .chat-edit-save:hover {
  background: var(--background-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(--background-elevated-primary);
  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(--background-secondary);
  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;
  overscroll-behavior: none;
  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(--background-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(--background-elevated-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;
  overscroll-behavior: none;
  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. */
/* The words are the composer's (chat.gleam sets --composer-placeholder
   inline from its strings, the same the wrapper draws before the mount:
   "Message" on the phone's pill, "Ask anything" in an AI chat, the desktop
   pill's own); the edit box keeps the fallback. */
.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. */

/* ─── Reaction detail sheet content ─────────────────────────────────── */

/* Emoji tab bar */
.reaction-sheet__tabs {
  display: flex;
  gap: 12px; /* 12px between tabs */
  flex-wrap: nowrap;
  overflow-x: auto;
  overscroll-behavior: none;
  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(--background-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;
  }

  /* The AI drawer's ↓ pill (chat_drawer.css §4a.10): 12 above the
     composer row. --chat-foot-h is the composer's layer, which starts
     --chat-scrim above the pill, so the 16 over the layer left it 40
     above the pill (Julian, 2026-11-05: "off-center too-high"). */
  .chat-scroll-to-bottom {
    bottom: calc(var(--chat-foot-h, 104px) - var(--chat-scrim, 24px) + 12px);
  }
}

@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(--background-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(--background-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(--background-tertiary);
  /* Over the list, right above the composer's layer (its own height is
     in --chat-foot-h, so the list clears both). */
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--chat-composer-h, 164px);
  z-index: 5;
  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(--background-secondary);
  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(--background-primary);
  border: 1px solid var(--border-primary);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow);
  max-height: 240px;
  overflow-y: auto;
  overscroll-behavior: none;
  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(--background-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;
}

.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;
}

.chat-list-search {
  flex: 1;
  min-width: 0;
}

.search-pill.chat-list-search {
  height: 48px;
}

@media (max-width: 834px) {
  .chat-list-compose {
    display: none;
  }
}

.chat-list-compose {
  flex-shrink: 0;
  width: 48px;
  height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background: var(--background-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-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).
     --background-secondary 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-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: 48px;
  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-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-xl);
  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-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).
     Dropped by accident with the hover-pill block in 9ec9fa49. */
  .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;
  }

}

/* ── 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-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(--background-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;
}

/* The landing's pill, the AI thread pill's twin (Julian, 2026-10-27):
   the paperclip disc, the text, the send disc that mounts with something
   to send, a staged strip above; the glass surface (glass.css) with the
   rim and the float shadow, where it was a bordered fill. */
.chat-landing-composer {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) auto;
  grid-template-areas:
    "strip strip strip"
    "add input send";
  align-items: center;
  column-gap: var(--sp-xs);
  width: min(560px, 100%);
  min-height: 56px;
  padding: var(--sp-xs);
  border: none;
  border-radius: 28px;
  box-shadow: var(--glass-rim), var(--glass-shadow);
}

.chat-landing-composer:focus-within {
  box-shadow: var(--glass-rim), 0 0 0 2px var(--accent-blue), var(--glass-shadow);
}

.chat-landing-composer .chat-composer--mediaPreviewStrip {
  grid-area: strip;
  padding: var(--sp-xs) var(--sp-xs) 0;
}

/* The paperclip disc: 40 in the elevated fill, as the thread pill's. */
.chat-landing-composer .chat-composer-add {
  grid-area: add;
  width: 40px;
  height: 40px;
  background: var(--fill-secondary);
  color: var(--label-primary);
}

.chat-landing-input {
  grid-area: 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 {
  grid-area: 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-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(--background-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-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-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-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(--background-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.tab-plus,
.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(var(--phone-top-inset) - 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);
  padding-bottom: calc(var(--chat-foot-h, 98px) - var(--chat-scrim) + var(--sp-xs));
  gap: 0;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 24px);
  mask-image: linear-gradient(to bottom, transparent, #000 24px);
}

/* =============================================================================
   THE PILL COMPOSER — iOS ChatInputBar with ComposerStyle.chat (2026-10-27
   for the AI chat; 2026-10-28 every chat on phones — Julian: "fix the
   styling of our composer bar in all chats on mobile web to match iOS").
   One bar for every full-screen chat: [✕ while focused][the pill][+ while
   not]. The layer carries .chat-composer--pill (chat.gleam: an AI chat at
   every width, any chat on a phone; a desktop human chat keeps the two-row
   pill above). iOS: 16 at the sides, 10 below the bar and 8 under it, over
   the home indicator; the scrim tail above (the layer's gradient,
   .chat-composer) dissolves content passing under.
   ============================================================================= */
.chat-composer--pill {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0 var(--sp-xs);
  padding: var(--chat-scrim) var(--sp-md) calc(24px + env(safe-area-inset-bottom, 0px)) var(--sp-md);
}
/* A human chat's typing row takes the line above the discs and the pill
   (0 tall until someone types; .chat-typing). */
.chat-composer--pill > .chat-typing {
  flex: 1 0 100%;
}
/* The keyboard up: the pill and its discs clear the keys by --kb-gap
   (Julian, 2026-11-05: was 18). */
.kb-open .chat-composer--pill {
  padding-bottom: var(--kb-gap);
}
.chat-composer--pill .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-composer--pill .chat-composer-form {
  /* Shares the line with the discs (the layer wraps for the typing row;
     the base rule's width: 100% would take the whole line). */
  flex: 1 1 0%;
  width: auto;
  min-width: 0;
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) auto;
  grid-template-areas:
    "strip strip strip"
    "reply reply reply"
    "link link link"
    "add input send";
  align-items: end;
  column-gap: var(--sp-xs);
  min-height: 56px;
  padding: var(--sp-xs);
  border: none;
  border-radius: 28px;
}
.chat-composer--pill .chat-composer-toolbar {
  display: contents;
}
.chat-composer--pill .chat-composer-toolbar-spacer,
.chat-composer--pill .chat-composer-emoji-btn {
  display: none;
}
.chat-composer--pill .chat-composer--mediaPreviewStrip {
  grid-area: strip;
  padding: var(--sp-xs) var(--sp-xs) 0;
}
.chat-composer--pill .chat-composer-reply {
  grid-area: reply;
}
.chat-composer--pill .chat-composer-link {
  grid-area: link;
}
.chat-composer--pill .chat-composer-add {
  grid-area: add;
  width: 40px;
  height: 40px;
  background: var(--fill-secondary);
  color: var(--label-primary);
}
.chat-composer--pill .chat-composer-input-wrap {
  grid-area: input;
  padding: 0;
  align-items: center;
}
/* The send disc: 40 in the trailing corner, the inverse fill (the accent
   once the user picked one); folded away until there is something to
   send ("The send disc folds" below, shared with the landing's). */
.chat-composer--pill .chat-composer-send {
  grid-area: send;
  width: 40px;
  height: 40px;
  background: var(--background-primary-inverse);
  color: var(--label-inverse-primary);
}
html[data-accent] .chat-composer--pill .chat-composer-send {
  background: var(--accent-blue);
  color: var(--label-on-accent);
}
.chat-composer--pill .chat-composer-send:disabled {
  background: var(--fill-vibrant-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
   glass discs with 28 glyphs (glass.css, as the nav bar's discs and the
   tab bar's +) — the ✕ in the page's colour while the pill has focus,
   the + tinted in the accent while not. */
.chat-composer--pill .chat-composer-dismiss {
  flex: none;
  width: 56px;
  height: 56px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  --glass-tint-color: var(--background-primary);
  box-shadow: var(--glass-rim-disc), 0 4px 8px rgba(0, 0, 0, 0.1);
  color: var(--label-primary);
  align-items: center;
  justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* "The keyboard is up" = the FIELD has focus (the form's focus-within), not
   any control's: a pressed + takes focus on Chromium, and must not hide
   itself (or raise the ✕) under its own click. */
.chat-composer--pill:has(.chat-composer-form:focus-within) .chat-composer-dismiss {
  display: flex;
}
/* The composer's + is the tab row's FAB (ui/plus_button, tab_bar.css's
   .tab-plus: the disc, the accent tint, the hold's 1.1×). Only when it
   shows is decided here — and outranks .tab-plus's own display, which
   tab_bar.css (later in index.css) would otherwise win. */
.chat-composer-fab {
  flex: none;
  width: 56px;
  height: 56px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  --glass-tint-color: var(--accent-blue);
  --glass-tint-strength: 85%;
  color: var(--label-inverse-primary);
  box-shadow: var(--glass-rim-disc), 0 4px 8px rgba(0, 0, 0, 0.1);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.chat-composer--pill .chat-composer-fab,
.chat--eph .chat-composer-fab {
  display: flex;
}
.chat-composer--pill:has(.chat-composer-form:focus-within) .chat-composer-fab,
.chat--eph.chat-landing:has(.chat-landing-composer: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 {
    padding-inline: max(var(--sp-md), calc((100% - 752px) / 2));
    padding-bottom: var(--sp-lg);
  }
}

@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;
  /* 24 at the foot, as the pill composer's (Julian's chat tuning,
     aa62e46b): /chats/new and the blank chat draw the same bar. */
  padding: var(--sp-md) var(--sp-md) calc(var(--sp-xl) + env(safe-area-inset-bottom, 0px));
}
.kb-open .chat-landing-bar {
  padding-bottom: var(--kb-gap);
}
.chat--eph .chat-landing-composer {
  flex: 1 1 auto;
  min-width: 0;
  width: auto;
  max-width: none;
}
/* No focus ring on the phone's page (the ✕ says the keyboard is up). */
.chat--eph .chat-landing-composer:focus-within {
  box-shadow: var(--glass-rim), var(--glass-shadow);
}
.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(--background-primary-inverse);
  color: var(--label-inverse-primary);
}
html[data-accent] .chat--eph .chat-landing-send {
  background: var(--accent-blue);
  color: var(--label-on-accent);
}

/* The send disc folds (iOS: `.transition(.scale(0.6) + .opacity)`,
   easeInOut 0.15): no width — the text keeps 16 from the trailing edge,
   the grid's gap and the pill's inset — clear, and not focusable, until
   there is something to send: text (the editor's has-content class; the
   landing's input past its placeholder) or a staged file. */
.chat-composer--pill .chat-composer-send,
.chat--eph .chat-landing-send {
  width: 0;
  padding: 0;
  overflow: hidden;
  opacity: 0;
  visibility: hidden;
  transform: scale(0.6);
  transition:
    width 0.15s ease-in-out,
    opacity 0.15s ease-in-out,
    transform 0.15s ease-in-out,
    visibility 0s linear 0.15s;
}
.chat-composer--pill .chat-composer-form:has(.composer-inline-input-wrap--has-content) .chat-composer-send,
.chat-composer--pill .chat-composer-form:has(.chat-composer--mediaPreviewStrip) .chat-composer-send,
.chat--eph .chat-landing-composer:not(:has(.chat-landing-input:placeholder-shown)) .chat-landing-send,
.chat--eph .chat-landing-composer:has(.chat-composer--mediaPreviewStrip) .chat-landing-send {
  width: 40px;
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}

/* 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;
    overscroll-behavior: none;
    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(--background-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(--background-secondary);
  }
}

.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(--background-secondary);
}

.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(--background-tertiary);
  color: var(--label-primary);
}
}

/* ── Avatar Section ──────────────────────────────────────────────────── */

.chat-detail-avatar-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--sp-xl) 0;
}

.sheet .chat-detail-avatar-section {
  padding-top: 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(--background-secondary);
  color: var(--label-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-xs);
  border: 2px solid var(--background-primary);
}

.chat-detail-avatar-loading {
  position: absolute;
  bottom: 0;
  right: 0;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  background: var(--background-tertiary);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--fill-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(--background-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(--background-primary);
  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(--background-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(--background-primary);
  color: var(--label-primary);
}
@media (hover: hover) {
  .chat-detail-name-actions button:hover {
  background: var(--background-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(--background-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(--background-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;
}

/* The accent's own wash under the accent's ink (2026-10-27): a raw blue
   wash stayed blue under a purple accent (accent.css retints the tokens,
   not --color-blue). */
.chat-detail-member-you {
  color: var(--accent-blue);
  background: var(--accent-blue-elevated);
  mix-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);
  mix-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(--background-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(--background-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(--background-primary);
  color: var(--label-primary);
  font-family: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}
@media (hover: hover) {
  .chat-detail-confirm-cancel:hover {
  background: var(--background-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(--background-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(--background-primary);
  color: var(--label-primary);
}
@media (hover: hover) {
  .chat-detail-name-save:hover,
  .chat-detail-name-cancel:hover {
  background: var(--background-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. */

/* 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;
  /* The canvas the cards' fades dissolve into (`--list-canvas`, read by
     .list-card-fade): bound to what this container paints, so a change
     of page colour takes the scrims with it (Julian, 2026-11-06, when the
     dark page moved and the cards' feet stayed black). */
  --list-canvas: var(--background-secondary);
  background: var(--list-canvas);
}

/* 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(--background-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(--background-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(--background-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;
  overscroll-behavior: none;
  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 list cards (iOS ListCardView / BucketGridView, PR 1260, 2026-10-28) ──
   Search's entry surface on the phone page and in the desktop list pane,
   one block at every width: one full-width card per collection, 240 tall,
   4 apart. The fill is the list's own colour or the brand tint wash; a
   hero sits in a 144 box at the top; a fade rises from the bottom to the
   theme's primary surface — never iOS's literal white (Julian: "that dark
   mode is horrid") — and the title and the count sit on it. */
.list-grid {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
}

.list-card {
  position: relative;
  display: block;
  flex: 0 0 auto;
  width: 100%;
  height: 240px;
  padding: 0;
  border: 0;
  border-radius: 24px;
  overflow: hidden;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  /* A div in a button's role since 2026-11-05 (its heart and bubble are
     buttons of their own): the button's no-select, and its focus ring. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  background: linear-gradient(
    to bottom right,
    var(--card-grad-from),
    var(--card-grad-to)
  );
}

.list-card:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

/* Each tint names its gradient's two stops (round 2's table; teal on the
   fourth). The cycle runs over display position and skips a card wearing
   its own colour (app/buckets assign_tints). */
.list-card.list-card-tint-0 {
  --card-grad-from: var(--bucket-grad-blue-from);
  --card-grad-to: var(--bucket-grad-blue-to);
}

.list-card.list-card-tint-1 {
  --card-grad-from: var(--bucket-grad-orange-from);
  --card-grad-to: var(--bucket-grad-orange-to);
}

.list-card.list-card-tint-2 {
  --card-grad-from: var(--bucket-grad-red-from);
  --card-grad-to: var(--bucket-grad-red-to);
}

.list-card.list-card-tint-3 {
  --card-grad-from: var(--bucket-grad-teal-from);
  --card-grad-to: var(--bucket-grad-teal-to);
}

.list-card.list-card-tint-4 {
  --card-grad-from: var(--bucket-grad-green-from);
  --card-grad-to: var(--bucket-grad-green-to);
}

/* A list wearing its own colour (the New List sheet's swatch, or a
   built-in's): the hex at full strength, flat. Dark mode wears it as a
   22% wash over the page, as iOS does (`ListCardView.fill`): the pastels
   at full strength were the brightest thing on a dark screen. */
.list-card.list-card--own {
  background: var(--list-color);
}

[data-theme="dark"] .list-card.list-card--own {
  background: color-mix(in srgb, var(--list-color) 22%, transparent);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .list-card.list-card--own {
    background: color-mix(in srgb, var(--list-color) 22%, transparent);
  }
}

/* Pressed, the card dims like an iOS plain button; no hover state (iOS has
   none, and a fill change would wash out the gradient). */
.list-card:active:not(:has(.list-card-metric:active)) {
  opacity: 0.7;
}

.list-card:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

/* The hero's 144 box, 4 from the top; whatever it holds is centred. */
.list-card-hero {
  position: absolute;
  top: 4px;
  left: 0;
  right: 0;
  height: 144px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

/* An emoji glyph renders a touch wider than its point size (iOS 116). */
.list-card-emoji {
  font-size: 116px;
  line-height: 144px;
}

/* The Reminders export's canvas carries the shadow's bleed (413px @2x: the
   163 icon plus its shadow), so it is drawn larger than the box and nudged
   down to sit where the frame's icon does: its centre 92.5 from the card's
   top (the box's centre is 76; a 33 top margin moves a centred item 16.5). */
.list-card-reminders {
  width: 206px;
  height: 206px;
  margin-top: 33px;
}

/* The Calendar tile (iOS CalendarHeroIcon): a 141 white continuous square
   tilted 4.4°, a white → #F6F6F6 face, the weekday in the calendar red over
   a large light day. The colours are the icon's own, both themes. */
.list-card-cal {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 141px;
  height: 141px;
  border-radius: 32px;
  background: linear-gradient(#ffffff, #f6f6f6);
  box-shadow: 0 4.36px 21.8px rgba(0, 0, 0, 0.15);
  transform: rotate(4.4deg);
  color: #000000;
}

/* Centres from the tile's centre: the weekday 44 up, the day 12 down. */
.list-card-cal-weekday {
  margin: -7px 0 1px;
  font-size: 30px;
  font-weight: 500;
  line-height: 30px;
  color: #eb4b46;
}

.list-card-cal-day {
  font-size: 87px;
  font-weight: 300;
  line-height: 80px;
  letter-spacing: -2px;
}

/* The fade (Figma's 120 band, the card's bottom half; 146 until
   2026-11-04, when it covered the hero's lower third): clear at its top,
   the theme's primary surface from 70% down — white in light, black in dark — OVER the hero,
   so the art dissolves into it. */
.list-card-fade {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 120px;
  /* Into the container's own canvas (`--list-canvas`: the phone page's,
     the list pane's, the New List sheet's), never a fixed surface — the
     dark page moved from near-black and left the feet black (2026-11-06).
     iOS fades to backgroundPrimary, which on its canvas reads the same. */
  background: linear-gradient(
    to bottom,
    transparent 0%,
    var(--list-canvas, var(--background-secondary)) 70%
  );
  pointer-events: none;
}

/* The text, bottom-left over the fade: the title (titleSmall) and, 4
   below, the count (footnote). */
.list-card-text {
  position: absolute;
  left: 16px;
  right: 8px;
  bottom: 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.list-card-title {
  font-size: 20px;
  font-weight: 600;
  line-height: 25px;
  color: var(--label-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.list-card-count {
  font-size: 13px;
  line-height: 18px;
  color: var(--label-secondary);
}

/* The social row under a user list's title (Julian's mock, 2026-11-05;
   iOS ListCardView.glyphStrip, 32 tall, 24 apart): heart · bubble ·
   sparkle · plane · bookmark, each a 24 glyph with its count at 4 in the
   row's grey. The heart and the bubble are buttons (their tap is theirs,
   not the card's); the rest are bare and inert. A lit heart (the viewer's
   own like) wears the house pastel, as the detail row's does. */
.list-card-row {
  display: flex;
  align-items: center;
  gap: 24px;
  height: 32px;
  color: var(--label-secondary);
}

.list-card-metric {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-width: 24px;
  height: 32px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: default;
}

button.list-card-metric {
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

button.list-card-metric:active {
  transform: scale(1.2);
}

button.list-card-metric:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
  border-radius: 8px;
}

.list-card-metric__count {
  font-size: 15px;
  font-weight: 600;
  line-height: 20px;
  color: var(--action-count);
  white-space: nowrap;
}

.list-card-metric--lit .icon {
  background: var(--gradient-pastel);
}

/* The footer: the count (a built-in) or the day the list was made at the
   left, the views at the right once someone else has opened it. */
.list-card-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.list-card-views {
  font-size: 13px;
  line-height: 18px;
  color: var(--label-secondary);
  white-space: nowrap;
}

/* "Suggested", top-left, on a suggestion only (captionRegular on the
   elevated quaternary fill). */
.list-card-pill {
  position: absolute;
  left: 16px;
  top: 16px;
  padding: 8px 12px;
  border-radius: 9999px;
  font-size: 12px;
  line-height: 16px;
  background: var(--fill-quaternary);
  color: var(--label-primary);
}

/* ── 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(--background-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-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). */
  .list-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 - var(--phone-top-inset) - 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) {
  .list-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,
.avatar-photo::before {
  content: attr(data-monogram);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: inherit;
  color: #ffffff;
}

/* The framed form (ui/avatar.avatar_framed, 2026-10-28): the frame is the
   circle — classes, colour, size — and the photo fills it. The frame's own
   ::before stays off (its monogram would paint over the photo); a broken
   photo shows its monogram through its own, as the bare image does. */
.avatar-frame {
  padding: 0;
}

.avatar-frame::before {
  content: none;
}

.avatar-photo {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  object-fit: cover;
  position: relative;
  background: inherit;
  overflow: hidden;
}

/* ─── 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(--background-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(--background-primary);
  box-shadow: 0 0 0 2px var(--background-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-vibrant-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(--background-secondary);
  border-radius: var(--radius-lg);
  animation: skeleton-pulse 1.5s ease infinite;
}

.skeleton-hero {
  width: 100%;
  aspect-ratio: 4 / 3;
  background: var(--background-secondary);
  border-radius: var(--radius-lg);
  animation: skeleton-pulse 1.5s ease infinite;
}

.skeleton-line {
  height: 14px;
  background: var(--background-secondary);
  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(--background-secondary);
  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; }

/* Clerk's CAPTCHA mount on the /sso-callback boot screen (2026-10-29).
   Empty until Turnstile needs an interactive challenge; then the widget
   it injects sits centred over the wordmark and spinner, on phones and
   desktop alike, inside the screen's side gutter. */
.boot-captcha {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 10;
  max-width: calc(100vw - 2 * var(--sp-md));
}

/* ─── 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. */

/* A toDo's only drop target is its card (detail.gleam `attachable_card`;
   the AI panel takes none, the document guard just cancels a drop there).
   Its page draws the overlay, not the card: on phones the card isolates
   its stacking (detail.css), which would put it under the nav bar and the
   drawer. Window-wide because no ancestor makes a containing block for
   fixed elements (no transform, filter, backdrop-filter or contain). */
.nr-root.drag-over::after,
.nr-root--detail:has(.nr-reminder-card.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: color-mix(in srgb, var(--accent-blue) 6%, transparent);
  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,
  .nr-root--detail:has(.nr-reminder-card.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(--background-elevated-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(--background-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(--background-tertiary, var(--background-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(--background-secondary);
  border-color: transparent;
}

.settings-privacy-toggle--public .settings-privacy-indicator {
  background: var(--background-primary);
}

/* 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(--background-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(--background-primary);
  color: var(--label-primary);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}

.settings-push-btn:hover,
.settings-push-disable-btn:hover {
  background: var(--background-secondary);
}

.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(--background-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(--background-secondary);
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: none;
  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(--background-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-secondary);
}

/* 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-secondary);
}

.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(--background-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(--background-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-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-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 --background-primary is the
     cards' own black. White either way in light. */
  background: var(--background-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(--background-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-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(--background-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(--background-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;
  overscroll-behavior: none;
  display: flex;
  padding: var(--sp-xl);
  background: var(--background-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(--background-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-secondary);
  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;
  }
}

/* The Organizations tab — app/organizations. After settings.css: its pages
   use Settings' canvas, cards and pills. */
/* The Organizations tab (app/organizations, 2026-11-05): the list, an
   organization's page and its info page. The pages sit on Settings' canvas
   and column (.settings-canvas, .settings) and use its sections, cards,
   rows and pills; this file adds only what those don't draw. */

.org-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xl);
}

.org-list-title {
  margin: 0;
  font-size: var(--text-3xl);
  font-weight: 700;
  color: var(--label-primary);
}

/* A row with an avatar: the name over its note, then whatever trails. */
.org-row,
.org-member {
  gap: var(--sp-sm);
}

.org-row-text {
  display: flex;
  flex: 1;
  min-width: 0;
  flex-direction: column;
  gap: var(--sp-4xs);
}

/* The header, after WhatsApp's community home: the square image, the
   name, "Organization · N members" and the description. */
.org-header {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
}

.org-header-avatar {
  flex: none;
}

.org-header-avatar .avatar,
.org-header-avatar img {
  border-radius: var(--radius-lg);
}

.org-header-text {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: var(--sp-3xs);
}

.org-header-name {
  margin: 0;
  font-size: var(--text-3xl);
  font-weight: 700;
  line-height: 1.15;
  color: var(--label-primary);
  overflow-wrap: anywhere;
}

.org-header-meta,
.org-header-description {
  margin: 0;
  font-size: var(--text-md);
  line-height: 20px;
  color: var(--label-secondary);
}

.org-header-description {
  color: var(--label-primary);
  white-space: pre-wrap;
}

/* The community menu's entries as pills in a row (Settings' pills are
   full width). */
.org-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs);
}

.org-actions .settings-pill,
.org-member .settings-pill,
.org-join,
.org-more {
  width: auto;
  flex: none;
  font-size: var(--text-md);
  min-height: 32px;
}

.org-more {
  align-self: center;
}

/* The channels: chat-list rows on a card. */
.org-channels {
  display: flex;
  flex-direction: column;
  background: var(--background-primary);
  border-radius: var(--radius-xl);
  overflow: hidden;
}

/* A toDo: the circle, the title over its line, Join. */
.org-todo-circle {
  display: inline-flex;
  flex: none;
  color: var(--label-secondary);
}

.org-todo-text {
  display: flex;
  flex: 1;
  min-width: 0;
  flex-direction: column;
  gap: var(--sp-4xs);
}

.org-todo-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--label-primary);
}

.org-todo-title--done {
  color: var(--label-secondary);
  text-decoration: line-through;
}

.org-add-label {
  color: var(--accent-blue);
}

.org-leave {
  margin-top: var(--sp-xs);
}

.org-description-input {
  box-sizing: border-box;
  width: 100%;
  padding: var(--sp-xs) 0;
  border: none;
  background: transparent;
  font: inherit;
  font-size: var(--text-md);
  color: var(--label-primary);
  resize: vertical;
}

.org-description-input:focus {
  outline: none;
}

/* The sheets over the page (ui/modal): a title, then the picker's or the
   form's own parts. */
.org-sheet {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  padding: var(--sp-xl);
  max-height: 80vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  box-sizing: border-box;
}

.org-sheet-title {
  margin: 0;
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--label-primary);
}

/* The people picked, as removable chips (WhatsApp's Add members). */
.org-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-sm);
}

.org-chip {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3xs);
  width: 56px;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  cursor: pointer;
  color: var(--label-primary);
}

.org-chip-name {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-sm);
}

.org-chip-x {
  position: absolute;
  top: -2px;
  right: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--label-secondary);
  color: var(--background-primary);
}

/* An org channel in the chat views (2026-11-05): its organization named
   under the channel title, a link to the organization's page. */
.chat-header-titles {
  display: flex;
  flex: 0 1 auto;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  gap: var(--sp-4xs);
}

.chat-header-org {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3xs);
  max-width: 100%;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--label-secondary);
  text-decoration: none;
}

.chat-header-org:hover {
  color: var(--accent-blue);
}

.chat-header-org-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The chat settings sheet of an org channel: who decides its members. */
.chat-detail-org-note {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3xs);
  margin: 0 0 var(--sp-md);
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

.chat-detail-org-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3xs);
  color: var(--accent-blue);
  font-weight: 500;
  text-decoration: none;
}

/* The New Chat sheet: a row per organization the viewer can make a
   channel in, and the channel form that replaces the people picker. */
.new-chat-org-row {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  width: 100%;
  padding: var(--sp-sm) var(--sp-md);
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  font: inherit;
  color: var(--label-primary);
  text-align: left;
  cursor: pointer;
}

.new-chat-org-row:hover {
  background: var(--fill-vibrant-tertiary);
}

.new-chat-org-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.new-chat-org-note {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

/* The chat list's org mark beside an org channel's title. */
.chat-list-row-org {
  display: inline-flex;
  flex: none;
  align-items: center;
  color: var(--label-tertiary);
}

/* An org channel's "Admins" row (WhatsApp "Group admins"): the label, then
   the owner's and admins' names. */
.chat-detail-org-admins {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3xs);
}

.chat-detail-org-admins-label {
  font-weight: 600;
  color: var(--label-primary);
}

/* Channel kinds (2026-11-05): an OPTIONAL or PRIVATE channel's mark beside
   its row and in the chat header's org line, and the kind picker of a new
   or added channel. */
.org-channel {
  position: relative;
}

.org-channel > .org-kind-mark {
  position: absolute;
  top: var(--sp-xs);
  right: var(--sp-sm);
}

.org-kind-mark {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: var(--sp-4xs);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--label-tertiary);
}

.chat-header-org .org-kind-mark::before {
  content: "·";
  margin-right: var(--sp-4xs);
}

.org-kind-choice {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

.org-kind-option {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4xs);
  padding: var(--sp-sm) var(--sp-md);
  border: 1px solid var(--border-secondary);
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--label-primary);
  text-align: left;
  cursor: pointer;
}

.org-kind-option--on {
  border-color: var(--accent-blue);
}

.org-kind-option-label {
  font-size: var(--text-md);
  font-weight: 600;
}

.org-kind-option-note {
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

.org-private-picker {
  display: flex;
  min-height: 0;
  flex-direction: column;
  gap: var(--sp-xs);
}

/* A plain group chat's "Add to <org>" rows in its settings sheet, and the
   dialog that says what the add does. */
.chat-detail-add-to-org {
  margin: var(--sp-md) 0 0;
}

.chat-detail-add-to-org .settings-card-row-label {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
}

.chat-detail-add-to-org-note {
  margin: var(--sp-3xs) 0 0;
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

.chat-detail-add-to-org-dialog .org-kind-choice {
  margin: 0 0 var(--sp-sm);
}

.chat-detail-add-to-org-kind {
  font-size: var(--text-sm);
  color: var(--label-secondary);
}

.chat-detail-add-to-org-error {
  color: var(--accent-red);
}

/* 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. */

/* 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(--background-secondary);
  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(--background-secondary);
}
@media (hover: hover) {
    .emoji-picker-category-btn:hover {
    color: var(--label-secondary);
    background: var(--background-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(--background-secondary);
  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(--background-secondary);
  outline-offset: 0;
}

/* Global skin-tone button */
.emoji-picker-tone-global-btn {
  flex-shrink: 0;
  width: 42px;
  height: 42px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  background: none;
  border-radius: var(--radius-full);
  font-size: 24px;
  cursor: pointer;
  padding: 0;
  color: inherit;
  transition: background 0.1s ease, border-color 0.1s ease;
}
.emoji-picker-tone-global-btn--active {
  background: var(--accent-blue-elevated);
}

.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: 1px solid transparent;
  background: none;
  border-radius: var(--radius-full);
  font-size: 20px;
  cursor: pointer;
  padding: 0;
  transition: background 0.1s ease, border-color 0.1s ease;
}

.emoji-picker-global-tone-swatch--active {
  border-color: var(--border-secondary);
}

.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;
  overflow-y: auto;
  overscroll-behavior: none;
  scroll-behavior: smooth;
}

.emoji-picker-section {
  padding-top: var(--sp-sm);
}

.emoji-picker-section-header {
  position: sticky;
  top: 0;
  padding: var(--sp-sm) var(--sp-sm) 0 var(--sp-sm);
  background: linear-gradient(180deg, var(--background-secondary) 80%, transparent);
  font-size: var(--text-md);
  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-full);
  font-size: 22px;
  cursor: pointer;
  padding: 0;
}

.emoji-picker-cell:focus-visible {
  background: var(--background-secondary);
}

.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(--background-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: 42px;
  height: 42px;
  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;
}


/* ─── Hover states ────────────────────────────────────────────── */


@media (hover: hover) {
  .emoji-picker-tone-global-btn:hover {
    background: var(--background-primary);
  }
  .emoji-picker-tone-global-btn--active:hover {
    border-color: var(--accent-blue);
  }
  .emoji-picker-global-tone-swatch:hover {
    background: var(--background-primary);
  }
  .emoji-picker-global-tone-swatch--active:hover {
    border-color: var(--accent-blue);
  }
  .emoji-picker-tone-btn:hover {
    background: var(--accent-blue);
  }  
  .emoji-picker-cell:hover {
    background: var(--accent-blue-elevated);
  }
  .emoji-picker-gif-cell:hover {
    background: var(--accent-blue);
  }    
}



/* Reduced motion */

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

@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;
  border-radius: var(--radius-md);
  overflow: hidden;
  cursor: pointer;
  transition: opacity 0.1s;
}

.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 --label-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);
  }
  .composer-emoji-sheet {
    padding-bottom: var(--sp-3xs) !important;
  }
}

@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-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--scrim: its foot fades, --sheet-scrim-fade)
           │   └─ .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 the centred modal (R5):
   layer and container display: contents, the .85 + 4px blur overlay,
   560 / 420 panel widths, fade-scale entrance, no grabber; the family's 32
   corners and the phone's 18 / 16 header inset (2026-10-29).
──────────────────────────────────────────────────────────────────────────── */

.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;
}

/* The pages painted --background-secondary flip a sheet's default fill to
   the primary surface (C11). A custom property the sheet reads, never the
   shell's own paint: the token refactor of 2026-11-06 wrote it as
   `background`, which painted the whole shell white on Home and Search —
   the desktop pane's canvas and the phone's safe-area strip with it. */
.app-shell--surface-secondary {
  --sheet-fill: var(--background-primary);
}

.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) - var(--safe-top) - 18px);
  background: var(--background-secondary);
  color: var(--label-primary);
  padding-bottom: 32px;
  overflow: hidden;
  animation: sheet-in 0.3s var(--ease-slide, cubic-bezier(0.32, 0.72, 0, 1)) forwards;
  will-change: transform, height;
  border-radius: var(--sheet-radius);
}



/* ── 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-vibrant-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-vibrant-primary);
  mix-blend-mode: var(--blend-mode);
}
.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-vibrant-tertiary));
  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-semibold);
  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;
}

/* ── Scrim (Config.scrim, 2026-11-02; stuck footer 2026-11-03): the
   scroller's foot dissolves into the card, as iOS fades its last row under
   a bottom bar. The card carries `--sheet-scrim-fade` from the config —
   the fade's height — and, when the sheet is `--stuck` (a scrim AND a
   footer), `--sheet-foot-h`, the footer's measured height (sheet_motion
   .mjs). The mask runs up from the bottom edge: transparent across the
   footer's body, then the fade over the footer's top `--sheet-scrim-fade`,
   solid above — Julian's Figma gradient, Background/Transparent at the
   footer's top edge to Background/Base 40 down. Without a footer the
   override below restores today's two-stop fade.
   A mask rather than a painted gradient, so whatever fills the card (a
   token, a material) is what shows through. A plain sheet's stack is the
   scroller, so it wears the mask; a fluid sheet's main child owns the
   scroll, so the fill wears it and the stack is left alone. ── */
.sheet--scrim .sheet__stack,
.sheet--scrim.sheet--fluid .sheet__fill {
  --_foot: var(--sheet-foot-h, 0px);
  --_fade: var(--sheet-scrim-fade, 40px);
  -webkit-mask-image: linear-gradient(
    to top,
    transparent 0,
    transparent calc(var(--_foot) - var(--_fade)),
    #000 var(--_foot)
  );
  mask-image: linear-gradient(
    to top,
    transparent 0,
    transparent calc(var(--_foot) - var(--_fade)),
    #000 var(--_foot)
  );
}
.sheet--scrim.sheet--fluid .sheet__stack {
  -webkit-mask-image: none;
  mask-image: none;
}
/* Without a footer `--_foot` is 0 and the middle stop reads
   "transparent calc(0 - 40px)": a stop behind its predecessor clamps
   forward to 0, so the three stops become `#000 0` — a hard edge, not a
   fade (the gradient spec, every engine). The no-footer case is spelled
   out as today's two-stop fade. A fluid sheet's stack still wears no mask
   (its fill does): the stack half is scoped to non-fluid sheets. */
.sheet--scrim:not(.sheet--fluid):not(.sheet--stuck) .sheet__stack,
.sheet--scrim.sheet--fluid:not(.sheet--stuck) .sheet__fill {
  -webkit-mask-image: linear-gradient(to top, transparent 0, #000 var(--_fade));
  mask-image: linear-gradient(to top, transparent 0, #000 var(--_fade));
}

/* ── Stuck footer (2026-11-03): absolute at the card's bottom, over the
   scroller, carrying the card's own bottom inset as its padding; it paints
   nothing, so the card's fill is what the fade dissolves into. The
   scroller pads its foot by the footer's height — plus `--sheet-foot-clear`,
   a sheet's own clearance above the footer (the Comments sheet's 12,
   2026-11-04; 0 unless set) — so the last row scrolls clear of it. ── */
.sheet--stuck {
  padding-bottom: 0;
}
.sheet--stuck .sheet__footer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding-bottom: 32px;
  background: none;
  box-shadow: none;
  transition: none;
}
.sheet--stuck .sheet__stack {
  padding-bottom: calc(var(--sheet-foot-h, 0px) + var(--sheet-foot-clear, 0px));
}
.sheet--stuck.sheet--fluid .sheet__stack {
  padding-bottom: 0;
}
.sheet--stuck.sheet--fluid .sheet__fill > * {
  padding-bottom: calc(var(--sheet-foot-h, 0px) + var(--sheet-foot-clear, 0px));
}

/* ── 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;
    border-radius: var(--sheet-radius) var(--sheet-radius) var(--sheet-bottom-radius) var(--sheet-bottom-radius);
    padding-bottom: 42px;
  }

  .sheet__header {
    padding-top: 8px;
    padding-bottom: 0px;
  }
  .sheet input,
  .sheet textarea,
  .sheet [contenteditable="true"] {
    -webkit-user-select: text;
    user-select: text;
  }
  .sheet.msg-action-sheet {
    padding-bottom: var(--sp-3xs);
  }
  .sheet--stuck {
    padding-bottom: 0;
  }
  .sheet--stuck .sheet__footer {
    padding-bottom: 42px;
  }
}
.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): the centred modal (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;
    /* The family radius, as the phone card wears it (2026-10-29). */
    border-radius: var(--sheet-radius);
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4);
    background: var(--background-elevated-primary, var(--background-primary));
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    padding: 0 0 var(--sp-xxl);
    animation: sheet-modal-in 0.22s ease forwards;
    will-change: auto;
  }
  /* A stuck footer on the modal: the card is fit-content under its 80vh,
     and the footer overlays the stack's padded tail, so the stack grows to
     the card's cap and scrolls under the footer — the same geometry as the
     phone's. (A first cut capped the stack at 80vh less the footer: that
     took the footer's height twice and left one row of grid showing,
     2026-11-03.) The footer wears the modal's bottom inset. */
  .sheet--stuck {
    padding-bottom: 0;
  }
  .sheet--stuck .sheet__footer {
    padding-bottom: var(--sp-xxl);
  }
  /* Menu-style sheets read better narrower than the 560 Medium default. */
  .sheet.sheet--menu {
    width: min(92vw, 420px);
  }
  .sheet__grabber-slot {
    display: none;
  }
  /* No grabber here, so the header takes the slot's 10 back: its discs sit
     18 from the card's top and 16 from its sides, where a phone puts them
     (the 10 slot + the phone header's 8). Without it the ✕ touched the
     card's top edge (Julian's report, 2026-10-29). */
  .sheet__header {
    padding-top: calc(10px + var(--sp-xs));
  }
}

/* ── Hero sheet (Julian's mocks, 2026-10-28; ui/sheet.view_hero): no header.
   The brand glow over the top of the card, a graphic, a title, a line, the
   Sign up pill and a Log in link, and a 48 glass ✕ floated 12 from the top
   and 12 from the right. A phone centres it like the desktop modal — 12
   off each edge, 24 radius, the fade-scale entrance, no grabber, no drag
   (sheet_motion.mjs) — rather than docking it at the foot. Measured off
   the phone mock: 40 over the graphic, 24 to the title, 8 to the line, 24
   to the 44 pill at 32 from the sides, 16 to Log in, 24 under it. ── */
.sheet.sheet--hero {
  text-align: center;
  padding: 0;
  background:
    radial-gradient(70% 160px at 28% -40px, var(--link-page-glow-1), transparent 100%),
    radial-gradient(70% 180px at 74% -60px, var(--link-page-glow-2), transparent 100%),
    var(--background-primary);
}
.sheet--hero .sheet__grabber-slot {
  display: none;
}
.sheet--hero .sheet__stack {
  /* 40 over the graphic, 32 under the actions: Julian's 369×383 artboard
     for "Never miss a thing" (2026-10-28), an 88 avatar hero. */
  padding: 40px 32px 32px;
  gap: 0;
  align-items: center;
}
.sheet-hero__close {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 1;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  --glass-tint-color: var(--background-primary);
  box-shadow: var(--glass-rim-disc), 0 4px 8px rgba(0, 0, 0, 0.1);
  color: var(--label-primary);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* The graphic box is its content's height: an 88 avatar, or a 132 art
   slot (link_page.css). */
.sheet-hero__graphic {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.sheet-hero__title {
  margin: 24px 0 0;
  font: 700 28px/34px var(--font-sans);
  letter-spacing: var(--tracking-body);
  color: var(--label-primary);
  text-wrap: balance;
}
.sheet-hero__subtitle {
  margin: 6px 0 0;
  font: 400 16px/21px;
  letter-spacing: var(--tracking-body);
  color: var(--label-primary);
  text-wrap: balance;
}
.sheet-hero__actions {
  width: 100%;
  margin-top: 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}
/* The filled pill, the bar's Sign up at the card's width (link_page.css). */
.sheet-hero__primary {
  width: 100%;
  height: 44px;
  padding: 0 var(--sp-lg);
  border: 0;
  border-radius: var(--radius-full);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-blue);
  color: var(--label-inverse-primary);
  font-size: var(--text-lg);
  font-weight: 600;
  font-family: inherit;
  text-decoration: none;
  cursor: pointer;
}
.sheet-hero__secondary {
  padding: var(--sp-3xs) var(--sp-xs);
  border: 0;
  background: none;
  color: var(--accent-blue);
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 20px;
  font-family: inherit;
  text-decoration: none;
  cursor: pointer;
}

@media (max-width: 834px) {
  .sheet-layer:has(> .sheet-container > .sheet--hero) {
    align-items: center;
  }
  .sheet-container:has(> .sheet--hero) {
    padding: 0 12px;
  }
  .sheet.sheet--hero {
    border-radius: 24px;
    max-height: calc(var(--vvh, 100dvh) - 24px);
    animation: sheet-modal-in 0.22s ease forwards;
    will-change: auto;
  }
}

@media (min-width: 835px) {
  .sheet.sheet--hero {
    width: min(92vw, 590px);
    border-radius: 24px;
    padding: 0;
  }
  .sheet-hero__title {
    font-size: 28px;
  }
}


/* ── New Message (Julian's mock, 2026-11-06; iOS NewChatView over
   PeoplePickerGrid) ──────────────────────────────────────────────────────
   The background tokens are the renamed ones (--background-*) with their
   old names as fallbacks (--bg-*): the rename was in flight in another
   session when this landed. Drop the fallbacks once it has.
   The picker (ui/sheet, id new-chat-modal, app/chat) on the sheet's own
   fill: the ✕ and the ✓ as white discs lifted off it, the search capsule,
   then three columns of people — a 72 avatar over the name — the picked
   ones wearing the accent badge, and the ✓ wearing their count. The
   numbers are the mock's at 393: a 44 capsule 24 under the bar, the first
   row 26 under it, rows 140 apart, the columns 136 wide (8 past each
   edge, which the stack clips). Scoped by the sheet's id: the Add members
   and share pickers keep their rows (chat.css .new-chat-*). */

/* The discs: white on the fill, a soft lift and a hairline rim. The ✓
   wearing the count brings its own fill (the inline --sheet-hbtn-fill,
   ui/sheet) and drops the rim. */
#new-chat-modal .sheet__hbtn {
  background: var(--sheet-hbtn-fill, var(--background-primary));
  box-shadow:
    0 2px 8px rgba(0, 0, 0, 0.08),
    inset 0 0 0 1px rgba(0, 0, 0, 0.04);
}
#new-chat-modal .sheet__hbtn:disabled {
  cursor: default;
}
#new-chat-modal .new-message-start--count {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14);
}
#new-chat-modal .new-message-start--count:disabled {
  opacity: 0.5;
}
.new-message-count {
  font: var(--type-body-semibold);
  letter-spacing: var(--tracking-body);
  line-height: 1;
}

/* The capsule: 44 tall, white, the text 24 in. Over chat.css's
   .new-chat-search (both classes ride the input; this sheet loads later). */
.new-message-search {
  box-sizing: border-box;
  width: 100%;
  height: 44px;
  /* Not a shrinking flex child: the stack is a column, and with the grid
     overflowing the half-open card the input lost its height (21 tall,
     the proxy at 393, 2026-11-06). */
  flex: 0 0 44px;
  margin: 0;
  padding: 0 24px;
  border: 0;
  border-radius: 9999px;
  background: var(--background-primary);
  box-shadow:
    0 2px 8px rgba(0, 0, 0, 0.06),
    inset 0 0 0 1px rgba(0, 0, 0, 0.04);
  font: 400 17px/22px var(--font-sans);
  letter-spacing: var(--tracking-body);
  color: var(--label-primary);
  caret-color: var(--accent-blue);
  outline: none;
  -webkit-appearance: none;
  appearance: none;
}
.new-message-search::placeholder {
  color: var(--label-tertiary);
}
.new-message-search:focus {
  box-shadow:
    0 2px 8px rgba(0, 0, 0, 0.06),
    inset 0 0 0 1px rgba(0, 0, 0, 0.04),
    0 0 0 2px var(--accent-blue-elevated);
}

/* The grid: the fluid sheet's scroller (sheet__fill). Three equal
   columns 8 past each edge of the stack's 16, so the outer cells centre
   where the mock's do (61 · 197 · 334 at 393). */
.new-message-grid {
  list-style: none;
  margin: 0 -24px;
  padding: 2px 0 24px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  row-gap: 40px;
  justify-items: center;
  align-content: start;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
}
.new-message-grid::-webkit-scrollbar {
  display: none;
}
.new-message-person {
  min-width: 0;
  width: 100%;
}
.new-message-pick {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.new-message-avatar {
  position: relative;
  display: inline-flex;
  width: 72px;
  height: 72px;
  border-radius: 50%;
}
.new-message-pick:focus-visible .new-message-avatar {
  outline: 2px solid var(--accent-blue);
  outline-offset: 3px;
}
/* The badge (iOS PersonGridCell: 28 with a 3 ring of the host surface —
   the card's fill — so it punches out of the avatar's edge). */
.new-message-badge {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-blue);
  color: var(--label-on-accent);
  box-shadow: 0 0 0 3px var(--background-primary, var(--background-secondary));
}
.new-message-name {
  box-sizing: border-box;
  max-width: 100%;
  padding: 0 4px;
  font: 400 15px/18px var(--font-sans);
  letter-spacing: -0.2px;
  color: var(--label-primary);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.new-message-empty {
  grid-column: 1 / -1;
  padding: 24px 32px;
  font: var(--type-callout);
  color: var(--label-secondary);
  text-align: center;
}

/* Loading: a cell's shape, six of them (states.css shimmers .skeleton-*). */
.new-message-person--placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.new-message-placeholder-avatar {
  width: 72px;
  height: 72px;
  border-radius: 50%;
}
.new-message-placeholder-name {
  width: 84px;
  height: 14px;
  margin: 2px 0;
  border-radius: 7px;
}

/* Dark: the discs and the capsule lift a tier off the fill. */
[data-theme="dark"] #new-chat-modal .sheet__hbtn,
[data-theme="dark"] .new-message-search {
  background: var(--sheet-hbtn-fill, var(--background-elevated-tertiary));
  box-shadow:
    0 2px 8px rgba(0, 0, 0, 0.40),
    inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
/* The placeholder a step up: the light tier's tertiary vanished on the
   dark capsule. */
[data-theme="dark"] .new-message-search::placeholder {
  color: var(--label-secondary);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) #new-chat-modal .sheet__hbtn,
  :root:not([data-theme]) .new-message-search {
    background: var(--sheet-hbtn-fill, var(--background-elevated-tertiary));
    box-shadow:
      0 2px 8px rgba(0, 0, 0, 0.40),
      inset 0 0 0 1px rgba(255, 255, 255, 0.06);
  }
  :root:not([data-theme]) .new-message-search::placeholder {
    color: var(--label-secondary);
  }
}

/* New List / Edit List on the sheet (iOS PR 1260) — app/search. */
/* ── New List / Edit List (iOS NewListView, PR 1260, 2026-10-28) ──────────
   The sheet (ui/sheet, id list-sheet) on the canvas fill, so its white
   capsules and picker cards read. Its stack takes the frame's 8 under the
   bar, 16 at the sides, 16 between the pieces: the name capsule, the
   preview card the list becomes, Ask Please, the colour rows, the emoji
   grid. The numbers are the frame's. */
#list-sheet .sheet__stack {
  padding: 8px 16px 16px;
  gap: 16px;
}

.list-sheet {
  display: flex;
  flex-direction: column;
  gap: 16px;
  /* The preview card's fade dissolves into the sheet's own fill
     (search.css .list-card-fade). */
  --list-canvas: var(--canvas-bg);
}

/* The save's failure line, under the bar. */
.list-sheet-error {
  margin: 0;
  font-size: 13px;
  line-height: 18px;
  color: var(--accent-red);
}

/* 49 white pill, the text centred: titleSmall over 12 of padding. */
.list-sheet-name {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  height: 49px;
  padding: 12px;
  border-radius: 9999px;
  background: var(--background-primary);
  cursor: text;
}

.list-sheet-name-input {
  width: 100%;
  border: 0;
  outline: 0;
  background: transparent;
  font: inherit;
  font-size: 20px;
  font-weight: 600;
  line-height: 25px;
  text-align: center;
  color: var(--label-primary);
  caret-color: var(--accent-blue);
}

.list-sheet-name-input::placeholder {
  color: var(--label-secondary);
}

/* The preview: Search's card, inert, its fill easing between swatches. */
.list-card--preview {
  flex: 0 0 240px;
  pointer-events: none;
  cursor: default;
  transition: background 0.2s ease-in-out;
}

/* The Home row's sparkle and plane between the title and the footer (the
   frame draws them; Search's cards don't). */
.list-card-strip {
  display: flex;
  align-items: center;
  gap: 24px;
  height: 32px;
  color: var(--label-secondary);
}

/* Ask Please: a 49 white pill, bodyMedium. */
.list-sheet-ask {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  box-sizing: border-box;
  height: 49px;
  padding: 12px;
  border: 0;
  border-radius: 9999px;
  background: var(--background-primary);
  font: inherit;
  font-size: 18px;
  font-weight: 500;
  line-height: 25.2px;
  color: var(--label-primary);
  cursor: pointer;
}

/* The pickers' white 24-radius cards. */
.list-sheet-colors,
.list-sheet-emoji {
  padding: 16px;
  border-radius: 24px;
  background: var(--background-primary);
}

/* Three rows of six 40 discs, 16 apart; the pick wears a 3 ring outside
   its edge. */
.list-sheet-colors {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.list-sheet-color-row {
  display: flex;
  justify-content: space-between;
}

.list-swatch {
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 9999px;
  cursor: pointer;
}

.list-swatch--on {
  box-shadow: 0 0 0 3px var(--label-secondary);
}

/* Eight rows of six 50 cells, 18 between rows; the pick sits on a
   canvas-coloured disc. */
.list-sheet-emoji {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  row-gap: 18px;
  justify-items: center;
}

.list-emoji {
  width: 50px;
  height: 50px;
  padding: 0;
  border: 0;
  border-radius: 9999px;
  background: transparent;
  font-size: 34px;
  line-height: 50px;
  text-align: center;
  cursor: pointer;
}

.list-emoji--on {
  background: var(--background-secondary);
}

/* ── Delete list's confirm (ui/confirm_modal, prefix list-confirm) ────────
   Rendered in the menu sheet's layer (`sheet.overlays`): above the card
   (the sheet tier is 401), and with its own pointer events, since the
   layer's are off. The house confirm's look (detail.css's). */
.list-confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 410;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  pointer-events: auto;
}

.list-confirm-backdrop {
  position: absolute;
  inset: 0;
}

.list-confirm-dialog {
  position: relative;
  width: min(400px, calc(100vw - 48px));
  padding: var(--sp-xl);
  border-radius: var(--radius-lg);
  background: var(--background-secondary);
  box-shadow: var(--shadow);
}

.list-confirm-title {
  margin: 0 0 var(--sp-xs);
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.3;
  color: var(--label-primary);
}

.list-confirm-body {
  margin: 0 0 var(--sp-lg);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--label-primary);
}

.list-confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-xs);
}

.list-confirm-btn {
  padding: var(--sp-xs) var(--sp-md);
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}

.list-confirm-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

.list-confirm-cancel {
  border: 1px solid var(--border-primary);
  background: var(--background-primary);
  color: var(--label-primary);
}

.list-confirm-delete {
  border: none;
  background: var(--accent-red);
  color: #ffffff;
  font-weight: 500;
}

@media (hover: hover) {
  .list-confirm-cancel:hover {
    background: var(--background-tertiary);
  }

  .list-confirm-delete:hover {
    opacity: 0.9;
  }
}

/* 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 (--glass-tint)
   painted over the material — a flat body with a darker band under the
   top edge alone, fading over 24px; the rim (--glass-rim) — a light on the
   top edge, a white glow along the foot, a hairline one device pixel wide
   — as inset shadows, so the box and its padding stay exact; and the
   glass's own soft drop shadow (--glass-shadow). One recipe in both
   themes, measured from Julian's glass mock (2026-10-28); the values 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. The surface
   carries its own shadow after the rim — index.css imports this sheet
   AFTER the feature sheets, so at equal specificity this box-shadow wins
   over a component's (which is how the composers lost their float shadow
   from 2026-10-27 until 10-28); a component's higher-specificity variant
   (focus, drag-over) composes the whole thing itself:
     box-shadow: var(--glass-rim), 0 0 0 2px var(--accent-blue), var(--glass-shadow);
   Something that cannot take the class (a pseudo-element) writes the same
   declarations from the tokens.

   A TINTED glass (Julian, 2026-10-27): the phone header's discs and the
   desktop list header's in the page's colour, the + buttons in the user's
   accent. Add .glass-surface--tinted and set --glass-tint-color on the
   element (--glass-tint-strength too, 72% unless said): the colour at that
   strength goes under the tint's edge shade and over the THIN material,
   so the backdrop still shows through and the rim stays. The colour is a
   token, so a theme or an accent (accent.css) retints it on its own:
     .nav-bar-disc { --glass-tint-color: var(--background-primary); }
     .tab-plus { --glass-tint-color: var(--accent-blue); --glass-tint-strength: 85%; }

   A DISC (a round button) adds .glass-surface--disc, or composes
   --glass-rim-disc itself: the rim without the slab's bottom glow, which
   paints a crescent along a circle's foot and squashes it to an oval
   (Julian, 2026-10-27, the + beside iOS's). */

.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), var(--glass-shadow);
}

/* 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);
}

.glass-surface--tinted {
  --glass-layer-1: var(--material-thin-1);
  --glass-layer-2: var(--material-thin-2);
  --glass-colour: color-mix(
    in srgb,
    var(--glass-tint-color) var(--glass-tint-strength, 72%),
    transparent
  );
  background:
    var(--glass-tint),
    linear-gradient(var(--glass-colour), var(--glass-colour)),
    linear-gradient(var(--glass-layer-1), var(--glass-layer-1)),
    linear-gradient(var(--glass-layer-2), var(--glass-layer-2));
}

/* A disc: the top light and the ring alone (tokens.css --glass-rim-disc),
   and the discs' own 4/8 drop shadow. */
.glass-surface--disc {
  box-shadow: var(--glass-rim-disc), var(--glass-shadow-disc);
}

/* The voice recorder sheet: glass discs, the accent bars, the 72 controls
   (ui/voice_capture, 2026-10-28). */
/* The voice recorder sheet (Julian's mocks on a 402 display, 2026-10-28;
   iOS VoicePickerView; ui/voice_capture). ui/sheet on the CHROME material
   (sheet.css paints it), two glass discs in the header (glass.css — the ✕
   in the page's colour, the ✓ in the user's accent once a take exists or
   is in progress, fill-secondary before), the 8-band visualizer in the
   accent, and the 72 controls in the footer. Everything a colour is a
   token, so the themes and the accents follow on their own. */

/* ── Header discs ───────────────────────────────────────────────────── */

.voice-sheet__close {
  --glass-tint-color: var(--background-primary);
  color: var(--label-primary);
}

.voice-sheet__confirm {
  --glass-tint-color: var(--accent-blue);
  --glass-tint-strength: 85%;
  color: var(--label-inverse-primary);
}

/* Before a take: the secondary fill and glyph (iOS), still a disc. */
.voice-sheet__confirm--off {
  --glass-tint-color: var(--fill-vibrant-secondary);
  --glass-tint-strength: 100%;
  color: var(--label-secondary);
  cursor: default;
}

/* ── The timer under the title ──────────────────────────────────────── */

/* The FFI writes data-elapsed at frame rate; the pseudo-element shows it.
   A fixed line, so the header never reflows as a take begins. */
.voice-timer {
  display: block;
  min-height: 18px;
  margin-top: 2px;
  font: var(--type-footnote);
  font-variant-numeric: tabular-nums;
  color: var(--label-secondary);
  text-align: center;
}

.voice-timer::after {
  content: attr(data-elapsed);
}

.voice-timer--placeholder {
  visibility: hidden;
}

/* ── The bars ───────────────────────────────────────────────────────── */

/* Eight capsules mirrored about the row's midline (the mock: 7 wide at a
   15 pitch, 7 at rest — a dot — to 54 at full). The silhouette is fixed
   so the sheet never breathes with the sound; the FFI drives --voice-bar-n
   (0…1) on the container, each bar reading its own. */
.voice-bars {
  --voice-bar-w: 7px;
  --voice-bar-min: 7px;
  --voice-bar-max: 54px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: calc(var(--voice-bar-max) + 4px);
  /* The mock: the bars' midline 172 under the sheet's top, the record
     disc's 306, the foot 398. */
  margin: 56px 0 68px;
}

/* Idle, the dots rest dimmed (the mock); a take's fingerprint and the
   live bands wear the full accent. */
.voice-bars--rest .voice-bar {
  background: color-mix(in srgb, var(--accent-blue) 32%, transparent);
}

.voice-bar {
  flex: none;
  width: var(--voice-bar-w);
  height: var(--voice-bar-min);
  border-radius: var(--radius-full);
  background: var(--accent-blue);
}

.voice-bar:nth-child(1) { height: calc(var(--voice-bar-min) + var(--voice-bar-0, 0) * (var(--voice-bar-max) - var(--voice-bar-min))); }
.voice-bar:nth-child(2) { height: calc(var(--voice-bar-min) + var(--voice-bar-1, 0) * (var(--voice-bar-max) - var(--voice-bar-min))); }
.voice-bar:nth-child(3) { height: calc(var(--voice-bar-min) + var(--voice-bar-2, 0) * (var(--voice-bar-max) - var(--voice-bar-min))); }
.voice-bar:nth-child(4) { height: calc(var(--voice-bar-min) + var(--voice-bar-3, 0) * (var(--voice-bar-max) - var(--voice-bar-min))); }
.voice-bar:nth-child(5) { height: calc(var(--voice-bar-min) + var(--voice-bar-4, 0) * (var(--voice-bar-max) - var(--voice-bar-min))); }
.voice-bar:nth-child(6) { height: calc(var(--voice-bar-min) + var(--voice-bar-5, 0) * (var(--voice-bar-max) - var(--voice-bar-min))); }
.voice-bar:nth-child(7) { height: calc(var(--voice-bar-min) + var(--voice-bar-6, 0) * (var(--voice-bar-max) - var(--voice-bar-min))); }
.voice-bar:nth-child(8) { height: calc(var(--voice-bar-min) + var(--voice-bar-7, 0) * (var(--voice-bar-max) - var(--voice-bar-min))); }

.voice-sheet__error {
  margin: 0 0 16px;
  font: var(--type-footnote);
  color: var(--accent-red);
  text-align: center;
}

/* ── The controls ───────────────────────────────────────────────────── */

.sheet--voice .sheet__footer {
  justify-content: center;
  padding: 0 16px 16px;
}

.voice-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 32px;
}

/* The 72 record toggle (iOS): one disc whose fill, ring and stop glyph
   move between states. Idle (and over a take, to re-record): a solid
   accent-red disc inside a thin red ring — the mock's 64 in 72. Recording:
   the disc grows to the ring, washed to red at 10%, the ring turns red
   and the 32 stop glyph fades in. */
.voice-record {
  position: relative;
  flex: none;
  width: 72px;
  height: 72px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-red) 45%, transparent);
  color: var(--accent-red);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: box-shadow 0.2s ease;
}

.voice-record::before {
  content: "";
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: var(--accent-red);
  transition: inset 0.2s ease, background 0.2s ease;
}

.voice-record__glyph {
  position: relative;
  display: inline-flex;
  opacity: 0;
  transition: opacity 0.2s ease;
}

.voice-record--recording {
  box-shadow: inset 0 0 0 1px var(--accent-red);
}

.voice-record--recording::before {
  inset: 0;
  background: color-mix(in srgb, var(--accent-red) 10%, transparent);
}

.voice-record--recording .voice-record__glyph {
  opacity: 1;
}

/* The 72 play/pause for the take (iOS): the accent on its elevated wash. */
.voice-play {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 72px;
  height: 72px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--accent-blue-elevated);
  color: var(--accent-blue);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  animation: voice-play-in 0.35s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

@keyframes voice-play-in {
  from {
    opacity: 0;
    transform: scale(0.6);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .voice-record,
  .voice-record::before,
  .voice-record__glyph {
    transition: none;
  }

  .voice-play {
    animation: none;
  }
}

/* ============================================================
   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 --background-primary: in dark mode that's pure black,
     and the panel vanished into the 0.85-black backdrop (prod 2026-09-29). */
  background: var(--background-elevated-primary, var(--background-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-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); }

/* ── desktop: the sheet modal's 32 corners (sheet.css, 2026-10-29), so a
   panel and a sheet presented on desktop read as one family. The close
   disc already sits --sp-sm in from the corner. ───────────────────── */

@media (min-width: 835px) {
  .mo-panel:not(.mo-fullscreen) {
    border-radius: var(--radius-2xl);
  }
}

/* ── 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(--background-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-secondary);
  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;
  overscroll-behavior: none;
  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-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(--background-elevated-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;
  overscroll-behavior: none;
  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(--background-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(--background-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(--background-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(--background-secondary);
  font-size: var(--text-lg);
  transition: background-color 120ms ease, box-shadow 120ms ease;
}

.home-switcher-list .palette-entry--selected {
  background: var(--fill-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(--background-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(--background-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-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(--background-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(--background-secondary);
  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;
  overscroll-behavior: none;
}
@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-secondary);
  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;
}
/* Desktop's set-alert line (iOS ReminderPill, bare since 2026-10-28): the
   day and time between the title and the strip, at the strip's inset. */
.nr-alert-line {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  height: 20px;
  margin: 0 0 var(--sp-xs) calc(var(--size-xxl) + var(--sp-xs));
  padding: 0;
  border: none;
  background: transparent;
  color: var(--label-secondary);
  font: 600 15px/20px var(--font-sans);
  white-space: nowrap;
  cursor: pointer;
}
.nr-alert-line:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
/* A counted round button: the strip's button in neutral ink, widened to
   carry its count — the bell (iOS's action-row bell, which also keeps its
   own `nr-alert-bell` classes), the heart and the bubble. */
.nr-counted-btn {
  color: var(--label-secondary);
}
.nr-counted-btn--count {
  width: auto;
  gap: 2px;
  padding: 0 var(--sp-sm) 0 10px;
}
.nr-counted-btn__count {
  font: 600 15px/20px var(--font-sans);
}
/* 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: 44px;
  height: 44px;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  /* background: var(--background-secondary); */
  color: var(--label-primary);
  cursor: pointer;
  transition: background 150ms ease;
}
@media (hover: hover) {
  .nr-round-btn:hover {
    background: var(--fill-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: the filled check glyph (iOS check-circle/checked/lg/
   filled), 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(--background-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(--background-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(--background-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 ──────────────────────────────────────────────────── */
/* ui/accessory_bar's cluster bar (.nr-toolbar) is the phone's; desktop has
   none since 2026-10-29 (the notes' right-click menu carries its tools). */

/* Mobile bar buttons — mock parity: white chrome with a hairline border
   (was solid bg-secondary fill). */
.nr-toolbar-circle,
.nr-toolbar-capsule {
  background: var(--background-primary);
  border: 1px solid var(--border-secondary);
}
/* 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;
}

@media (min-width: 835px) {
  .nr-root .nr-toolbar {
    display: none;
  }
}

/* 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 media_viewer.css — and the shared image lightbox in modal.css.) */

/* (The voice recorder is ui/voice_capture on ui/sheet since 2026-10-28 —
   styles in voice_capture.css.) */

.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 --background-secondary (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(--background-secondary);
  }

  /* .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(--background-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(--background-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);
  }

  /* 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);
  }

  .nr-toolbar-capsule {
    height: var(--size-5xl);
  }
  .nr-toolbar-btn, .nr-toolbar-circle {
    height: var(--size-5xl);
    width: var(--size-5xl);
  }
}

/* A tick's celebration (spec §4c.2, round 3a's, C9; iOS
   CompletionCelebration): while the detail root carries data-celebrating
   the checked glyph wears the user's 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 to the width's own resting colour. Every width since
   2026-11-01 (Julian: the desktop circle had neither the animation nor the
   accent); it follows the phone block so its border-color beats detail.css's
   desktop hover ring by order. 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 circle grows to 1.2 while pressed, on the nav discs' curve (spec
   §4c.2, C10; iOS PressableButtonStyle's spring 0.3 / 0.6), at every width
   since 2026-11-01; not under reduced motion, like the card circle. The
   celebration's eases (above) run beside it. */
@media (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);
  }

  /* Only a live circle (a <button>; completion_circle renders a <span>
     when it has no handler — the receipt's inert circle, desktop capture's)
     grows: iOS's PressableButtonStyle wraps a real Button. */
  :is(.nr-root--detail, .nr-root--capture) .reminder-heading-container button.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
       top inset itself (tokens.css: the status bar, or the 62 floor where
       the browser owns it). The sides stay their own declarations: a
       shorthand would lose them with the inset if env() were unknown. */
    min-height: calc(var(--phone-top-inset) + 44px);
    padding-top: var(--phone-top-inset);
    padding-inline: 16px;
    padding-bottom: 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(--background-secondary));
    filter: blur(16px);
    pointer-events: none;
  }

  /* A toDo scrolls under a primary-background scrim. */
  .nav-bar--scrolls-under {
    --nav-bar-scrim: var(--background-primary);
  }

  .nav-bar-leading,
  .nav-bar-trailing {
    display: flex;
    align-items: center;
    width: 44px;
    height: 44px;
  }

  .nav-bar-trailing {
    justify-content: flex-end;
  }

  /* Two discs trailing (Search's sort "…" and list-plus, iOS PR 1260), 8
     apart: the bar's trailing column grows to 96 (the leading stays 44, so
     the title's track shifts 26 left of centre; iOS's InlineNavBar does
     the same under two circles). */
  .nav-bar--wide-trailing {
    grid-template-columns: 44px minmax(0, 1fr) 96px;
  }

  .nav-bar--wide-trailing .nav-bar-trailing {
    width: 96px;
    gap: 8px;
  }

  .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 (Julian, 2026-10-27): the tinted glass surface in the
     page's colour (glass.css; was BarChrome's below-26 glass — --glass-fill,
     blur 12 and a hairline), the disc's rim, then the toolbar shadow; 44 with a
     24 glyph in label primary. 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%;
    --glass-tint-color: var(--background-primary);
    /* box-shadow: var(--glass-rim-disc), 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;
  }
}

/* A toDo's ⋯ and its save ✓ (nav_bar.TrailingWithDone; Julian, 2026-11-05,
   off Apple Notes: while a field is edited the trailing edge is the menu
   then a round accent ✓; once editing ends the ✓ goes and the menu takes
   the edge back). The slot is 96 wide in both states (the two discs, 8
   apart), so nothing else in the row moves.

   At rest the ✓ hides under the ⋯: its leading margin is −52 (its width
   and the gap), which puts the ⋯ on the trailing edge IN LAYOUT — a
   translate would leave the ⋯'s laid-out box 52 to the left, and its menu
   is placed over that box (ffi/menu_place.mjs layoutRect). Editing, the
   margin eases to 0 on the house slide (--motion-slide, --ease-slide),
   which carries the ⋯ 52 left, and back.

   The ✓ pops in as a menu does — from 0.6, blurred 5 and clear, on the
   press's spring curve — and leaves on the pop's way out (ui/motion.menu's
   exit: 80ms to 0.6, blur 5, faded, ease-in). Each state's rule carries the
   transition it is reached by. Its visibility flips at the end of the way
   out (and at once on the way in), so a resting ✓ is never hit or
   painted; `inert` (nav_bar.gleam) keeps it out of the tab order and the
   accessibility tree as well. Whichever disc is live is on top: the ⋯ at
   rest, the ✓ while editing (a tap the moment it shows, with the ⋯ still
   sliding off it, is the ✓'s). */
@media (max-width: 834px) {
  .nav-bar--with-done {
    grid-template-columns: 44px minmax(0, 1fr) 96px;
  }

  .nav-bar--with-done .nav-bar-trailing {
    width: 96px;
    gap: 8px;
  }

  .nav-bar--with-done .nav-bar-trailing > .nav-bar-disc:not(.nav-bar-done) {
    z-index: 1;
  }

  /* The ✓: a solid accent disc, as Apple Notes' Done and the bar's ✓ were
     (Julian, 2026-11-05), the inverse label on it. */
  .nav-bar-disc.nav-bar-done {
    --glass-tint-color: var(--accent-blue);
    --glass-tint-strength: 100%;
    color: var(--label-inverse-primary);
    margin-inline-start: -52px;
    scale: 0.6;
    opacity: 0;
    filter: blur(5px);
    visibility: hidden;
    pointer-events: none;
    transition:
      transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
      margin-inline-start var(--motion-slide) var(--ease-slide),
      scale 80ms cubic-bezier(0.4, 0, 1, 1),
      opacity 80ms cubic-bezier(0.4, 0, 1, 1),
      filter 80ms cubic-bezier(0.4, 0, 1, 1),
      visibility 0s linear 80ms;
  }

  .nav-bar-trailing[data-editing] > .nav-bar-disc.nav-bar-done {
    z-index: 2;
    margin-inline-start: 0;
    scale: 1;
    opacity: 1;
    filter: none;
    visibility: visible;
    pointer-events: auto;
    transition:
      transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
      margin-inline-start var(--motion-slide) var(--ease-slide),
      scale 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
      opacity 150ms linear,
      filter 230ms linear,
      visibility 0s;
  }

  /* The ⋯'s menu is put over the ⋯'s laid-out box as it opens
     (menu_place.mjs). Tapped mid-edit, the same tap ends editing (the
     field blurs), so the ⋯ would still be sliding home under a menu placed
     52 short of it: while its menu is open the slide is a cut, and the ⋯
     is home by the time the menu reads its box. The ✓ still pops out. */
  .nav-bar-trailing:has(> [aria-expanded="true"]) > .nav-bar-disc.nav-bar-done {
    transition:
      transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
      scale 80ms cubic-bezier(0.4, 0, 1, 1),
      opacity 80ms cubic-bezier(0.4, 0, 1, 1),
      filter 80ms cubic-bezier(0.4, 0, 1, 1),
      visibility 0s linear 80ms;
  }

  /* base.css's ring is --background-secondary; on the accent it would vanish. */
  .nav-bar-disc.nav-bar-done:focus-visible {
    outline-color: var(--label-primary);
  }
}

/* Reduced motion: the ⋯ jumps and the ✓ comes and goes as a cut, as the
   bar's own discs snap (format_bar.css §4b.14). */
@media (max-width: 834px) and (prefers-reduced-motion: reduce) {
  .nav-bar-disc,
  .nav-bar-disc.nav-bar-done,
  .nav-bar-trailing[data-editing] > .nav-bar-disc.nav-bar-done,
  .nav-bar-trailing:has(> [aria-expanded="true"]) > .nav-bar-disc.nav-bar-done {
    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(--background-secondary);
  }
}

/* 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(--background-secondary);
    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(var(--tab-count, 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) / var(--tab-count, 4));
    width: calc((100% - 12px) / var(--tab-count, 4));
    border-radius: 999px;
    background: var(--fill-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(var(--tab-count, 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(--background-primary);
  }

  :root[data-accent] .tab-badge {
    background:
      linear-gradient(var(--accent-blue-elevated), var(--accent-blue-elevated)),
      var(--background-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(--background-primary);
  }

  :root[data-accent] .tab-badge--dot {
    background: var(--accent-blue);
  }

  /* The + (iOS CaptureFABButton): the tinted glass surface in the user's
     accent (glass.css; accent.css retints --accent-blue — Julian,
     2026-10-27; was a solid accent disc) with the inverse label, the disc's
     rim, the toolbar shadow, and the 1.1× hold (iOS 2026-10-27, from 1.2). */
  .tab-plus {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    --glass-tint-color: var(--accent-blue);
    --glass-tint-strength: 85%;
    color: var(--label-inverse-primary);
    box-shadow: var(--glass-rim-disc), 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.1);
  }
}

@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. Pops out of the + and shrinks away
   (ui/tab_bar's motion.pop, styles/motion.css). */
.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;
  }

  /* Placed from the + by ffi/plus_menu.mjs (8 in from the right, 9 over
     the +'s top — where right: 8 / bottom: safe + 80 used to put it) and
     only shown once placed, so the pop unfolds from its bottom-right
     corner, over the +. */
  .plus-menu {
    position: fixed;
    left: var(--plus-menu-left, 0px);
    top: var(--plus-menu-top, 0px);
    z-index: 41;
    display: flex;
    flex-direction: column;
    padding: 12px 16px;
    /* Its corners: the pop (styles/motion.css) morphs into them. */
    --pop-corner: 20px;
  }

  .plus-menu:not([data-placed]) {
    visibility: hidden;
    animation: none;
  }

  .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);
  }
}

@media (max-width: 834px) and (prefers-reduced-motion: reduce) {
  .plus-menu-row {
    transition: none;
  }
}

/* The plus menu's in-place captures: hidden pickers + saved toast — app/quick_capture. */
/* The phone plus menu's in-place captures — app/quick_capture (iOS
   QuickCaptureReducer + SavedToastPill, parity 2026-11-01). */

/* The camera and library pickers a menu row clicks: never drawn. */
.qc-input {
  display: none;
}

/* The keyboard's anchor (quick_add.mjs primeKeyboard): focusable, so it
   must be laid out, but invisible and out of the way. */
.qc-focus-anchor {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  margin: 0;
  opacity: 0;
  resize: none;
  pointer-events: none;
  font-size: 16px;
}

/* iOS SavedToastPill: a capsule 92 above the bottom (over the tab row),
   a 14 green check and 13/500 text in the accent on background primary,
   shadow black 25% radius 12 y 4; a tap opens the toDo. */
.quick-toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--safe-bottom) + 92px);
  z-index: 60;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 12px;
  border: 0;
  border-radius: var(--radius-full, 999px);
  background: var(--background-primary);
  color: var(--accent-blue);
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  line-height: 16px;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
  -webkit-tap-highlight-color: transparent;
  transform: translateX(-50%);
  animation: quick-toast-in 350ms cubic-bezier(0.34, 1.3, 0.64, 1);
}

.quick-toast__check {
  display: inline-flex;
  color: var(--accent-green);
}

@keyframes quick-toast-in {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(12px) scale(0.96);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .quick-toast {
    animation: none;
  }
}

/* ── The New toDo tray (iOS QuickAddTray) ─────────────────────────────
   A floating card in the bottom safe-area inset of the visual viewport,
   so it sits on the keyboard; a 30% black scrim behind it that closes it.
   The layer is sized by --vvh (dom.mjs), the card absolute at its foot. */
.quick-add-layer {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--vvh, 100dvh);
  z-index: 62;
}

.quick-add-scrim {
  position: absolute;
  inset: 0;
  padding: 0;
  border: 0;
  background: rgba(0, 0, 0, 0.3);
  cursor: default;
  animation: quick-add-scrim-in 340ms ease-out;
}

/* iOS: the tab row gives way to the tray. */
html:has(.quick-add-layer) .tab-row {
  visibility: hidden;
}

/* Outer insets as the toolbars': 8 at the sides, 12 above the bottom safe
   edge; with the keyboard up (.kb-open) that edge is the keyboard. */
.quick-add-card {
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: calc(var(--safe-bottom) + 12px);
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px;
  border-radius: 32px;
  background: var(--background-primary);
  box-shadow: 0 4px 4px rgba(0, 0, 0, 0.1);
  /* The swipe down gets the finger (quick_add.mjs); a field that
     overflows takes pan-y back for itself (autosize). */
  touch-action: none;
  /* With a mouse (a phone-width window) the swipe also dragged a text
     selection across the card, and Chromium took the next swipe that
     started on it as a drag of the selection (pointercancel: the card
     sprang back). The card's chrome selects nothing; its fields do. */
  user-select: none;
  -webkit-user-select: none;
  transition: transform 340ms cubic-bezier(0.32, 1.1, 0.5, 1);
  animation: quick-add-in 340ms cubic-bezier(0.32, 1.1, 0.5, 1);
}

html.kb-open .quick-add-card {
  bottom: 12px;
}

@keyframes quick-add-in {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes quick-add-scrim-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* The fields: title 20/600 (one line, growing to two), notes 18/25.2
   (to four), both padded 8 at the sides, growing with their text
   (quick_add.mjs autosize). */
.quick-add-title,
.quick-add-notes {
  display: block;
  width: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: 0 8px;
  border: 0;
  background: transparent;
  resize: none;
  outline: none;
  font-family: inherit;
  color: var(--label-primary);
  caret-color: var(--accent-blue);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* The card selects nothing; its fields' text does. */
  user-select: text;
  -webkit-user-select: text;
}

.quick-add-title {
  min-height: 32px;
  max-height: 50px;
  font-size: 20px;
  font-weight: 600;
  line-height: 25px;
}

.quick-add-notes {
  max-height: 101px;
  font-size: 18px;
  font-weight: 400;
  line-height: 25.2px;
  color: var(--label-special);
}

.quick-add-title::placeholder,
.quick-add-notes::placeholder {
  color: var(--label-secondary);
  opacity: 1;
}

.quick-add-separator {
  height: 0.5px;
  background: var(--border-secondary);
}

/* The action row: 44 circles with 24 glyphs, 8 apart; the ✓ at the end
   in the accent, half-faded until there is something to create. */
.quick-add-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.quick-add-row > .quick-add-spacer {
  flex: 1;
}

.quick-add-btn,
.quick-add-create {
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full, 999px);
  background: var(--background-secondary);
  color: var(--label-primary);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition:
    transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
    background-color 150ms ease,
    opacity 150ms ease;
}

.quick-add-btn:active,
.quick-add-create:active:not(:disabled) {
  transform: scale(1.2);
}

/* An alert on the draft: the bell's circle in the accent. */
.quick-add-btn--set,
.quick-add-create {
  background: var(--accent-blue);
  color: var(--label-inverse-primary);
}

.quick-add-btn--live {
  background: var(--accent-red);
  color: var(--label-inverse-primary);
}

.quick-add-create:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Attached media above the title: 56 thumbs with a ✕ on each. */
.quick-add-media {
  display: flex;
  gap: 8px;
  padding: 0 8px;
  overflow-x: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
}

.quick-add-media-item {
  position: relative;
  flex-shrink: 0;
  width: 56px;
  height: 56px;
}

.quick-add-media-item img,
.quick-add-media-item video,
.quick-add-media-file {
  width: 56px;
  height: 56px;
  border-radius: 12px;
  object-fit: cover;
  background: var(--background-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--label-secondary);
}

.quick-add-media-remove {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full, 999px);
  background: var(--label-primary);
  color: var(--background-primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
  .quick-add-card,
  .quick-add-scrim {
    animation: none;
    transition: none;
  }
}

/* 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. Pops out of the pointer
   and shrinks away (ui/motion, styles/motion.css). 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;
  /* Its corners: the pop (styles/motion.css) morphs into them. */
  --pop-corner: 20px;
  background: var(--background-primary);
  box-shadow:
    inset 0 0 0 1px var(--border-secondary),
    0 4px 8px rgba(0, 0, 0, 0.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);
}

@media (prefers-reduced-motion: reduce) {
  .card-menu-row {
    transition: none;
  }
}

/* The notes menu (Julian's mock, 2026-10-29; ui/note_menu.gleam), measured
   off the mock at 1x (its notes' 18/25 body text runs a 39px pitch there,
   so 1.56x): a glass popover at least 236 wide, radius 28, 8 around; rows
   40 tall, a 20 glyph in a 24 slot 16 in, the label at 54 in 16px, the
   shortcut ending 16 from the edge in the secondary label; 1px dividers
   inset 16 with 8 above and below. The surface is .glass-surface's (both
   themes, from the tokens), so this rule sets no background or shadow.
   The highlight (hover, or the arrow keys' `data-active`) is the tertiary
   fill; a lit row (the caret's format) wears the accent, like the format
   bar's lit tools. Placed by ffi/note_menu.mjs (`--note-menu-left` /
   `-top`, flipped inside the window) and only shown once placed; pops
   out of the pointer and shrinks away, as the card
   popover (ui/motion, styles/motion.css). */
.note-menu-catcher {
  position: fixed;
  inset: 0;
  z-index: 100;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: default;
}

.note-menu {
  position: fixed;
  left: var(--note-menu-left, 0px);
  top: var(--note-menu-top, 0px);
  z-index: 101;
  display: flex;
  flex-direction: column;
  min-width: 236px;
  padding: 8px;
  /* Its corners: the pop (styles/motion.css) morphs into them. */
  --pop-corner: 28px;
  outline: none;
  user-select: none;
  -webkit-user-select: none;
}

.note-menu:not([data-placed]) {
  visibility: hidden;
  animation: none;
}

.note-menu-row {
  display: flex;
  align-items: center;
  gap: 14px;
  height: 40px;
  padding: 0 8px;
  border: 0;
  border-radius: 12px;
  background: none;
  color: var(--label-primary);
  font-family: inherit;
  font-size: 16px;
  font-weight: 400;
  line-height: 22px;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}

.note-menu-row > .icon {
  flex: none;
  margin: 0 2px;
}

.note-menu-row[data-active] {
  background: var(--fill-vibrant-tertiary);
}

.note-menu-row--lit {
  color: var(--accent-blue);
}

.note-menu-row--lit[data-active] {
  background: var(--accent-blue-elevated);
}

.note-menu-label {
  flex: 1;
}

.note-menu-kbd {
  margin-left: 24px;
  color: var(--label-secondary);
  font-weight: 500;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}

.note-menu-divider {
  flex: none;
  height: 1px;
  margin: 8px;
  background: var(--border-secondary);
}

/* A composer's paperclip menu — ui/attach_menu. */
/* A composer's paperclip menu (ui/attach_menu.gleam; Julian's mock,
   2026-10-31): iOS's AttachmentMenuCard on glass — radius 20, 12/16
   padding, rows a 20 icon, 12 gap, body 18/25.2, 10 above and below, 1px
   dividers centred in 22 — at least the mock's 232 wide. The surface is
   .glass-surface's (both themes, from the tokens), so this rule sets no
   background or shadow. Placed above the composer by ffi/attach_menu.mjs
   (`--attach-menu-left` / `-top`, inside the visual viewport) and only
   shown once placed; pops out of the paperclip and shrinks away (ui/motion,
   styles/motion.css). Drawn with the app's overlays, above the Messages
   panel and the phone drawer, so neither clips it. A clear catcher behind
   it closes it. */
.attach-menu-catcher {
  position: fixed;
  inset: 0;
  z-index: 100;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: default;
}

.attach-menu {
  position: fixed;
  left: var(--attach-menu-left, 0px);
  top: var(--attach-menu-top, 0px);
  z-index: 101;
  display: flex;
  flex-direction: column;
  min-width: 232px;
  /* Shortened to the room there is (ffi/attach_menu.mjs sets it), its
     rows then scroll inside — only then, so a pressed row's grow isn't
     clipped by a scroll box the rest of the time. */
  max-height: var(--attach-menu-max-h, none);
  padding: 12px 16px;
  /* Its corners: the pop (styles/motion.css) morphs into them. */
  --pop-corner: 20px;
  outline: none;
  user-select: none;
  -webkit-user-select: none;
}

.attach-menu[data-scrolls] {
  overflow-y: auto;
  overscroll-behavior: contain;
}

.attach-menu:not([data-placed]) {
  visibility: hidden;
  animation: none;
}

.attach-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);
}

.attach-menu-row > .icon {
  flex: none;
}

.attach-menu-row:active {
  transform: scale(1.2);
}

.attach-menu-row:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
  border-radius: 8px;
}

.attach-menu-divider {
  position: relative;
  flex: none;
  height: 22px;
}

.attach-menu-divider::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: var(--border-secondary);
}

@media (prefers-reduced-motion: reduce) {
  .attach-menu-row {
    transition: none;
  }
}

/* A header action's popover (ui/popover.gleam; Julian's Apple Notes
   recording, 2026-11-03): iOS's Menu under a toolbar button, on glass, at
   every width — the house popover's card (the + menu's: radius 20, 12/16
   padding, rows a 20 icon, 12 gap, body 18/25.2, 10 above and below) with
   a sublabel line under a label, a chevron at a row's end, a hairline
   between rows and a thicker band between groups, and iOS's control row
   across the top: up to three tiles, an icon over a 12 label, drawn only
   when a control is passed. The surface is .glass-surface's (both themes),
   so this sets no background or shadow. Hung under its trigger by
   ffi/popover.mjs (`--popover-left` / `-top`, inside the visual viewport,
   shortened to the room there is) and only shown once placed; pops out of
   the trigger's corner and shrinks away (ui/motion, styles/motion.css). A
   clear catcher behind it closes it. */
.popover-layer {
  display: contents;
}

.popover-catcher {
  position: fixed;
  inset: 0;
  z-index: 100;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: default;
}

.popover {
  position: fixed;
  left: var(--popover-left, 0px);
  top: var(--popover-top, 0px);
  z-index: 101;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  min-width: 250px;
  max-width: min(300px, calc(100vw - 16px));
  /* Shortened to the room there is (ffi/popover.mjs sets it), its rows
     then scroll inside — only then, so a pressed row's grow isn't clipped
     by a scroll box the rest of the time. */
  max-height: var(--popover-max-h, none);
  padding: 12px 16px;
  /* Its corners: the pop (styles/motion.css) morphs into them. */
  --pop-corner: 20px;
  outline: none;
  user-select: none;
  -webkit-user-select: none;
}

.popover[data-scrolls] {
  overflow-y: auto;
  overscroll-behavior: contain;
}

.popover:not([data-placed]) {
  visibility: hidden;
  animation: none;
}

/* ── the control row ── */

.popover-controls {
  display: flex;
  flex: none;
  gap: 8px;
  margin-bottom: 4px;
}

.popover-control {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: 0;
  min-height: 64px;
  padding: 10px 4px;
  border: 0;
  border-radius: 14px;
  background: var(--fill-quaternary);
  color: var(--label-primary);
  font-family: inherit;
  font-size: 12px;
  font-weight: 500;
  line-height: 14px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.popover-control > .icon {
  flex: none;
}

.popover-control__label {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.popover-control:active {
  transform: scale(1.08);
}

.popover-control:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

/* ── the groups ── */

.popover-group {
  display: flex;
  flex: none;
  flex-direction: column;
}

/* A hairline between two rows of a group. */
.popover-divider {
  flex: none;
  height: 1px;
  background: var(--border-secondary);
}

/* iOS's section separator: a thicker band, edge to edge. */
.popover-break {
  flex: none;
  height: 1px;
  margin: 6px -16px;
  background: var(--border-secondary);
}

.popover-controls + .popover-group {
  margin-top: 8px;
}

.popover-row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  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;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.popover-row--destructive .popover-row__icon,
.popover-row--destructive .popover-row__label {
  color: var(--accent-red);
}

.popover-row__icon {
  display: inline-flex;
  flex: none;
}

.popover-row__icon:empty {
  display: none;
}

/* iOS's check column (2026-11-05, the Lists menu's Sort By rows): every
   row of a level that holds a checked row reserves it, so the labels line
   up; only the pick's is drawn. */
.popover-row__check {
  display: inline-flex;
  flex: none;
  width: 20px;
  color: var(--label-primary);
  visibility: hidden;
}

.popover-row__check--on {
  visibility: visible;
}

.popover-row__text {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-width: 0;
}

.popover-row__label {
  font: var(--type-body);
  color: var(--label-vibrant-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.popover-row__sublabel {
  font: var(--type-footnote);
  color: var(--label-vibrant-primary);
}

.popover-row__chevron {
  display: inline-flex;
  flex: none;
  color: var(--label-tertiary);
}

.popover-row:active {
  transform: scale(1.04);
}

.popover-row:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
  border-radius: 8px;
}

@media (prefers-reduced-motion: reduce) {
  .popover-row,
  .popover-control {
    transition: none;
  }
}

/* ============================================================================
   The desktop Messages panel (app/messages_panel; Julian's mock, 2026-10-29,
   after Instagram's). The Messages pill on a shared toDo's page opens it in
   the pill's corner: a 396×594 card of ultra thick material, 32 round, the
   chat list or one conversation under a header. It grows out of the pill's
   corner; the pill hides under it. Desktop only — phones keep the page.
   ========================================================================= */

.messages-panel {
  display: none;
}

@media (min-width: 835px) {
  .messages-panel {
    position: absolute;
    right: var(--sp-xl);
    bottom: var(--sp-xl);
    z-index: 6;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    width: 396px;
    /* The mock's 594, short of the pane's height less the pill's 24 above
       and below. */
    height: min(594px, calc(100% - 2 * var(--sp-xl)));
    border-radius: var(--radius-2xl);
    overflow: hidden;
    box-shadow: var(--shadow-panel);
    color: var(--label-primary);
    transform-origin: bottom right;
    animation: messages-panel-in 260ms cubic-bezier(0.3, 1.25, 0.5, 1);
    transition: right var(--motion-slide) var(--ease-slide);
  }

  /* A sheet the panel's conversation opens — its message actions on a
     touch-only screen, a reaction's reactors — is fixed to the window but
     stacks inside the panel: while one is up, the panel rises over the
     app rail (z 20) so the sheet's backdrop covers the rail and takes its
     taps. */
  .messages-panel:has(.sheet-layer) {
    z-index: 40;
  }

  /* The material, on a layer under the content rather than on the panel:
     a backdrop-filter on the panel would make it the containing block of
     every position: fixed sheet the conversation opens (the reaction
     picker, a card's sheet, the emoji sheet), trapping them in its 396. */
  .messages-panel::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background:
      linear-gradient(var(--material-ultrathick-1), var(--material-ultrathick-1)),
      linear-gradient(var(--material-ultrathick-2), var(--material-ultrathick-2));
    -webkit-backdrop-filter: blur(var(--material-blur));
    backdrop-filter: blur(var(--material-blur));
    pointer-events: none;
  }

  /* It keeps the pill's corner: left of the open AI panel, narrowing (not
     spilling under the panel) when the column beside it is short of its
     396 and the 24s. */
  .nr-root--detail:has(.detail-ai-panel:not([hidden])) .messages-panel {
    right: calc(var(--ai-panel-w) + var(--sp-xl));
    width: min(396px, calc(100% - var(--ai-panel-w) - 2 * var(--sp-xl)));
  }

  .nr-root--detail:has(.messages-panel) .detail-messages-pill {
    visibility: hidden;
  }

  /* ── Header: the title, then ↗ and ✕ (the conversation leads with its
     back). The mock's title sits 41 from the top, its glyphs' centres 40
     and 97 from the right edge. */
  .messages-panel-header {
    display: flex;
    align-items: center;
    gap: 13px;
    flex-shrink: 0;
    padding: 19px var(--sp-md) var(--sp-xs);
  }

  .messages-panel-title {
    flex: 1;
    min-width: 0;
    margin: 0;
    font-size: var(--text-xl);
    font-weight: 700;
    line-height: 25px;
    overflow: clip;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .messages-panel-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 44px;
    height: 44px;
    margin: 0;
    padding: 0;
    border: none;
    border-radius: var(--radius-full);
    background: transparent;
    color: var(--label-primary);
    cursor: pointer;
  }

  /* The back pulls in to the edge, as the title did. */
  .messages-panel-btn:first-child {
    margin-left: -10px;
  }

  .messages-panel-btn + .messages-panel-title--thread {
    margin-left: -9px;
  }

  @media (hover: hover) {
    .messages-panel-btn:hover {
      background: var(--fill-vibrant-tertiary);
    }
  }

  .messages-panel-btn:focus-visible {
    outline: 2px solid var(--accent-blue);
    outline-offset: -2px;
  }

  /* ── The list: rows 88 tall (a 52 avatar, 18 above and below), 18 in
     from the sides; the last rows fade out under the + (the mock blurs
     the one under it away). */
  .messages-panel-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
    padding-bottom: 96px;
    -webkit-mask-image: linear-gradient(to top, transparent 0, #000 88px);
    mask-image: linear-gradient(to top, transparent 0, #000 88px);
  }

  .messages-panel-scroll::-webkit-scrollbar {
    display: none;
  }

  .messages-panel-rows .chat-list-row {
    gap: var(--sp-md);
    padding: var(--sp-md);
    border-radius: 0;
  }

  .messages-panel-rows .chat-list-avatar {
    width: 52px;
  }

  .messages-panel-rows .chat-list-row-timestamp {
    font-size: var(--text-sm);
  }

  .messages-panel-rows .chat-list-row-subtitle {
    max-width: 100%;
  }

  .messages-panel-divider {
    flex-shrink: 0;
    height: 1px;
    margin: var(--sp-xs) var(--sp-md);
    background: var(--border-secondary);
  }

  /* A request's tag where its message would be (mock: a blue capsule,
     15/600, on the accent wash). */
  .chat-list-row-tag {
    align-self: flex-start;
    padding: 2px 10px;
    border-radius: var(--radius-full);
    background: var(--accent-blue-elevated);
    color: var(--accent-blue);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: 22px;
  }

  /* The + (new message): a 52 white disc, 44 in from the corner. */
  .messages-panel-new {
    position: absolute;
    right: 18px;
    bottom: 17px;
    width: 52px;
    height: 52px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: none;
    border-radius: var(--radius-full);
    color: var(--label-primary);
    cursor: pointer;
  }

  .messages-panel-new:focus-visible {
    outline: 2px solid var(--accent-blue);
    outline-offset: 2px;
  }

  /* ── A conversation: the thread fills what the header leaves, on the
     panel's material. Its own page header gives way to the panel's. */
  .messages-panel-thread {
    flex: 1;
    min-height: 0;
    position: relative;
  }

  .messages-panel .chat {
    background: transparent;
  }

  .messages-panel .chat > .page-header,
  .messages-panel .chat > .nav-bar {
    display: none;
  }

  /* No scrim under the composer: the page's colour it fades to is not the
     panel's. The rows fade out on their own mask instead, gone by the
     composer's layer (--chat-foot-h, dom.mjs watchChatChrome). */
  .messages-panel .chat-composer {
    padding: var(--sp-xs) var(--sp-md) var(--sp-md);
    background: none;
  }

  .messages-panel .chat > .chat-messages,
  .messages-panel .chat.chat--human > .chat-messages {
    grid-row: 1 / 5;
    padding: var(--sp-xs) var(--sp-md);
    padding-bottom: calc(var(--chat-foot-h, 124px) + var(--sp-xs));
    scroll-padding-top: 0;
    -webkit-mask-image: linear-gradient(
      to top,
      transparent calc(var(--chat-foot-h, 124px) - var(--sp-xs)),
      #000 calc(var(--chat-foot-h, 124px) + var(--sp-md))
    );
    mask-image: linear-gradient(
      to top,
      transparent calc(var(--chat-foot-h, 124px) - var(--sp-xs)),
      #000 calc(var(--chat-foot-h, 124px) + var(--sp-md))
    );
  }
}

@keyframes messages-panel-in {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
}

@media (prefers-reduced-motion: reduce) {
  .messages-panel {
    animation: none;
    transition: none;
  }
}

/* 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;
    /* The header's own colour, not only its ::before scrim: iOS 26 Safari
       paints the status strip from the fixed container touching the top
       edge — its background-color, nothing else (not html/body, not
       theme-color). Transparent here left a white / black strip over the
       grey canvas on Home and Search (measured on the 26.4 simulator,
       2026-11-01). The scrim below still feathers the bottom edge; the
       spec's softScrim is solid to that edge anyway. */
    background-color: var(--background-secondary);
    /* A list's colour comes and goes on Home's curve (2026-11-05). */
    transition: background-color 180ms cubic-bezier(0.42, 0, 0.58, 1);
  }

  /* iOS softScrim(solidInset: 0, blur 16) — scrim-tuner (a). */
  .app-shell--root .root-header::before {
    content: "";
    position: absolute;
    z-index: -2;
    top: 44px;
    left: 0px;
    right: 0px;
    bottom: 0;
    background: var(--background-secondary);
    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(var(--phone-top-inset) - 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;
  }

  /* Inside a list the header wears the list's colour (Julian's Figma
     "List Fill", 2026-11-05 night — the ellipses of the two earlier takes
     either painted over the rows or wouldn't read as one unit): ONE solid
     accent fill — the container's own colour, which iOS 26 Safari's status
     strip is painted from — with the scrim (`::before`, Figma's nested
     "Scrim") laid over its lower part as a linear fade from clear to the
     canvas, so the solid reads as a gradient. No ellipse, no second layer.
     The colour eases 180ms (the `.root-header` transition above); nothing
     breathes. The fade starts where Figma's does — 40 into a 59 status
     bar, 19 above the bar (`--phone-top-inset`) — and runs to the header's
     bottom edge; the scrim's 16 blur feathers it on. Home's and the grid's
     scrim (the solid canvas band above) is untouched. In dark the accent
     is washed to the glow's opacity, as the cards' fills are. */
  .app-shell--root .root-header--washed::before {
    top: calc(var(--phone-top-inset) - 19px);
    background: linear-gradient(to bottom, transparent, var(--background-secondary));
  }

  .app-shell--root .root-header--washed {
    background-color: color-mix(
      in srgb,
      var(--list-wash) calc(100% * var(--home-glow-opacity)),
      var(--background-secondary)
    );
  }

  /* The tint cycle's five, opaque (the card's --bucket-grad-* run at 22%
     over the canvas; a translucent container would show the rows through). */
  .root-header--wash-0 {
    --list-wash: color-mix(in srgb, rgb(20 146 255) 60%, var(--background-secondary));
  }

  .root-header--wash-1 {
    --list-wash: color-mix(in srgb, rgb(255 152 60) 60%, var(--background-secondary));
  }

  .root-header--wash-2 {
    --list-wash: color-mix(in srgb, rgb(255 76 80) 60%, var(--background-secondary));
  }

  .root-header--wash-3 {
    --list-wash: color-mix(in srgb, rgb(0 220 197) 60%, var(--background-secondary));
  }

  .root-header--wash-4 {
    --list-wash: color-mix(in srgb, rgb(66 206 101) 60%, var(--background-secondary));
  }

  /* 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(--background-secondary);
  }

  /* The cards' fades follow the page's paint (`--list-canvas`, search.css):
     whatever the root paints its pages — --background-secondary since the
     token refactor of 2026-11-06 (it was --canvas-bg, which in dark sat at
     #0C0C0C against the page's #1C1C1E, and a fade bound to the other
     token left the cards' feet lighter than the page). */
  .app-shell--root .search-page {
    --list-canvas: var(--background-secondary);
  }

  /* 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(var(--phone-top-inset) + 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;
    overscroll-behavior: none;
    padding-top: calc(var(--phone-top-inset) + 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(var(--phone-top-inset) + 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(--background-secondary);
  }

  .messages-page-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .app-shell--root .messages-page-scroll {
    padding-top: calc(var(--phone-top-inset) + 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;
  }
}

/* ── Inside a list (iOS ListScreenView; Julian's recordings, 2026-11-06) ──
   Three things iOS does that the grid doesn't, on the phone page and, in
   parity, in the desktop list pane:

   1. The wash. The list's colour from the top edge — strongest there, a
      step down by 45%, gone at its foot — FIXED to its surface, not
      scrolling with the rows (search.view_wash: .search-wash). On the
      phone it is iOS's band, 360 tall, and the root header goes clear over
      it, its canvas scrim traded for iOS's soft scroll-edge blur, so the
      wash shows through the bar and the status strip. In the pane it is
      Julian's Figma ellipse instead (2026-11-06): 348 × 160, its middle
      20 above the pane's top edge, a dome behind the header (which paints
      nothing of its own) that fades out short of the pane's sides.
      The stops are iOS's: a list's own pick at 1 → 0.55 (dark 0.65 →
      0.35); a cycle tint — a classified bucket, a list without a pick — at
      the card's 0.22 → 0.121 (dark 0.45 → 0.2475), the cycle's five
      full-strength brand colours (tokens --bucket-grad-*).
   2. The large title. The list's name at 34, left under the bar; the
      bar's own title waits until it has gone under
      (search-large-title--collapsed, which list_title.mjs keeps off the
      scroller), then fades in on Home's curve. On the phone it is first
      in the scroller and scrolls away; in the pane it sits over the
      pinned field and folds away instead.
   3. The field. Always up, 12 under the large title. On the phone it is
      stuck 10 under the bar as the rows pass (iOS's search drawer,
      .always); the pane's was pinned above its scroller already.

   The background tokens are the renamed ones (--background-*) with their
   old names as fallbacks (--bg-*): the rename was in flight in another
   session when this landed. The 2026-11-05 take (a solid accent on the
   header under a scrim, phone_shell.css .root-header--washed, parked that
   night) is superseded: its rules and client.gleam's parked arm go once
   the rename has. */

/* ── Shared: the wash's colour and stops, the large title's face ──────── */

.search-wash {
  pointer-events: none;
}

.search-wash--own {
  --wash-top: 100%;
  --wash-mid: 55%;
}

.search-wash--tint {
  --wash-top: 22%;
  --wash-mid: 12.1%;
}

.search-wash--tint-0 { --list-wash: rgb(20 146 255); }
.search-wash--tint-1 { --list-wash: rgb(255 152 60); }
.search-wash--tint-2 { --list-wash: rgb(255 76 80); }
.search-wash--tint-3 { --list-wash: rgb(0 220 197); }
.search-wash--tint-4 { --list-wash: rgb(66 206 101); }

/* Dark: a pick reads translucent on black, a tint steps up (iOS). */
[data-theme="dark"] .search-wash--own {
  --wash-top: 65%;
  --wash-mid: 35%;
}

[data-theme="dark"] .search-wash--tint {
  --wash-top: 45%;
  --wash-mid: 24.75%;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .search-wash--own {
    --wash-top: 65%;
    --wash-mid: 35%;
  }

  :root:not([data-theme]) .search-wash--tint {
    --wash-top: 45%;
    --wash-mid: 24.75%;
  }
}

/* The large title: iOS's 34/41 bold. `flex: none`: both surfaces lay it
   in a column, and with the rows overflowing a flex item that hides its
   overflow shrinks to nothing. */
.search-large-title {
  flex: none;
  margin: 0 0 12px;
  padding: 0;
  font: 700 34px/41px var(--font-sans);
  letter-spacing: 0.4px;
  color: var(--label-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* It arrives with its surface (Home's curve). */
@keyframes list-wash-in {
  from {
    opacity: 0;
  }
}

/* ── The phone page ───────────────────────────────────────────────────── */

@media (max-width: 834px) {
  /* The page frames the wash; the scroller rides over it. */
  .app-shell--root .search-page {
    position: relative;
  }

  .app-shell--root .search {
    position: relative;
    z-index: 1;
  }

  /* The wash is two pieces. The element itself is the status strip: a
     fixed container touching the top edge, wearing the wash's top colour
     opaque — what a full-bleed browser (iOS 26 Safari's rule, Brave) paints
     the strip from. Its ::after is the 360 gradient, hung from the strip's
     foot and opening at the same colour, so the two meet without an edge. */
  .search-wash {
    position: fixed;
    z-index: 0;
    top: 0;
    left: 0;
    right: 0;
    height: var(--phone-top-inset);
    background-color: color-mix(
      in srgb,
      var(--list-wash) var(--wash-top),
      var(--list-canvas, var(--background-secondary))
    );
    animation: list-wash-in 180ms cubic-bezier(0.42, 0, 0.58, 1);
  }

  .search-wash::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    height: 360px;
    background: linear-gradient(
      to bottom,
      color-mix(in srgb, var(--list-wash) var(--wash-top), transparent) 0%,
      color-mix(in srgb, var(--list-wash) var(--wash-mid), transparent) 45%,
      transparent 100%
    );
  }

  /* The bar over a washed page: clear, and its scrim a progressive blur —
     the system's scroll-edge effect — fading out under its foot so the
     rows pass under it softly. ONE layer at a time: while the large title
     shows, this one; once it has gone under and the field has stuck, the
     field row's below takes over the whole top (two layers stacked left a
     seam where one's fade crossed the other's edge, Julian's screenshot
     2026-11-06). */
  .app-shell--root:has(.search-wash) .root-header {
    background-color: transparent;
  }

  .app-shell--root:has(.search-wash) .root-header::before {
    top: 0;
    bottom: -32px;
    background: none;
    filter: none;
    -webkit-backdrop-filter: blur(20px);
    backdrop-filter: blur(20px);
    -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 32px), transparent);
    mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 32px), transparent);
  }

  .app-shell--root:has(.search-large-title--collapsed) .root-header::before {
    display: none;
  }

  /* The bar's title waits while the large one shows, and fades in on
     Home's curve once it has gone under. */
  .app-shell--root .root-header-title {
    transition: opacity 180ms cubic-bezier(0.42, 0, 0.58, 1);
  }

  .app-shell--root:has(.search-large-title:not(.search-large-title--collapsed)) .root-header-title {
    opacity: 0;
  }

  /* The field inside a list: stuck 10 under the bar as the rows pass.
     `top: 0` is the scroller's own padding edge — the bar and its 10
     (phone_shell.css) — not the viewport's. */
  .search--list .search-header {
    flex: none;
    position: sticky;
    z-index: 2;
    top: 0;
  }

  /* Once the large title has gone under (and so the field has stuck), the
     field row carries the one blur: from the page's top edge — the status
     strip, the bar, the 10 — down to 32 under the pill, fading out over
     that 32. Not before: reaching up over the large title would blur it. */
  .search-large-title--collapsed ~ .search-header::before {
    content: "";
    position: absolute;
    z-index: -1;
    top: calc(-1 * (var(--phone-top-inset) + 44px + 10px));
    right: -16px;
    bottom: -32px;
    left: -16px;
    -webkit-backdrop-filter: blur(20px);
    backdrop-filter: blur(20px);
    -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 32px), transparent);
    mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 32px), transparent);
    pointer-events: none;
  }
}

/* ── The desktop list pane ────────────────────────────────────────────── */

@media (min-width: 835px) {
  /* The pane frames the wash: a stacking context of its own, so the
     wash's -1 sits under the header, the field and the rows and over the
     shell's canvas showing through the pane. */
  .sidebar--search-mode {
    isolation: isolate;
  }

  /* An arc, not a band (Julian's Figma, 2026-11-06): the Figma's ellipse,
     348 × 160, centred in the pane and pulled up so its middle sits 20
     above the pane's top edge. What shows is a dome 60 deep behind the
     header, fading out 29 short of the pane's sides at its 406 — no stark
     overflow at the edges. iOS's stops, along the radius: the colour at
     the centre, the step at 45%, gone at the rim. */
  .sidebar .search-wash {
    position: absolute;
    z-index: -1;
    top: 0;
    left: 0;
    right: 0;
    height: 160px;
    background: radial-gradient(
      ellipse 174px 80px at 50% -20px,
      color-mix(in srgb, var(--list-wash) var(--wash-top), transparent) 0%,
      color-mix(in srgb, var(--list-wash) var(--wash-mid), transparent) 45%,
      transparent 100%
    );
    animation: list-wash-in 180ms cubic-bezier(0.42, 0, 0.58, 1);
  }

  /* The large title over the pinned field folds away — height, ink and
     its 12 — as the pane scrolls past it, and unfolds coming back. */
  .sidebar .search-large-title {
    max-height: 41px;
    transition:
      max-height 180ms cubic-bezier(0.42, 0, 0.58, 1),
      margin-bottom 180ms cubic-bezier(0.42, 0, 0.58, 1),
      opacity 180ms cubic-bezier(0.42, 0, 0.58, 1);
  }

  .sidebar .search-large-title--collapsed {
    max-height: 0;
    margin-bottom: 0;
    opacity: 0;
  }

  /* The header's own title waits while the large one shows. */
  .sidebar--search-mode .sidebar-list-title {
    transition: opacity 180ms cubic-bezier(0.42, 0, 0.58, 1);
  }

  .sidebar--search-mode:has(.search-large-title:not(.search-large-title--collapsed)) .sidebar-list-title {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .search-wash,
  .sidebar .search-large-title {
    animation: none;
    transition: none;
  }

  .app-shell--root .root-header-title,
  .sidebar--search-mode .sidebar-list-title {
    transition: 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(--background-secondary));
    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(--background-elevated-secondary) 85%, transparent);
    -webkit-backdrop-filter: blur(12px); */
    background: color-mix(in srgb, var(--background-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(--background-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
     (`--background-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-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;
    overscroll-behavior: none;
  }

  .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 the card fill, --background-tertiary (Julian, 2026-10-26: bg-primary,
     the sent-bubble token went with the restyle; 2026-11-04: the card
     token, iOS's #2C2C2E in dark, where black vanished into the body). */
  .chat-drawer-todos,
  .chat-drawer-rich {
    display: flex;
    flex-direction: column;
    gap: 16px;
    align-self: stretch;
  }

  .chat-drawer-todos .tl-row {
    background: var(--background-tertiary);
  }

  /* 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-vibrant-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(--background-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(--background-primary);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    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;
    overscroll-behavior: none;
    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-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(--background-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;
    padding: 0;
    border: 0;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin-bottom: 8px;
    border-radius: 50%;
    background: var(--fill-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;
    overscroll-behavior: none;
    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(--background-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(--background-tertiary);
    color: var(--label-quaternary);
    cursor: default;
  }

  /* Offline: an opaque dim disc that does nothing. */
  .chat-drawer-send[data-offline] {
    background: var(--fill-vibrant-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;
  }

  /* Away, the layer reaches the layout viewport's bottom, not the
     keyboard's top (--layout-vh, ffi/dom.mjs): sized to --vvh, the parked
     card rode up with the keyboard, showing through Safari's clear band
     around its address pill (Julian, 2026-11-05). Editing starts a frame
     before the keyboard moves and ends with --vvh already back at the full
     height (dom.mjs), so neither swap moves the card. */
  .chat-drawer[data-away] {
    height: var(--layout-vh, 100lvh);
  }

  /* ── The keyboard (§4a.12) ─────────────────────────────────────────
     Up, the card is full-bleed at any stop: no side, gap or bottom radius,
     no hairline, the row --kb-gap (10) above the keyboard (Julian,
     2026-11-05: was iOS's 8; chat_drawer_math prices the card with the
     same number). 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: var(--kb-gap);
    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, 82px) + 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-plus.
   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
     pad below is its only gap to the keys: 8, or --kb-gap (10) with the
     keyboard up (below). 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 keyboard up (html.kb-open, ffi/dom.mjs): the capsule and the ✓
     clear the keys by --kb-gap, not the 8 (Julian, 2026-11-05: the 8 left
     the capsule's foot under Safari's accessory row on his 17 Pro). The
     row grows by the difference, so the 8 above stays. Keyboard down (the
     capture bar's + at rest) it is the 72 above. */
  .kb-open .fmt-bar {
    height: calc(64px + var(--kb-gap));
    padding-bottom: var(--kb-gap);
  }

  /* ── 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; the ✓ until 2026-11-05) ───────────────────────────
     The drawer's + (chat_drawer.css .chat-drawer-plus): a solid accent
     disc with the inverse label, the toolbar shadow and the 1.2× press, in
     the seat the drawer's + holds at rest, so the bottom-right disc is the
     same New toDo whether the page is edited or not. 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. The ✓ is the
     header's now (nav_bar.css, Julian, 2026-11-05). */
  .fmt-plus {
    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);
    text-decoration: none;
    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-plus:active {
    transform: scale(1.2);
  }

  /* base.css's ring is --background-secondary, close to the page behind the disc. */
  .fmt-plus: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-plus {
    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,
  :is(.nr-root--detail, .nr-root--capture) .nr-alert-line {
    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 day and time, bare — no bell, no capsule, no fill (iOS
     ReminderPill since 2026-10-28). */
  :is(.nr-root--detail, .nr-root--capture) .detail-alert-line {
    display: inline-flex;
    align-items: center;
    height: 20px;
    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-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 --background-secondary ring all but
     vanishes on the phone page's --background-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. The plane, with its count,
     closes the row (2026-10-27; the right-aligned pile of faces went). */
  :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 split heart (2026-11-04): the item is a group of two buttons, the
     glyph (the like toggle) and the count (the Likes sheet), each its own
     44-tall target; the item's padding is shared between them so the pair
     measures as the one button did. */
  :is(.nr-root--detail, .nr-root--capture) .detail-action-item--split {
    padding: 0;
    gap: 0;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-action-item__glyph,
  :is(.nr-root--detail, .nr-root--capture) .detail-action-item__count {
    display: inline-flex;
    align-items: center;
    height: 44px;
    border: none;
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-action-item__glyph {
    padding: 0 1px 0 8px;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-action-item__count {
    padding: 0 8px 0 1px;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-action-item__glyph[aria-disabled="true"],
  :is(.nr-root--detail, .nr-root--capture) .detail-action-item__count[aria-disabled="true"] {
    cursor: default;
  }

  /* 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;
  }

  :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;
  }
}

/* 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). */
@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;
  }
}

/* Six items beside a thumbnail deck stop short of it, which the row's 44s
   can't: bare glyphs go to 32 wide, 44 tall, and the gaps to none (iOS's
   tight rendition). The same below 360, where six don't fit at all. */
@media (max-width: 834px) {
  :is(.nr-root--detail, .nr-root--capture)
    .reminder-heading-container:has(.thumb-stack)
    .detail-action-row {
    gap: 0;
    margin-inline: -4px;
  }

  :is(.nr-root--detail, .nr-root--capture)
    .reminder-heading-container:has(.thumb-stack)
    .detail-action-item {
    min-width: 32px;
    padding-inline: 4px;
  }
}

/* Beside a deck the tight row spreads its spare width between the items
   (Julian, 2026-11-05). It stops 92 short of the header's end: the deck's
   96 frame sits 16 from the edge, the header ends 24 from it, so the row
   ends 4 before the frame and the last item's 4 padding keeps its ink 8
   clear. The first glyph stays where the outset puts it. Items too wide
   for that just pack from the start, as before. Only a row of four or
   more: two (bell and AI, or a reader's heart and bubble) stay together
   rather than splitting to the ends. Not below 360, where six never fit
   short of the deck. */
@media (min-width: 360px) and (max-width: 834px) {
  :is(.nr-root--detail, .nr-root--capture)
    .reminder-heading-container:has(.thumb-stack)
    .detail-action-row:has(> :nth-child(4)) {
    justify-content: space-between;
    margin-right: 92px;
  }
}

@media (max-width: 359px) {
  :is(.nr-root--detail, .nr-root--capture) .detail-action-row {
    gap: 0;
  }

  :is(.nr-root--detail, .nr-root--capture) .detail-action-item {
    min-width: 36px;
  }
}

/* The pencil disc closing each row (iOS backgroundPrimary): the alert
   sheets paint --background-secondary (their config), so it reads in both themes;
   16 from the text (the row's 12 gap + 4). */
.alerts-sheet__edit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  margin-left: 4px;
  border-radius: var(--radius-full);
  background: var(--background-primary);
  color: var(--label-primary);
}

/* The alert sheets' content: the alert menu, Set alert / Custom time. */
/* The alert sheets' content (iOS ReminderMenuSheet, DismissRecurringMenuSheet,
   ReminderSheetView, 2026-11-01): what sits INSIDE the shared sheet chrome
   (ui/sheet — its header, rows, dividers and motion are sheet.css's). The
   menu is ui/alert_menu_sheet, Set alert / Custom time ui/alert_editor_sheet. */

/* ── the alert menu ───────────────────────────────────────────────────── */

/* A repeating alert's cadence under the title: the repeat glyph IS the word. */
.alert-menu__cadence {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3xs);
}

.alert-menu__cadence-glyph {
  display: inline-flex;
  color: var(--label-secondary);
}

/* The menu's two pages each take their own height, so the card shrinks to
   the chooser (iOS swaps in a shorter sheet). Pages share one grid cell,
   which would otherwise stretch the chooser to the menu's height. */
.alert-menu-sheet .sheet__page {
  align-self: start;
}

/* The dismiss-repeating chooser: two capsules, 12 apart, 12 / 16 around. */
.alert-menu__chooser {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  padding: var(--sp-sm) 0;
}

.alert-menu__choice {
  width: 100%;
  padding: var(--sp-sm) 0;
  border: 1px solid var(--ui-borders);
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--label-primary);
  font: 400 17px/22px var(--font-sans);
  letter-spacing: 0;
  text-align: center;
  cursor: pointer;
}

/* ── Set alert: Custom time ───────────────────────────────────────────── */

.alert-editor {
  display: flex;
  flex-direction: column;
}

/* A row: a 20 glyph, 12 to its text, 12 above and below (iOS Padding.sm). */
.alert-editor__row {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  width: 100%;
  min-height: 44px;
  padding: var(--sp-sm) 0;
  border: 0;
  background: none;
  color: var(--label-primary);
  font: 600 17px/22px var(--font-sans);
  letter-spacing: 0;
  text-align: start;
}

button.alert-editor__row {
  cursor: pointer;
}

.alert-editor__glyph {
  display: inline-flex;
  flex: none;
  width: 20px;
  height: 20px;
  color: var(--label-vibrant-secondary);
}

.alert-editor__value {
  min-width: 0;
}

.alert-editor__name {
  flex: none;
}

/* "Daily ⌄" beside Repeats: the accent, 4 to its chevron. */
.alert-editor__frequency {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3xs);
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent-blue);
  font: inherit;
  cursor: pointer;
}

/* The Repeats switch: iOS's Toggle in brandGreen (the settings switch's
   look, in this feature's own classes). */
.alert-editor__switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: none;
  width: 51px;
  height: 31px;
  margin-left: auto;
  padding: 2px;
  border: none;
  border-radius: var(--radius-full);
  background: var(--label-quaternary);
  cursor: pointer;
  transition: background 180ms ease;
}

.alert-editor__switch--on {
  background: var(--accent-green);
}

.alert-editor__switch-thumb {
  width: 27px;
  height: 27px;
  border-radius: 50%;
  background: var(--label-inverse-primary);
  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);
}

.alert-editor__switch--on .alert-editor__switch-thumb {
  transform: translateX(20px);
}

/* The frequency chips (iOS ChoiceChipRow): 4 apart, 15pt capsules. */
.alert-editor__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3xs);
  padding-bottom: var(--sp-sm);
}

.alert-editor__chip {
  padding: 6px var(--sp-sm);
  border: 0;
  border-radius: var(--radius-full);
  background: var(--background-primary);
  box-shadow: inset 0 0 0 1px var(--border-secondary);
  color: var(--label-secondary);
  font: 400 15px/20px var(--font-sans);
  cursor: pointer;
}

.alert-editor__chip--selected {
  background: var(--accent-blue-elevated);
  box-shadow: none;
  color: var(--accent-blue);
  font-weight: 600;
}

/* "This alert will start repeating *after* June 21st." */
.alert-editor__footnote {
  display: flex;
  align-items: baseline;
  gap: var(--sp-xs);
  margin: 0;
  padding: var(--sp-sm) 0 var(--sp-md);
  color: var(--label-secondary);
  font: 400 13px/18px var(--font-sans);
}

.alert-editor__footnote-glyph {
  display: inline-flex;
  flex: none;
  align-self: center;
}

.alert-editor__label-value,
.alert-editor__label-empty {
  min-width: 0;
  margin-left: auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.alert-editor__label-value {
  color: var(--accent-blue);
}

.alert-editor__label-empty {
  color: var(--label-secondary);
  font-weight: 400;
}

.alert-editor__label-input {
  width: 100%;
  margin: 0;
  padding: 0 0 var(--sp-sm);
  border: 0;
  outline: none;
  background: transparent;
  color: var(--label-primary);
  caret-color: var(--accent-blue);
  font: 400 17px/22px var(--font-sans);
}

/* Save: the inverted pill. */
.alert-editor__save {
  width: 100%;
  margin-top: var(--sp-sm);
  padding: var(--sp-sm) 0;
  border: 0;
  border-radius: var(--radius-full);
  background: var(--label-primary);
  color: var(--label-inverse-primary);
  font: 600 17px/22px var(--font-sans);
  cursor: pointer;
}

/* ── the calendar (after iOS's graphical date picker) ─────────────────── */

.alert-cal {
  padding: var(--sp-3xs) 0 var(--sp-sm);
}

.alert-cal__head {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding-bottom: var(--sp-xs);
}

.alert-cal__month {
  margin-right: auto;
  color: var(--label-primary);
  font: 600 17px/22px var(--font-sans);
}

.alert-cal__step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent-blue);
  cursor: pointer;
}

.alert-cal__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  row-gap: 2px;
  justify-items: center;
}

.alert-cal__weekday {
  padding: var(--sp-3xs) 0;
  color: var(--label-tertiary);
  font: 600 13px/18px var(--font-sans);
}

.alert-cal__day {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--label-primary);
  font: 400 20px/24px var(--font-sans);
  cursor: pointer;
}

.alert-cal__day--today {
  color: var(--accent-blue);
}

.alert-cal__day--selected {
  background: var(--accent-blue-elevated);
  color: var(--accent-blue);
  font-weight: 600;
}

.alert-cal__day--selected.alert-cal__day--today {
  background: var(--accent-blue);
  color: var(--label-on-accent);
}

/* "Time" and its pill; on phones the pill holds a transparent native time
   field (Mobile Safari's wheel on iPhones), so a tap anywhere on it opens
   it. Desktop browsers open no popup from such a field, so there the field
   itself is the pill (below). */
.alert-cal__time {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: var(--sp-xs);
  color: var(--label-primary);
  font: 400 17px/22px var(--font-sans);
}

.alert-cal__time-pill {
  position: relative;
  padding: 6px 11px;
  border-radius: 8px;
  background: var(--fill-vibrant-tertiary);
}

.alert-cal__time-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}

.alert-cal__time-pill:focus-within {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

@media (min-width: 835px) {
  .alert-cal__time-pill {
    padding: 0;
  }

  .alert-cal__time-face {
    display: none;
  }

  .alert-cal__time-input {
    position: static;
    width: auto;
    height: auto;
    padding: 6px 11px;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    letter-spacing: 0;
    opacity: 1;
    cursor: text;
  }

  .alert-cal__time-input:focus {
    outline: none;
  }

  .alert-cal__time-input::-webkit-calendar-picker-indicator {
    display: none;
  }
}

/* Keyboard focus: the accent ring, as the sheet's rows wear it (the global
   ring is --background-secondary, which is this sheet's own fill). Full-width rows
   draw it inside their box. */
.alert-editor__row:focus-visible,
.alert-editor__frequency:focus-visible,
.alert-editor__chip:focus-visible,
.alert-editor__save:focus-visible,
.alert-editor__switch:focus-visible,
.alert-cal__step:focus-visible,
.alert-cal__day:focus-visible,
.alert-menu__choice:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

.alert-editor__row:focus-visible {
  outline-offset: -2px;
  border-radius: var(--radius-sm);
}

@media (prefers-reduced-motion: no-preference) {
  .alert-menu__choice,
  .alert-editor__save,
  .alert-editor__chip {
    transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }
  .alert-menu__choice:active,
  .alert-editor__save:active {
    transform: scale(1.04);
  }
  .alert-editor__chip:active {
    transform: scale(1.2);
  }
}

/* ── the occurrence receipt ───────────────────────────────────────────── */

/* "Next: Tomorrow 9am ›" (iOS NextOccurrencePill): the quiet capsule that
   moves past the receipt — 15/500 secondary ink, a 12 chevron 8 after it,
   44 tall, 12 in, on the primary label at 8%. Under the action row on
   phones (the phone header's column, 8 below), under the strip on desktop. */
.nr-next-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  height: 44px;
  padding: 0 var(--sp-sm);
  border: none;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--label-primary) 8%, transparent);
  color: var(--label-secondary);
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  line-height: 20px;
  letter-spacing: 0;
  white-space: nowrap;
  cursor: pointer;
}

.nr-next-pill:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}

/* Desktop's: at the strip's inset (the title's text edge). The phone header
   draws its own, so phones hide this one with the strip. */
.nr-next-row {
  display: flex;
  padding-left: calc(var(--size-xxl) + var(--sp-xs));
}

@media (max-width: 834px) {
  :is(.nr-root--detail, .nr-root--capture) .nr-next-row {
    display: none;
  }
}

/* ── desktop hand-offs ────────────────────────────────────────────────── */

/* On desktop a sheet hands off to the next at once (ui/sheet: the called
   modal replaces this one, no exit), so a double-click's second click
   lands on the sheet it just opened — on a preset under the pointer, say.
   These sheets' content takes no pointer for a double-click's span after
   they appear; the keyboard is unaffected. (Phones play the exit, which
   the leaving sheet's own guard covers.) */
@keyframes alert-sheet-arming {
  from,
  to {
    pointer-events: none;
  }
}

@media (min-width: 835px) {
  .alert-menu-sheet .sheet__stack,
  .alert-editor-sheet .sheet__stack {
    animation: alert-sheet-arming 300ms;
  }
}

