/* ---------- brand palette ----------
   One source of truth; no raw colors below this block (white text on
   accent surfaces is the lone literal). Everything derives from the logo:
   ink line-art on periwinkle sky, warm paper around it, and one carrot
   for social warmth. Dark mode re-derives the same roles from the ink
   side of the logo; every text/background pair holds ≥4.5:1. */
:root {
  color-scheme: light;
  /* the logo itself */
  --sky: #dbe6f9;          /* logo background; "my events" on calendars */
  --sky-edge: #b7c6ea;     /* sky, darkened — edges on sky surfaces */
  --fg: #1c1b1a;           /* the line-art ink — all primary text */
  /* warm paper neutrals, light → dark */
  --bg: #faf9f7;           /* app background */
  --surface: #fff;         /* cards, inputs, chips */
  --hairline: #f0ede7;     /* faintest dividers */
  --wash: #efece7;         /* filled neutral: bubbles, badges */
  --wash-deep: #e3dfd8;    /* pressed states, skeletons, kbd */
  --border-soft: #e6e2db;  /* card edges */
  --border: #d8d3ca;       /* control edges */
  --faint: #c4beb4;        /* decorative only (disabled dots) — never text */
  --muted: #66625b;        /* secondary text (≥4.5:1 on --bg, and on the
                              wash at its densest — see --tint-sky) */
  /* accent: the sky, saturated — everything interactive or "you" */
  --accent: #2f6df6;       /* accent as text/borders */
  --accent-wash: #e8effc;
  /* the default list's soft edge: accent muted toward the light card edge */
  --default-edge: color-mix(in srgb, var(--accent) 40%, var(--border-soft));
  --user: var(--accent);   /* accent as a filled surface (white text on it) */
  --assistant: var(--wash);
  /* carrot: friends, invites, shared things */
  --carrot: #e8963e; --carrot-deep: #9a5a14;
  --carrot-tint: #fdf6ea; --carrot-wash: #f9e8cf;
  /* status */
  --ok: #2e7031; --ok-tint: #e4f0e2;
  --done: #4f8a48;   /* todo completion: progress bars + ticked checkboxes (dark mode keeps the lighter #a8c5a2) */
  --danger: #b3261e; --danger-tint: #fbeae9;
  /* bunnyfriend closeness: bestie = gold spotlight, friend = accent spotlight */
  --bestie: #d99a1c; --bestie-wash: #fbf1d8;
  /* calendar overlays: whose event is this */
  --mine: var(--sky); --mine-edge: var(--sky-edge);
  --invite: var(--carrot); --invite-tint: var(--carrot-tint);
  --friend: #996a6a; --friend-tint: #f7f1f1;   /* warm mauve */
  --google: #7f9070; --google-tint: #f1f4ec;   /* sage */
  /* the same four overlays as solid fills, dark enough to carry white text —
     all-day events fill instead of tinting, the way iOS separates them */
  --mine-solid: var(--user); --invite-solid: var(--carrot-deep);
  --friend-solid: #7d5555; --google-solid: #5d6b52;
  /* the parts a calendar is split into: one blue per category, mirrored
     from app/event_colors.py (iOS: Theme.categoryColor). The four above say
     WHOSE an event is; these say what it's about, so only an event of the
     user's own ever takes one. Everything else is derived — `--cat-tint`
     thins the color into the card behind it for a timed chip, an all-day
     bar fills with the color itself — which is why adding a color is one
     line here and one line in the palette. */
  --cat-mist: #d7e8fc;
  --cat-powder: #b8d7fa;
  --cat-cornflower: #8fb7f8;
  --cat-periwinkle: #a7b6fa;
  --cat-dusk: #9fb6e2;
  --cat-indigo: #8399d6;
  /* how much color a tint carries. Light paper thins it on its own; near-
     black doesn't, so the dark theme cuts it — at the light figure the
     tint came out mid-grey and --fg on it fell under 4.5:1. */
  --cat-mix: 42%;
  /* the ink on a filled category bar, in BOTH themes: every one of these is
     light enough that white fails on it, and the deepest still clears 6:1
     against this. The one place a color below the palette block is literal
     — it is the light theme's --fg, held still. */
  --cat-ink: #1c1b1a;
  /* "now" on the calendar — the one loud color, as on iOS: today's date
     circle and the current-time line, nothing else */
  --today: #e8352b; --today-fg: #fff;
  /* the bunny's frame. Every outfit and both poses are drawn into one shared
     296×368 canvas (scripts/normalize_bunny_size.py) with his head on x133
     and his soles 70px up, so `object-fit: contain` can only ever bind on
     height and he is the same size whatever he is wearing. Most of that frame
     is transparent — it is as tall as its tallest tenant, and fireghost's
     flame is half again the height of the bunny inside it — so anything
     placing *him* rather than his box works from these. --bunny-h is the only
     dial: the rest are that canvas's proportions at that height. iOS holds
     the same numbers in BunnyMascot. */
  --bunny-h: 123px;
  --bunny-w: calc(var(--bunny-h) * 296 / 368);
  --bunny-sole: calc(var(--bunny-h) * 70 / 368);    /* his feet, up from the frame's floor */
  --bunny-head: calc(var(--bunny-h) * 301 / 368);   /* his ears, likewise */
  /* how far the frame is inset in the corner. Less than the 1rem everything
     else gets, because he is not centred in it: his head sits left of the
     middle so the laptop his typing pose holds out has somewhere to go, and
     this puts *him* where he has always stood. */
  --bunny-inset: 1px;
  /* glass: the chrome, the panels, the tiles and the bubbles are frosted —
     a translucent fill over a blurred backdrop, a lit line along the top
     edge, a soft shadow (the "glass" section at the bottom of this file;
     iOS Theme.glass + GlassSurface). Under them the paper carries one wash
     (body's background): the logo's sky coming down from the top-left, the
     carrot rising from the bottom-right — a sunrise, so there is colour to
     show through. Alpha on white/ink here is the one place this file mixes
     its own colours: every text/background pair on glass still holds
     ≥4.5:1 over the wash at its densest (muted on a card over the full
     sky ≈ 5.2, white on the user's bubble ≈ 4.7). */
  --glass: rgba(255, 255, 255, .5);         /* chrome: header, dock, menus */
  --glass-card: rgba(255, 255, 255, .35);   /* cards and tiles in the scroll */
  --glass-edge: rgba(255, 255, 255, .85);   /* the lit top edge */
  --glass-line: rgba(28, 27, 26, .09);      /* the outline: ink, faint */
  --glass-shadow: 0 8px 28px rgba(28, 27, 26, .10), 0 1px 2px rgba(28, 27, 26, .05);
  --glass-shadow-soft: 0 2px 8px rgba(28, 27, 26, .06);   /* a bubble's */
  --glass-blur: 22px; --glass-sat: 1.5;
  /* the user's bubble as glass: powder chrome, lit at the top and settling
     into the sky at the bottom, let through so the wash still reads. Blue
     on blue — the text is --accent two notches deeper, the same blue this
     bubble used to be filled with, because --accent itself only reaches
     3.4:1 on the deep stop over the sky. This fill is as deep as that text
     allows: 4.6:1 at the worst point on the wash, where a shade further
     drops under 4.5. The accent stays --user, for the things you press.
     Dark keeps the blue fill white was chosen for. */
  --user-glass: linear-gradient(180deg, rgba(224, 230, 244, .8), rgba(194, 207, 233, .9));
  --user-fg: #1f4fc4;
  --user-chip: rgba(31, 79, 196, .08);     /* an attachment chip on it */
  /* the rim: the lit edge of the glass, one weight the whole way round —
     top included. Lighting it from a corner makes that corner read as
     the border and the rest as an accident; an even rim reads as a pane. */
  --user-rim: rgba(255, 255, 255, .5);
  /* the lens: a pane has thickness, and light gathers in it — a bright
     ring standing just inside the rim, and a sheen off the top. It has to
     be drawn: what makes the real thing convince is refraction of what
     lies behind, and behind this is one smooth wash with nothing in it to
     bend. Kept faint — enough that the edge has thickness, and no more:
     turned up it stops looking like glass and starts looking lit from
     within. Every layer lightens, so text reads better over it. */
  --user-lens:
    inset 0 0 0 1px rgba(255, 255, 255, .3),
    inset 0 1px 2px rgba(255, 255, 255, .45);
  --user-cast: var(--glass-shadow-soft);
  /* the wash: --sky and --carrot-wash, nearly full strength — the paper is
     what thins them, not alpha */
  --tint-sky: rgba(219, 230, 249, .9); --tint-carrot: rgba(249, 232, 207, .8);
  /* the wash as one image, reused: `body` paints it, and the single-list
     header repaints the same thing viewport-fixed so its strip matches the
     paper behind it rather than sitting as a flat panel */
  --wash-gradient: linear-gradient(150deg, var(--tint-sky) 0%, transparent 46%,
                    transparent 60%, var(--tint-carrot) 100%);
}
/* applied via data-theme, set before first paint by the head script:
   user override from settings, or the device preference (default) */
:root[data-theme="dark"] {
    color-scheme: dark;
    --sky: #33436a; --sky-edge: #4d5f8e;
    --fg: #ece9e4;
    --bg: #191817; --surface: #232120;
    --hairline: #2a2826;
    --wash: #2e2b28; --wash-deep: #3b3833;
    --border-soft: #343130; --border: #4a463f;
    --faint: #5b564d; --muted: #aba69d;
    --accent: #8fb0ff; --accent-wash: #26334f;
    /* dark: mix toward the dark card edge would only dim it — soften with
       translucency instead, so the edge stays a light, airy blue */
    --default-edge: color-mix(in srgb, var(--accent) 55%, transparent);
    --user: #2b57bd;           /* darker fill so white text stays readable */
    --carrot: #e8963e; --carrot-deep: #edb277;
    --carrot-tint: #322818; --carrot-wash: #46351d;
    --ok: #8cc98f; --ok-tint: #223523;
    --done: #a8c5a2;
    --danger: #f28b84; --danger-tint: #402320;
    --bestie: #e6b23c; --bestie-wash: #3a2f16;
    --friend: #c39494; --friend-tint: #332929;
    --google: #a9bb97; --google-tint: #2a2f24;
    --invite-solid: #7a4a12;   /* carrot-deep is a text color in the dark */
    --friend-solid: #6d4747; --google-solid: #4a5741;
    --today: #ff453a; --today-fg: #fff;
    --cat-mix: 26%;
    --glass: rgba(35, 33, 32, .5); --glass-card: rgba(35, 33, 32, .35);
    --glass-edge: rgba(255, 255, 255, .10); --glass-line: rgba(255, 255, 255, .10);
    --glass-shadow: 0 8px 28px rgba(0, 0, 0, .45), 0 1px 2px rgba(0, 0, 0, .3);
    --glass-shadow-soft: 0 2px 8px rgba(0, 0, 0, .25);
    --user-glass: rgba(43, 87, 189, .78);
    --user-fg: #fff; --user-chip: rgba(255, 255, 255, .22);
    --user-rim: rgba(255, 255, 255, .16);
    --user-lens:
      inset 0 0 0 1px rgba(255, 255, 255, .1),
      inset 0 1px 2px rgba(255, 255, 255, .16);
    --user-cast: var(--glass-shadow-soft);
    /* the wash in the dark: near-black paper doesn't thin anything, so
       alpha has to — sky at 60% read as a blue corner, not a hint */
    --tint-sky: rgba(51, 67, 106, .3); --tint-carrot: rgba(70, 53, 29, .55);
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; }
button, select { font-family: inherit; touch-action: manipulation; }
body {
  margin: 0; font-family: -apple-system, system-ui, sans-serif;
  color: var(--fg);
  /* the wash: one diagonal, sky down from the top-left into paper, paper
     into carrot at the bottom-right. Body never scrolls (below), so it
     stays put while every screen moves over it. */
  background: var(--wash-gradient), var(--bg);
  display: flex; flex-direction: column;
  /* app shell: the page itself never scrolls (kills iOS keyboard blank-void
     panning); only #messages and .page scroll internally */
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  overflow: hidden; overscroll-behavior: none;
  /* The whole app is lowercase — the bunny's voice, and the house style the
     hand-written copy already follows. It has to be a paint-time transform
     rather than discipline in the strings, because most of what's on screen
     was never ours to write: Google Calendar titles, items a friend added
     from their own client, a friend's display name, dates, anything the
     agent said. Display only — the stored text is untouched, and the input
     fields inherit it honestly (their handlers lowercase on save, so you see
     what you'll get). iOS does the same thing once, at its root
     (`MrBunnyApp`'s .textCase(.lowercase)). */
  text-transform: lowercase;
}
/* The form controls have to be named: the UA stylesheet sets
   `text-transform: none` on all four, so they don't inherit the rule above
   and every button in the app would keep whatever case it was given. */
button, input, select, textarea { text-transform: lowercase; }
/* screen-reader-only live region (chat replies, announcements) */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
header {
  display: flex; align-items: center; justify-content: space-between;
  /* the top inset is the iOS status bar (viewport-fit=cover + the
     black-translucent status-bar style): the header paints under it, so the
     strip with the clock and battery is this same paper, not a system band */
  padding: calc(.6rem + env(safe-area-inset-top)) 1rem .6rem;
  border-bottom: 1px solid var(--border-soft);
  background: var(--bg); position: sticky; top: 0;
}
header h1 {
  font-size: 1.05rem; margin: 0; display: flex; align-items: center; gap: .45rem;
  cursor: pointer; -webkit-user-select: none; user-select: none;
}
/* the art is the bunny filled white with the ink on top, so he is the same
   white-faced bunny on the light wash and on the dark bar — the line work
   alone took the background through his face. One asset, no theme swap. */
#logo { width: 26px; height: 26px; border-radius: 7px; display: block; }
/* presence badge: the ws dot rides the logo's corner like an online light.
   Filled ● = connected, hollow ○ = not — never color alone; the words live
   in title/aria-label (set by setConn). */
.avatar { position: relative; display: inline-flex; }
#conn {
  position: absolute; right: -4px; bottom: -4px;
  font-size: .6rem; line-height: 1;
  background: var(--bg); border-radius: 50%; padding: 1.5px;
}
header .actions { display: flex; gap: .5rem; align-items: center; }
header button {
  border: 1px solid var(--border); background: var(--surface); border-radius: 999px;
  padding: .5rem .85rem; font-size: .85rem; cursor: pointer; color: var(--fg);
  position: relative; min-height: 2.6rem;
}
header button::after { content: ""; position: absolute; inset: -6px; }
header button:active { background: var(--wash-deep); }

/* two-tap destructive confirm: first tap arms the button for 3s */
button.armed {
  border-color: var(--danger); color: var(--danger);
  background: var(--danger-tint);
}

/* ---------- hamburger menu ---------- */
/* 44px-class visible target (2.6rem circle + hit extension), centered glyph */
#menu-btn {
  width: 2.6rem; padding: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.25rem; line-height: 1;
}
#menu-btn[hidden] { display: none; }
#menu[hidden] { display: none; }
#menu {
  position: fixed; top: calc(4rem + env(safe-area-inset-top)); right: .8rem; z-index: 50;
  background: var(--surface); border: 1px solid var(--border-soft); border-radius: 14px;
  box-shadow: 0 6px 24px rgba(0,0,0,.15);
  display: flex; flex-direction: column; min-width: 12.5rem;
  padding: .4rem;
}
#menu button {
  border: 0; background: none; font-size: .95rem;
  padding: .7rem .9rem; min-height: 2.75rem;   /* ≥44px rows */
  border-radius: 9px; cursor: pointer; color: var(--fg);
  display: flex; align-items: center; gap: .6rem;
}
/* the drawn mark. flex: none so a long label can never squash it, and the six
   rows keep one text column */
#menu button img { width: 1.3rem; height: 1.3rem; flex: none; }
#menu button:active { background: var(--wash); }
#menu button.active { color: var(--accent); font-weight: 600; }

/* unread dot: reply arrived while another view was showing. ::before on the
   button — ::after is the shared header hit-extension */
#menu-btn.unread::before {
  content: ""; position: absolute; top: .15rem; right: .3rem;
  width: 8px; height: 8px; border-radius: 50%; background: var(--danger);
}
/* a flex item now, so the row's gap spaces it rather than a margin */
#menu button.unread::after {
  content: ""; width: 7px; height: 7px; flex: none;
  border-radius: 50%; background: var(--danger);
}

