:root {
  --bg: #fafafa;
  --card: #ffffff;
  --ink: #262626;
  --muted: #8e8e8e;
  --line: #dbdbdb;
  --brand: #0095f6;        /* Instagram blue (primary action) */
  --brand-dark: #1877f2;
  --accent: #405de6;
  --heart: #ed4956;        /* Instagram like red */
  --beta: #f97316;         /* beta badge — deliberately not a brand colour */
  --radius: 8px;
  --shadow: none;
  --topbar-h: 54px;
  --bottomnav-h: 50px;
  /* The in-game chat widget's COLLAPSED bar height. The game boards subtract
     this (plus their own furniture) so board + bar fit one screen with no page
     scroll; the expanded chat overlays the board instead of reflowing it. */
  --gc-h: 52px;
  --feed-w: 614px;
}
* { box-sizing: border-box; }
/* The HTML `hidden` attribute must always win — component rules like
   `.btn { display:inline-flex }` would otherwise override the UA display:none
   and reveal elements toggled via `hidden` (editor footer buttons, etc.). */
[hidden] { display: none !important; }
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--ink);
  line-height: 1.5;
  -webkit-tap-highlight-color: transparent;
}
/* Mobile-first: never allow a stray element (hidden file inputs, wide media) to
   create horizontal scroll. MUST be `clip`, not `hidden`: `overflow-x: hidden`
   on body makes iOS Safari treat the body as the scroll container, and the
   position:fixed bottom nav then scrolls away with the page (Chromium is
   unaffected, so headless screenshots can't catch it). `clip` blocks the
   overflow without creating a scroll container. The `hidden` line is only a
   fallback for ancient browsers without `clip` support (< Safari 16). */
body { overflow-x: hidden; }
body { overflow-x: clip; }
a { color: var(--ink); text-decoration: none; }
a:hover { text-decoration: none; }
img { max-width: 100%; }
video { max-width: 100%; }
.link-blue, .link-blue:hover { color: var(--brand); font-weight: 600; }

h1 { font-size: 1.4rem; line-height: 1.25; margin: 0 0 14px; font-weight: 700; }
h2 { font-size: 1.15rem; line-height: 1.3; margin: 0 0 10px; font-weight: 700; }
h3 { font-size: 1rem; line-height: 1.3; margin: 22px 0 10px; font-weight: 700; }

/* ---- Top bar ---- */
.nav {
  background: var(--card); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 30; height: var(--topbar-h);
}
.nav-inner {
  max-width: 935px; margin: 0 auto; display: flex; align-items: center;
  gap: 18px; padding: 0 16px; height: var(--topbar-h);
}
.brand {
  font-weight: 700; font-size: 1.4rem; color: var(--ink);
  font-family: "Segoe Script", "Bradley Hand", "Snell Roundhand", cursive;
  letter-spacing: .02em;
}
.brand:hover { text-decoration: none; }
/* The full text link row lives in the bottom nav on every viewport; the top bar
   keeps only the brand + a couple of quick icons. */
.nav-links { display: none; }
.nav-user { display: flex; align-items: center; gap: 16px; margin-left: auto; }
.nav-user .who { display: flex; align-items: center; gap: 7px; color: var(--ink); font-weight: 600; }
/* Beta-only badge (BETA_MODE), so you always know the family data you're looking
   at isn't the real thing. Rendered nowhere on production. */
.beta-pill { background: var(--beta); color: #fff; font-size: .68rem; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; padding: 3px 9px; border-radius: 999px;
  line-height: 1.4; white-space: nowrap; cursor: default; }
.nav-icon { display: inline-flex; align-items: center; justify-content: center; font-size: 1.5rem; line-height: 1; position: relative; color: var(--ink); }
.badge {
  position: absolute; top: -5px; right: -8px; background: var(--heart); color: #fff;
  border-radius: 999px; font-size: .65rem; font-weight: 700; padding: 1px 5px; min-width: 16px; text-align: center;
}