/* ---------- toast / snackbar ---------- */
.toast {
  /* clears the chat composer; floats near the bottom on other pages */
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(4.6rem + env(safe-area-inset-bottom));
  max-width: min(92vw, 26rem); z-index: 60;
  background: var(--fg); color: var(--bg);
  border-radius: 999px; padding: .6rem 1.1rem; font-size: .85rem;
  display: flex; gap: .9rem; align-items: center;
  box-shadow: 0 4px 16px rgba(0,0,0,.2);
}
.toast span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.toast.error { background: var(--danger); color: #fff; }
.toast button {
  border: 0; background: none; color: var(--sky); font-weight: 600;
  font-size: .85rem; cursor: pointer; padding: .2rem .1rem; position: relative;
}
.toast button::after { content: ""; position: absolute; inset: -10px; }
.toast.error button { color: #fff; }

/* sign-in view */
#signin-view {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 1.2rem; padding: 1rem;
}
#signin-view .bunny { width: 112px; height: 112px; border-radius: 24px; }
#signin-status { color: var(--muted); font-size: .85rem; text-align: center; }
.signin-retry {
  color: var(--accent); font-size: .85rem; background: none; border: none;
  padding: 0; cursor: pointer; text-decoration: underline;
}

/* chat view */
#chat-view { flex: 1; display: none; flex-direction: column; min-height: 0; }
#messages {
  flex: 1; overflow-y: auto; padding: 1rem; display: flex;
  flex-direction: column; gap: .55rem; scroll-behavior: smooth;
  overscroll-behavior: contain;   /* no bounce-chaining into the page */
}
.msg {
  max-width: 82%; padding: .55rem .8rem; border-radius: 16px;
  line-height: 1.35; font-size: .95rem; white-space: pre-wrap;
  overflow-wrap: break-word;
}
.msg.user { align-self: flex-end; color: var(--user-fg);   /* surface: the glass section */
            border-bottom-right-radius: 5px; }
.msg.assistant { align-self: flex-start; background: var(--assistant);
                 border-bottom-left-radius: 5px; }
.msg.assistant.streaming::after { content: "▍"; opacity: .5; }
.sys {
  align-self: center; font-size: .78rem; color: var(--muted);
  background: var(--wash); border-radius: 999px; padding: .18rem .7rem;
}
.sys.error { color: var(--danger); background: var(--danger-tint); }
.quick-actions {
  display: flex; gap: .5rem; align-self: flex-start; padding: .1rem 0;
  flex-wrap: wrap;
}
.quick-actions button, .card-actions button {
  border: 1px solid var(--border); background: var(--surface); color: var(--fg);
  border-radius: 999px; padding: .55rem 1rem; font-size: .85rem;
  cursor: pointer;
}
.quick-actions button:active, .card-actions button:active { background: var(--wash-deep); }
/* ---- answering yes or no, wherever it is asked ----
   Saying yes to a thing looks the same in all four places the app asks: a
   chat card's pill, a pending list share on the todo screen, a bunnyfriend
   request, an event invitation. They used to be four different buttons —
   solid accent, outlined accent, red text, plain surface — which made the
   same decision look like four different kinds of decision.

   Yes is the ok green, no is the neutral wash. No is deliberately NOT
   --danger: declining an invitation is an ordinary answer, not a
   destructive act, and red would make saying no feel like a warning.

   Colour only — each caller keeps its own shape (pill, chip, block button),
   which is why every selector below is spelled out: the base rules are more
   specific than a bare class and would otherwise win. On a chat card the
   hook is `data-intent` from the wire, never the label, because the label is
   localized (수락, not "accept"). */
.answer-yes,
.pending-actions button.answer-yes,
.person-row button.answer-yes,
.ev-btn.answer-yes,
.card-actions button[data-intent="affirm"] {
  background: var(--ok-tint); color: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 30%, var(--border));
}
.answer-no,
.pending-actions button.answer-no,
.person-row button.answer-no,
.ev-btn.answer-no,
.card-actions button[data-intent="reject"] {
  background: var(--wash); color: var(--fg); border-color: var(--border);
}
.answer-yes:active,
.ev-btn.answer-yes:active,
.card-actions button[data-intent="affirm"]:active {
  background: color-mix(in srgb, var(--ok) 18%, var(--ok-tint));
}
.answer-no:active,
.ev-btn.answer-no:active,
.card-actions button[data-intent="reject"]:active { background: var(--wash-deep); }
/* an invite pill after it's been tapped: the answer, standing in for the row */
.card-answered { color: var(--muted); font-size: 0.85rem; padding: 4px 2px; }
/* actions.v1: a bare pill row under a reply (share my location) — the
   card-actions row, minus the card */
.reply-actions { align-self: flex-start; }
.reply-actions .card-actions { margin: 0; padding: .1rem 0; }
/* rich cards (card.v1): summarized digest with tap-to-expand sections */
.msg.assistant.card { max-width: 92%; padding: .7rem .9rem; white-space: normal; }
.card-title { font-weight: 600; margin-bottom: .3rem; }
.card-section { border-top: 1px solid var(--hairline); }
.card-line { padding: .45rem 0; }
.card-summary {
  display: flex; align-items: baseline; width: 100%; gap: .45rem;
  border: 0; background: none; text-align: left; padding: .5rem 0;
  color: var(--fg); font-size: .95rem; cursor: pointer;
}
.card-summary .chev { color: var(--muted); transition: transform .15s; font-size: .7rem; }
.card-summary[aria-expanded="true"] .chev { transform: rotate(90deg); }
.card-details {
  display: flex; flex-direction: column; gap: .25rem;
  padding: 0 0 .55rem 1.1rem; color: var(--muted); font-size: .88rem;
}
.card-details[hidden] { display: none; }   /* display:flex would beat the UA rule */
.card-actions { display: flex; flex-wrap: wrap; gap: .5rem; padding: .35rem 0 .15rem; }
/* the working indicator is an assistant bubble whose pulsing phrase gets
   replaced in place by the first real tokens — no visual swap */
.msg.assistant.typing {
  color: var(--muted); font-style: italic;
  animation: typing-pulse 1.4s ease-in-out infinite;
}
@keyframes typing-pulse { 50% { opacity: .35; } }

/* ---- the composer: one box, the draft on top running the full width, the
   controls in a row beneath it (iOS ChatComposerView is the same shape).
   The dock behind it is the bottom bar — chip rows above the box, the safe
   area below — and what the bunny floats above. It's transparent with no
   top rule, so the solid-white box sits straight on the page's paper
   (the body's wash-gradient), mirroring iOS's composer on PaperBackground —
   no banded strip, no separator line above the bar. ---- */
#dock {
  position: relative; display: flex; flex-direction: column; flex: none;
  background: transparent;
  padding-bottom: env(safe-area-inset-bottom);
}
form {
  display: flex; flex-direction: column; gap: .2rem;
  margin: .5rem 1rem; padding: .3rem .4rem .4rem;
  border: 1px solid var(--border); border-radius: 22px; background: var(--surface);
  position: static;   /* the fabs and the bunny anchor to the dock, not the box */
}
form:focus-within { border-color: var(--accent); }
form textarea {
  border: 0; padding: .45rem .7rem .25rem; background: transparent;
  font-size: 1rem; outline: none; color: var(--fg);
  resize: none; font-family: inherit; line-height: 1.4;
  max-height: 7.4rem;   /* ~4 rows; beyond that it scrolls */
  overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: none;   /* Firefox */
}
/* Android Chrome paints a classic, always-on scrollbar rail inside the
   rounded composer the moment a long dictation passes four rows. The box
   still scrolls by touch and wheel — it just shouldn't wear a rail. */
form textarea::-webkit-scrollbar { width: 0; height: 0; }
#composer-row { display: flex; align-items: center; gap: .3rem; }
/* drawn at 36px so the box stays a box (a 44px circle plus the box's own
   padding would make the composer taller than what it composes); the tap
   target is lifted back to 44 by the ::after pad — iOS's controlSize/touchPad */
#composer-row button {
  width: 2.25rem; height: 2.25rem; padding: 0; border: 0; border-radius: 50%;
  flex: none; display: flex; align-items: center; justify-content: center;
  cursor: pointer; position: relative; font-family: inherit;
}
#composer-row button::after { content: ""; position: absolute; inset: -4px; }
#composer-row button svg { display: block; }
/* attach and the mic: neutral, on the left — the two ways of putting
   something other than typing into the message */
#mic { background: var(--wash); color: var(--fg); }
#mic:active { background: var(--wash-deep); }
/* send is the same pane of glass as the message it sends — the fill, the
   rim and the lens, round instead of a bubble */
#send {
  margin-left: auto; background: var(--user-glass); color: var(--user-fg);
  border: 1px solid var(--user-rim); box-shadow: var(--user-lens), var(--user-cast);
}
#send:active { filter: brightness(.92); }
#send:disabled { opacity: .4; }
#mic[hidden], #new-chat[hidden] { display: none; }
/* dictation review: what got put back, each one a tap from undone. Sits on
   the composer's top edge so the eye goes transcript → chips → send. */
#dictation-fixes { display: flex; flex-wrap: wrap; gap: .4rem; padding: .35rem 1rem 0; }
#dictation-fixes[hidden] { display: none; }
#dictation-fixes .fix, #ask-fixes .fix {
  display: inline-flex; align-items: center; gap: .35rem;
  border: 1px solid var(--accent); background: var(--accent-wash);
  color: var(--fg); border-radius: 999px; padding: .2rem .55rem .2rem .7rem;
  font-size: .8rem; font-family: inherit; cursor: pointer; line-height: 1.4;
}
#dictation-fixes .fix s, #ask-fixes .fix s { opacity: .55; }
#dictation-fixes .fix b, #ask-fixes .fix b { font-weight: 600; color: var(--accent); }
#dictation-fixes .fix .x, #ask-fixes .fix .x { opacity: .6; font-size: .9em; }
#dictation-fixes .fix:active, #ask-fixes .fix:active { background: var(--wash-deep); }
/* the strip above the box lists what will ride with the next message */
#attachments { display: flex; flex-wrap: wrap; gap: .4rem; padding: .35rem 1rem 0; }
#attachments[hidden] { display: none; }
#attachments .att {
  display: inline-flex; align-items: center; gap: .4rem;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--fg); border-radius: 999px; padding: .2rem .5rem .2rem .3rem;
  font-size: .8rem; max-width: 14rem;
}
#attachments .att img, #attachments .att .att-icon {
  width: 1.5rem; height: 1.5rem; border-radius: 50%; object-fit: cover;
  display: flex; align-items: center; justify-content: center; flex: none;
  background: var(--wash); font-size: .75rem;
}
#attachments .att .att-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#attachments .att .x {
  border: 0; background: none; color: var(--muted); cursor: pointer;
  padding: 0 .1rem; font-size: 1rem; line-height: 1;
}
#chat-view.dropping #messages { outline: 2px dashed var(--accent); outline-offset: -8px; }
/* the user's bubble for an upload: chips over the note */
.msg.user .att-chips { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .25rem; }
.msg.user .att-chips span {
  background: var(--user-chip); border-radius: 999px; padding: .1rem .5rem;
  font-size: .8rem;
}
/* the import review: what the bunny read out of the file, one checkbox
   row per event/todo, then add / skip */
.msg.assistant.card.import-review .card-title { margin-bottom: .1rem; }
.import-summary { color: var(--muted); font-size: .88rem; margin-bottom: .35rem; }
.import-warnings { color: var(--muted); font-size: .82rem; margin: .1rem 0 .4rem; }
.import-warnings div::before { content: "⚠︎ "; }
.import-group { border-top: 1px solid var(--hairline); padding: .4rem 0 .2rem; }
.import-group h4 { margin: 0 0 .25rem; font-size: .8rem; font-weight: 600;
                   color: var(--muted); }
.import-row {
  display: flex; align-items: flex-start; gap: .55rem; padding: .3rem 0;
  cursor: pointer; font-size: .92rem;
}
.import-row input { margin-top: .25rem; flex: none; accent-color: var(--accent); }
.import-row .import-main { display: flex; flex-direction: column; min-width: 0; }
.import-row .import-sub { color: var(--muted); font-size: .82rem; }
.import-row .tag {
  display: inline-block; font-size: .72rem; border-radius: 999px;
  padding: 0 .45rem; margin-left: .3rem; background: var(--wash);
  color: var(--muted); vertical-align: middle;
}
.import-row .tag.dup { background: var(--danger-tint); color: var(--danger); }
.import-list-pick {
  display: flex; align-items: center; gap: .5rem; font-size: .85rem;
  color: var(--muted); padding: .35rem 0 .1rem;
}
.import-list-pick input {
  flex: 1; border: 1px solid var(--border); border-radius: 999px;
  padding: .3rem .7rem; font-size: .85rem; background: var(--surface);
  color: var(--fg); font-family: inherit; min-width: 0;
}
.import-review .card-actions button.primary {
  background: var(--user); color: #fff; border-color: var(--user);
}
.import-review .card-actions button:disabled { opacity: .5; }
.import-review.done { opacity: .7; }

/* the mascot (iOS BunnyMascot): waving in the bottom-right corner of every
   screen, head down at a laptop while a chat turn is in flight. Placement
   belongs to whoever hosts him — chat and the calendar float theirs above a
   bottom bar that grows with its content, the rest of the app pins one to
   the corner of the screen. Decoration by default — taps fall through and
   screen readers never meet him — until `.bunny-btn` makes him the button he
   is on every page (the mic on chat, the question elsewhere).

   The box is the art's shared frame at corner size (--bunny-*). Most of the
   top of that frame is empty, so the offsets aim at *him*: --bunny-sole takes
   his footing back out of the bottom, and the right inset is reckoned to his
   middle rather than to the frame's, which sits left of it to make room for
   the laptop his typing pose holds out. */
.mascot {
  height: var(--bunny-h); width: var(--bunny-w);
  object-fit: contain; object-position: center bottom;
  position: absolute;
  right: var(--bunny-inset); bottom: calc(100% + .55rem - var(--bunny-sole));
  pointer-events: none; user-select: none; -webkit-user-select: none;
  /* over the screen he floats on — the calendar's dates, chips and now-line
     are positioned layers of their own (z 1–3) and were painting through
     him as the grid scrolled past — but under the fab stack (5) */
  z-index: 4;
}
#mascot-page {
  /* absolute, not fixed: body is pinned to the *visible* height (app.js's
     keyboard sync), so he rides up with the keyboard instead of under it —
     the ask box hangs off his head and has to be seen while typing */
  position: absolute;
  bottom: calc(.55rem + env(safe-area-inset-bottom) - var(--bunny-sole));
}
#mascot-page[hidden] { display: none; }
/* wherever he is a button — off the chat page he asks a question (app.js
   toggleAsk), on it he is the mic (app.js's #mic block) — taps land and
   focus shows */
.bunny-btn { pointer-events: auto; cursor: pointer; }
.bunny-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 12px; }

/* the celebration (app.js celebrate, iOS BunnyCheer): the corner bunny, in
   the middle of the screen, when a list runs out of open todos. Parked dead
   centre by CSS — app.js animates him out of the corner and back with a
   transform that carries this same centring, so the geometry lives in one
   place. Fixed, unlike the corner bunny: he is over the whole screen for a
   second and a half and has nothing to do with the keyboard. */
.bunny-cheer {
  position: fixed; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  /* the cheer is drawn on a frame of its own, but one grown to the same
     bunny-to-frame ratio as the standing art — that ratio is what lets
     `celebrate` shrink him back into the corner by comparing the two boxes */
  width: 316px; height: 321px; object-fit: contain;
  /* under the toast (60), which still has things to say while he performs */
  z-index: 55;
  pointer-events: none; user-select: none; -webkit-user-select: none;
}
/* his corner, for as long as he isn't in it — `visibility`, not `display`,
   so the ask panel and bubble keep hanging off the head he left behind */
.bunny-away { visibility: hidden; }

/* ---- ask mrbunny from any page: the panel he listens through (no keyboard
   here — voice only) and the speech bubble the reply comes back in, both
   hanging off the top of the corner bunny's head (--ask-bottom, set by
   app.js positionAsk from where he stands on this page) ---- */
#ask, #ask-bubble {
  position: absolute; right: 1rem; bottom: var(--ask-bottom, 7rem);
  max-width: min(22rem, calc(100% - 2rem));
  z-index: 6;   /* over the bunny (4) and the fabs (5) */
}
#ask[hidden], #ask-bubble[hidden] { display: none; }
#ask {
  width: min(22rem, calc(100% - 2rem));
  background: var(--surface); border: 1px solid var(--border); border-radius: 18px;
  padding: .6rem 2rem .5rem .9rem;
  box-shadow: 0 6px 24px rgba(0, 0, 0, .12);
}
#ask[data-state="listening"] { border-color: var(--accent); }
#ask-heard {
  font-size: 1rem; line-height: 1.4; white-space: pre-wrap; overflow-wrap: break-word;
  min-height: 1.4rem; max-height: 30vh; overflow-y: auto; overscroll-behavior: contain;
}
/* listening: a live dot ahead of the words, and "listening…" until there are any */
#ask[data-state="listening"] #ask-heard::before {
  content: ""; display: inline-block; width: .55rem; height: .55rem;
  border-radius: 50%; background: var(--danger); margin: 0 .45rem .1rem 0;
  animation: typing-pulse 1.4s ease-in-out infinite;
}
#ask[data-state="listening"] #ask-heard:empty::after {
  content: "listening…"; color: var(--muted); font-style: italic;
}
#ask[data-state="note"] #ask-heard { color: var(--muted); font-size: .92rem; }
/* review: the transcript is editable — tap to fix a misheard word */
#ask[data-state="review"] #ask-heard { cursor: text; }
#ask[data-state="review"] #ask-heard:focus {
  outline: none; box-shadow: 0 2px 0 -1px var(--accent);
}
#ask-fixes { display: flex; flex-wrap: wrap; gap: .4rem; padding-top: .4rem; }
#ask-fixes[hidden] { display: none; }
#ask-controls { display: flex; align-items: center; gap: .4rem; padding-top: .5rem; }
#ask-controls button {
  width: 2.25rem; height: 2.25rem; padding: 0; border: 0; border-radius: 50%;
  flex: none; display: flex; align-items: center; justify-content: center;
  cursor: pointer; position: relative; font-family: inherit;
}
#ask-controls button::after { content: ""; position: absolute; inset: -4px; }
#ask-controls button svg { display: block; }
#ask-controls button[hidden] { display: none; }
#ask-redo { background: var(--wash); color: var(--fg); }
#ask-stop { background: var(--danger-tint); color: var(--danger); }
#ask-send { margin-left: auto; background: var(--user); color: #fff; }
#ask-send:active { filter: brightness(.85); }
#ask-send:disabled { opacity: .4; }
/* the no-recogniser note points at the page that has a keyboard */
#ask-controls #ask-open {
  width: auto; height: auto; border-radius: 0; background: none;
  color: var(--accent); font-size: .85rem; padding: .2rem 0;
}
/* which controls each state shows: listening has the stop; review has
   say-it-again and send; a note has whatever app.js left unhidden */
#ask:not([data-state="listening"]) #ask-stop { display: none; }
#ask:not([data-state="review"]) #ask-send { display: none; }
#ask[data-state="listening"] #ask-redo { display: none; }
#ask-cancel {
  position: absolute; top: .3rem; right: .35rem;
  width: 1.7rem; height: 1.7rem; border: 0; border-radius: 50%;
  background: none; color: var(--muted); font-size: 1.1rem; line-height: 1;
  cursor: pointer; padding: 0;
}
#ask-cancel:active { background: var(--wash-deep); }
/* the bubble hugs a short reply, like one in the transcript, and wraps a
   long one at the same cap */
#ask-bubble { width: max-content; }
/* the bubble: an assistant bubble with a tail pointing down at him */
#ask-bubble {
  background: var(--assistant); color: var(--fg); border-radius: 16px;
  border-bottom-right-radius: 5px; padding: .55rem 2rem .5rem .8rem;
  font-size: .95rem; line-height: 1.35;
  box-shadow: 0 6px 24px rgba(0, 0, 0, .12);
}
#ask-bubble::after {
  content: ""; position: absolute; right: 1.6rem; bottom: -7px;
  border: 8px solid transparent; border-bottom: 0;
  border-top-color: var(--assistant);
}
#ask-text {
  white-space: pre-wrap; overflow-wrap: break-word;
  max-height: 40vh; overflow-y: auto; overscroll-behavior: contain;
}
#ask-bubble[data-state="working"] #ask-text {
  color: var(--muted); font-style: italic;
  animation: typing-pulse 1.4s ease-in-out infinite;
}
#ask-bubble[data-state="stream"] #ask-text::after { content: "▍"; opacity: .5; }
#ask-bubble[data-state="error"] { background: var(--danger-tint); color: var(--danger); }
#ask-bubble[data-state="error"]::after { border-top-color: var(--danger-tint); }
#ask-close {
  position: absolute; top: .2rem; right: .3rem;
  width: 1.7rem; height: 1.7rem; border: 0; border-radius: 50%;
  background: none; color: var(--muted); font-size: 1.1rem; line-height: 1;
  cursor: pointer; padding: 0;
}
#ask-close:active { background: var(--wash-deep); }
/* the transcript has the whole exchange — one tap away */
#ask-open-chat {
  display: block; border: 0; background: none; padding: .3rem 0 0;
  color: var(--accent); font-size: .8rem; font-family: inherit; cursor: pointer;
}
/* while he's still working there's nothing to open yet */
#ask-bubble[data-state="working"] #ask-open-chat { display: none; }

/* new chat, in the row beside send. Armed, the icon gives way to words
   ("clear chat?") for the second tap and the circle becomes a pill, growing
   into the empty middle of the row, tinted like every armed button */
#new-chat { background: var(--wash); color: var(--fg); margin-left: auto; }
#new-chat:not([hidden]) ~ #send { margin-left: 0; }   /* it took the gap already */
#new-chat.armed {
  width: auto; padding: 0 .8rem; font-size: .85rem; white-space: nowrap;
  border-radius: 999px; border: 1px solid var(--danger);
  color: var(--danger); background: var(--danger-tint);   /* the id outranks button.armed */
}
#new-chat:active { background: var(--wash-deep); }
#mic {
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  /* the button is tap/hold, never a scroll surface — but a pinch that happens
     to start on it is still the browser's zoom, not a gesture of ours */
  touch-action: pinch-zoom;
}
/* the glyph is currentColor throughout, so it inherits --fg: ink on the
   light paper surface, paper on the dark one — one icon, both themes */
#mic.listening {
  background: var(--danger); border-color: var(--danger);
  /* --surface is the counterpart of --danger in either theme: white on the
     light red, near-black on the dark salmon */
  color: var(--surface);
  animation: pulse 1.2s ease-in-out infinite;
}
@keyframes pulse { 50% { opacity: .55; } }

.page {
  flex: 1; overflow-y: auto; padding: 1rem; min-height: 0;
  padding-bottom: calc(1rem + env(safe-area-inset-bottom));
  overscroll-behavior: contain;
}

/* ---------- lists page: tile grid ---------- */
/* two up on a phone, more as the viewport allows — 10.5rem keeps a 2-col
   grid ≥150px wide on a 320px screen, and never goes 1-up on a real phone */
.tile-grid {
  display: grid; gap: .7rem;
  grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr));
}
/* the "reorder"/"done" toggle above the grid (views.js tileReorderToggle);
   the button's own margin-left:auto pushes it to the right edge */
.tile-reorder-head { display: flex; margin-bottom: .7rem; }
/* in reorder mode a tap no longer opens — the tiles are grab handles */
.tile-grid.reordering .list-tile:not(.default) { cursor: grab; }
.tile-grid.reordering .list-tile.tile-dragging { cursor: grabbing; }
.list-tile {
  display: flex; flex-direction: column; gap: .4rem;
  min-height: 7.5rem; padding: .8rem; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: 14px;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  /* what a displaced neighbour travels along while the dragged tile takes
     its slot — views.js `slidingMove` hands it the distance to cover */
  transition: transform .15s ease;
}
.list-tile:active { background: var(--wash); }
.list-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* the pinned default list: it leads the grid, never drags, and reads apart.
   the edge is the accent softened toward the neutral card edge — present, not loud */
.list-tile.default {
  border-color: var(--default-edge); cursor: pointer;
  background: var(--accent-wash);
}
/* the default list is always private, never shared — so where a shared tile
   shows its group pill (footer, bottom-right), this one wears an incognito glyph */
.tile-private { display: flex; align-items: center; color: var(--accent); line-height: 1; }
.tile-private svg { display: block; }
/* lifted for reorder: above its neighbors, tracking the finger, no easing lag */
.list-tile.tile-dragging {
  position: relative; z-index: 5; transition: none;
  box-shadow: 0 4px 14px rgba(0,0,0,.18); background: var(--surface);
}
/* title row: the name, and a green tick pinned to the corner when the list is
   finished (opposite the name, aligned to its first line) */