/* ---- Bottom nav (mobile-style tab bar, shown on all sizes) ---- */
.bottom-nav {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 30;
  /* The home-indicator inset is ADDED below the nav row, not carved out of it
     (box-sizing: border-box would otherwise shrink the row into the inset,
     cramming the icons — the "wonky" bar seen in the iOS home-screen webapp). */
  min-height: calc(var(--bottomnav-h) + env(safe-area-inset-bottom, 0px));
  background: var(--card); border-top: 1px solid var(--line);
  display: flex; align-items: center; justify-content: space-around;
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.bottom-nav a {
  flex: 1; display: flex; align-items: center; justify-content: center;
  line-height: 1; color: var(--ink); height: var(--bottomnav-h); position: relative;
  transition: transform .1s ease;
}
.bottom-nav a:active { transform: scale(.92); }
.bottom-nav a .ic { width: 26px; height: 26px; display: block; }
.bottom-nav a.create .ic { width: 27px; height: 27px; }
/* Inactive tabs show the outline glyph, the active tab shows the solid one. */
.bottom-nav a .i-outline, .bottom-nav a .i-solid { display: inline-flex; }
.bottom-nav a .i-solid { display: none; }
.bottom-nav a.active .i-outline { display: none; }
.bottom-nav a.active .i-solid { display: inline-flex; }
.bottom-nav a.create.active .ic { color: var(--brand); }
.bottom-nav .nav-avatar { width: 26px; height: 26px; border-radius: 50%; object-fit: cover; border: 1px solid var(--line); }
.bottom-nav a.profile-tab.active .nav-avatar { border-color: var(--ink); border-width: 2px; padding: 0; }
.bottom-nav .badge { top: 6px; right: 22%; }

/* ---- Layout ---- */
.container { max-width: var(--feed-w); margin: 20px auto; padding: 0 0 calc(var(--bottomnav-h) + env(safe-area-inset-bottom, 0px) + 28px); }
.container.wide { max-width: 935px; padding-left: 16px; padding-right: 16px; }
.footer { display: none; }
@media (min-width: 641px) { .footer { display: block; text-align: center; color: var(--muted); padding: 20px 16px calc(var(--bottomnav-h) + 20px); font-size: .8rem; } }

/* ---- Cards ---- */
.card {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 16px; margin: 0 12px 16px;
}
@media (min-width: 641px) { .card { margin-left: 0; margin-right: 0; } }
.card h2, .card h3 { margin-top: 0; }
.form-narrow { max-width: 560px; margin: 0 auto; }
.muted { color: var(--muted); }
.small { font-size: .85rem; }
.center { text-align: center; }

/* ---- Buttons & forms ---- */
.btn { display: inline-block; background: var(--brand); color: #fff; border: none; padding: 8px 18px; border-radius: 8px; font-weight: 600; cursor: pointer; font-size: .9rem; }
.btn:hover { background: var(--brand-dark); text-decoration: none; }
.btn:disabled { opacity: .4; cursor: default; }
.btn-ghost { background: transparent; color: var(--ink); border: 1px solid var(--line); }
.btn-ghost:hover { background: var(--bg); }
.btn-sm { padding: 5px 12px; font-size: .82rem; }
/* Square icon-only button (e.g. the composer's "+" add-media button), sized to
   sit inline with the text buttons in a .form-actions row. */
.btn-icon { padding: 8px; width: 38px; height: 38px; line-height: 0; }
.btn-icon svg { width: 18px; height: 18px; }
.btn-danger { background: transparent; color: var(--heart); border: 1px solid #f0c4c8; }
.btn-block { display: block; width: 100%; text-align: center; }
.link-btn { background: none; border: none; color: var(--brand); cursor: pointer; font: inherit; font-weight: 600; padding: 0; }
.icon-btn { display: inline-flex; align-items: center; justify-content: center; background: none; border: none; padding: 3px; border-radius: 6px; cursor: pointer; color: var(--muted); opacity: .7; transition: color .15s, background .15s, opacity .15s; }
.icon-btn:hover { opacity: 1; background: rgba(0,0,0,.06); }
.icon-btn svg { display: block; }
.icon-btn-danger:hover { color: var(--heart); background: rgba(237,73,86,.1); }
.msg.me .icon-btn { color: rgba(255,255,255,.8); }
.msg.me .icon-btn:hover { color: #fff; background: rgba(255,255,255,.22); }
.inline { display: inline; }

input[type=text], input[type=password], input[type=email], input[type=datetime-local],
input[type=date], input[type=search], input[type=number], input[type=file], textarea, select {
  width: 100%; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px;
  font: inherit; background: var(--bg); color: var(--ink); margin-top: 4px;
}
/* iOS/WebKit (Safari AND Chrome-for-iOS, which is WebKit under the hood) render
   native date/time inputs at their own intrinsic width and IGNORE width:100%, so
   the Birthday field overflows the page. Resetting the appearance ON PHONES makes
   them honor width:100% + box-sizing like every other input, so they fit their
   box. Scoped to mobile so desktop keeps its native calendar-picker button. */
@media (max-width: 640px) {
  input[type=date], input[type=datetime-local], input[type=time], input[type=month] {
    -webkit-appearance: none; -moz-appearance: none; appearance: none;
    min-width: 0; max-width: 100%;
  }
  input[type=date]::-webkit-date-and-time-value { text-align: left; }
}
input:focus, textarea:focus, select:focus { outline: none; border-color: var(--muted); background: var(--card); color: var(--ink); }
::placeholder { color: var(--muted); opacity: 1; }
textarea { resize: vertical; }

/* Placeholder overlay for native date inputs. They ignore the `placeholder`
   attribute, and mobile resets their appearance (see the ≤640px rule above),
   which strips the mm/dd/yyyy hint and leaves an empty field looking blank.
   Wrap the input in .date-ph and drop a <span class="ph"> after it; a small
   per-page script toggles .filled to hide the hint once a value is set. Desktop
   keeps its own native hint, so the overlay is mobile-only. Also used by the
   Activity (.act-dates) and games (.gip-date) filters via their own scoping. */
.date-ph { position: relative; display: block; }
.date-ph > input[type=date] { width: 100%; }
.date-ph > .ph { position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  color: var(--muted); pointer-events: none; }
/* WebKit/Blink keep painting mm/dd/yyyy even with the appearance reset, so it
   bleeds through our overlay (iOS Safari, which blanks it, is the exception).
   Make the native text transparent while empty+unfocused so only the overlay
   shows; the real value returns the instant the field is filled or focused. */
.date-ph:not(.filled) > input[type=date]::-webkit-datetime-edit { color: transparent; }
.date-ph > input[type=date]:focus::-webkit-datetime-edit { color: var(--ink); }
.date-ph.filled > .ph, .date-ph > input[type=date]:focus ~ .ph { display: none; }

/* The feed has no title bar, so its refresh button gets a slim right-aligned row. */
.feed-top { display: flex; justify-content: flex-end; padding: 2px 12px 0; }
@media (min-width: 641px) { .feed-top { padding-right: 0; } }
/* Post detail: the back-link row above the card. */
.detail-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.detail-top .back-link { margin-bottom: 0; }

/* PWA install prompt (partials/pwa_install.html) */
.pwa-install[hidden], .pwa-modal[hidden] { display: none; }   /* author display must not beat the hidden attr */
/* Show only the step-set that matches the browser (mode set in JS). */
.pwa-modal .pwa-steps { display: none; }
.pwa-modal.mode-ios-safari .pwa-steps-ios-safari,
.pwa-modal.mode-ios-other .pwa-steps-ios-other,
.pwa-modal.mode-other .pwa-steps-other { display: flex; }
.pwa-install { display: flex; align-items: center; gap: 12px; margin: 0 0 14px;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: 12px; background: var(--card); }
.pwa-install .pwa-logo { width: 40px; height: 40px; border-radius: 9px; flex: none; }
.pwa-install .pwa-body { flex: 1; min-width: 0; }
.pwa-install .pwa-title { font-weight: 700; font-size: .92rem; }
.pwa-install .pwa-sub { font-size: .8rem; color: var(--muted); }
.pwa-install .pwa-x { flex: none; background: none; border: 0; color: var(--muted);
  font-size: 1rem; cursor: pointer; padding: 4px 6px; line-height: 1; }
.pwa-modal { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center;
  justify-content: center; padding: 20px; background: rgba(0, 0, 0, .5); }
.pwa-modal-card { background: var(--card); color: var(--ink); border-radius: 16px;
  max-width: 340px; width: 100%; padding: 22px; text-align: center; }
.pwa-modal-card .pwa-logo-lg { width: 64px; height: 64px; border-radius: 14px; margin-bottom: 10px; }
.pwa-modal-card .pwa-title { font-weight: 700; font-size: 1.05rem; margin-bottom: 14px; }
.pwa-steps { text-align: left; margin: 0 0 16px; padding-left: 20px; display: flex;
  flex-direction: column; gap: 9px; font-size: .9rem; }
.pwa-steps .pwa-share { display: inline-flex; vertical-align: middle; color: var(--brand); }

/* Pull-to-refresh spinner (see the #ptr block in base.html). */
@keyframes fh-spin { to { transform: rotate(360deg); } }

/* In-progress game row — shared by the Our Family roll-up (.games-ip-list) and
   the per-opponent games hub, so both list unfinished matches identically. Each
   row leads with the game's emoji icon (GAME_META), not a text title. */
.games-ip-list { display: flex; flex-direction: column; gap: 6px; }
.gip-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  border-radius: 10px; border: 1px solid var(--line); background: var(--card); }
.gip-row:hover { background: var(--bg); text-decoration: none; }
.gip-badge { flex: none; width: 34px; height: 34px; display: inline-flex; align-items: center;
  justify-content: center; font-size: 1.15rem; line-height: 1; border-radius: 10px;
  background: var(--bg); border: 1px solid var(--line); }
.gip-vs { flex: 1; display: flex; align-items: center; gap: 8px; min-width: 0; }
.gip-who { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.gip-turn { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Running score, for the games that keep one across hands (canasta). It WRAPS
   rather than ellipsising: a side's name is the only thing that says whose
   number it is, so a cut-off one is no score at all. The gap is the separator
   — a "·" between them ends up dangling at the end of a wrapped line. Only
   YOUR side is bold, so which number is yours reads at a glance. */
.gip-score { display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px;
  color: var(--muted); margin-top: 1px; }
.gip-score b { font-weight: 400; }
.gip-score b.me { color: var(--text); font-weight: 700; }
.gip-opp-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gip-when { flex: none; }
.gip-go { flex: none; display: inline-flex; align-items: center; gap: 4px; font-size: .78rem;
  font-weight: 700; color: var(--brand); }
.gip-empty { padding: 16px 4px; text-align: center; }
@media (max-width: 480px) { .gip-when { display: none; } }
label { display: block; margin-bottom: 12px; font-weight: 600; font-size: .9rem; }
label .helptext, label input, label textarea, label select { font-weight: 400; }
form p { margin: 0 0 12px; }
form p label { font-weight: 600; }
.errorlist { color: var(--heart); list-style: none; padding: 0; margin: 4px 0; font-size: .85rem; }
.form-actions { margin-top: 8px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }

/* ---- Avatars ---- */
/* flex:none keeps avatars from being squished into ovals inside flex rows
   (activity/comment/member lists, message meta, etc.). aspect-ratio is a
   belt-and-suspenders guard so the box stays square even if a container
   constrains one dimension. */
.avatar-sm { width: 26px; height: 26px; border-radius: 50%; object-fit: cover; flex: none; aspect-ratio: 1; }
.avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; background: var(--line); flex: none; aspect-ratio: 1; }
.avatar-lg { width: 90px; height: 90px; border-radius: 50%; object-fit: cover; background: var(--line); flex: none; aspect-ratio: 1; }
.avatar-fallback { display: inline-flex; align-items: center; justify-content: center;
  background: linear-gradient(135deg, #405de6, #833ab4, #e1306c); color: #fff; border-radius: 50%; font-weight: 700;
  flex: none; aspect-ratio: 1; }

/* Live "online now" dot overlaid on an avatar (opt-in via avatar partial's
   presence=1). Hidden until base.html's presence tracker adds .on. */
.avatar-wrap { position: relative; display: inline-flex; flex: none; }
.online-dot { position: absolute; bottom: 1px; right: 1px; width: 13px; height: 13px; box-sizing: border-box;
  border-radius: 50%; background: #16a34a; border: 2px solid var(--card); display: none; }
.online-dot.on { display: block; }
.avatar-wrap:has(.avatar-sm) .online-dot { width: 10px; height: 10px; border-width: 1.5px; }
.avatar-wrap:has(.avatar-lg) .online-dot { width: 20px; height: 20px; bottom: 5px; right: 5px; }

/* ---- Flash messages ---- */
.flashes { margin: 0 12px 12px; }
@media (min-width: 641px) { .flashes { margin-left: 0; margin-right: 0; } }
.flash { padding: 10px 14px; border-radius: 8px; margin-bottom: 8px; font-size: .9rem; border: 1px solid var(--line); background: var(--card); }
.flash-success { border-color: #b7e1cd; background: #eafaf1; }
.flash-error { border-color: #f0c4c8; background: #fdecee; color: #a11; }

/* ---- Stories bar ---- */
.story-bar {
  display: flex; gap: 14px; overflow-x: auto; padding: 14px 12px; margin: 0 0 16px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  scrollbar-width: none;
}
.story-bar::-webkit-scrollbar { display: none; }
@media (min-width: 641px) { .story-bar { margin: 0 0 16px; } }
.story { flex: 0 0 auto; width: 66px; text-align: center; }
.story-ring {
  width: 62px; height: 62px; border-radius: 50%; padding: 3px; margin: 0 auto 4px;
  background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888);
  display: flex; align-items: center; justify-content: center;
}
.story-ring.seen { background: var(--line); }
.story-ring img, .story-ring .avatar-fallback {
  width: 100%; height: 100%; border-radius: 50%; object-fit: cover;
  border: 2px solid var(--card); background: var(--card);
}
.story-ring .avatar-fallback { display: flex; align-items: center; justify-content: center; font-size: 1.3rem; }
.story .story-name { font-size: .72rem; color: var(--ink); display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.story.add .story-ring { background: var(--line); position: relative; }
.story.add .plus {
  position: absolute; bottom: 0; right: 0; width: 22px; height: 22px; border-radius: 50%;
  background: var(--brand); color: #fff; border: 2px solid var(--card);
  display: flex; align-items: center; justify-content: center; font-size: 1rem; line-height: 1;
}
/* Your own story bubble when a story already exists: ring opens the viewer,
   the + badge (a separate link) adds another story. */
.story.mine { position: relative; display: inline-block; }
.story.mine .story-open { display: block; }
.story.mine .plus {
  position: absolute; top: 40px; right: 4px; width: 22px; height: 22px; border-radius: 50%;
  background: var(--brand); color: #fff; border: 2px solid var(--card);
  display: flex; align-items: center; justify-content: center; line-height: 1;
}

/* ---- Instagram-style post ---- */
.ig-post { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); margin: 0 0 16px; overflow: hidden; }
@media (max-width: 640px) { .ig-post { border-left: none; border-right: none; border-radius: 0; margin-bottom: 8px; } }
.ig-head { display: flex; align-items: center; gap: 10px; padding: 10px 14px; }
.ig-head .name { font-weight: 600; font-size: .9rem; }
.ig-head .sub { font-size: .78rem; color: var(--muted); }
.ig-head .menu { margin-left: auto; display: flex; gap: 10px; align-items: center; }
.ig-media { position: relative; background: #000; display: flex; justify-content: center; }
.ig-media img { width: 100%; max-height: 720px; object-fit: contain; display: block; }
.ig-media .heart-burst {
  position: absolute; inset: 0; margin: auto; width: 90px; height: 90px; color: #fff;
  opacity: 0; pointer-events: none; font-size: 90px; display: flex; align-items: center; justify-content: center;
  filter: drop-shadow(0 2px 8px rgba(0,0,0,.4));
}
.ig-media .heart-burst.animate { animation: heartpop .9s ease; }
/* Multi-photo carousel: horizontal scroll-snap of full-width slides. */
.ig-carousel { display: flex; width: 100%; overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.ig-carousel::-webkit-scrollbar { display: none; }
.ig-slide { flex: 0 0 100%; scroll-snap-align: center; display: flex; align-items: center; justify-content: center; }
.ig-media video, .ig-slide video { width: 100%; max-height: 720px; object-fit: contain; display: block; background: #000; }
/* contain (not cover): show the whole photo within the frame, never crop.
   Slides share the carousel height (tallest photo); shorter ones are centered
   with the black backdrop showing. Cropping only happens if the user crops in
   the editor. */
.ig-slide img { width: 100%; max-height: 720px; object-fit: contain; display: block; }
/* While a press-and-hold is in flight, instagram.js puts `holding` on <html>:
   the browser's own long-press-to-select-text must not run alongside ours, or
   iOS drops selection handles and a Copy/Look Up bar over the picker or sheet
   the hold just opened. Form fields keep their selection — suppressing those
   would fight the caret in the message composer. */
html.holding *:not(input):not(textarea):not([contenteditable]):not([contenteditable] *) {
  -webkit-user-select: none !important; user-select: none !important;
  -webkit-touch-callout: none !important;
}
/* Neither pop-up's own labels are ever worth selecting. */
.react-picker, .save-sheet {
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}

/* Press-and-hold on a slide opens our own save sheet (instagram.js), so the
   iOS callout menu must not pop first — it would offer to save the shrunken
   copy in src instead of the full-res original. */
.ig-slide[data-save-url] img, .ig-slide[data-save-url] video {
  -webkit-touch-callout: none; -webkit-user-select: none; user-select: none;
}
.save-sheet { position: fixed; inset: 0; z-index: 200; background: rgba(0,0,0,.35);
  display: flex; align-items: flex-end; justify-content: center; animation: sheetfade .15s ease; }
.save-sheet-card {
  width: 100%; max-width: 420px; margin: 0 8px calc(8px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; gap: 8px; animation: sheetup .18s ease;
}
.save-sheet-card button {
  width: 100%; padding: 15px; border: none; border-radius: 14px; cursor: pointer;
  background: var(--card, #fff); color: var(--ink); font-size: 1rem; font-weight: 600;
  box-shadow: 0 6px 22px rgba(0,0,0,.20);
}
.save-sheet-card .save-sheet-go { color: #0095f6; }
.save-sheet-card .save-sheet-go[disabled] { color: var(--muted); cursor: default; }
.save-sheet-card .save-sheet-cancel { font-weight: 500; }
@keyframes sheetfade { from { opacity: 0; } to { opacity: 1; } }
@keyframes sheetup { from { transform: translateY(20px); } to { transform: none; } }
.ig-count { position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,.6); color: #fff; font-size: .72rem; font-weight: 600; padding: 3px 9px; border-radius: 999px; pointer-events: none; z-index: 1; }
.ig-dots { position: absolute; left: 0; right: 0; bottom: 8px; display: flex; justify-content: center; gap: 5px; pointer-events: none; z-index: 1; }
.ig-dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,.55); transition: background .15s ease, transform .15s ease; }
.ig-dot.active { background: #0095f6; transform: scale(1.15); }
/* Edit-post: existing photos, each with an edit + remove toggle. */
.edit-gallery { display: flex; flex-wrap: wrap; gap: 10px; }
.edit-thumb { position: relative; width: 100px; }
.edit-thumb > img, .edit-thumb > video { width: 100px; height: 100px; object-fit: cover; border-radius: 8px; display: block; background: #000; }
.edit-thumb-tools { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 5px; }
.edit-thumb .ie-mini { position: static; }
/* Remove control = a trash-can toggle (a hidden checkbox still carries the state
   to the form). Grey when idle, red once marked; the thumb dims to match. */
.edit-thumb-rm { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: 8px; color: var(--muted); cursor: pointer; background: var(--card, #fff); border: 1px solid var(--line); }
.edit-thumb-rm input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }
.edit-thumb-rm:has(input:checked) { color: #fff; background: var(--heart, #ed4956); border-color: var(--heart, #ed4956); }
.edit-thumb:has(input:checked) > img,
.edit-thumb:has(input:checked) > video { outline: 2px solid var(--heart, #ed4956); opacity: .45; }
.edit-thumb.edited > img { outline: 2px solid #0095f6; }
@keyframes heartpop {
  0% { opacity: 0; transform: scale(.2); }
  15% { opacity: 1; transform: scale(1.2); }
  30% { transform: scale(.95); }
  45% { transform: scale(1); }
  80% { opacity: 1; }
  100% { opacity: 0; transform: scale(1); }
}
.ig-actions { display: flex; align-items: center; gap: 14px; padding: 8px 14px 4px; }
.ig-action { background: none; border: none; padding: 0; cursor: pointer; font-size: 1.6rem; line-height: 1; color: var(--ink); display: inline-flex; align-items: center; transition: transform .12s ease; }
.ig-action:active { transform: scale(1.2); }
.ig-action.like.liked { color: var(--heart); }
.ig-action.share { margin-left: auto; }

/* ---- Press-and-hold reaction picker on the post heart ---- */
.like-wrap { position: relative; display: inline-flex; }
/* Don't let a long-press select text or pop the iOS callout menu. */
.ig-action.like { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
/* The heart shows one of three faces: outline (none), solid red (❤️ like),
   or the chosen emoji glyph (any other reaction). */
.ig-action.like .i-emoji { display: none; font-size: 1.5rem; line-height: 1; }
.ig-action.like.reacted .i-outline,
.ig-action.like.reacted .i-solid { display: none; }
.ig-action.like.reacted .i-emoji { display: inline-flex; }

.react-picker {
  position: absolute; bottom: calc(100% + 8px); left: -6px;
  display: flex; gap: 2px; padding: 5px 8px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 999px; box-shadow: 0 6px 22px rgba(0,0,0,.20);
  z-index: 40; animation: reactpop .15s ease;
}
/* Flipped underneath by instagram.js when there isn't room above. */
.react-picker.below { bottom: auto; top: calc(100% + 8px); }
@keyframes reactpop { from { opacity: 0; transform: translateY(6px) scale(.92); } }
.react-pick {
  background: none; border: none; cursor: pointer; padding: 3px;
  font-size: 1.65rem; line-height: 1; border-radius: 50%;
  transition: transform .12s ease;
}
.react-pick:hover, .react-pick:focus { transform: scale(1.3) translateY(-2px); outline: none; }
.react-pick.active { background: var(--bg); }
.likes-wrap { position: relative; }
.likes-count {
  display: inline-block; padding: 2px 14px; font-weight: 600; font-size: .9rem;
  background: none; border: 0; color: var(--ink); cursor: pointer; text-align: left;
  font-family: inherit;
}
.likes-count:hover { text-decoration: underline; }
/* "Who reacted" dropdown */
.reactors-pop {
  position: absolute; left: 10px; top: 100%; z-index: 40; margin-top: 4px;
  min-width: 210px; max-width: 280px; max-height: 260px; overflow-y: auto;
  background: var(--card); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 8px 30px rgba(0,0,0,.22); padding: 6px;
}
.reactors-pop[hidden] { display: none; }
.reactor {
  display: flex; align-items: center; gap: 10px; padding: 6px 8px;
  border-radius: 8px; color: var(--ink); text-decoration: none;
}
.reactor:hover { background: var(--bg); }
.reactor-av {
  width: 32px; height: 32px; border-radius: 50%; object-fit: cover;
  flex: none; aspect-ratio: 1; border: 1px solid var(--line);
}
.reactor-av.fallback {
  display: inline-flex; align-items: center; justify-content: center;
  background: linear-gradient(135deg, #f09433, #e6683c 30%, #dc2743 60%, #cc2366);
  color: #fff; font-weight: 700; font-size: .85rem; border: 0;
}
.reactor-name {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-size: .88rem; font-weight: 500;
}
.reactor-emoji { flex: none; font-size: 1.05rem; }
.reactors-empty { padding: 12px 10px; color: var(--muted); font-size: .85rem; text-align: center; }
.ig-caption { padding: 2px 14px 4px; font-size: .9rem; white-space: pre-wrap; }
.ig-caption .name { font-weight: 600; margin-right: 6px; }
.ig-comments { padding: 6px 14px 8px; display: flex; flex-direction: column; gap: 12px; }
.ig-comments:empty { display: none; }
.ig-comments .view-all { color: var(--muted); font-size: .88rem; display: inline-block; cursor: pointer; }
/* The folded-away older comments get their own flex column so they keep the
   same 12px rhythm as the always-visible ones once expanded. */
.ig-comments .older-comments { display: flex; flex-direction: column; gap: 12px; }
.ig-comments .older-comments[hidden] { display: none; }
.ig-comments .comment { display: flex; align-items: flex-start; gap: 10px; font-size: .9rem; }
.ig-comments .comment .comment-av { flex: none; }
.ig-comments .comment .avatar-sm { width: 30px; height: 30px; }
.comment-body { flex: 1; min-width: 0; }
/* pre-wrap so a comment written over several lines reads back that way. Safe
   here because the name link and the text sit on ONE template line — any
   indentation inside this div would otherwise show up as real whitespace. */
.comment-text { line-height: 1.35; overflow-wrap: anywhere; white-space: pre-wrap; }
.comment-text .name { font-weight: 600; }
.comment-meta { display: flex; align-items: center; gap: 14px; margin-top: 3px; font-size: .72rem; color: var(--muted); }
/* The count sits in the meta row, so it drops the .likes-count box padding and
   inherits the muted meta type. It disappears entirely at zero reactions. */
.comment-likecount { padding: 0; font-size: inherit; color: inherit; font-weight: 400; }
.comment-likecount:empty { display: none; }
.comment-meta .likes-wrap { display: inline-block; }
/* Edit-your-own-comment. The Edit link sits in the meta row; the inline editor
   is hidden until JS adds .editing to the row (and hides the static text/meta). */
.comment-edit-btn { background: none; border: none; padding: 0; font: inherit;
  font-size: .72rem; font-weight: 600; color: var(--muted); cursor: pointer; }
.comment-edit-btn:hover { color: var(--ink); }
.comment-edited { font-size: .72rem; color: var(--muted); }
.comment-editform { display: none; gap: 6px; align-items: center; flex-wrap: wrap; margin-top: 4px; }
.comment-editinput { flex: 1 1 auto; min-width: 0; margin: 0; padding: 5px 9px; font: inherit; font-size: .88rem; }
.comment-editacts { display: flex; gap: 6px; flex: none; }
.comment.editing .comment-text,
.comment.editing .comment-meta { display: none; }
.comment.editing .comment-editform { display: flex; }
.comment.editing .like-wrap { display: none; }
.comment-like { background: none; border: none; padding: 0; cursor: pointer; color: var(--ink); flex: none; display: inline-flex; align-items: center; justify-content: center; min-width: 30px; min-height: 30px; }
.comment-like .i-solid, .comment-like .i-emoji { display: none; }
.comment-like.liked .i-outline { display: none; }
.comment-like.liked .i-solid { display: inline-flex; color: var(--heart); }
/* Reacted with something other than a heart: the glyph replaces the icon. */
.comment-like.reacted .i-outline, .comment-like.reacted .i-solid { display: none; }
.comment-like.reacted .i-emoji { display: inline-flex; font-size: 1.25rem; line-height: 1; }
.comment-like .ic { width: 20px; height: 20px; }
/* A comment's picker is smaller than a post's — it sits in a tighter row. */
.comment .react-picker { left: auto; right: -6px; padding: 4px 6px; }
.comment .react-pick { font-size: 1.25rem; padding: 2px; }
.ig-time { padding: 2px 14px 12px; font-size: .68rem; color: var(--muted); text-transform: uppercase; letter-spacing: .02em; }
.ig-addcomment { display: flex; align-items: center; border-top: 1px solid var(--line); padding: 4px 14px; }
.ig-addcomment input[type=text] { border: none; background: none; padding: 10px 0; margin: 0; }
.ig-addcomment input[type=text]:focus { background: none; }
.ig-addcomment button { color: var(--brand); font-weight: 600; background: none; border: none; cursor: pointer; font-size: .9rem; }
/* Optional emoji reactions, tucked behind a details toggle to keep the family
   flavour without cluttering the Instagram layout. */
.more-reacts { padding: 0 14px 8px; }
.more-reacts summary { cursor: pointer; color: var(--muted); font-size: .8rem; list-style: none; }
.more-reacts summary::-webkit-details-marker { display: none; }
.reactions { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; padding-top: 8px; }
.react-btn { background: var(--bg); border: 1px solid var(--line); border-radius: 999px; cursor: pointer; padding: 3px 9px; font-size: 1rem; }
.react-btn.active { background: #e7f0fe; border-color: var(--brand); }
.react-count { color: var(--muted); font-size: .82rem; margin-left: auto; }
.reactions-sm { padding-top: 4px; gap: 4px; }
.reactions-sm .react-btn { padding: 1px 7px; font-size: .82rem; }

/* legacy post classes still used by post_edit preview etc. */
.post-image { border-radius: 8px; margin-bottom: 12px; }
.post-text { white-space: pre-wrap; margin: 4px 0 12px; }
.comment .bubble { background: var(--bg); border-radius: 12px; padding: 7px 12px; }
.comment .bubble .name { font-weight: 600; font-size: .85rem; }

/* ---- Profile ---- */
.profile-header { display: flex; align-items: center; gap: 22px; padding: 8px 16px 16px; }
.profile-header .avatar-lg { flex: none; }
.profile-header .avatar-open { flex: none; display: inline-flex; text-decoration: none; border-radius: 50%; }
.profile-header .avatar-open:active { transform: scale(.97); }
.profile-id { flex: 1; min-width: 0; }
.profile-id .uname { font-size: 1.4rem; font-weight: 400; margin-bottom: 12px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.profile-stats { display: flex; gap: 26px; margin-bottom: 10px; }
.profile-stats .stat b { font-weight: 700; }
.profile-bio { padding: 0 16px 16px; font-size: .9rem; }
.profile-bio .fullname { font-weight: 700; }
.profile-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px; border-top: 1px solid var(--line); padding-top: 3px; }
.profile-grid .tile { position: relative; aspect-ratio: 1; overflow: hidden; background: var(--line); }
/* Square fallback for browsers without aspect-ratio (older iOS/WebView): the
   padding-top ratio box holds the height, and the image is absolutely laid over
   it — otherwise height:100% resolves against an auto-height parent and the
   tiles collapse. */
.profile-grid .tile::before { content: ""; display: block; padding-top: 100%; }
.profile-grid .tile img, .profile-grid .tile video { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
/* Play badge on a video cover — a white triangle in a translucent disc. */
.profile-grid .tile .tile-play { position: absolute; top: 6px; right: 6px; width: 22px; height: 22px; border-radius: 50%; background: rgba(0,0,0,.5); pointer-events: none; }
.profile-grid .tile .tile-play::after { content: ""; position: absolute; top: 50%; left: 54%; transform: translate(-50%,-50%); border-style: solid; border-width: 5px 0 5px 8px; border-color: transparent transparent transparent #fff; }
.profile-grid .tile .tile-cap { position: absolute; inset: 0; background: rgba(0,0,0,.35); color: #fff; opacity: 0; display: flex; align-items: center; justify-content: center; gap: 12px; font-weight: 600; font-size: .85rem; transition: opacity .15s; padding: 8px; text-align: center; }
.profile-grid .tile:hover .tile-cap { opacity: 1; }
.profile-grid .tile .tile-multi { position: absolute; top: 6px; right: 6px; color: #fff; filter: drop-shadow(0 1px 2px rgba(0,0,0,.5)); pointer-events: none; display: inline-flex; }
.profile-tabs { display: flex; justify-content: center; gap: 40px; border-top: 1px solid var(--line); margin-top: 8px; }
.profile-tabs a { padding: 12px; font-size: .75rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 600; border-top: 1px solid transparent; margin-top: -1px; }
.profile-tabs a.active { color: var(--ink); border-top-color: var(--ink); }

/* ---- Stories full-screen viewer ---- */
.story-viewer { position: fixed; inset: 0; background: #000; z-index: 60; display: flex; align-items: center; justify-content: center; }
.story-stage { position: relative; width: 100%; max-width: 440px; height: 100%; max-height: 100vh; background: #000; display: flex; align-items: center; justify-content: center; }
.story-stage img, .story-stage video { max-width: 100%; max-height: 100%; object-fit: contain; }
/* Story EDIT page preview — a centered, height-capped box so a portrait video
   (or photo) fits on screen with its controls instead of overflowing the card. */
.story-edit-preview { display: flex; justify-content: center; align-items: center; margin: 8px 0 14px; background: #000; border-radius: 10px; overflow: hidden; }
.story-edit-preview img, .story-edit-preview video { max-width: 100%; max-height: 60vh; object-fit: contain; display: block; }
.story-slide .story-img video { width: 100%; height: 100%; object-fit: contain; }
/* One slide per story; the viewer JS shows exactly one at a time. */
.story-slide { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; }
.story-slide .story-img { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.story-progress { position: absolute; top: 8px; left: 8px; right: 8px; display: flex; gap: 4px; z-index: 3; }
/* Sound toggle for video stories — sits just under the top bar, right side. */
.story-mute { position: absolute; top: 54px; right: 12px; z-index: 6; width: 38px; height: 38px; border: none; border-radius: 50%; background: rgba(0,0,0,.45); color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
.story-mute[hidden] { display: none; }
.story-progress .bar { flex: 1; height: 3px; background: rgba(255,255,255,.35); border-radius: 3px; overflow: hidden; }
.story-progress .bar > span { display: block; height: 100%; width: 0; background: #fff; }
.story-topbar { position: absolute; top: 18px; left: 10px; right: 10px; display: flex; align-items: center; gap: 10px; z-index: 3; color: #fff; }
.story-topbar .avatar-sm { border: 1px solid rgba(255,255,255,.6); }
.story-topbar .name { font-weight: 600; font-size: .9rem; }
.story-author { display: inline-flex; align-items: center; gap: 10px; color: inherit; text-decoration: none; }
.story-author:hover .name { text-decoration: underline; }
.story-topbar .t { font-size: .78rem; opacity: .8; }
.story-top-actions { margin-left: auto; display: flex; align-items: center; gap: 14px; }
.story-top-actions form { display: inline-flex; }
.s-ic { background: none; border: none; color: #fff; cursor: pointer; line-height: 1; display: inline-flex; align-items: center; padding: 0; }
/* Caption + reactions/replies share one bottom-anchored column so opening the
   engagement panel pushes the caption UP instead of covering it. */
.story-bottom { position: absolute; left: 0; right: 0; bottom: 0; z-index: 5; display: flex; flex-direction: column; }
.story-cap { text-align: center; color: #fff; padding: 0 20px 10px; font-size: 1rem; text-shadow: 0 1px 4px rgba(0,0,0,.6); }
.story-nav { position: absolute; top: 56px; bottom: 108px; width: 40%; z-index: 2; cursor: pointer; }
.story-nav.prev { left: 0; } .story-nav.next { right: 0; }

/* ---- Story reactions + replies (bottom bar) ---- */
.story-reply-bar {
  padding: 10px 14px calc(14px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; gap: 10px;
  background: linear-gradient(to top, rgba(0,0,0,.55), rgba(0,0,0,0)); }
.story-react-row { display: flex; justify-content: space-between; gap: 4px; }
.sr-emoji { background: none; border: none; cursor: pointer; font-size: 1.9rem; line-height: 1;
  padding: 2px 4px; border-radius: 50%; transition: transform .12s ease; }
.sr-emoji:hover, .sr-emoji:focus { transform: scale(1.2); outline: none; }
.sr-emoji.active { transform: scale(1.15); filter: drop-shadow(0 0 6px rgba(255,255,255,.7)); }
.sr-emoji.pop { animation: reactpop .3s ease; }
.story-reply { display: flex; align-items: center; gap: 8px; }
.story-reply input { flex: 1; background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.5);
  color: #fff; border-radius: 999px; padding: 10px 16px; font-size: .95rem; }
.story-reply input::placeholder { color: rgba(255,255,255,.75); }
.story-reply input:focus { background: rgba(255,255,255,.2); outline: none; }
.story-reply button { background: none; border: none; color: #fff; cursor: pointer; display: inline-flex; padding: 4px; }
.story-sent { color: #fff; text-align: center; font-size: .85rem; opacity: .9; }

/* Author's own-story engagement (private, like IG). */
.story-engage {
  padding: 10px 14px calc(14px + env(safe-area-inset-bottom));
  background: linear-gradient(to top, rgba(0,0,0,.6), rgba(0,0,0,0)); color: #fff; }
.story-engage summary { cursor: pointer; list-style: none; font-size: .9rem; text-align: center;
  padding: 8px; border-radius: 999px; background: rgba(255,255,255,.12); }
.story-engage summary::-webkit-details-marker { display: none; }
.engage-body { margin-top: 10px; max-height: 40vh; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
.engage-reacts { display: flex; flex-wrap: wrap; gap: 6px 12px; }
.engage-reacts .er { font-size: .88rem; }
.engage-reply { font-size: .9rem; }
.engage-reply b { margin-right: 6px; }
/* Edit your own story reply, and reply to a reply (threads), inline in the
   engagement panel. */
.er-edit, .er-reply { background: none; border: none; padding: 0 0 0 6px; font: inherit;
  font-size: .72rem; font-weight: 600; color: var(--muted); cursor: pointer; }
.er-edit:hover, .er-reply:hover { color: var(--ink); }
.er-edited { font-size: .72rem; color: var(--muted); }
/* Keeps the line breaks a reply was written with. inline-block because the span
   sits inline after the author's name — a plain inline box ignores pre-wrap's
   breaks at the start and end of its own content. */
.er-text { white-space: pre-wrap; overflow-wrap: anywhere; display: inline-block;
  vertical-align: top; }
.er-editform, .er-replyform { display: none; gap: 6px; align-items: center; flex-wrap: wrap; margin-top: 4px; }
.er-replyform.open { display: flex; }
.er-editinput, .er-replyinput { flex: 1 1 auto; min-width: 0; margin: 0; padding: 5px 9px; font: inherit; font-size: .88rem; }
.er-editacts { display: flex; gap: 6px; flex: none; }
/* Child combinator so an editing top-level reply never hides its thread. */
.engage-reply.editing > .er-text,
.engage-reply.editing > .er-edit,
.engage-reply.editing > .er-edited { display: none; }
.engage-reply.editing > .er-editform { display: flex; }
/* Threaded sub-replies, indented under their top-level reply. */
.engage-subreplies { display: flex; flex-direction: column; gap: 6px; }
.engage-subreply { margin-left: 16px; padding-left: 10px; border-left: 2px solid var(--line, rgba(128,128,128,.25)); }
.engage-thread .er-reply { margin-top: 2px; }
/* @-mention composer inside the compact edit/reply rows: behave like the input
   it replaced (mentions.js swaps [data-mentions] inputs for a .mention-editor). */
.er-editform .mention-editor, .er-replyform .mention-editor {
  flex: 1 1 auto; width: auto; min-height: 32px; max-height: 120px;
  padding: 5px 9px; font-size: .88rem; }
/* The bottom reply bar sits over the story image — keep the editor translucent
   white like the pill input it replaced. */
.story-reply .mention-editor {
  flex: 1; width: auto; min-height: 0; max-height: 96px;
  background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.5);
  color: #fff; border-radius: 999px; padding: 10px 16px; font-size: .95rem; }
.story-reply .mention-editor:focus { background: rgba(255,255,255,.2); border-color: rgba(255,255,255,.5); }
.story-reply .mention-editor:empty::before { color: rgba(255,255,255,.75); }

/* ---- Grids (albums/events/members) ---- */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
.grid > .card { margin: 0; }
.gallery { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px; }
.gallery .tile { position: relative; aspect-ratio: 1; overflow: hidden; border-radius: 4px; background: var(--line); }
.gallery .tile::before { content: ""; display: block; padding-top: 100%; }   /* square fallback — see .profile-grid .tile */
.gallery .tile img, .gallery .tile video { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
.album-cover { aspect-ratio: 1; object-fit: cover; border-radius: 8px; width: 100%; background: var(--line); }

/* ---- Members ---- */
.member { display: flex; align-items: center; gap: 12px; }
.pill { display: inline-block; background: var(--bg); border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px; font-size: .78rem; color: var(--muted); }

/* ---- Events ---- */
.event-date { display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: var(--ink); color: #fff; border-radius: 10px; width: 60px; height: 60px; flex: none; }
.event-date .m { font-size: .68rem; text-transform: uppercase; letter-spacing: .05em; }
.event-date .d { font-size: 1.5rem; font-weight: 800; line-height: 1; }
.event-row { display: flex; gap: 14px; align-items: center; }

/* ---- Messaging ---- */
.thread-list .thread-row { display: flex; align-items: center; border-radius: 10px; }
.thread-list .thread-row:hover { background: var(--bg); }
.thread-list .thread-row.unread { background: #e7f0fe; }
.thread-list .thread { flex: 1; display: flex; gap: 12px; align-items: center; padding: 12px; border-radius: 10px; min-width: 0; }
.thread-list .thread:hover { text-decoration: none; }
/* Unread count sits INLINE at the row's end. The generic corner .badge
   (top:-5px/right:-8px) pokes past the row and gets clipped by the swipe
   wrapper's overflow:hidden — so pin it back inside the bounds here. */
.thread-list .thread .badge { position: static; top: auto; right: auto; flex: none; }
.thread-list .thread-del { padding: 0 10px; display: flex; align-items: center; }
/* Inbox swipe-to-act reuses the Activity feed mechanic (see .act-swipe): the row
   slides over two action panes. Its left action is Leave (amber) where the feed
   has Archive, so it hangs off the feed's `armed-archive` state; right stays
   Delete. The row must be OPAQUE or the buttons show through mid-swipe — and, as
   with .act-row, a hard light hex would go invisible under a phone's auto dark
   mode, so we sit on var(--card) and layer the unread tint as a gradient. */
.act-swipe .thread-row { position: relative; z-index: 1; border-radius: 12px;
  background: var(--card); }
.act-swipe .thread-row.unread {
  background: linear-gradient(rgba(0, 149, 246, .10), rgba(0, 149, 246, .10)), var(--card); }
.act-act.leave { background: #f59e0b; }
.act-swipe.armed-archive .act-actions.right .act-act.leave { flex: 1; width: auto; }
.act-swipe.armed-delete .act-actions.left .act-act.leave { width: 0; padding: 0; overflow: hidden; }
.select-tile { display: flex; align-items: center; gap: 10px; border: 1px solid var(--line); }
.select-tile input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--brand); flex: none; }
.select-tile:has(input:checked) { border-color: var(--brand); background: #e7f0fe; }
details.rename summary { list-style: none; cursor: pointer; }
details.rename summary::-webkit-details-marker { display: none; }
details.rename input[type="text"] { min-width: 0; }
.messages { display: flex; flex-direction: column; gap: 8px; margin: 14px 0; }

/* Chat thread = a fixed-height column: the message list scrolls INSIDE #messages
   and the composer stays pinned. Previously the whole body scrolled, so in the
   installed PWA the messages spilled past the bottom and the fixed nav rubber-
   banded. 100dvh tracks the real visible height on mobile. */
.container.thread-page { margin: 8px auto 0; padding-bottom: 0; min-height: 0;
  height: calc(100dvh - var(--topbar-h) - var(--bottomnav-h) - env(safe-area-inset-bottom, 0px) - 14px);
  display: flex; flex-direction: column; }
.thread-page > .card { flex: 1; min-height: 0; margin-bottom: 0;
  display: flex; flex-direction: column; overflow: hidden; }
/* The header is already pinned as the first (flex:none) item of the fixed-height
   card, so drop the old body-scroll sticky — its `top` offset was reserving a
   gap above the header now that the card (overflow:hidden) is the scroll root. */
.thread-page .thread-bar { flex: none; position: static; top: auto; }
.thread-page .messages { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; margin: 8px 0; }
.thread-page .seen-row, .thread-page .typing-row, .thread-page .msg-form { flex: none; }
.msg { max-width: 72%; padding: 8px 13px; border-radius: 18px; }
.msg.them { background: var(--bg); border: 1px solid var(--line); align-self: flex-start; }
.msg.me { background: var(--brand); color: #fff; align-self: flex-end; }
.msg .who { font-size: .72rem; opacity: .8; margin-bottom: 2px; }
/* The message itself keeps the line breaks the sender typed — Enter in the
   composer is a break now, so a message written as a list has to arrive as
   one. pre-wrap only; the template puts the text on a single line, so there is
   no stray indentation for it to render. */
.msg-body { white-space: pre-wrap; overflow-wrap: anywhere; }
/* Reactions on a chat bubble: the heart and the count live in the bubble's meta
   row beside the timestamp, so nothing shifts the thread's layout. */
.msg-like { background: none; border: none; padding: 0; cursor: pointer; color: inherit;
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  min-width: 30px; min-height: 30px; }
.msg-like .i-solid, .msg-like .i-emoji { display: none; }
.msg-like.liked .i-outline { display: none; }
.msg-like.liked .i-solid { display: inline-flex; color: var(--heart); }
.msg-like.reacted .i-outline, .msg-like.reacted .i-solid { display: none; }
.msg-like.reacted .i-emoji { display: inline-flex; font-size: 1.25rem; line-height: 1; }
.msg-like .ic { width: 20px; height: 20px; }
.msg-likecount { padding: 0; font-size: inherit; color: inherit; font-weight: 400; }
.msg-likecount:empty { display: none; }
.msg .likes-wrap { display: inline-block; }
/* On your own (brand-colored) bubble a red heart still reads; keep it white. */
.msg.me .msg-like.liked .i-solid { color: #fff; }
/* The picker pops above the bubble and out of its meta row's small type. */
.msg .react-picker { left: auto; right: -6px; padding: 4px 6px; }
.msg .react-pick { font-size: 1.25rem; padding: 2px; }
/* The bubble sets a tiny font; the dropdown is its own surface. */
.msg .reactors-pop { left: auto; right: 0; font-size: .82rem; color: var(--ink);
  font-style: normal; text-align: left; }

.msg-form { display: flex; gap: 8px; }
.msg-form input { flex: 1; }
.msg-form .mention-editor { flex: 1; margin-top: 0; max-height: 140px; }
/* The thread's chrome — back link, title, Rename/Add/Leave — rides in one bar
   that sticks under the top nav while the messages scroll past beneath it. It
   bleeds out to the card's padding edge so no bubble shows around its sides,
   and sits below the nav's z-index so it tucks under rather than over it. */
.thread-bar {
  position: sticky; top: var(--topbar-h); z-index: 20;
  background: var(--card); border-bottom: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
  margin: -16px -16px 0; padding: 12px 16px 10px;
}
.thread-bar .back-link { padding-left: 0; margin-bottom: 8px; }
.thread-head { display: flex; align-items: flex-start; gap: 12px; }
.thread-tools { display: flex; flex-direction: column; gap: 8px; align-items: flex-end; flex: none; }
/* Cohesive pill buttons for the thread's Rename / Add people / Leave actions. */
.thread-tools .tool-btn { display: inline-flex; align-items: center; gap: 6px;
  background: transparent; color: var(--ink); border: 1px solid var(--line);
  border-radius: 999px; padding: 6px 13px; font-size: .8rem; font-weight: 600;
  cursor: pointer; list-style: none; white-space: nowrap; transition: background .15s, border-color .15s; }
.thread-tools .tool-btn::-webkit-details-marker { display: none; }
.thread-tools .tool-btn .ic { flex: none; }
.thread-tools .tool-btn:hover { background: var(--bg); }
.thread-tools details[open] > .tool-btn { background: var(--bg); border-color: var(--muted); }
.thread-tools .tool-btn.danger { color: var(--heart); border-color: rgba(237,73,86,.4); }
.thread-tools .tool-btn.danger:hover { background: rgba(237,73,86,.08); border-color: var(--heart); }
.thread-tools details { width: 260px; max-width: 60vw; }
.thread-tools summary { text-align: right; }
.thread-tools details[open] summary { margin-bottom: 2px; }
.add-people-list { display: flex; flex-direction: column; gap: 6px; max-height: 240px; overflow-y: auto; }
.add-people-list .select-tile { margin: 0; }

/* ---- Auth screens ---- */
.auth-wrap { max-width: 360px; margin: 6vh auto; }
.invited-by { display: flex; flex-direction: column; align-items: center; gap: 10px; margin: 10px 0 6px; }
.auth-wrap .brand-big { font-size: 2.6rem; font-weight: 700; color: var(--ink); text-align: center; display: block; margin-bottom: 16px;
  font-family: "Segoe Script", "Bradley Hand", "Snell Roundhand", cursive; }

/* ---- Relationships ---- */
.rel-list { display: flex; flex-direction: column; gap: 10px; }
.rel-row { display: flex; align-items: center; gap: 10px; }
.rel-request { border-top: 1px solid var(--line); margin-top: 14px; padding-top: 14px; }
.rel-request select { max-width: 320px; }
.rel-request .custom-kind { max-width: 320px; }

.section-title { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; padding: 0 12px; }
@media (min-width: 641px) { .section-title { padding: 0; } }
.section-title h1 { margin: 0; }
.empty { text-align: center; color: var(--muted); padding: 40px 20px; }

/* ---- Activity ---- */
.activity-row { display: flex; align-items: center; gap: 12px; padding: 8px 4px; }
.activity-row .thumb { width: 44px; height: 44px; border-radius: 6px; object-fit: cover; margin-left: auto; background: var(--line); }
.activity-row .txt { font-size: .9rem; }

/* Activity rows (grouped). One row = one thing that happened, which may stand
   for several notifications. Avatar wears a kind badge; the right edge is
   either a thumbnail of what it's about or the one action worth taking. */
.act-head form { margin: 0; }
.act-head-actions { display: inline-flex; align-items: center; gap: 6px; }
.act-readall { background: none; border: 0; padding: 0; cursor: pointer;
               font-size: .82rem; font-weight: 600; color: var(--brand); }
.act-section { font-size: .78rem; font-weight: 700; letter-spacing: .04em;
               text-transform: uppercase; color: var(--muted); margin: 2px 0 8px; }
.act-row { display: flex; align-items: center; gap: 10px; padding: 8px 6px;
           border-radius: 12px; overflow: hidden; will-change: transform; }
.act-row.unread { background: rgba(0, 149, 246, .07); }
/* The unread cue has to survive a colour-blind glance, so it's a tinted row AND
   a blue edge — not the tint alone. */
.act-row.unread { box-shadow: inset 3px 0 0 var(--brand); }
.act-main { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0;
            color: inherit; }
.act-main:hover { text-decoration: none; }
.act-av { position: relative; display: inline-flex; flex: none; }
.act-badge { position: absolute; right: -2px; bottom: -2px; width: 18px; height: 18px;
             border-radius: 50%; display: inline-flex; align-items: center;
             justify-content: center; color: #fff; border: 2px solid var(--card); }
.act-badge.heart  { background: var(--heart); }
.act-badge.blue   { background: var(--brand); }
.act-badge.violet { background: #8e44ad; }
.act-badge.game   { background: var(--accent); }
.act-badge.grey   { background: var(--muted); }
.act-txt { font-size: .9rem; line-height: 1.35; min-width: 0; }
.act-time { color: var(--muted); margin-left: 4px; }
.act-times { color: var(--muted); font-weight: 600; }
.act-preview { display: block; color: var(--muted); overflow: hidden;
               text-overflow: ellipsis; white-space: nowrap; }
.act-cta { flex: none; margin-left: auto; }
.act-follow { margin: 0 0 0 auto; flex: none; }
.act-thumb-link { flex: none; margin-left: auto; line-height: 0; }
.act-thumb { width: 44px; height: 44px; border-radius: 6px; object-fit: cover;
             background: var(--line); }

/* ---- Composer photo preview (image editor) ---- */
.composer-photo { margin: 8px 0; }
.composer-preview { display: block; width: 100%; border-radius: 8px; margin-bottom: 8px; }
.composer-preview.ie-empty { display: none; }

/* ---- Responsive ---- */
@media (max-width: 640px) {
  h1 { font-size: 1.25rem; }
  .profile-header { gap: 16px; padding: 12px; }
  .profile-header .avatar-lg { width: 86px; height: 86px; }
  .profile-id .uname { font-size: 1.2rem; }
  .profile-stats { gap: 18px; }
  .msg { max-width: 85%; }
  .thread-head { flex-wrap: wrap; }
  .thread-tools { flex-direction: row; flex-wrap: wrap; align-items: center; width: 100%; }
  /* Tools sit in a row but must WRAP — otherwise the wide "Leave conversation"
     pill runs off the right edge of the page on narrow screens. */
  .thread-tools > * { max-width: 100%; }
  .thread-tools details { width: auto; max-width: none; }
  .thread-tools details[open] { width: 100%; }
}

/* ============================================================= SVG icons
   Inline icons rendered via partials/icon.html. They inherit currentColor so a
   parent's `color` tints them, and swap outline<->solid for active states. */
.ic { display: block; flex: none; vertical-align: middle; }
.i-outline, .i-solid { display: inline-flex; }
.nav-icon { color: var(--ink); }
.nav-icon .ic { width: 24px; height: 24px; }
.nav-user { gap: 18px; }

/* Feed post action row — bigger tap targets, IG spacing. */
.ig-actions { gap: 16px; padding: 8px 12px 4px; }
.ig-action { font-size: 0; }
.ig-action .ic { width: 25px; height: 25px; }
.ig-action.like .i-solid { display: none; }
.ig-action.like.liked .i-outline { display: none; }
.ig-action.like.liked .i-solid { display: inline-flex; }
.ig-action.like.liked { color: var(--heart); }
.ig-action.like:active .ic { animation: likepop .28s ease; }
@keyframes likepop { 0%{transform:scale(1)} 40%{transform:scale(.8)} 100%{transform:scale(1)} }
.icon-btn .ic { width: 20px; height: 20px; }

/* Small inline meta icons (location/cake/family), aligned to text baseline. */
.meta-ic { width: 15px; height: 15px; display: inline-block; vertical-align: -3px; color: var(--muted); }
.tile-cap .ic { width: 18px; height: 18px; }

/* ---- Header refinements ---- */
.nav-inner { gap: 12px; }
.brand { font-size: 1.55rem; }

/* ---- Buttons: tighter, more IG ---- */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 7px 16px; border-radius: 8px; font-size: .875rem; line-height: 1.3; }
.btn-block { display: flex; width: 100%; }
.btn .ic { flex: none; }
.btn-sm { padding: 5px 12px; font-size: .8rem; }
.btn-ghost { border-color: var(--line); font-weight: 600; }
.btn-follow { width: 100%; }

/* ---- Profile polish ---- */
.profile-header { padding: 14px 16px 10px; }
.profile-id .uname { font-weight: 600; }
.profile-actions { display: flex; gap: 8px; margin: 4px 12px 16px; }
.profile-actions .btn { flex: 1; text-align: center; }
.profile-grid { gap: 2px; padding-top: 2px; }

/* ---- Members / search rows ---- */
.member-list { display: flex; flex-direction: column; }
.member-row { display: flex; align-items: center; gap: 12px; padding: 8px 14px; }
.member-row:hover { background: var(--bg); }
.member-row .who { min-width: 0; }
.member-row .who .nm { font-weight: 600; font-size: .9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.member-row .who .sub { font-size: .82rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- Cleaner form fields (edit profile, composers) ---- */
.invite-row { display: flex; align-items: center; gap: 8px 10px; flex-wrap: wrap; }
.invite-link-row { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
.invite-link-row input { flex: 1; min-width: 0; font-size: .82rem; color: var(--muted); }
.invite-link-row .btn { flex: none; }
.ff { margin: 0 0 14px; }
.ff > label { display: block; font-weight: 600; font-size: .78rem; color: var(--muted);
  margin: 0 0 5px; letter-spacing: .02em; }
.ff input, .ff textarea, .ff select { margin-top: 0; }
.field-row.two { display: flex; gap: 12px; }
.field-row.two .ff { flex: 1; min-width: 0; }
/* A native date input (Birthday) has an intrinsic min-width on iOS Safari and
   refuses to shrink into a half-width column, so it spills off the page edge.
   Stack the two-up rows into one full-width column on phones — the date field
   then has room to fit. Tablets/desktop (641+) keep the two-column layout. */
@media (max-width: 640px) { .field-row.two { display: block; } }
.edit-profile h1 { padding: 0 12px; }
@media (min-width: 641px) { .edit-profile h1 { padding: 0; } }
.avatar-editor-row { align-items: center; margin-bottom: 18px; }
/* Keep the round preview square — without flex:none the flex row shrinks its
   width and it renders as an ellipse. */
.avatar-editor-row .avatar-lg, #avatar-preview { flex: none; }
.avatar-editor-title { font-weight: 600; font-size: .95rem; margin-bottom: 4px; }
.avatar-editor-controls .ie-control { margin-top: 4px; }
.cap-label { display: block; font-weight: 600; font-size: .78rem; color: var(--muted); margin: 0 0 5px; letter-spacing: .02em; }

/* ---- Profile meta / empty state / family panel ---- */
.profile-meta-line { margin-top: 5px; display: flex; flex-wrap: wrap; gap: 14px; }
.profile-meta-line span { display: inline-flex; align-items: center; gap: 4px; }
.empty-ic { display: inline-flex; align-items: center; justify-content: center;
  width: 68px; height: 68px; border: 2px solid var(--ink); border-radius: 50%;
  margin: 0 auto 12px; color: var(--ink); text-decoration: none;
  transition: background .15s, color .15s; }
a.empty-ic { cursor: pointer; }
a.empty-ic:hover { background: var(--ink); color: var(--card); }
.empty-ic .ic { width: 34px; height: 34px; }
.fam-summary { cursor: pointer; font-weight: 700; display: flex; align-items: center; gap: 8px; list-style: none; }
.fam-summary::-webkit-details-marker { display: none; }
details.card > .fam-summary .ic { color: var(--ink); }

/* ---- Back-link row (secondary pages) ---- */
.back-link { display: inline-flex; align-items: center; gap: 4px; color: var(--ink);
  font-weight: 600; font-size: .9rem; padding: 2px 12px; margin: 0 0 10px; }
@media (min-width: 641px) { .back-link { padding-left: 0; } }
.back-link .ic { width: 18px; height: 18px; }
/* Intro/helper text under a page heading — align to the same 12px inset as the
   heading, card and back-link on mobile (they all sit inset; a bare <p> doesn't). */
.page-intro { padding: 0 12px; }
@media (min-width: 641px) { .page-intro { padding: 0; } }

/* ---- Media placeholders + tile delete (albums/stories) ---- */
.cover-empty { display: flex; align-items: center; justify-content: center; color: var(--muted); }
.cover-empty .ic { width: 40px; height: 40px; }
/* Per-photo tools (edit + delete) in the album grid, top-right of each tile. */
.tile-tools { position: absolute; top: 6px; right: 6px; display: flex; gap: 6px; z-index: 2; }
.tile-tools .inline { display: inline-flex; }
.tile-del, .tile-edit { position: static; width: 28px; height: 28px; border-radius: 50%;
  background: rgba(0,0,0,.55); color: #fff; border: none; display: inline-flex; align-items: center;
  justify-content: center; cursor: pointer; padding: 0; }
.tile-del .ic, .tile-edit .ic { width: 15px; height: 15px; }

/* ---- Events ---- */
.rsvp-row { display: flex; gap: 8px; flex-wrap: wrap; }
.rsvp-row .btn { flex: 1; min-width: 96px; }
.bucket-title { display: flex; align-items: center; gap: 6px; margin: 0 0 10px; font-size: .95rem; font-weight: 700; }
.bucket-title .ic { width: 18px; height: 18px; }
.event-meta { display: inline-flex; align-items: flex-start; gap: 5px; vertical-align: top; }
.event-meta .ic { width: 15px; height: 15px; color: var(--muted); flex: none; margin-top: 2px; }

/* ---- @mentions (links + autocomplete menu) ---- */
.mention, .mention:hover { color: var(--brand); font-weight: 600; }
/* Your own chat bubbles are brand-colored, so a brand-colored tag would vanish
   into them. Underline carries the "this is a link" cue instead. */
.msg.me .mention, .msg.me .mention:hover { color: #fff; text-decoration: underline; }

/* The chip composer (mentions.js): a contenteditable dressed as the field it
   replaces. Tags sit in it as atomic chips showing the display name, while the
   hidden field it shadows still carries plain "@username". Only built when JS
   runs — without it the original input/textarea stays put. */
.mention-editor { width: 100%; min-height: 42px; max-height: 220px; overflow-y: auto;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px;
  font: inherit; background: var(--bg); color: var(--ink); margin-top: 4px;
  white-space: pre-wrap; overflow-wrap: anywhere; text-align: left; }
.mention-editor:focus { outline: none; border-color: var(--muted); background: var(--card); }
.mention-editor:empty::before { content: attr(data-placeholder); color: var(--muted); }
.mention-editor.multiline { min-height: 72px; }   /* the post caption's rows=3 */
/* The comment box is a borderless strip inside the post card, not a boxed field. */
.ig-addcomment .mention-editor { border: none; background: none; padding: 10px 0;
  margin: 0; min-height: auto; max-height: 96px; }
.mention-chip { color: var(--brand); font-weight: 600; white-space: nowrap;
  background: rgba(0,149,246,.10); border-radius: 4px; padding: 0 3px; }
.mention-menu { background: var(--card); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 8px 28px rgba(0,0,0,.18); overflow-y: auto; max-height: 258px; z-index: 1200; }
.mention-item { display: flex; align-items: center; gap: 10px; padding: 8px 12px; cursor: pointer; }
.mention-item.sel, .mention-item:hover { background: var(--bg); }
/* Tick box: Space (or clicking the box) toggles, so several people can go in at
   once. A plain click on an untouched menu still inserts just that person. */
.mention-check { flex: none; width: 16px; height: 16px; border-radius: 4px;
  border: 1.5px solid var(--line); background: var(--card); position: relative; }
.mention-item.picked .mention-check { background: var(--brand); border-color: var(--brand); }
.mention-item.picked .mention-check::after { content: ""; position: absolute;
  left: 4.5px; top: 1px; width: 4px; height: 8px; border: solid #fff;
  border-width: 0 2px 2px 0; transform: rotate(45deg); }
.mention-hint { padding: 6px 12px 7px; font-size: .72rem; color: var(--muted);
  border-top: 1px solid var(--line); white-space: nowrap; }
.mention-av { width: 30px; height: 30px; border-radius: 50%; object-fit: cover; flex: none; }
.mention-av-fb { display: inline-flex; align-items: center; justify-content: center;
  background: linear-gradient(135deg, #405de6, #833ab4, #e1306c); color: #fff; font-weight: 700; font-size: .85rem; }
.mention-nm { font-weight: 600; font-size: .88rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mention-un { color: var(--muted); font-size: .82rem; margin-left: auto; flex: none; }

/* =================================================================== dark */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #000; --card: #000; --ink: #f5f5f5; --muted: #a8a8a8; --line: #262626;
  }
  input[type=text], input[type=password], input[type=email], input[type=datetime-local],
  input[type=date], input[type=file], textarea, select { background: #121212; }
  input:focus, textarea:focus, select:focus { background: #1a1a1a; border-color: #3a3a3a; }
  .flash-success { background: #10241a; border-color: #1d503a; color: #7ee2b0; }
  .flash-error { background: #2a1416; border-color: #5a2a2f; color: #f3a3aa; }
  .react-btn.active, .thread-list .thread-row.unread, .select-tile:has(input:checked) { background: #12263b; }
  .icon-btn:hover { background: rgba(255,255,255,.08); }
  .story-ring img, .story-ring .avatar-fallback { border-color: #000; }
}

/* ---- in-game chat widget (games/_chat.html) --------------------------- */
/* A collapsible drawer PINNED to the bottom of the screen (just above the
   bottom nav). Collapsed it's a slim bar (latest message + unread badge) so the
   board gets the screen; tapping it slides the conversation UP over the board —
   the bar sits at the bottom so the drawer pops up into open space. Fixed, so
   opening never reflows a live game. Only the message list scrolls. */
.gc { position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--bottomnav-h) + env(safe-area-inset-bottom, 0px));
  width: 100%; max-width: var(--feed-w); z-index: 29;
  height: var(--gc-h); padding: 0; margin: 0; }
.gc-bar { display: flex; align-items: center; gap: 8px; width: 100%; height: 100%;
  padding: 0 12px; background: none; border: none; font: inherit; color: var(--ink);
  cursor: pointer; text-align: left; }
.gc-bar .gc-ico { flex: none; }
.gc-bar .gc-prev { flex: 1; min-width: 0; font-size: .85rem; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gc-bar .gc-prev b { color: var(--ink); font-size: .88rem; }
.gc-badge { flex: none; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
  background: var(--heart); color: #fff; font-size: .72rem; font-weight: 700;
  display: flex; align-items: center; justify-content: center; }
.gc-chev { flex: none; color: var(--muted); display: flex;
  transform: rotate(180deg); transition: transform .18s ease; }
.gc.open .gc-chev { transform: rotate(0); }
.gc-body { position: absolute; bottom: calc(100% + 8px); left: 0; right: 0; z-index: 30;
  display: none; flex-direction: column; height: min(320px, 48dvh);
  background: var(--card); border: 1px solid var(--line); border-radius: 14px;
  padding: 10px 12px; box-shadow: 0 12px 32px rgba(0, 0, 0, .22); }
.gc.open .gc-body { display: flex; }
.gc-head { flex: none; font-weight: 700; font-size: .9rem; margin-bottom: 6px; }
.gc-list { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  list-style: none; margin: 0 0 6px; padding: 0; }
.gc-msg { display: flex; align-items: baseline; gap: 7px; padding: 3px 0; font-size: .86rem; }
.gc-msg .gc-who { font-weight: 700; flex: none; color: var(--ink); }
.gc-msg.mine .gc-who { color: var(--brand); }
/* Said from the sidelines: a watcher may talk, and their line says so, so a
   remark from an onlooker is never taken for the other player's. */
.gc-watching { margin-left: 5px; font-size: .62rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .03em; color: var(--muted);
  border: 1px solid var(--line); border-radius: 999px; padding: 0 5px; }
.gc-msg .gc-text { min-width: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
/* A tagged player, in the thread. Their NAME, not the handle that was typed —
   the handle is only how it's stored (see feed/mentions.py). */
.gc-msg .gc-tag { color: var(--brand); font-weight: 700; }
/* The "@" picker above the box: only the people at this table are in it. */
.gc-tags { display: flex; gap: 6px; flex-wrap: wrap; padding: 6px 10px 0; }
.gc-tags[hidden] { display: none; }
.gc-tagopt { border: 1px solid var(--line); background: var(--card); color: inherit;
  border-radius: 999px; padding: 4px 10px; font-size: .78rem; font-weight: 700;
  cursor: pointer; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.gc-tagopt:active { background: var(--bg); }
.gc-msg .gc-time { margin-left: auto; flex: none; font-size: .7rem; color: var(--muted); }
.gc-empty { flex: none; color: var(--muted); font-size: .84rem; padding: 2px 0 6px; }
/* Delivered/Read under your latest line, and "… is typing" above the input —
   the drawer's own tiny take on the messaging receipts. */
.gc-rcpt { list-style: none; text-align: right; font-size: .68rem; font-weight: 600;
  color: var(--muted); padding: 0 0 3px; }
.gc-typing { flex: none; color: var(--muted); font-size: .76rem; font-style: italic;
  padding: 1px 0 4px; }
.gc-form { flex: none; display: flex; gap: 8px; align-items: flex-end; }
/* The box is a textarea (Enter = line break) sized by JS as you type, so no
   manual resize grip and no rogue baseline gap from it being inline. */
.gc-form textarea { flex: 1; margin-top: 0; padding: 7px 10px; resize: none;
  font: inherit; line-height: 1.35; overflow-y: auto; display: block; }
.gc-form .btn { flex: none; }
.gc-note { flex: none; color: var(--muted); font-size: .76rem; margin-top: 5px; }
.gc-form textarea:disabled { background: var(--bg); color: var(--muted); }

/* ---- shared game shell (games/_head.html + all three boards) ----------- */
/* One visual dialect for every game: header row (back link + game chip), a
   "vs opponent" line with their avatar and whether they're at the board, and
   shared turn/status/end-card styles. Boards keep only their own game CSS. */
.gm-head { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 10px; }
.gm-back { display: inline-flex; align-items: center; gap: 5px; color: var(--muted);
  font-size: .85rem; font-weight: 600; }
.gm-chip { display: inline-flex; align-items: center; gap: 6px; font-weight: 700;
  font-size: .88rem; background: var(--card); box-shadow: inset 0 0 0 1px var(--line);
  border-radius: 999px; padding: 5px 12px; }
.gm-vs { display: flex; align-items: center; justify-content: center; gap: 8px;
  font-size: .92rem; margin-bottom: 8px; }
.gm-vs .muted { font-size: .85rem; }
.gm-vs b { font-weight: 700; }
/* Who is watching your game (templates/games/_watchers.html).

   A chip in a row that already exists, never a row of its own: every board
   budgets its height to the pixel, and this comes and goes as people watch,
   so a line of its own could not be paid for out of any fixed constant. The
   names live behind a tap — three of them would wrap the header on the
   family's narrowest phone, which is the same problem again. */
.gm-watch-wrap { position: relative; flex: none; margin-left: 6px;
  display: inline-flex; }
.gm-watch-chip { display: inline-flex; align-items: center; gap: 4px;
  border: 1px solid var(--line); background: var(--card); color: var(--muted);
  border-radius: 999px; padding: 1px 8px; font: inherit; font-size: .72rem;
  font-weight: 700; line-height: 1.5; cursor: pointer; white-space: nowrap; }
.gm-watch-chip .ic-eye { flex: none; }
.gm-watch-chip:hover { background: var(--bg); color: var(--ink); }
/* Hangs BELOW the chip, out of the flow, so opening it moves nothing. Right-
   anchored: the chip sits at the end of the header on every board, and a
   left-anchored panel would run off a 320px screen. */
.gm-watch-list { position: absolute; top: calc(100% + 5px); right: 0;
  z-index: 30; min-width: 150px; max-width: min(70vw, 240px);
  background: var(--card); border: 1px solid var(--line); border-radius: 10px;
  padding: 7px 10px; font-size: .76rem; color: var(--muted); text-align: left;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .18); }
.gm-watch-list b { color: var(--ink); }

/* Looking over a shoulder: a board with nothing tappable on it reads as broken
   unless something says why (templates/games/_head.html). */
.gm-watch { display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px;
  border-radius: 999px; font-size: .7rem; font-weight: 700; color: var(--muted);
  background: var(--bg); border: 1px solid var(--line); }
.gm-pres { font-size: .78rem; color: var(--muted); }
.gm-pres.here { color: #0a7d3e; font-weight: 600; }
.gm-role { text-align: center; font-size: .9rem; color: var(--muted); margin: 0 0 6px; }
.gm-role b { color: var(--ink); }
.gm-turn { text-align: center; font-size: .9rem; font-weight: 600; min-height: 1.2em;
  margin-bottom: 10px; }
.gm-turn.you { color: var(--brand); }
.gm-status { text-align: center; font-size: .78rem; color: var(--muted);
  margin-top: 8px; min-height: 1em; }
.gm-end { text-align: center; padding: 14px; margin-top: 12px; }
.gm-end h2 { margin: 0 0 4px; font-size: 1.15rem; }
.gm-end .reveal { color: var(--muted); font-size: .9rem; margin-bottom: 10px; }
.gm-end .reveal b { color: var(--ink); letter-spacing: .08em; }
.gm-end .acts { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;
  margin-top: 8px; }
.gm-saved { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line);
  font-size: .85rem; font-weight: 600; color: #0a7d3e; }

/* The host's "Stop this game" card (templates/games/_stop.html) — the same
   pair of buttons a canasta host gets, and deliberately below the board so
   neither can be hit while reaching for a letter, a square or the paddle. */
.gm-stop { max-width: 460px; margin: 14px auto 0; padding: 12px 14px; }
.gm-stop h3 { margin: 0 0 8px; font-size: .95rem; }
.gm-stop .acts { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.gm-stop form { margin: 0; }
.gm-stop .note { flex: 1 1 100%; color: var(--muted); font-size: .82rem; }
.gm-danger { color: var(--heart); }
/* A copy-link button says so on itself for a moment (static/js/copy-link.js).
   A board has no toast to say it in, and the room's was being drawn behind the
   very sheet that raised it — the button is the one place it can't be missed. */
.btn.copied { background: var(--brand); border-color: var(--brand); color: #fff; }
/* Said over whatever was on screen when the game stopped existing. */
.gm-gone { position: fixed; left: 50%; top: 18%; transform: translateX(-50%);
  z-index: 9999; max-width: min(92vw, 420px); text-align: center;
  background: rgba(20, 20, 22, .94); color: #fff; padding: 14px 18px;
  border-radius: 12px; font-size: .95rem; box-shadow: 0 12px 40px rgba(0, 0, 0, .4); }

/* A game's drawn mark (partials/game_icon.html), used wherever 🔤 ⭕ 🏓 used to
   be. Inherits the text colour, and sits on the text baseline like a glyph. */
.gicon { display: inline-block; vertical-align: -.16em; color: inherit; }

/* ---- leaderboard widget (games/_leaderboard.html) --------------------- */
.lb-card { padding: 14px 16px; }
.lb-head { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 6px; }
.lb-head h2 { margin: 0; font-size: 1.02rem; flex: none; }
/* The reset deadline is pinned to the right of the title: a live countdown with
   the actual date under it (baseline-aligned to the title, right-aligned as a
   block so the two lines stack flush against the card edge). */
.lb-reset { margin-left: auto; flex: none; display: flex; flex-direction: column;
  align-items: flex-end; line-height: 1.25; text-align: right; }
.lb-reset .lb-left { font-size: .8rem; font-weight: 700; white-space: nowrap;
  font-variant-numeric: tabular-nums; }
.lb-reset .lb-on { white-space: nowrap; }
.lb-filters { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 4px;
  padding-bottom: 6px; border-bottom: 1px solid var(--line); }
.lb-blurb { flex: 1; min-width: 0; }
.lb-pills { display: flex; gap: 5px; margin-left: auto; flex: none; }
.lb-pills button { background: var(--card); color: var(--muted); border: 1px solid var(--line);
  border-radius: 999px; padding: 3px 10px; font-weight: 600; font-size: .74rem;
  line-height: 1.5; cursor: pointer; white-space: nowrap; }
.lb-pills button.active { background: var(--brand); border-color: var(--brand); color: #fff; }
/* Which month these standings are — the contest the countdown above is timing. */
.lb-month { font-weight: 700; font-size: .82rem; letter-spacing: .02em; flex: none; }
.lb-list { list-style: none; margin: 0; padding: 0; }
.lb-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-top: 1px solid var(--line); }
.lb-row:first-child { border-top: none; }
.lb-rank { width: 24px; text-align: center; font-weight: 700; font-size: .95rem; flex: none; }
.lb-who { display: flex; align-items: center; gap: 9px; min-width: 0; flex: 1; color: var(--ink); }
.lb-who .avatar, .lb-who .avatar-fallback { width: 34px; height: 34px; font-size: .8rem; }
.lb-name { font-weight: 600; font-size: .9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-stat { flex: none; font-size: .86rem; }
.lb-stat b { font-size: 1rem; }
.lb-sub { flex: none; }
.lb-empty { color: var(--muted); font-size: .88rem; padding: 8px 0; }
/* On a phone the blurb is the first thing to go, then the win-% column — the
   pills and the names must never wrap. */
@media (max-width: 560px) { .lb-blurb { display: none; } }
@media (max-width: 460px) { .lb-sub { display: none; } }
/* Narrow phones: the deadline drops onto its own line under the title instead of
   being squeezed past the card edge beside it. */
@media (max-width: 400px) {
  .lb-head { flex-wrap: wrap; }
  .lb-reset { width: 100%; margin-left: 0; flex-direction: row; flex-wrap: wrap;
    align-items: baseline; justify-content: flex-start; gap: 6px; text-align: left; }
}

/* ---- Activity: expanding a collapsed row ------------------------------ */
.act-expand { flex: none; background: none; border: none; padding: 6px; margin-left: 2px;
  color: var(--muted); cursor: pointer; display: inline-flex; align-items: center;
  border-radius: 50%; transition: transform .15s ease, background .15s ease; }
.act-expand:hover { background: rgba(0,0,0,.06); color: var(--ink); }
.act-expand.open { transform: rotate(180deg); }
.act-sub { list-style: none; margin: 0 0 4px; padding: 0 12px 4px 52px; }
.act-sub-row a { display: flex; align-items: center; gap: 9px; padding: 7px 0;
  border-top: 1px solid var(--line); color: var(--ink); }
.act-sub-row .avatar, .act-sub-row .avatar-fallback { width: 26px; height: 26px; font-size: .7rem; }
.act-sub-txt { flex: 1; min-width: 0; font-size: .85rem; line-height: 1.35; }
.act-sub-row.unread .act-sub-txt { font-weight: 600; }
.act-sub-row .act-time { flex: none; }

/* ---- Activity: swipe actions ------------------------------------------ */
/* The row slides over a pair of buttons. A partial swipe parks it there; a full
   swipe does the thing outright (right = delete, left = archive). */
/* Same rounded shape as the row it holds. The row has a 12px radius; the pane
   behind it is square, so square corners peeked out past the row's curves as
   little coloured slivers. Clipping the container to the same shape hides them. */
.act-swipe { position: relative; overflow: hidden; border-radius: 12px; }
.act-actions { position: absolute; top: 0; bottom: 0; display: flex; align-items: stretch; }
.act-actions.left { left: 0; }
.act-actions.right { right: 0; }
.act-act { border: none; cursor: pointer; width: 76px; font: inherit; font-size: .8rem;
  font-weight: 700; color: #fff; display: flex; align-items: center; justify-content: center; }
.act-act.archive { background: #f59e0b; }
.act-act.restore { background: var(--brand); }
.act-act.delete  { background: var(--heart); }
/* The row slides OVER the buttons, so it must be OPAQUE or they show through it
   mid-swipe. But WHICH opaque colour matters on a dark-mode phone: a browser's
   auto-dark inverts `var(--card)` (it paints black) and stubbornly leaves a
   literal `#ffffff` — or `Canvas` — alone. Hard-coding a light hex here is what
   made every notification title invisible: white text on a white row.
   So: the card variable for the base, and the unread tint layered on top as a
   gradient (which keeps it translucent without letting the buttons through). */
.act-swipe .act-row { position: relative; z-index: 1; background-color: var(--card); }
.act-swipe .act-row.unread {
  background-image: linear-gradient(rgba(0, 149, 246, .10), rgba(0, 149, 246, .10));
}
/* The buttons don't render until a swipe is under way — and then only the side
   you're swiping toward. Both panes at once looked like a jumble: the armed
   button on one side and the other pane's buttons peeking out on the other. */
.act-actions { visibility: hidden; }
.act-swipe.swiping[data-dir="right"] .act-actions.left,
.act-swipe.swiping[data-dir="left"] .act-actions.right { visibility: visible; }

/* Past the point of no return, the button that's ABOUT to fire takes over the
   whole revealed strip — so you can see what letting go will do before you do
   it. Right = delete, left = archive (restore, in the archived view), matching
   the full-swipe actions. */
.act-actions { transition: width .12s ease; }
.act-act { transition: flex-grow .12s ease, width .12s ease, padding .12s ease; flex: none; }
.act-swipe.armed-delete .act-actions.left,
.act-swipe.armed-archive .act-actions.right { width: 100%; }
.act-swipe.armed-delete .act-actions.left .act-act.delete,
.act-swipe.armed-archive .act-actions.right .act-act.archive,
.act-swipe.armed-archive .act-actions.right .act-act.restore { flex: 1; width: auto; }
/* ...and its partner gets out of the way. */
.act-swipe.armed-delete .act-actions.left .act-act.archive,
.act-swipe.armed-delete .act-actions.left .act-act.restore,
.act-swipe.armed-archive .act-actions.right .act-act.delete {
  width: 0; padding: 0; overflow: hidden;
}

/* Read/unread checkbox: the rightmost control in the right pane (revealed by a
   left swipe). Neutral, not a destructive action — a check box that fills in
   when the row is read. It collapses away the moment either full swipe arms, so
   it never lingers behind the flying row. */
.act-act.act-read { width: 54px; flex-direction: column; gap: 3px; background: var(--bg);
  color: var(--muted); font-size: .6rem; font-weight: 600; }
.act-read-box { width: 18px; height: 18px; border: 2px solid currentColor; border-radius: 5px;
  display: inline-flex; align-items: center; justify-content: center; }
.act-read-box .ic { opacity: 0; }
.act-read.checked { color: var(--brand); }
.act-read.checked .act-read-box { background: var(--brand); border-color: var(--brand); }
.act-read.checked .act-read-box .ic { opacity: 1; color: #fff; }
.act-swipe.armed-archive .act-actions.right .act-act.act-read,
.act-swipe.armed-delete .act-actions.left .act-act.act-read {
  width: 0; padding: 0; overflow: hidden;
}

/* ---- Activity: filters -------------------------------------------------- */
.act-filters { display: flex; gap: 6px; flex-wrap: wrap; align-items: center;
  margin: 0 12px 12px; }
@media (min-width: 641px) { .act-filters { margin-left: 0; margin-right: 0; } }
.act-filters a { background: var(--card); color: var(--muted); border: 1px solid var(--line);
  border-radius: 999px; padding: 4px 12px; font-size: .8rem; font-weight: 600; }
.act-filters a.on { background: var(--brand); border-color: var(--brand); color: #fff; }
.act-dates { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin-left: auto; }
/* A visible From/To label keeps the native date pill legible once iOS strips its
   mm/dd/yyyy hint (the global ≤640px appearance reset), and the min sizing stops
   an empty field collapsing to a blank sliver — same fix as the games filters. */
.act-dates .act-date { position: relative; display: inline-flex; margin: 0; }
.act-dates input[type=date] { margin-top: 0; padding: 1px 5px; font-size: .7rem;
  min-height: 24px; min-width: 92px; font-weight: 400; }
/* "Start"/"End" placeholder overlay: shown while the field is empty, hidden by
   the .filled class (toggled in activity.html) once a date is picked, and while
   focused so it never sits over the value. Desktop keeps its native mm/dd/yyyy
   hint, so the overlay is mobile-only — the appearance reset there is what
   blanks the field and creates the need for it. */
.act-dates .ph { position: absolute; left: 6px; top: 50%; transform: translateY(-50%);
  font-size: .7rem; color: var(--muted); pointer-events: none; }
.act-dates .act-date:not(.filled) input[type=date]::-webkit-datetime-edit { color: transparent; }
.act-dates .act-date input[type=date]:focus::-webkit-datetime-edit { color: var(--ink); }
.act-dates .act-date.filled .ph,
.act-dates .act-date input[type=date]:focus ~ .ph { display: none; }
.act-dates button { padding: 4px 10px; font-size: .78rem; }
.act-clear { font-size: .78rem; color: var(--muted); font-weight: 600; }
@media (max-width: 480px) {
  /* From and To share ONE row (two columns) rather than a full-width bar each —
     the visible From/To label keeps them legible, and min-width:0 lets each
     shrink to fit its half instead of blowing the row to full width. */
  .act-dates { width: 100%; margin-left: 0; flex-wrap: wrap; }
  .act-dates .act-date { flex: 1 1 0; min-width: 0; }
  .act-dates .act-date input[type=date] { flex: 1; min-width: 0; width: 100%; }
}