.tile-head { display: flex; align-items: flex-start; gap: .4rem; }
.tile-head .tile-name { flex: 1; min-width: 0; }
.tile-name {
  font-weight: 600; font-size: .95rem; line-height: 1.25;
  overflow: hidden; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
/* the green tick a finished list wears — a filled disc with a white check,
   the tile-scale cousin of the row .check inside an open list */
.tile-done {
  flex: none; width: 1.35rem; height: 1.35rem; border-radius: 50%;
  background: var(--done); position: relative;
}
.tile-done::after {
  content: ""; position: absolute; left: .5rem; top: .28rem;
  width: .28rem; height: .55rem;
  border: solid var(--surface); border-width: 0 2px 2px 0;
  transform: rotate(42deg);
}
/* on a tile the high tally + due date stay one row, never stacked (the open
   list's .detail-chips re-enables wrap for its fuller chip set) */
.tile-chips { display: flex; flex-wrap: nowrap; gap: .3rem; }
.tile-foot { margin-top: auto; display: flex; flex-direction: column; gap: .35rem; }
/* done/total as a bar — on a tile, and again in the open list's header */
.prog { height: 4px; border-radius: 999px; background: var(--wash-deep); overflow: hidden; }
.prog span {
  display: block; height: 100%; background: var(--done); border-radius: 999px;
  transition: width .2s ease;
}
.tile-stats {
  display: flex; align-items: center; justify-content: space-between;
  gap: .35rem; font-size: .78rem;
}
.tile-count { color: var(--muted); white-space: nowrap; }
/* right cluster of the footer: high tally + who it's shared with, opposite the count */
.tile-stats-right { display: flex; align-items: center; gap: .3rem; }

/* ---------- lists page: the completed fold ---------- */
/* finished lists, tucked below the grid — collapsed by default, one flat row
   each. The whole thing steps aside when there's nothing done. */
.done-lists { margin-top: 1.4rem; }
.done-lists-head {
  display: flex; align-items: center; gap: .4rem; width: 100%;
  background: none; border: 0; padding: .2rem; cursor: pointer;
  color: var(--fg); font-size: .95rem; font-weight: 600; text-align: left;
}
.done-lists-head .chev { color: var(--muted); transition: transform .15s ease; }
.done-lists-head[aria-expanded="true"] .chev { transform: rotate(180deg); }
.done-lists-count {
  margin-left: auto; background: var(--wash); color: var(--muted);
  border-radius: 999px; font-size: .8rem; font-weight: 600; padding: .1rem .55rem;
}
.done-lists-body { display: flex; flex-direction: column; gap: .5rem; margin-top: .6rem; }
/* an author `display` beats the UA `[hidden]` rule, so restore the collapse */
.done-lists-body[hidden] { display: none; }
.done-list-row {
  display: flex; align-items: center; gap: .7rem; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: 14px; padding: .8rem .9rem;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
.done-list-row:active { background: var(--wash); }
.done-list-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.done-row-info { flex: 1; min-width: 0; }
.done-row-name {
  font-weight: 600; font-size: .95rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.done-row-sub {
  display: flex; align-items: center; gap: .3rem;
  color: var(--muted); font-size: .78rem; margin-top: .15rem;
}
.done-row-tick { color: var(--done); font-weight: 700; }
.done-row-chev { flex: none; color: var(--muted); display: flex; }

/* ---------- lists page: one list, taking the page over ---------- */
.detail-head {
  display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem;
  position: sticky; top: -1rem; z-index: 2;
  /* the same wash as `body`, painted viewport-fixed so it lines up with the
     paper behind it — the strip reads as the page, not a flat panel — while
     staying opaque enough to hide the list rows scrolling up under it */
  background: var(--wash-gradient), var(--bg);
  background-attachment: fixed;
  padding: 1rem 0 .5rem; margin-top: -1rem;
}
.back-btn {
  border: 1px solid var(--border); background: var(--surface); color: var(--fg);
  border-radius: 999px; width: 2.2rem; height: 2.2rem; flex: none;
  font-size: 1.1rem; line-height: 1; cursor: pointer;
}
.back-btn:active { background: var(--wash-deep); }
.dhead-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .3rem; }
.dtitle { display: flex; align-items: baseline; gap: .5rem; }
.detail-head .dname {
  font-weight: 600; font-size: 1.05rem; flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.detail-head .count { color: var(--muted); font-size: .78rem; white-space: nowrap; }
.detail-chips { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: .6rem; }
/* room for the floating "+" so the share footer / last tile never hides under it */
#list-detail, #lists-container { padding-bottom: 4rem; }
.detail-body, .done-box {
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: 14px; overflow: hidden;
}
/* pending and completed are two separate cards; the paper shows between them */
.done-box { margin-top: .7rem; }
.detail-body .list-items:empty { padding: 0; }
.detail-body .list-items > .skel-row { margin: .4rem .7rem; }
.list-empty { color: var(--muted); font-size: .88rem; padding: 1.1rem 1rem .3rem; }

/* completed work: its own box, always open, with a plain label — not a fold */
.done-head { color: var(--muted); font-size: .82rem; padding: .7rem 1rem; }
.done-items { border-top: 1px solid var(--hairline); }
.badge {
  font-size: .75rem; padding: .12rem .5rem; border-radius: 999px;
}
.badge.high { background: var(--danger-tint); color: var(--danger); }
.badge.low { background: var(--wash); color: var(--muted); }
.badge.shared { background: var(--accent-wash); color: var(--accent); }
.badge.ok { background: var(--ok-tint); color: var(--ok); }
/* a due date on a tile reads as a plan, not a warning — the accent, not grey;
   scoped to the grid so the open list's header keeps its neutral chip. overdue
   still overrides to red below. */
.list-tile .due { background: var(--accent-wash); color: var(--accent); }
.list-tile .due.overdue { background: var(--danger-tint); color: var(--danger); }

/* pending shares — the lower part of the tiles page: lists a friend shared
   that you haven't accepted yet. Dashed and dimmed so they read as not-yet-
   yours, sitting apart from the grid of real lists above. */
.pending-shares { margin-top: 1.4rem; display: flex; flex-direction: column; gap: .5rem; }
.pending-head {
  color: var(--muted); font-size: .82rem; padding: 0 .2rem .1rem;
}
.pending-share {
  display: flex; align-items: center; gap: .6rem;
  background: var(--accent-wash);
  border: 1px dashed var(--accent); border-radius: 14px;
  padding: .7rem .85rem;
}
.pending-info { flex: 1; min-width: 0; }
.pending-name {
  font-weight: 600; font-size: .95rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pending-by { color: var(--muted); font-size: .78rem; margin-top: .1rem; }
.pending-actions { display: flex; gap: .4rem; flex-shrink: 0; }
.pending-actions button {
  border-radius: 999px; font-size: .8rem; padding: .35rem .8rem;
  cursor: pointer; border: 1px solid transparent;
}
/* .pending-accept / .pending-decline carry no colour of their own any more:
   they answer yes or no, so they wear .answer-yes / .answer-no like every
   other place the app asks. */

/* share controls — a footer under the list's card, not part of it */
.share-section {
  margin-top: .9rem; padding: 0 .2rem; font-size: .82rem;
  color: var(--muted); display: flex; flex-wrap: wrap; gap: .45rem; align-items: center;
}
.share-section .share-label { width: 100%; margin-bottom: -.1rem; }
/* leaving a list shared WITH you — quiet until armed, like remove-a-friend */
.leave-list {
  margin-left: auto; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--muted); cursor: pointer;
  font-size: .78rem; padding: .25rem .7rem;
}
.leave-list.armed { border-color: var(--danger); color: var(--danger); }
.share-chip {
  background: var(--accent-wash); color: var(--accent); border-radius: 999px;
  padding: .3rem .35rem .3rem .65rem; font-size: .8rem;
  display: inline-flex; align-items: center; gap: .1rem;
}
.share-chip button {
  border: 0; background: none; color: var(--accent); cursor: pointer;
  font-size: .85rem; padding: .1rem .35rem; position: relative;
}
.share-chip button::after { content: ""; position: absolute; inset: -8px; }
/* The padding above is short on the right because the ✕ pads itself back out.
   A chip with no ✕ — the "+ connect" one — has nothing to take it back and
   sits visibly off-centre in its own capsule, so it pads evenly. */
.share-chip.bare { padding-right: .65rem; }
/* An invitation nobody has answered yet: they can't see the list, so the
   chip must not read like the ones who can. Dashed and muted, the way the
   pending-share section a sharee sees is (`.pending-share`), and the ✕ on it
   cancels the invitation rather than taking access away. */
.share-chip.pending {
  background: none; color: var(--muted);
  border: 1px dashed var(--border);
}
.share-chip.pending button { color: var(--muted); }
select {
  border: 1px solid var(--border); border-radius: 999px; background: var(--surface);
  padding: .4rem .7rem; color: var(--fg);
  font-size: 16px;   /* <16px makes iOS auto-zoom on focus (zoom is enabled now) */
}
.share-hint { font-size: .8rem; }

/* ---------- social page ---------- */
.social-section { margin-bottom: 1.4rem; }
/* the heading, and whatever control the section carries on the right of it */
.social-head {
  display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem;
}
.social-section h2 {
  font-size: .85rem; color: var(--muted); font-weight: 600; margin: 0;
}
.section-edit {
  position: relative; margin-left: auto;
  border: 1px solid var(--border); background: var(--surface); color: var(--muted);
  border-radius: 999px; padding: .25rem .75rem; font-size: .78rem;
  line-height: 1.4; cursor: pointer;
}
/* same compromise as .bunny-remove: a small chip beside a small heading,
   with the tap target padded out to a thumb's worth around it */
.section-edit::after { content: ""; position: absolute; inset: -9px; }
.section-edit[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }
.add-friend { display: flex; gap: .5rem; }
/* `display: flex` outranks the UA sheet's [hidden], and the field is
   hidden until "add bunnyfriend" asks for it */
.add-friend[hidden] { display: none; }
.add-friend input {
  flex: 1; border: 1px solid var(--border); border-radius: 999px;
  padding: .6rem .9rem; font-size: 16px; background: var(--surface);
  color: var(--fg); outline: none;
}
.add-friend input:focus { border-color: var(--accent); }
.add-friend button {
  border: 0; background: var(--user); color: #fff; border-radius: 999px;
  padding: 0 1.1rem; cursor: pointer; min-height: 2.75rem;
}
.add-friend button:active { filter: brightness(.85); }
.social-msg { font-size: .8rem; color: var(--muted); margin-top: .4rem; min-height: 1em; }
/* live search matches, between the field and the message line */
.friend-results { margin-top: .5rem; }
.friend-results:empty { margin-top: 0; }
.person-row .person-note { color: var(--muted); font-size: .8rem; }

/* your own friend code: a quiet line, always in reach, so a friend can add
   you without an email. The code itself is the copy button. */
.friend-code {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin-top: .9rem; font-size: .85rem;
}
.friend-code[hidden] { display: none; }
.friend-code .code-label { color: var(--muted); }
.friend-code .code-value {
  font-family: ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: .08em;
  border: 1px solid var(--border); background: var(--surface); color: var(--fg);
  border-radius: 999px; padding: .35rem .7rem; cursor: pointer; position: relative;
}
.friend-code .code-value:active { background: var(--wash-deep); }
.friend-code .code-rotate {
  border: 0; background: none; color: var(--muted); cursor: pointer;
  font-size: .8rem; padding: .35rem .4rem; position: relative;
}
.friend-code .code-rotate::after { content: ""; position: absolute; inset: -6px; }
.person-row {
  display: flex; align-items: center; gap: .5rem;
  background: var(--surface); border: 1px solid var(--border-soft); border-radius: 12px;
  padding: .55rem .8rem; margin-bottom: .45rem; font-size: .9rem;
}
.person-row .who { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.person-row button {
  border: 1px solid var(--border); background: var(--surface); border-radius: 999px;
  padding: .45rem .8rem; font-size: .8rem; cursor: pointer; color: var(--fg);
  position: relative;
}
.person-row button::after { content: ""; position: absolute; inset: -6px; }
.person-row button:active { background: var(--wash-deep); }
/* .ok is still the accent outline for a row's affirmative that ISN'T an
   answer to a request (adding someone). Accepting a request wears
   .answer-yes. */
.person-row button.ok { border-color: var(--accent); color: var(--accent); }
.person-row button.danger { color: var(--danger); }

/* the bunnyfriends themselves: a grid of waving bunnies, each in the outfit
   its owner wears, name underneath. The bunny is the row here — the name is
   the caption — so this is a gallery rather than the .person-row list the
   pending requests still use. */
.bunny-grid {
  /* three across on a phone, which is the size the art reads at */
  display: grid; grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
  gap: .5rem .25rem;
}
.bunny-tile {
  position: relative; display: flex; flex-direction: column; align-items: center;
  gap: .1rem; padding: .3rem .15rem;
}
.bunny-portrait {
  /* his frame again, so a friend in any outfit is the same bunny as a
     friend in any other. The name goes under the whole frame rather than
     under his feet: the footing inside it isn't empty for everyone —
     fireghost trails a flame down through it — and a caption written across
     that reads as a collision. */
  height: 111px; width: 100%; object-fit: contain; object-position: center bottom;
  user-select: none; -webkit-user-select: none;
}
.bunny-name {
  font-size: .85rem; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* pinned to the tile's top-right so the armed "sure?" grows leftward over
   the bunny instead of resizing the grid cell */
.bunny-remove {
  position: absolute; top: 0; right: 0; white-space: nowrap;
  border: 1px solid var(--border); background: var(--surface); border-radius: 999px;
  min-width: 1.5rem; padding: .1rem .35rem; line-height: 1.3;
  font-size: .78rem; color: var(--muted); cursor: pointer;
  z-index: 2;   /* above the portrait (which sits above its spotlight ring) */
}
/* off until "edit" asks for it — display, not opacity, so a hidden × is out
   of the tab order too and can't be reached by a key */
.bunny-grid:not(.editing) .bunny-remove { display: none; }
.bunny-remove::after { content: ""; position: absolute; inset: -8px; }
.bunny-remove.armed { color: var(--danger); border-color: var(--danger); }
/* the bunny is the way in to what you two share — a real button, so it is
   reachable by keyboard, but drawn as nothing at all. In edit mode it is
   disabled (views.js): the grid is about removing then, not visiting. */
.bunny-open {
  display: flex; flex-direction: column; align-items: center; gap: .1rem;
  width: 100%; border: 0; background: none; padding: 0; cursor: pointer;
  color: inherit; font: inherit;
}
.bunny-open:disabled { cursor: default; }   /* still fully drawn, just inert */
.bunny-open:not(:disabled):active .bunny-portrait { transform: scale(.96); }
.bunny-open .bunny-portrait { transition: transform .12s; }
.bunny-open:focus-visible { outline: 2px solid var(--accent); border-radius: 12px; }

/* closeness spotlight: a soft ringed disc behind the bunny — none for an
   acquaintance, an accent ring for a friend, a bolder gold one for a bestie.
   A full-body bunny can't wear an avatar ring, so it stands in the ring. */
.bunny-portrait-wrap {
  position: relative; width: 100%; display: flex; justify-content: center;
}
.bunny-portrait-wrap .bunny-portrait { position: relative; z-index: 1; }
.bunny-portrait-wrap::before {
  content: ""; position: absolute; left: 50%; bottom: 3px;
  transform: translateX(-50%); width: 58px; height: 58px; border-radius: 50%;
  z-index: 0; opacity: 0;   /* acquaintance: invisible */
}
.bunny-tile[data-level="friend"] .bunny-portrait-wrap::before {
  opacity: 1; background: var(--accent-wash);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent);
}
.bunny-tile[data-level="bestie"] .bunny-portrait-wrap::before {
  opacity: 1; width: 62px; height: 62px; background: var(--bestie-wash);
  box-shadow: 0 0 0 2.5px var(--bestie);
}

/* the level picker: a small centered sheet, three choices, current one ticked */
.level-picker {
  position: fixed; inset: 0; z-index: 60; display: flex;
  align-items: center; justify-content: center; padding: 1rem;
  background: rgba(0, 0, 0, .35);
}
.level-picker-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 16px; padding: .6rem; width: min(20rem, 100%);
  display: flex; flex-direction: column; gap: .3rem;
  box-shadow: 0 12px 40px rgba(0, 0, 0, .25);
}
.level-picker-title {
  font-weight: 600; padding: .2rem .5rem .3rem; color: var(--muted);
}
.level-opt {
  display: flex; flex-direction: column; align-items: flex-start; gap: .05rem;
  text-align: left; border: 1px solid var(--border); background: var(--surface);
  border-radius: 12px; padding: .5rem .7rem; cursor: pointer; color: inherit;
  font: inherit;
}
.level-opt:hover, .level-opt:focus-visible {
  border-color: var(--accent); outline: none;
}
.level-opt.current {
  border-color: var(--accent); background: var(--accent-wash);
}
.level-opt.current .level-opt-name::after { content: " ✓"; color: var(--accent); }
.level-opt-name { font-weight: 600; }
.level-opt-blurb { font-size: .8rem; color: var(--muted); }

/* ---------- shop (maison usagi) ---------- */
/* A storefront, not a settings row: a name over the page, the categories
   the place will hold, then the rack. */
.shop-head {
  display: flex; align-items: center; gap: .7rem;
  padding: .3rem 0 1rem;
}
.shop-mark { width: 2.9rem; height: 2.9rem; flex: none; color: var(--fg); }
.shop-id { min-width: 0; }
/* Lowercase like everything else. It was set in caps as a wordmark, which is
   a real exemption the sign-in screen still uses — but the app is lowercase
   everywhere, and one shouted line at the top of one page read as a mistake
   rather than as branding. */
.shop-name {
  letter-spacing: .04em;
  font-family: var(--font-hand); font-size: 1.45rem; font-weight: 700;
  margin: 0; line-height: 1.1; color: var(--fg);
}
.shop-tag { font-size: .82rem; color: var(--muted); margin-top: .15rem; }

/* the categories. Only one is real; the others say so rather than looking
   live and doing nothing. */
.shop-tabs {
  display: flex; gap: .3rem; margin-bottom: .6rem;
  border-bottom: 1px solid var(--border-soft); padding-bottom: .5rem;
}
.shop-tab {
  display: flex; flex-direction: column; align-items: center; gap: .1rem;
  flex: 1; min-height: 3rem; padding: .35rem .3rem;
  border: 0; border-radius: 14px; background: none; cursor: pointer;
  color: var(--muted); font-family: var(--font-hand); font-size: .9rem;
}
/* Masked rather than <img>, so the mark takes the tab's own colour — the
   accent on the live one, muted on the two that aren't. An <img> would be
   its own document and would always draw itself black. */
.shop-tab-glyph {
  width: 1.25rem; height: 1.25rem; background: currentColor;
  -webkit-mask: var(--shop-icon) center / contain no-repeat;
  mask: var(--shop-icon) center / contain no-repeat;
}
.shop-tab.active { background: var(--accent-wash); color: var(--accent); font-weight: 700; }
.shop-tab:disabled { cursor: default; opacity: .75; }

/* the rack: three across on a phone, which is the size the art reads at */
.shop-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(6.4rem, 1fr));
  gap: .55rem;
}
/* a card is one button — the whole thing is the tap target, and what the tap
   DOES is what the price line says */
.shop-card {
  display: flex; flex-direction: column; align-items: center; gap: .15rem;
  padding: .5rem .3rem .6rem;
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: 16px; cursor: pointer; color: inherit;
  font: inherit; text-align: center;
}
.shop-card .bunny-portrait {
  height: 123px; transition: transform .12s;
}
/* mid-cheer the src swaps to the cheer art, which lives on a wider frame than
   the standing pose (.bunny-cheer). Bind on height so he stays the same-size
   bunny rather than the same-size box (scripts/normalize_bunny_size.py); the
   little that spills of the transparent frame sits in the grid gap. */
.shop-card .bunny-portrait.cheering { width: auto; max-width: none; }
.shop-card:not(:disabled):active .bunny-portrait { transform: scale(.96); }
.shop-card:focus-visible { outline: 2px solid var(--accent); }
.shop-card:disabled { cursor: default; }
.shop-card-name {
  font-size: .84rem; font-weight: 600; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* the card's state in one line: what you're in, what you could put on, or
   what it costs to */
.shop-tag-line { font-size: .8rem; color: var(--muted); }
.shop-tag-line.priced { color: var(--accent); font-weight: 700; }
/* the coin, drawn only where there's a number to sit beside */
/* The coin is drawn, not an emoji: U+1FA99 is recent enough that a phone or
   a test browser with an older emoji font falls back to a grey sphere, and a
   price is the one thing on the card that has to be legible. */
.shop-tag-line.priced::before {
  content: ""; display: inline-block; vertical-align: -.05em;
  width: .72em; height: .72em; margin-right: .3rem;
  border-radius: 50%; background: #e8b93b; box-shadow: inset 0 0 0 1px #c9962a;
}
.shop-card.wearing { border-color: var(--accent); background: var(--accent-wash); }
.shop-card.wearing .shop-tag-line { color: var(--accent); font-weight: 700; }

/* the blind box, at the foot of the rack: one wide card rather than a tenth
   in the grid, since the one thing it can't do is show you the outfit. The
   dashed edge is the tell — everything else on this page is a thing you can
   see. */
/* Glass with a colour of its own: the accent wash it wore was the one opaque
   slab on the page, and a box you can't see into reads better as something
   the paper shows through. The dashed edge is still the tell. */
.shop-blind {
  display: flex; align-items: center; gap: .75rem; width: 100%;
  margin-top: .55rem; padding: .55rem .8rem;
  border: 1px dashed var(--default-edge); border-radius: 18px;
  background: color-mix(in srgb, var(--accent-wash) 50%, transparent);
  color: inherit;
  font: inherit; text-align: left; cursor: pointer;
}
.shop-blind:focus-visible { outline: 2px solid var(--accent); }
.shop-blind:active { background: var(--wash-deep); }
/* one drawing at two sizes — the card's and the note's — off one knob, so
   the mark on his face scales with him instead of drifting off it.
   `--blind-art` is the shared FRAME's height, like every other bunny box. */
.shop-blind-art {
  --blind-art: var(--bunny-h);
  display: inline-block; position: relative; flex: none;
}
.shop-blind-art img { display: block; height: var(--blind-art); }
/* His frame is not him: 67 of its 368 rows are empty over his ears and his
   soles sit 70 up from its floor (:root's --bunny-head / --bunny-sole, in
   this box's own height). The row is laid out around HIM, so the margins
   take that empty back — otherwise the banner is as tall as fireghost's
   flame. A shade under the rack's bunnies, which keeps his line of copy on
   one row. */
.shop-blind .shop-blind-art {
  --blind-art: calc(var(--bunny-h) * .88);
  margin-top: calc(var(--blind-art) * -67 / 368);
  margin-bottom: calc(var(--blind-art) * -70 / 368);
}
/* the ? sits where his face is: 41% down the frame, which is where the head
   lands in every bunny's art. Its ink is fixed rather than a token: what it
   is drawn on is the silhouette, which is black in either theme, so a
   surface colour that follows the theme would put it out in the dark. */
.shop-blind-mark {
  position: absolute; left: 50%; top: 41%; transform: translate(-50%, -50%);
  color: #faf9f7; font-weight: 700;
  font-size: calc(var(--blind-art) * .19); line-height: 1;
}
.shop-blind-id { min-width: 0; flex: 1; }
.shop-blind-badge {
  display: inline-block; font-size: .62rem; letter-spacing: .04em;
  color: var(--accent); background: var(--surface);
  border-radius: 999px; padding: .05rem .45rem;
}
.shop-blind-name {
  display: block; margin-top: .15rem; font-size: .95rem; font-weight: 700;
}
/* one row, always: it is an aside, and an aside that wraps onto a second
   line pushes the price out of the card. A narrow phone shrinks the type
   rather than breaking the sentence (iOS `minimumScaleFactor` does the same). */
.shop-blind-tag {
  display: block; font-size: min(.78rem, 3.3vw); color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.shop-blind .shop-tag-line { display: block; margin-top: .2rem; }
/* masked like the category marks, so the parcel tints with the page rather
   than drawing itself black through an <img> */
.shop-blind-gift {
  flex: none; width: 2.8rem; height: 2.8rem;
  color: color-mix(in srgb, var(--accent) 38%, transparent);
  background: currentColor;
  -webkit-mask: url("/static/icons/blind-box.svg") center / contain no-repeat;
  mask: url("/static/icons/blind-box.svg") center / contain no-repeat;
}

/* the unlock note. Not a checkout — nothing here takes money — so it says
   who to pay and gets out of the way. */
.shop-note { margin-top: 1rem; }

.shop-lock {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center; padding: .9rem;
  background: rgba(0,0,0,.35);
}
/* A pane of glass, like every other thing that floats over a screen here
   (#menu, #ask, #cal-panel): frosted, so the rack it came off is still behind
   it rather than painted out. Keeps its own modal drop — this one floats
   clear of everything — with the lit top edge inside it. */
.shop-lock-panel {
  width: min(20rem, 100%); padding: 1.1rem 1rem 1rem;
  background: var(--glass); border: 1px solid var(--glass-line);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border-radius: 18px; text-align: center;
  box-shadow: inset 0 1px 0 var(--glass-edge), 0 12px 40px rgba(0,0,0,.22);
}
.shop-lock-art {
  height: 140px; width: 100%; object-fit: contain;
}
/* the blind box's art is the bunny with a mark on him rather than a bare
   image, so the note sizes the drawing inside it — to the same frame height
   as an outfit's portrait; the panel's own text-align centres it */
.shop-lock .shop-blind-art { --blind-art: 140px; }
.shop-lock-name { margin: .3rem 0 0; font-size: 1.05rem; font-weight: 700; }
.shop-lock-note { margin: .45rem 0 0; font-size: .88rem; color: var(--muted); line-height: 1.45; }
/* the box's odds, said before the pull: quieter than the blurb above it,
   because it is the small print and reads as such */
.shop-lock-odds {
  margin: .35rem 0 0; font-size: .78rem; color: var(--muted);
  line-height: 1.4;
}
.shop-lock-price {
  margin: .5rem 0 0; font-size: .95rem; font-weight: 700; color: var(--accent);
}
.shop-lock-price::before {
  content: ""; display: inline-block; vertical-align: -.05em;
  width: .72em; height: .72em; margin-right: .3rem;
  border-radius: 50%; background: #e8b93b; box-shadow: inset 0 0 0 1px #c9962a;
}
/* the note's one button, drawn as quietly as the rest of the chrome. One
   word now, so it sits at the app's ordinary control size again — the
   shrink-to-fit sizing was there to keep a whole sentence on one row. */
/* The venmo line. It is a whole sentence on one row (`nowrap`) inside a
   panel the width of a phone, and the longest of them — the blind box's — is
   longer than any outfit's, so the type is set small enough that it still
   fits rather than being cut. iOS answers the same question with
   `minimumScaleFactor`. */
.shop-lock-close {
  margin-top: .9rem; min-height: 44px; width: 100%;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--fg); cursor: pointer;
  font-size: .78rem; padding: .55rem .7rem; line-height: 1.3;
  white-space: nowrap;
}
.shop-lock-close:active { background: var(--wash-deep); }
/* The passcode row, unfolded by the ask above it (`askPasscode`). The field
   and its button share one line — the code is short and the word is one —
   and the answer sits under them, on the row rather than in a toast, because
   it is about the thing you just typed. */
.shop-code {
  margin-top: .55rem; display: flex; flex-wrap: wrap; gap: .4rem;
  align-items: center;
}
.shop-code-input {
  flex: 1; min-width: 0; min-height: 44px;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--fg);
  padding: .55rem .85rem; font-size: .9rem;
}
.shop-code-input:focus-visible { outline: 2px solid var(--accent); }
.shop-code-go {
  min-height: 44px; padding: .55rem 1.05rem;
  border: 1px solid var(--accent); border-radius: 999px;
  background: var(--accent-wash); color: var(--accent);
  font-size: .9rem; font-weight: 700; cursor: pointer;
}
.shop-code-go:active { background: var(--wash-deep); }
/* while the code is with the server: still readable, plainly not idle */
.shop-code.working { opacity: .6; pointer-events: none; }
.shop-code-said {
  flex-basis: 100%; margin: 0; min-height: 1.1em;
  font-size: .78rem; color: var(--danger);
}


/* ---------- a bunnyfriend's message: the got-it overlay ---------- */
/* Body-level and topmost (above the toast/shop-lock tier at 60), a dimming
   scrim that captures taps — it rides over whatever page you're on and stays
   until "got it". */
.fm-overlay {
  position: fixed; inset: 0; z-index: 70;
  display: flex; align-items: center; justify-content: center; padding: .9rem;
  background: rgba(0,0,0,.45);
}
.fm-card {
  width: min(21rem, 100%); padding: 1.2rem 1.1rem 1.1rem;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 20px; text-align: center;
  box-shadow: 0 16px 48px rgba(0,0,0,.28);
}
.fm-bunny { height: 108px; width: 100%; object-fit: contain; }
.fm-from { margin: .35rem 0 0; font-size: .85rem; color: var(--muted); }
.fm-text {
  margin: .5rem 0 0; font-size: 1.08rem; font-weight: 600; line-height: 1.4;
  overflow-wrap: anywhere;
}
.fm-got {
  margin-top: 1.1rem; min-height: 46px; width: 100%;
  border: 0; border-radius: 999px; cursor: pointer;
  background: var(--accent); color: #fff; font-weight: 700; font-size: .98rem;
}
.fm-got:active { filter: brightness(.94); }
.fm-got:disabled { opacity: .6; cursor: default; }


/* ---------- one bunnyfriend: what you have in common ---------- */
/* The header is `.detail-head`, so it carries the page's wash rather than a
   flat strip, and holds nothing but the back arrow and the two names. */
/* Under it, the two of you: your bunny and theirs, palm to palm.
   It is composed here rather than drawn as one picture, because one picture
   per PAIR of outfits is a square of files that grows every time the shop
   gains one. Instead each outfit has a single `highfive` pose facing right,
   and the left bunny is that same art mirrored — so a new outfit costs one
   drawing, not one against every outfit already in the shop.
   The overlap is the whole trick, and it is ONE number for every pairing:
   every pose sits on the same 296x368 frame with his head on the same column
   and his soles on the same line (scripts/normalize_bunny_size.py), so a paw
   raised at a given inset is at the same place whatever he is wearing. Pull
   the frames together by twice that inset and the two paws meet — for any two
   outfits, with no per-pair number anywhere.
   34 is where they meet in the art that is here now, which is each outfit's
   wave standing in until its own high-five drawing lands: that pose raises
   the paw beside the head rather than reaching, so the frames close less far
   than a drawn-out arm will want. Once the poses are authored to the script's
   PALM_X this becomes 2 * PALM_X, and `--check`'s palm line is what says so.
   iOS holds the same number in BunnyHighFive. */
.highfive {
  /* small, because they ride on the end of the header line rather than in a
     band of their own — big enough to read as two bunnies, short enough that
     the bar stays a bar */
  --hf-h: 74px;
  --hf-w: calc(var(--hf-h) * 296 / 368);
  --hf-overlap: calc(var(--hf-h) * 90 / 368);
  /* Most of a frame's width is transparent — the bunny only occupies its
     middle — so the mirrored one on the left carries a band of nothing where
     it meets the name, and butting the boxes up leaves a gap that isn't
     there. This takes that band back, in the same canvas units as everything
     else, so it holds if --hf-h changes. */
  margin-left: calc(var(--hf-h) * -70 / 368);
  display: flex; align-items: flex-end;
  flex: none;   /* the name takes the slack and ellipsises; these don't shrink */
}
/* they sit against the name rather than against the far edge, so the pair
   reads as belonging to it. `.dhead-main` stops taking the slack for that —
   scoped to this page, since the open-list header wants the opposite. */
#friend-detail .dhead-main { flex: 0 1 auto; }
/* The bunnies make this header taller than a bare title, but they stand well
   clear of its bottom edge — his soles are 70/368 up his frame — so the
   default gap under it reads as a hole. Close it up and what you share sits
   where it did before the pair arrived. */
#friend-detail .detail-head { padding-bottom: .15rem; margin-bottom: .15rem; }
.highfive-bunny {
  height: var(--hf-h); width: var(--hf-w);
  object-fit: contain; object-position: center bottom;
  user-select: none; -webkit-user-select: none;
}
.highfive-bunny + .highfive-bunny { margin-left: calc(-1 * var(--hf-overlap)); }
/* yours is on the left, so it is the one turned to face the other way. No
   outfit carries lettering, so nothing here reads as backwards. */
.highfive-mine { transform: scaleX(-1); }
/* a button, since every row leads somewhere — drawn as the card it was */
.shared-row {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: 12px; padding: .55rem .8rem; margin-bottom: .45rem;
  font-size: .9rem; text-align: left; color: inherit; font-family: inherit;
  cursor: pointer;
}
.shared-row:active { background: var(--wash); }
/* a done todo / a plan that's over — struck through and dimmed, still tappable */
.shared-row.done .shared-title { text-decoration: line-through; color: var(--muted); }
.shared-row.done { opacity: .72; }
.shared-row.message-row { cursor: default; }
.shared-row.message-row:active { background: var(--surface); }
.shared-row.message-row .shared-title { white-space: normal; word-break: break-word; }
.shared-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.shared-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.shared-meta { color: var(--muted); font-size: .78rem; }
.shared-more { color: var(--muted); font-size: .78rem; margin: -.2rem 0 .45rem; }
/* the same note, when it is the way to the todos it is counting */
button.shared-more {
  border: 0; background: none; padding: 0; font-family: inherit;
  text-align: left; cursor: pointer; text-decoration: underline;
  text-underline-offset: 2px; display: block;
}
button.shared-more:disabled { opacity: .5; cursor: default; }

/* share a list with them, from their own page — unfolded on being asked,
   the way the bunnyfriends page unfolds its email field */
.share-list { margin-top: .1rem; }
.share-list-btn { font-size: .82rem; padding: .4rem .9rem; min-height: 2.2rem; }
.share-list-picker { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .5rem; }
.share-list-picker[hidden] { display: none; }
/* a note inside the picker is a line, not a whole empty screen */
.share-list-picker .empty-note {
  margin: 0; padding: .2rem 0; font-size: .8rem; text-align: left;
}
.share-pick {
  border: 1px solid var(--border); background: var(--surface); color: var(--fg);
  border-radius: 999px; padding: .35rem .8rem; font-size: .82rem;
  font-family: inherit; cursor: pointer; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.share-pick:active { background: var(--wash-deep); }

/* the answer to a question the row raises — an invitation of theirs, a list
   or a plan. Under the row rather than in it: the row is itself a button. */
.shared-answer {
  display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
  margin: -.25rem 0 .55rem; padding: 0 .2rem;
}
.shared-answer .answer-ask {
  flex: 1; min-width: 0; color: var(--muted); font-size: .76rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.shared-answer button {
  border-radius: 999px; padding: .3rem .7rem; font-size: .78rem;
  cursor: pointer; font-family: inherit;
}
.shared-answer button:disabled { opacity: .5; cursor: default; }

/* the line above the sections: the next plan together and what's late
   between you — both otherwise below the fold */
.shared-lead {
  display: flex; align-items: center; gap: .45rem; flex-wrap: wrap;
  font-size: .82rem; color: var(--muted); margin: 0 0 .9rem;
}
.shared-lead .lead-next { min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }

/* say something to them, from their own page. Sits at the top of the
   messages section, since the thread reads newest first. */
.fm-compose { display: flex; gap: .5rem; margin-bottom: .5rem; }
.fm-input {
  flex: 1; min-width: 0; border: 1px solid var(--border); border-radius: 999px;
  padding: .55rem .85rem; font-size: 16px; background: var(--surface);
  color: var(--fg); outline: none;
}
.fm-input:focus { border-color: var(--accent); }
.fm-input:disabled { opacity: .6; }
.fm-send {
  border: 0; background: var(--user); color: #fff; border-radius: 999px;
  padding: 0 1rem; cursor: pointer; min-height: 2.5rem; flex: none;
}
.fm-send:active { filter: brightness(.85); }
.fm-send:disabled { opacity: .6; cursor: default; }

/* how close they are, under the line that names them — and what it lets them
   see of your calendar, which is the thing a closeness level actually does.
   A tap opens the same picker the grid's edit mode opens.

   On a row of its own: beside the bunnies it had the header's width less the
   back button and less their 101px, and "acquaintance · sees none of your
   calendar" needs 251px — so the DEFAULT level read "sees none of your …" on
   every phone narrower than a Pro Max. The row is the full header wide (288px
   at 320, the narrowest we draw for). Indented to clear the back button, so
   it lines up under the name — the same 44pt iOS uses. */
.level-row { display: flex; min-width: 0; margin: 0 0 .5rem 44px; }
.level-chip {
  display: inline-flex; align-items: baseline; gap: .35rem; align-self: flex-start;
  border: 1px solid var(--border-soft); background: var(--surface); color: inherit;
  border-radius: 999px; padding: .2rem .6rem; font: inherit; font-size: .74rem;
  cursor: pointer; max-width: 100%;
}
.level-chip:active { background: var(--wash); }
.level-chip-blurb { color: var(--muted); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.level-chip[data-level="bestie"] { border-color: var(--bestie); }
.level-chip[data-level="friend"] { border-color: var(--accent); }

.add-bunnyfriend {
  border: 1px solid var(--border); background: var(--surface); color: var(--fg);
  border-radius: 999px; padding: .6rem 1.1rem; font-size: .9rem;
  min-height: 2.75rem; cursor: pointer;
}
.add-bunnyfriend:active { background: var(--wash-deep); }
/* How tall a todo is, in one place — the screen's height divided by this is
   how much of a list you can see at once, so it is worth being deliberate
   about. It ran at 48px and a phone held fourteen todos; 32 holds twenty, and
   there was never anything in the sixteen it gave back — no ink, no tap
   target, just a shorter list.

   32 is near the floor and deliberately not at it: the todo's own line box is
   17.4px and its padding another 9.6, so ~27 is as short as this row can be
   without taking room from the words themselves, and each pixel below here
   buys a fraction of a row while costing a checkbox that much accuracy. The
   checkbox keeps its full 44 ACROSS (see .check) — width is the direction
   where room costs the list nothing.

   iOS holds the same number as ItemRowView.rowHeight. */
.list-items {
  padding: .15rem 0;
  --todo-row: 2rem;
  --todo-dot: min(1.55rem, calc(var(--todo-row) - .5rem));
}
.list-items .item {
  display: flex; align-items: center; gap: .5rem;
  padding: 0 .8rem 0 .5rem; min-height: var(--todo-row); font-size: .93rem;
  border-left: 3px solid transparent;
}
/* the tap target that does the one thing this screen is for. It spends its
   44px WIDE and takes the row's height — a taller one would be a taller row,
   and the row is the whole list's density. The circle it draws is centred
   rather than inset, so the row's height can change without the tick sliding
   off it. */
.check {
  flex: none; width: 2.75rem; height: var(--todo-row); padding: 0; border: 0;
  background: none; cursor: pointer; position: relative;
}
.check::before {
  /* the circle follows the row down: as tall as the row less a hair of room,
     up to the 24.8px it drew back when the row was 44 and could spare it */
  content: ""; position: absolute; left: 50%; top: 50%;
  width: var(--todo-dot); height: var(--todo-dot);
  margin: calc(var(--todo-dot) / -2) 0 0 calc(var(--todo-dot) / -2);
  border: 1.8px solid var(--border); border-radius: 50%;
  transition: background .12s ease, border-color .12s ease;
}
.check::after {   /* the tick, drawn as two strokes of a rotated corner */
  content: ""; position: absolute; left: 50%; top: 50%;
  margin: -.36rem 0 0 -.31rem;
  width: .34rem; height: .62rem; opacity: 0;
  border: solid var(--surface); border-width: 0 2px 2px 0;
  transform: rotate(42deg);
}
.check[aria-checked="true"]::before { background: var(--done); border-color: var(--done); }
.check[aria-checked="true"]::after { opacity: 1; }
.check:active::before { border-color: var(--done); }
.check:focus-visible { outline: 2px solid var(--accent); outline-offset: -6px; border-radius: 50%; }
/* the padding only ever binds when the todo wraps to two lines — a one-line
   row is --todo-row and nothing else decides it */
.list-items .item .itext { flex: 1; min-width: 0; overflow-wrap: anywhere; padding: .3rem 0; }
.imeta { display: flex; align-items: center; gap: .3rem; flex: none; }
.list-items .item.done .itext { color: var(--muted); text-decoration: line-through; }
/* explicitly assigned to me on a shared list — a carrot rail, not a wash:
   the row is still a row, it just has your name on it */
.list-items .item.mine { border-left-color: var(--carrot); }
.list-items .item.mine .itext { font-weight: 600; }
/* high priority earns the same treatment on the other side of the palette */
.item-wrap[data-priority="high"] .item:not(.done) { border-left-color: var(--danger); }
.due {
  font-size: .78rem; color: var(--muted); background: var(--wash);
  border-radius: 999px; padding: .12rem .5rem; white-space: nowrap;
}
.due.overdue { background: var(--danger-tint); color: var(--danger); }
.owner-chip {
  font-size: .78rem; color: var(--accent); background: var(--accent-wash);
  border-radius: 999px; padding: .12rem .5rem; white-space: nowrap;
}
.owner-chip.me { color: var(--carrot-deep); background: var(--carrot-wash); }

/* ---------- todo gestures: the left-swipe action tray ----------
   The tray sits under the row's right edge and never moves; the row slides
   left off it and latches there until it is put back. One direction only —
   there is no swipe-right action to fire by accident, so nothing on this
   screen commits from a flick. */
/* the transition is the row's half of the same slide the tiles get: the wrap
   carries it, because the wrap is what changes place in the list, while the
   `.item` inside keeps its own transform for the swipe tray */
.item-wrap {
  position: relative; overflow: hidden; --tray: 8.8rem;
  transition: transform .15s ease;
}
.item-actions {
  position: absolute; top: 0; right: 0; bottom: 0; z-index: 0;
  display: flex; width: var(--tray);
}
.item-actions .act {
  flex: 1; border: 0; cursor: pointer; color: var(--surface);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .3rem; font-size: .72rem; line-height: 1; padding: 0;
}
.item-actions svg { display: block; }
.item-actions .act:active { filter: brightness(.88); }
.item-actions .act-edit { background: var(--muted); }
.item-actions .act-flag { background: var(--carrot-deep); }
.item-actions .act-del { background: var(--danger); }
.item-actions .act:focus-visible { outline: 2px solid var(--fg); outline-offset: -3px; }
.item-wrap .item {
  position: relative; z-index: 1; background: var(--surface);
  touch-action: pan-y pinch-zoom;         /* horizontal = ours, the rest the browser's */
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  transition: transform .18s cubic-bezier(.22,.61,.36,1);
}
.item-wrap.tray-open .item { transform: translateX(calc(-1 * var(--tray))); }
/* only the row's own surface slides; a tinted wrap would leak past the tray */
.item-wrap .item.mine { background: var(--carrot-tint); }
.item-wrap.dragging { overflow: visible; z-index: 5; }
/* a lifted row leaves its slot empty — without this the tray it was covering
   would show through the gap */
.item-wrap.dragging .item-actions { visibility: hidden; }
.item-wrap.dragging .item {
  transition: none; box-shadow: 0 4px 14px rgba(0,0,0,.18);
  border-radius: 10px; background: var(--surface);
}
.inline-edit {
  flex: 1; min-width: 0; font: inherit; font-size: 16px;
  border: 1px solid var(--accent); background: var(--surface); color: var(--fg);
  border-radius: 8px; padding: .15rem .45rem; outline: none;
}
/* add a todo: a "+" pinned to the bottom-left of the open list. It hangs off
   #list-detail, whose containing block is the keyboard-pinned body, so the
   composer rides up with the keyboard instead of hiding under it. Bottom-left
   keeps clear of the corner bunny (bottom-right). */
.todo-add {
  position: absolute; z-index: 6;
  left: calc(1rem + env(safe-area-inset-left));
  bottom: calc(.9rem + env(safe-area-inset-bottom));
  display: flex; align-items: center;
}
/* the flex display above beats the UA [hidden] rule, so hiding the new-list
   "+" (while a single list is open) needs to say so explicitly */
.todo-add[hidden] { display: none; }
/* the fill, the blur and the edge are the glass section's; here just the shape
   and the accent + (white would vanish on frosted glass) */
.todo-add-btn {
  flex: none; width: 3rem; height: 3rem; border-radius: 50%;
  border: 0; color: var(--accent); cursor: pointer;
  font-size: 1.6rem; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
.todo-add-btn:active { filter: brightness(.94); }
/* opened: the round button folds into a pill carrying the text field (the pill
   is the glass now, so the + inside it goes bare — see the glass section) */
.todo-add.open {
  border-radius: 999px; padding-right: .3rem;
  max-width: calc(100vw - 2rem);
}
.todo-add.open .todo-add-btn {
  width: 2.4rem; height: 2.4rem; font-size: 1.3rem;
  margin: .15rem;
}
.todo-add-input {
  min-width: 0; border: 0; background: none; outline: none;
  font-size: 16px; padding: .5rem .35rem; color: var(--fg);
  width: min(13rem, calc(100vw - 9rem));
  /* 16px minimum: anything smaller makes iOS auto-zoom on focus */
}
.todo-add-input::placeholder { color: var(--muted); }
.swipe-hint {
  padding: .55rem .2rem 0; font-size: .78rem; color: var(--muted);
  text-align: center;
}
.empty-note { color: var(--muted); text-align: center; padding: 2.5rem 1rem; font-size: .9rem; }
/* the same note when the screen is blank because a read failed, not because
   there is nothing — it carries the way to ask again, since otherwise the
   only retry is leaving the page and coming back */
.load-failed { display: flex; flex-direction: column; align-items: center; gap: .8rem; }
.retry-btn {
  border: 1px solid var(--border); background: var(--surface); color: var(--fg);
  border-radius: 999px; padding: .4rem 1rem; font-size: .85rem;
  font-family: inherit; cursor: pointer;
}
.retry-btn:active { background: var(--glass); }
.retry-btn:disabled { opacity: .5; }

/* ---------- segmented control (settings) ---------- */
.seg { display: flex; border-radius: 999px; padding: 3px; }
.seg button {
  flex: 1; border: 0; background: none; border-radius: 999px;
  padding: .5rem 0; font-size: .85rem; color: var(--muted); cursor: pointer;
}
.seg button:not(.active):active { background: var(--glass); }
.seg button.active { color: var(--fg); font-weight: 600; }

/* ---------- calendar page ----------
   Shaped like the iOS Calendar: fixed chrome top and bottom, the month grid
   filling everything between them edge to edge. Levels nest instead of
   sitting side by side — year → month → day — so there is no view picker. */
#calendar-view {
  /* let the page own vertical panning; horizontal drags stay ours for the
     prev/next swipe. also kills double-tap zoom on the swipe surface —
     pinch-zoom is named so two fingers still reach the browser's own zoom. */
  touch-action: pan-y pinch-zoom;
  -webkit-user-select: none; user-select: none;
  /* not a scroller itself: the grid region scrolls between pinned chrome */
  display: flex; flex-direction: column; overflow: hidden; padding: 0;
  position: relative;   /* the event sheet pins itself over the whole view */
}
/* explicit display beats the hidden attribute's UA display:none */
#calendar-view[hidden] { display: none; }

#cal-top { display: flex; flex-direction: column; padding-top: .3rem; }
.cal-bar {
  display: flex; align-items: center; gap: .5rem; padding: 0 .7rem .2rem;
  order: 0;
}
/* the two pill shapes iOS uses: a labelled one, and icon clusters */
.cal-pill {
  display: inline-flex; align-items: center; min-height: 2.3rem;
  border: 0; border-radius: 999px; padding: .4rem .95rem .4rem .7rem;
  background: var(--wash); color: var(--fg);
  font-size: .95rem; font-weight: 500; cursor: pointer;
}
.cal-pill[hidden] { display: none; }
.cal-pill:active { background: var(--wash-deep); }
.cal-pill .chev { font-size: 1.3rem; line-height: 1; margin-right: .25rem; }
.cal-group {
  display: flex; align-items: center;
  background: var(--wash); border-radius: 999px; padding: 2px;
}
/* the top-right cluster: prev/next, settings, today — pinned to the right of
   the bar (and to the right of the day view's inline title, which is flex:1) */
.cal-actions { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
/* the month view scrolls; its bar has nothing to page, so no chevrons */
#calendar-view[data-mode="month"] #cal-nav { display: none; }
.cal-icon {
  display: flex; align-items: center; justify-content: center;
  border: 0; background: none; color: var(--fg); cursor: pointer;
  min-width: 2.6rem; height: 2.1rem; border-radius: 999px;
  font-size: 1.25rem; line-height: 1;
}
.cal-icon:active { background: var(--wash-deep); }
.cal-icon.on { background: var(--surface); color: var(--accent); }

#cal-title {
  order: 1; margin: 0; padding: .1rem .8rem .45rem;
  font-size: 2.3rem; font-weight: 700; letter-spacing: -.02em; line-height: 1.1;
  cursor: pointer;
}
/* accent while today is on screen; plain ink once you've wandered off, so
   the color says "you're here now" rather than "tap me" */
#cal-title { color: var(--accent); }
#cal-title.away { color: var(--fg); }

#cal-dow {
  order: 2; display: grid; grid-template-columns: repeat(7, 1fr);
  padding-bottom: .3rem; font-size: .72rem; color: var(--muted);
  letter-spacing: .04em;
}
#cal-dow span { text-align: center; }
#calendar-view[data-mode="year"] #cal-dow { display: none; }

/* day view stacks its chrome differently: week strip first, then the date */
#calendar-view[data-mode="day"] #cal-dow { order: 1; }
#cal-week {
  order: 2; display: grid; grid-template-columns: repeat(7, 1fr);
  padding-bottom: .4rem; border-bottom: 1px solid var(--border-soft);
}
#cal-week[hidden] { display: none; }
.wk-day {
  justify-self: center; width: 2.2rem; height: 2.2rem; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.05rem; font-weight: 600; cursor: pointer;
}
.wk-day.weekend { color: var(--muted); }
.wk-day.sel { background: var(--wash-deep); }
#cal-week .wk-day.today { background: var(--today); color: var(--today-fg); }
/* a headline above the week strip — smaller than the month/year one, so the
   week strip and the timeline keep the room the inline title used to save */
#calendar-view[data-mode="day"] #cal-title {
  order: 0; padding: .1rem .8rem .35rem;
  font-size: 1.25rem; font-weight: 700; letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

#cal-scroll {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column;
}
#cal-body { flex: 1 0 auto; display: flex; flex-direction: column; }

/* calendars + sharing: a sheet that drops from the settings button in the top
   bar, so the grid keeps the whole screen until you ask for the controls */
#cal-panel {
  position: absolute; z-index: 5;
  top: 3.1rem; right: .6rem; left: auto;
  max-width: min(24rem, calc(100% - 1.2rem));
  border: 1px solid var(--border-soft); border-radius: 14px;
  background: var(--surface); padding: .7rem .85rem;
}
#cal-panel[hidden] { display: none; }

/* ---- one event, over the grid ----
   A screen, not a fourth mode: the level underneath keeps its place, so the
   pill in the top-left only has to put it back on screen. */
#event-detail {
  display: none; flex: 1; min-height: 0; overflow-y: auto;
  overscroll-behavior: contain;
  /* flat paper, not the body's sky→carrot wash: the screen is mostly empty
     around the title, so the diagonal tint read as a colored panel behind
     the back pill and title. Same call the header made — solid paper up top. */
  background: var(--bg);
}
#calendar-view[data-detail] > :not(#event-detail) { display: none; }
#calendar-view[data-detail] > #event-detail { display: block; }
/* the grid's top bar gets its breathing room from #cal-top; this one is the
   first thing in the box, so it brings its own */
#event-detail .cal-bar { padding-top: .6rem; }
/* the bar's right-hand pills: "edit" while reading, "save" while editing */
.cal-pill.push { margin-left: auto; }
.cal-pill.primary { background: var(--user); color: #fff; }
.cal-pill.primary:active { opacity: .85; background: var(--user); }
.cal-pill[disabled], .cal-icon[disabled] { opacity: .5; cursor: default; }
.ev-body { padding: .4rem .95rem 2rem; }
.ev-title {
  margin: .2rem 0 .35rem; font-size: 1.55rem; font-weight: 700;
  letter-spacing: -.01em; line-height: 1.2;
  border-left: 4px solid var(--mine-edge); padding-left: .6rem;
}
.ev-title[data-cat] { border-left-color: var(--cat); }
.ev-title.friend { border-left-color: var(--friend); }
.ev-title.google { border-left-color: var(--google); }
.ev-title.invite { border-left-color: var(--invite); }
.ev-title.pending { border-left-style: dashed; }   /* unanswered, like its block */
/* cancelled on me: struck through, faded, until i remove it */
.ev-title.cancelled { text-decoration: line-through; color: var(--muted); }
.ev-when {
  color: var(--muted); font-size: .95rem; padding-left: calc(.6rem + 4px);
  margin-bottom: 1.1rem;
}
.ev-rows {
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: 14px; overflow: hidden;
}
.ev-row {
  display: flex; gap: .7rem; padding: .75rem .9rem; font-size: .95rem;
  line-height: 1.45;
}
.ev-row + .ev-row { border-top: 1px solid var(--hairline); }
.ev-icon { flex: none; width: 1.2rem; text-align: center; opacity: .8; }
.ev-text { min-width: 0; overflow-wrap: anywhere; }
/* notes keep the line breaks they were written with */
.ev-row.notes .ev-text { white-space: pre-wrap; }
.ev-empty { margin: 0; padding: .9rem; color: var(--muted); font-size: .9rem; }
/* answering an invite addressed to you */
.ev-reply { margin-top: 1.1rem; }
.ev-ask { margin: 0 0 .5rem; font-size: .95rem; font-weight: 600; }
.ev-actions { display: flex; gap: .6rem; }
.ev-btn {
  flex: 1; padding: .7rem .9rem; font: inherit; font-weight: 600;
  border-radius: 12px; border: 1px solid var(--border); cursor: pointer;
  background: var(--surface); color: var(--fg); min-height: 2.75rem;
}
.ev-btn.primary { background: var(--user); border-color: var(--user); color: #fff; }
.ev-btn:active { background: var(--wash-deep); }
.ev-btn.primary:active { opacity: .85; background: var(--user); }
.ev-btn[disabled] { opacity: .5; cursor: default; }
.ev-btn.danger { color: var(--danger); }   /* armed → button.armed */

/* ---- the same screen as a form ----
   Fields sit where the rows did: the title where the headline was, the
   rest in the card. 16px inputs — smaller and iOS zooms in on focus. */
.ev-title-input {
  display: block; width: 100%; box-sizing: border-box; margin: .2rem 0 1.1rem;
  padding: .3rem .6rem; border: 0; border-left: 4px solid var(--mine-edge);
  border-radius: 0; background: none; outline: none; color: var(--fg);
  font: inherit; font-size: 1.35rem; font-weight: 700; letter-spacing: -.01em;
}
.ev-title-input:focus { border-left-color: var(--accent); }
.ev-field {
  display: flex; align-items: center; gap: .7rem; padding: .35rem .9rem;
  font-size: .95rem;
}
.ev-field + .ev-field { border-top: 1px solid var(--hairline); }
.ev-field .ev-label.glyph {   /* 📍 📝 — the same icons the rows use */
  flex: none; width: 1.2rem; text-align: center; opacity: .8;
}
.ev-field .ev-label.word {     /* starts / ends */
  flex: none; min-width: 3rem; color: var(--muted); font-size: .85rem;
}
.ev-field input, .ev-field textarea {
  flex: 1; min-width: 0; border: 0; background: none; outline: none;
  color: var(--fg); font: inherit; font-size: 16px; padding: .4rem 0;
}
.ev-field input::placeholder, .ev-field textarea::placeholder { color: var(--muted); }
.ev-field textarea { resize: vertical; min-height: 4.4rem; line-height: 1.4; }
.ev-when-inputs { flex: 1; display: flex; gap: .6rem; min-width: 0; }
.ev-when-inputs input {
  flex: 1 1 0; min-width: 0; min-height: 2.1rem;   /* an empty end stays a target */
  -webkit-appearance: none; appearance: none;
  border-radius: 8px; background: var(--wash); padding: .35rem .45rem;
}
.ev-when-inputs input[type="date"] { flex-grow: 1.15; }
/* the notes row grows with its text; keep the glyph on the first line */
.ev-field.multi { align-items: flex-start; }
.ev-field.multi .ev-label { padding-top: .5rem; }
.ev-form .ev-btn.danger { display: block; width: 100%; margin-top: 1.4rem; }
/* the guest list in the form: chips like the list share picker's */
.ev-guests {
  margin-top: .9rem; padding: 0 .2rem; font-size: .82rem; color: var(--muted);
  display: flex; flex-wrap: wrap; gap: .45rem; align-items: center;
}
.ev-guest-list { display: contents; }
.ev-guest.pending { opacity: .8; }
.ev-guest.declined { text-decoration: line-through; opacity: .7; }
.ev-guest.declined button { text-decoration: none; }

/* the category tray in the event form: which part of the calendar this
   event is, as chips in their own colors. The event's half is staged like
   every other field (a tap selects, save writes it); the CATEGORY's half —
   its color, and dropping it — is not, because a category belongs to the
   whole calendar rather than to the event being edited, and staging it here
   would make one event's cancel undo a change meant for all of them. So the
   swatches and the ✕ only appear under the selected chip, where what they
   act on is named on screen. */
.ev-cats {
  margin-top: .9rem; padding: 0 .2rem; font-size: .82rem; color: var(--muted);
  display: flex; flex-wrap: wrap; gap: .45rem; align-items: center;
}
.ev-cat-list { display: contents; }
.cat-chip {
  display: inline-flex; align-items: center; gap: .3rem;
  border: 1px solid var(--border); background: var(--surface); color: var(--fg);
  border-radius: 999px; padding: .22rem .6rem; font: inherit; cursor: pointer;
}
.cat-chip[data-cat] { background: var(--cat-tint); border-color: var(--cat); }
/* the chosen one is ringed, not merely filled: at 42% the tints are close
   enough that "which is on" can't rest on the fill alone */
.cat-chip.sel { box-shadow: inset 0 0 0 2px var(--cat, var(--accent)); font-weight: 600; }
.cat-chip:active { filter: brightness(.95); }
.cat-edit .cat-x { margin-left: auto; color: var(--danger); }
.cat-edit .cat-x.armed { background: var(--danger-tint); border-color: var(--danger); }
/* the new-category form and the recolor row both break the line: they are
   about one thing, and sitting them beside the chips reads as another chip */
.cat-edit { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: .45rem; align-items: center; }
.cat-edit[hidden] { display: none; }
.cat-edit input {
  flex: 1; min-width: 7rem; border: 1px solid var(--border); border-radius: 8px;
  padding: .3rem .5rem; font: inherit; background: var(--surface); color: var(--fg);
}
.cat-edit input:focus { border-color: var(--accent); outline: none; }
.cat-swatches { display: flex; gap: .4rem; }
.cat-swatch {
  width: 1.5rem; height: 1.5rem; border-radius: 50%; border: 1px solid var(--glass-line);
  background: var(--cat); cursor: pointer; padding: 0; position: relative;
}
.cat-swatch::after { content: ""; position: absolute; inset: -8px; }
.cat-swatch.sel { box-shadow: 0 0 0 2px var(--fg); }
.cat-add {
  border: 1px solid var(--border); background: var(--wash); color: var(--fg);
  border-radius: 999px; padding: .22rem .7rem; font: inherit; cursor: pointer;
}
.cat-add:active { background: var(--wash-deep); }

/* ---- month view: stacked months in one scroll ----
   The body holds a run of months; you scroll through them, and the title
   follows whichever is at the top. Each month is its name over its weeks.
   A week is its own 7-column grid: row 1 holds the dates, the rows under it
   hold all-day bars (which span columns) and per-day chips. The tap targets
   are full-height cells behind it all. Weeks are a fixed height — the
   scroll needs the run to have a length — sized for a date and three rows. */
:root { --week-h: 5.6rem; }
.cal-months { display: flex; flex-direction: column; }
.cal-month { display: flex; flex-direction: column; }
/* the month names ride inline, iOS-style: the one at the top is the title,
   so the header's own headline stands down in this mode (it still carries
   the state — .away — and comes back for the day view) */
#calendar-view[data-mode="month"] #cal-title { display: none; }
.cal-month-name {
  margin: 0; padding: .9rem .8rem .3rem; font-size: 1.7rem;
  font-weight: 700; letter-spacing: -.02em; line-height: 1.1;
}
.cal-month-name.now { color: var(--accent); }   /* the month today is in */
.cal-grid { display: grid; }
.cal-week {
  display: grid; grid-template-columns: repeat(7, 1fr);
  grid-template-rows: auto repeat(3, auto) 1fr;
  height: var(--week-h); box-sizing: border-box;
  row-gap: 2px; padding-bottom: .15rem; min-height: 0; overflow: hidden;
  border-top: 1px solid var(--border-soft);   /* the week separators */
}
.cal-cell { grid-row: 1 / -1; cursor: pointer; }
.cal-cell.blank { cursor: default; }          /* neighboring months stay empty */
.cal-cell:not(.blank):active { background: var(--wash); }
/* dates and events ride above the hit cells and never swallow a tap */
.cal-num, .cal-week .cal-chip { z-index: 1; pointer-events: none; }
.cal-week .cal-chip { min-width: 0; margin: 0 1px; }
.cal-num {
  grid-row: 1; justify-self: center; margin: .2rem 0 1px;
  min-width: 1.6rem; height: 1.6rem; line-height: 1.6rem; text-align: center;
  border-radius: 50%; font-size: .95rem; font-weight: 600;
}
.cal-num.weekend { color: var(--muted); }
.cal-num.today {
  background: var(--today); color: var(--today-fg); font-weight: 700;
}
.cal-chip {
  font-size: .62rem; line-height: 1.35; font-weight: 600;
  padding: .05rem .25rem; border-radius: 4px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  background: var(--mine); color: var(--fg);
  border-left: 2px solid var(--mine-edge);
}
.cal-chip.friend { background: var(--friend-tint); border-left-color: var(--friend); }
.cal-chip.google { background: var(--google-tint); border-left-color: var(--google); }
.cal-chip.invite { background: var(--invite-tint); border-left-color: var(--invite); }
.cal-chip.more { background: none; border-left-color: transparent; color: var(--muted); }
/* An event of the user's own, filed under a category, wears that category's
   color instead of the plain sky. Only theirs: `srcClass` leaves an overlay
   or an invite with a class, and every rule below is written so a color that
   says WHOSE an event is can't be painted over by one that says what it's
   about. The tint fills, the color itself edges — the same two roles
   --mine/--mine-edge play, so nothing else about a chip changes. */
[data-cat] { --cat-tint: color-mix(in srgb, var(--cat) var(--cat-mix), var(--surface)); }
[data-cat="mist"] { --cat: var(--cat-mist); }
[data-cat="powder"] { --cat: var(--cat-powder); }
[data-cat="cornflower"] { --cat: var(--cat-cornflower); }
[data-cat="periwinkle"] { --cat: var(--cat-periwinkle); }
[data-cat="dusk"] { --cat: var(--cat-dusk); }
[data-cat="indigo"] { --cat: var(--cat-indigo); }
.cal-chip[data-cat] { background: var(--cat-tint); border-left-color: var(--cat); }
.cal-chip.allday[data-cat] { background: var(--cat); color: var(--cat-ink); }
/* all-day: a solid pill, no clock stripe */
.cal-chip.allday {
  background: var(--mine-solid); color: #fff;
  border-left: 0; padding-left: .35rem;
}
.cal-chip.allday.friend { background: var(--friend-solid); }
.cal-chip.allday.google { background: var(--google-solid); }
.cal-chip.allday.invite { background: var(--invite-solid); }
/* an unanswered invite: outlined, not filled — it's holding the slot, not
   owning it. The day-view twin lives with the other .event-block rules. */
.cal-chip.pending {
  background: none; color: var(--muted);
  border: 1px dashed var(--invite); border-left: 2px dashed var(--invite);
}
.cal-chip.allday.pending { border-left-width: 1px; padding-left: .3rem; }
/* cancelled on me (the organizer ended it, or took me off): struck through
   and faded — still there, so i see it went, until i remove it */
.cal-chip.cancelled {
  background: none; color: var(--muted); text-decoration: line-through;
  border: 1px solid var(--border); border-left: 2px solid var(--muted);
}
.cal-chip.allday.cancelled { border-left-width: 1px; padding-left: .3rem; }
/* a multi-day bar keeps its rounded ends only where the event really ends;
   where it runs off the row it squares up and carries on next week */
.cal-chip.bar { border-radius: 999px; margin: 0 1px; }
.cal-chip.bar.cont-left {
  border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: 0;
  border-left-color: transparent;   /* nothing starts here — it carried over */
}
.cal-chip.bar.cont-right { border-top-right-radius: 0; border-bottom-right-radius: 0; margin-right: 0; }

/* ---- day view: hour timeline ---- */
.day-view { padding: .6rem .8rem 2.5rem; }
/* pinned above the timeline, its label lined up with the hour gutter */
#cal-allday {
  order: 4; display: flex; align-items: flex-start; gap: .55rem;
  padding: .3rem .8rem .4rem; border-bottom: 1px solid var(--border-soft);
}
#cal-allday[hidden] { display: none; }
.allday-label {
  width: 3.05rem; flex: none; text-align: right; line-height: 1.5rem;
  font-size: .72rem; color: var(--muted);
}
.allday-events { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.allday-events .cal-chip {
  font-size: .8rem; padding: .18rem .7rem; border-radius: 999px;
  cursor: pointer;
}
.hour-grid { position: relative; --gutter: 3.6rem; }
.hour-line {
  position: absolute; left: var(--gutter); right: 0; height: 0;
  border-top: 1px solid var(--hairline);
}
.hour-label {
  position: absolute; left: calc(-1 * var(--gutter)); top: -.55rem;
  width: calc(var(--gutter) - .55rem); text-align: right;
  font-size: .72rem; color: var(--muted);
}
.event-block {
  position: absolute; overflow: hidden; border-radius: 7px; cursor: pointer;
  padding: .15rem .4rem; font-size: .8rem; line-height: 1.25;
  background: var(--mine); border-left: 3px solid var(--mine-edge);
}
.event-block .etitle { font-weight: 600; }
.event-block .etime, .event-block .ewho {
  display: block; color: var(--muted); font-size: .7rem;
}
.event-block[data-cat] { background: var(--cat-tint); border-left-color: var(--cat); }
.event-block.friend { background: var(--friend-tint); border-left-color: var(--friend); }
.event-block.google { background: var(--google-tint); border-left-color: var(--google); }
.event-block.invite { background: var(--invite-tint); border-left-color: var(--invite); }
/* unanswered — hollow, so it reads as a slot being held open (see the
   matching .cal-chip.pending rule up in the month grid) */
.event-block.pending {
  background: none; border: 1px dashed var(--invite);
  border-left: 3px dashed var(--invite);
}
.event-block.pending .etitle { font-weight: 500; color: var(--muted); }
/* cancelled on me — see .cal-chip.cancelled */
.event-block.cancelled {
  background: none; border: 1px solid var(--border);
  border-left: 3px solid var(--muted);
}
.event-block.cancelled .etitle {
  font-weight: 500; color: var(--muted); text-decoration: line-through;
}
/* the live now-line, red like iOS — the only thing that moves on its own */
.now-line {
  position: absolute; left: var(--gutter); right: 0; height: 0; z-index: 3;
  border-top: 2px solid var(--today);
}
.now-time {
  position: absolute; left: calc(-1 * var(--gutter)); top: -.68rem;
  background: var(--today); color: var(--today-fg);
  font-size: .68rem; font-weight: 700; padding: .1rem .4rem; border-radius: 999px;
}

/* ---- year view: twelve mini months ---- */
.year-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem .6rem;
  padding: .4rem .8rem 1.5rem;
}
.year-month { cursor: pointer; }
.ym-name {
  font-size: .85rem; font-weight: 700;
  margin-bottom: .15rem;
}
.ym-name.now { color: var(--today); }
.ym-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.ym-day {
  font-size: .55rem; text-align: center; height: 1.1rem; line-height: 1.1rem;
  color: var(--muted);
}
.ym-day.today {
  background: var(--today); color: var(--today-fg);
  border-radius: 50%; font-weight: 700;
}

/* loading placeholders: same footprint as the real rows, gentle pulse */
@keyframes skel-pulse { 50% { opacity: .45; } }
.skel {
  background: var(--wash-deep); border-radius: 12px;
  animation: skel-pulse 1.2s ease-in-out infinite;
}
.skel-row { height: 2.35rem; margin-bottom: .4rem; }   /* one item row */
.skel-tile { height: 7.5rem; border-radius: 14px; }   /* .list-tile's footprint */

/* calendar sharing + overlay (colors defined in the brand palette) */
#cal-share-row {
  display: flex; flex-wrap: wrap; gap: .45rem; align-items: center;
  font-size: .8rem; color: var(--muted);
}
.legend-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: .25rem; vertical-align: baseline;
}
.legend-dot[data-cat] { background: var(--cat); }
.legend-dot.mine { background: var(--mine); box-shadow: inset 0 0 0 1px var(--mine-edge); }
.legend-dot.friend { background: var(--friend); }
.legend-dot.none { background: var(--faint); }   /* they haven't shared back */
.legend-dot.google { background: var(--google); }
.legend-dot.google.off { background: var(--faint); }   /* overlay toggled off */
.gcal-connect, .gcal-toggle {
  background: none; border: none; padding: .3rem .2rem; font: inherit;
  color: var(--fg); cursor: pointer;
}
.gcal-connect { color: var(--muted); }

/* ---------- dashboard ---------- */
.usage-total { color: var(--muted); margin: .3rem 0 1rem; font-size: .9rem; }
.usage-block h3 { margin: 1rem 0 .4rem; font-size: .95rem; }
/* the app-usage table is 9 columns — let it scroll sideways on phones */
.usage-block { overflow-x: auto; }
.usage-table {
  width: 100%; border-collapse: collapse; font-size: .85rem;
  background: var(--surface); border: 1px solid var(--border-soft); border-radius: 10px;
  overflow: hidden;
}
.usage-table th, .usage-table td {
  padding: .45rem .6rem; text-align: right; border-top: 1px solid var(--hairline);
}
.usage-table th:first-child, .usage-table td:first-child {
  text-align: left; max-width: 11rem; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap;
}
.usage-table thead th { border-top: none; color: var(--muted); font-weight: 500; }
.usage-note { color: var(--muted); margin: .35rem 0 0; font-size: .78rem; }

/* account: which one you're signed into, the first thing on the settings
   page. One row per sign-in — the address is the identifying line, so it
   leads, and how you got in sits under it in the muted weight. An address
   is one long unbreakable word, so it wraps rather than being ellipsised
   away: the whole point of the row is reading it. */
.account-box {
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: 10px; overflow: hidden;
}
.account-row { padding: .55rem .7rem; border-top: 1px solid var(--hairline); }
.account-row:first-child { border-top: none; }
.account-email { font-size: .92rem; overflow-wrap: anywhere; }
.account-email.account-noemail { color: var(--muted); }
.account-how { font-size: .75rem; color: var(--muted); margin-top: .1rem; }
.account-box .empty-note { padding: .55rem .7rem; }
.signout-btn {
  display: block; margin: 1.6rem auto .4rem; min-height: 44px;
  border: 1px solid var(--border); background: var(--surface); color: var(--danger);
  border-radius: 999px; padding: .5rem 1.5rem; font-size: .9rem; cursor: pointer;
}
.signout-btn:active { background: var(--wash-deep); }

/* delete account: the same pill as sign out, quieter until it is opened —
   it sits below the way out that isn't permanent */
.delete-btn { margin-top: .5rem; }
.delete-btn[hidden] { display: none; }

.delete-confirm {
  max-width: 22rem; margin: .6rem auto 0; padding: .8rem .9rem;
  border: 1px solid var(--danger); border-radius: 14px; background: var(--surface);
}
.delete-confirm[hidden] { display: none; }
.delete-confirm p { margin: 0 0 .7rem; font-size: .82rem; color: var(--muted); }
.delete-row { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; }
.delete-row button {
  min-height: 44px; border-radius: 999px; padding: .5rem 1.1rem;
  font-size: .88rem; cursor: pointer; border: 1px solid var(--border);
}
.delete-go { background: var(--danger); border-color: var(--danger); color: #fff; }
.delete-keep { background: var(--surface); color: var(--fg); }
.delete-row button:disabled { opacity: .6; cursor: default; }

.privacy-link {
  display: block; text-align: center; margin: 0 auto 1rem;
  color: var(--muted); font-size: .82rem;
}

/* markdown inside assistant bubbles (and the corner bunny's speech bubble),
   plus event notes, which are rendered through the same mdRender subset */
.msg.assistant ul, .msg.assistant ol, #ask-text ul, #ask-text ol,
.ev-row.notes .ev-text ul, .ev-row.notes .ev-text ol {
  margin: .25rem 0; padding-left: 1.25rem;
}
.msg.assistant li, #ask-text li, .ev-row.notes .ev-text li { margin: .12rem 0; }
.msg.assistant code, #ask-text code, .ev-row.notes .ev-text code {
  font-family: ui-monospace, SFMono-Regular, monospace; font-size: .85em;
  background: var(--wash-deep); padding: .05rem .3rem; border-radius: 5px;
}
.inline-edit { user-select: text; -webkit-user-select: text; }
.dash-seg { margin-bottom: .9rem; width: fit-content; }
.theme-seg { width: fit-content; margin-bottom: 1.2rem; }
.theme-seg button { padding: .5rem 1rem; }
.lang-seg { width: fit-content; }
/* no letter-spacing here on purpose: the labels are in their own scripts and
   tracking applied to Hangul syllable blocks reads as broken spacing */
.lang-seg button { padding: .5rem 1rem; letter-spacing: normal; }
.location-seg { width: fit-content; }
/* the wardrobe grows, the phone does not: the outfit segment hugs its
   names like the others but wraps onto a second row instead of running
   off the edge, so a new outfit is a taller pill, never a clipped one.
   Buttons size to their word (the .seg default stretches them, which
   would make a lone wrapped name full-width). */
.outfit-seg { width: fit-content; max-width: 100%; flex-wrap: wrap; }
.outfit-seg button { flex: 0 0 auto; padding: .5rem 1rem; }
.location-seg button { padding: .5rem 1rem; }
/* microphone: a single action button, not a seg — it re-asks (or, when the
   browser has blocked the mic for good, just re-checks). */
.mic-allow-btn {
  min-height: 40px; border: 1px solid var(--border); background: var(--surface);
  color: var(--fg); border-radius: 999px; padding: .45rem 1.1rem;
  font-size: .85rem; cursor: pointer;
}
.mic-allow-btn:active { background: var(--wash-deep); }
.mic-allow-btn:disabled { opacity: .6; cursor: default; }
.mic-allow-btn.is-denied { color: var(--danger); }
.settings-note {
  color: var(--muted); font-size: .8rem; line-height: 1.4;
  margin: .45rem 0 1.2rem; max-width: 34rem;
}
.settings-h { margin: 0 0 .4rem; font-size: .95rem; }

/* ---------- glass ----------
   One recipe, declared once, last in the file so it wins over each surface's
   own paint above: the selectors name every surface that wears it, nothing
   in the markup is "glass". Chrome and floating panels blur what moves under
   them; a card in the scroll is the translucent surface over the paper
   *without* a blur — a backdrop-filter per tile in a scrolling grid is what
   makes an old phone stutter. A browser with no backdrop-filter, and anyone who
   asked the OS for less transparency, get solid paper back (the fallbacks
   at the end). iOS: DesignSystem/Glass.swift, the same surfaces.
   The header is NOT glass: it runs under the phone's status bar (see the
   header rule), and the wash's sky corner showing through it there read as
   a blue gradient behind the clock — it stays solid paper so the strip is one
   flat colour in either theme. The single-list screen's sticky .detail-head is
   not glass either, but it is not flat --bg: it repaints the wash itself,
   viewport-fixed (its own rule above), so the strip IS the paper behind it
   rather than a panel floating over it. */
#cal-panel, #menu, #ask {
  background: var(--glass);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
}
header, #cal-panel { border-color: var(--glass-line); }
/* the composer's dock is NOT glass — it's transparent so the solid-white box
   sits flush on the paper (see #dock above), with no frosted strip behind it.
   Its position:relative + z-index still make it a stacking context, so the
   bunny it hosts (.mascot, z 4) ranks through it: the dock takes his level,
   so he floats over the transcript's pills and under the ask panel (6) */
#dock { z-index: 4; }
/* the transcript runs under the composer's glass: the dock climbs over the
   last 1.1rem of it, and the transcript pads by the same so its last bubble
   still scrolls clear. scroll-padding-bottom matches that 1.1rem overlap so
   scrollIntoView({block:"end"}) — how every append and streamed token scrolls
   — rests the last bubble's foot at the dock's top edge, not tucked under its
   glass. Exactly the overlap, no more: a larger inset lifts the newest card
   enough that one of its action pills lands under the corner bunny's tap
   point (see ui_mic_test's "a card is not a button"). */
#dock { margin-top: -1.1rem; }
#messages { padding-bottom: 2.1rem; scroll-padding-bottom: 1.1rem; }
/* floating panels and cards: a lit top edge, a soft drop, a hairline of ink
   for the outline instead of the paper border. The todo list's two boxes
   (pending, completed) are glass like the rest; their rows stay opaque
   (see .item-wrap .item) so the action tray they slide over can't show
   through the frost. */
#menu, #ask, form, .list-tile, .ev-rows, .person-row, .detail-body, .done-box,
.usage-table, .account-box, .delete-confirm, #cal-panel {
  box-shadow: inset 0 1px 0 var(--glass-edge), var(--glass-shadow);
}
#menu, #ask, form, .list-tile, .ev-rows, .person-row, .detail-body, .done-box,
.usage-table, .account-box {
  border-color: var(--glass-line);
}
form, .list-tile, .ev-rows, .person-row, .detail-body, .done-box, .usage-table,
.account-box {
  background: var(--glass-card);
}
/* ---- the controls ----
   A button is the same material as the surfaces, one size down: the fill,
   the lit top edge, the ink hairline, a soft drop. Which fill depends on
   what is behind it. Over the paper (and over the event screen's flat
   paper) the card weight is enough. Raised on top of another glass surface
   — the calendar's bars, the composer, a card, a row — it takes the heavier
   chrome weight, so it reads as lifted rather than dented. A control never
   blurs on its own account: whatever it sits on has already blurred what
   moves under it, and a backdrop-filter per pill is what makes an old phone
   stutter (the one exception, .todo-add below, floats over the list itself).
   The dropdowns come along: a select is a control the same way a pill is.
   No drop shadow either, for the same reason `.seg` and `header button`
   don't have one: `--glass-shadow-soft` is for a pane that floats clear of
   its neighbours (a bubble on the paper, the active segment lifting out of
   its trough), and a control sitting in the chrome is not that.
   Two kinds stay out. A bare text button inside a glass container — the
   menu's rows, a toast's undo, a chip's × — would be noise as a pane each.
   And the filled ones say their piece in colour; see below. */
header button, .back-btn, .quick-actions button, .ev-btn, .section-edit,
.add-bunnyfriend, .bunny-remove, .signout-btn, .delete-keep,
.shop-card, .shop-lock-close, .shop-code-input, select {
  background: var(--glass-card);
  border: 1px solid var(--glass-line);
  box-shadow: inset 0 1px 0 var(--glass-edge);
}
.cal-pill, .cal-group, .card-actions button, .person-row button,
#mic, #new-chat, #ask-redo {
  background: var(--glass);
  border: 1px solid var(--glass-line);
  box-shadow: inset 0 1px 0 var(--glass-edge);
}
/* the filled ones are the pane the message you send is made of — the powder
   gradient inside its rim and lens (--user-glass and friends, above), which
   is still glass, just glass with a colour of its own. #send already wears
   it from the composer's own rule; these are the rest of the primaries. The
   text goes with the fill: white was chosen for a solid accent and does not
   survive the gradient, --user-fg does.
   Danger stays flat red — a warning you can see the paper through is a
   quieter warning — and takes the lens alone, for the thickness. */
#ask-send, .add-friend button, .ev-btn.primary, .cal-pill.primary,
.import-review .card-actions button.primary {
  background: var(--user-glass); color: var(--user-fg);
  border: 1px solid var(--user-rim);
  box-shadow: var(--user-lens), var(--user-cast);
}
.delete-go { box-shadow: var(--user-lens), var(--user-cast); }
/* pressed: dim the pane rather than repainting it — the solid accent those
   two reach for underneath would be --user-fg's own blue, on blue */
.ev-btn.primary:active, .cal-pill.primary:active {
  background: var(--user-glass); opacity: 1; filter: brightness(.92);
}
/* armed outranks all of it: red is the entire point of the state. Its recipe
   is up with the header; only the fill needs saying again, for the controls
   whose selector here is as specific as `button.armed` itself. */
button.armed { background: var(--danger-tint); border-color: var(--danger); }
/* the add-todo control floats over the scrolling list, so it is chrome — the
   frosted glass and a blur, a lit edge and a soft drop. Opened, the pill wears
   the glass and the + folds in bare (no fill, no blur of its own). */
.todo-add-btn, .todo-add.open {
  background: var(--glass);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border: 1px solid var(--glass-line);
  box-shadow: inset 0 1px 0 var(--glass-edge), var(--glass-shadow);
}
.todo-add.open .todo-add-btn {
  background: none; border: 0; box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
/* the settings segmented controls ("toggle switches"): the trough is
   frosted glass like the chrome, the active segment a brighter pane lifted
   over it (the chrome fill, a lit edge, a soft drop). Solid paper again in
   the fallbacks below, like the rest of the glass. */
.seg {
  background: var(--glass-card); border: 1px solid var(--glass-line);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow: inset 0 1px 0 var(--glass-edge);
}
.seg button.active {
  background: var(--glass);
  box-shadow: inset 0 1px 0 var(--glass-edge), var(--glass-shadow-soft);
}
/* the transcript: a bubble is the translucent card with the lit edge, a
   hairline and a soft drop — no blur, there is nothing behind it but the
   paper. The user's is the powder gradient (--user-glass, above) inside
   a lit rim (--user-rim) of one weight the whole way round, top
   included — and the lens inside it (--user-lens), where the light a pane
   of that thickness would gather sits: along the whole inside edge,
   brightest top and bottom, gone before the middle. Nothing DARKENS the
   inside: shading deep enough to see is deep enough to drag the text
   under 4.5:1, and the fill's 4.6 is the whole budget — the lens only
   lightens, so every glyph reads better over it than off it. No backdrop blur either — there is only the smooth wash
   behind, which blurs to itself, and a transcript's worth of blurring
   layers is real cost on a phone for no pixels. The corner
   bunny's reply bubble floats over a whole page, so it is chrome: the
   heavier fill, and the blur; its tail is a border triangle, so it takes
   the fill without the blur — 8px, over him. */
.msg.assistant {
  background: var(--glass-card);
  box-shadow: inset 0 1px 0 var(--glass-edge), 0 0 0 1px var(--glass-line), var(--glass-shadow-soft);
}
.msg.user {
  background: var(--user-glass);
  border: 1px solid var(--user-rim);
  box-shadow: var(--user-lens), var(--user-cast);
}
#ask-bubble {
  background: var(--glass);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow: inset 0 1px 0 var(--glass-edge), 0 0 0 1px var(--glass-line), var(--glass-shadow);
}
#ask-bubble::after { border-top-color: var(--glass); }
/* no backdrop-filter (older Firefox; fixtures): the chrome is solid paper
   again, the cards stay a translucent surface */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  :root, :root[data-theme="dark"] { --glass: var(--bg); }
}
/* the OS setting (iOS: accessibilityReduceTransparency): solid surfaces,
   the wash and the edges stay */
@media (prefers-reduced-transparency: reduce) {
  :root, :root[data-theme="dark"] { --glass: var(--bg); --glass-card: var(--surface); }
}

/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  #messages { scroll-behavior: auto; }
}

/* ---------- keyboard shortcuts dialog ---------- */
.shortcuts-overlay {
  position: fixed; inset: 0; z-index: 70;
  display: flex; align-items: center; justify-content: center; padding: 1rem;
  background: rgba(0,0,0,.4);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
}
.shortcuts-card {
  width: min(28rem, 100%); max-height: 85vh; overflow-y: auto;
  padding: 1.25rem 1.25rem 1.4rem; background: var(--glass);
  border: 1px solid var(--glass-line); border-radius: 20px;
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow: inset 0 1px 0 var(--glass-edge), 0 16px 48px rgba(0,0,0,.28);
}
.shortcuts-head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 1.1rem; padding-bottom: .6rem;
  border-bottom: 1px solid var(--glass-line);
}
.shortcuts-title {
  margin: 0; font-size: 1.05rem; font-weight: 700; color: var(--fg);
}
.shortcuts-close {
  background: none; border: none; font-size: 1.4rem; color: var(--muted);
  cursor: pointer; padding: .2rem .45rem; line-height: 1; border-radius: 6px;
}
.shortcuts-close:hover { color: var(--fg); }
.shortcuts-list {
  display: grid; grid-template-columns: 1fr auto; gap: .65rem .8rem;
  align-items: center; font-size: .88rem;
}
.shortcuts-desc { color: var(--fg); line-height: 1.35; }
.shortcuts-keys { display: flex; gap: .3rem; justify-content: flex-end; }
.kbd-key {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.65rem; height: 1.55rem; padding: 0 .4rem;
  font-family: inherit; font-size: .75rem; font-weight: 600;
  background: var(--surface); color: var(--fg);
  border: 1px solid var(--border); border-radius: 6px;
  box-shadow: 0 1px 2px rgba(0,0,0,.08);
}
.shortcuts-btn {
  margin-top: .8rem; width: 100%; min-height: 44px;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--fg);
  cursor: pointer; font-size: .88rem; font-weight: 600;
  padding: .55rem 1rem;
}
.shortcuts-btn:active { background: var(--wash-deep); }

/* ---- party organizer card inside event detail ---- */
.party-card {
  margin-top: 1.1rem; border-radius: 14px;
  border: 1px solid var(--border); overflow: hidden;
}
.party-card-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: .7rem .9rem; background: var(--wash);
  font-size: .85rem; font-weight: 600; color: var(--fg);
}
.party-card-header .party-badge {
  display: flex; align-items: center; gap: .35rem;
}
.party-card-body { padding: .6rem .9rem; }
.party-link-row {
  display: flex; align-items: center; gap: .5rem;
  font-size: .82rem; color: var(--muted); padding: .35rem 0;
}
.party-link-row a {
  color: var(--accent); text-decoration: none; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.party-link-row a:active { opacity: .7; }
.party-copy-btn, .party-open-btn {
  flex: none; padding: .3rem .55rem; font-size: .75rem; font-weight: 600;
  border-radius: 8px; border: 1px solid var(--border); cursor: pointer;
  background: var(--surface); color: var(--fg); white-space: nowrap;
}
.party-copy-btn:active, .party-open-btn:active { background: var(--wash-deep); }
.party-counts {
  display: flex; gap: .1rem; padding: .45rem 0;
  font-size: .82rem; color: var(--muted);
}
.party-counts span {
  flex: 1; text-align: center; padding: .3rem 0;
}
.party-counts span:not(:last-child) {
  border-right: 1px solid var(--hairline);
}
.party-counts .ct-num { font-weight: 700; color: var(--fg); }
.party-guests-section { padding-top: .45rem; }
.party-guests-toggle {
  display: flex; align-items: center; justify-content: space-between;
  padding: .35rem 0; cursor: pointer; font-size: .82rem;
  font-weight: 600; color: var(--fg); -webkit-user-select: none;
  user-select: none;
}
.party-guests-toggle .chevron {
  font-size: .65rem; color: var(--muted); transition: transform .15s;
}
.party-guests-toggle.open .chevron { transform: rotate(90deg); }
.party-guest-list {
  max-height: 0; overflow: hidden; transition: max-height .2s ease;
}
.party-guest-list.open { max-height: 40rem; }
.party-guest-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: .4rem 0; font-size: .82rem; border-top: 1px solid var(--hairline);
}
.party-guest-row .pg-name { font-weight: 500; color: var(--fg); }
.party-guest-row .pg-meta { color: var(--muted); font-size: .75rem; }
.party-guest-row .pg-status {
  font-size: .7rem; padding: .15rem .45rem; border-radius: 6px;
  font-weight: 600; text-transform: capitalize;
}
.party-guest-row .pg-status.going { background: #e8f5e9; color: #2e7d32; }
.party-guest-row .pg-status.maybe { background: #fff8e1; color: #f9a825; }
.party-guest-row .pg-status.declined { background: #fbe9e7; color: #c62828; }
.party-blast-section {
  padding-top: .5rem; border-top: 1px solid var(--hairline);
  margin-top: .45rem;
}
.party-blast-row {
  display: flex; gap: .4rem; align-items: stretch;
}
.party-blast-input {
  flex: 1; min-width: 0; padding: .5rem .6rem; font: inherit; font-size: 14px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface); color: var(--fg); outline: none; resize: none;
}
.party-blast-input:focus { border-color: var(--accent); }
.party-blast-input::placeholder { color: var(--muted); }
.party-blast-send {
  flex: none; padding: .5rem .7rem; font: inherit; font-size: .78rem;
  font-weight: 600; border-radius: 10px; border: none; cursor: pointer;
  background: var(--user); color: #fff; white-space: nowrap;
}
.party-blast-send:active { opacity: .85; }
.party-blast-send[disabled] { opacity: .5; cursor: default; }
.party-blast-target {
  font-size: .72rem; color: var(--muted); padding-top: .3rem;
  cursor: pointer; -webkit-user-select: none; user-select: none;
}
.party-blast-target:active { color: var(--fg); }
/* create-party CTA for non-party events */
.party-create-btn {
  display: flex; align-items: center; justify-content: center; gap: .4rem;
  width: 100%; padding: .7rem; margin-top: 1.1rem; font: inherit;
  font-size: .88rem; font-weight: 600; border-radius: 12px;
  border: 1.5px dashed var(--border); cursor: pointer;
  background: var(--surface); color: var(--muted);
  transition: border-color .15s, color .15s;
}
.party-create-btn:hover { border-color: var(--accent); color: var(--fg); }
.party-create-btn:active { background: var(--wash); }
/* disable party link */
.party-disable-btn {
  font-size: .72rem; color: var(--muted); cursor: pointer;
  text-decoration: underline; padding: .25rem 0;
  -webkit-user-select: none; user-select: none;
}
.party-disable-btn:active { color: var(--danger); }

