/* Fonts loaded via <link> in each page's <head> for faster rendering */

/* ── Cross-document View Transitions (Chrome 111+, Safari 18+) ──────────
   Makes page-to-page navigation feel instant with a smooth fade instead
   of a hard white flash. Zero JS required — browser handles everything. */
@view-transition {
  navigation: auto;
}
::view-transition-old(root) {
  animation-duration: 100ms;
  animation-timing-function: ease-in;
  animation-name: vt-fade-out;
}
::view-transition-new(root) {
  animation-duration: 160ms;
  animation-timing-function: ease-out;
  animation-name: vt-fade-in;
}
@keyframes vt-fade-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@keyframes vt-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) { animation-duration: 0ms; }
}

:root {
  /* ── LIGHT THEME (default) ────────────────────────────────────────
     Flipped 2026-04-22: default is now white/cream with mobile-background
     art; the navy cowrie look is under [data-theme="dark"].  Same
     material language, inverted palette.

     --tile-ink  is the text color on --tile-blue backgrounds. In light
     mode it's dark ink (readable on cream tiles); in dark mode it's
     white (readable on the navy tiles).  Per-page rules should use
     var(--tile-ink) instead of hardcoding #fff so the theme toggle
     works automatically. */
  --ink: #1a1b26;
  --muted: #61637a;
  /* Brand indigo as RGB channels — single source of truth for translucent
     navy tints app-wide.  Was hardcoded as `rgba(26, 28, 58, X)` literals
     ~400 times across PHP files before the 5ce8e62 mass refactor.  This
     token MUST be defined in EVERY stylesheet that pages actually import
     (this file is the one most pages load via style.20260421b.css; the
     newer style.css also has it).  Missing it means every rgba(var(--brand-rgb),X)
     call resolves to an invalid color and the element renders unstyled
     — exact bug the founder reported 2026-05-27 on the login screen
     ("logo at the top is white on a white background, the boxes where I
     would type aren't showing up").  Dark-theme override below flips
     channels to white so tints auto-neutralize against near-black. */
  --brand-rgb: 26, 28, 58;
  --bg-fallback: #ffffff;
  /* Inset distance for hairline dividers between list items.  16px
     breathing room from each edge so divider lines don't slice the
     full viewport.  Modern app convention — Material's "inset
     divider", Threads, Substack, Twitter all use ~16-20px.  Founder
     ask 2026-05-27: "lines should end just before they touch the
     two edges of the screen."  Defined once here so future surfaces
     can opt in with `left: var(--divider-inset); right: var(--divider-inset)`. */
  --divider-inset: 16px;
  /* Faint hairline color for dividers between list items — "just a
     psychological divide, doesn't need to pop at all" per founder
     ask 2026-05-27.  6% navy on light, 6% white on dark.  Fainter
     than var(--line) (12% / 10%) — reserved specifically for the
     between-item seams.  Other surfaces that need a STRONGER
     hairline (panel borders, button strokes, etc) keep using --line. */
  --divider-soft: rgba(42, 44, 69, 0.06);
  --bg-gradient: linear-gradient(145deg, #ffffff 0%, #fafafa 50%, #f5f5f5 100%);
  --bg: var(--bg-gradient);
  --bg-image: url('./assets/mobile-background.png?v=20260503');
  /* --panel: surface color for elevated UI (modals, popovers, dropdown
     sheets, inputs).  Was rgba(255, 251, 245, .92) — a warm cream with
     8% page-bg bleed-through, which read as a visible BEIGE tint on
     the white page background.  Founder ask 2026-05-28: "I'm not
     sure if it's intentional that we're using that beige on the
     white background, but I think keeping it white is good.  I'm
     talking about with the popover and some other things."
     Flipped to solid #ffffff so modal surfaces match the page bg
     exactly — no perceptible tint.  Affects every consumer of
     var(--panel): post share sheet, repost actions sheet, ÀJỌ
     discussion modal, language/word suggestion popovers, badge
     modal, word page modals, input fills, etc. */
  --panel: #ffffff;
  --line: rgba(42, 44, 69, .12);
  --accent: #2a2ea5;
  --forest: #1f6f43;
  --shadow: 0 24px 60px rgba(33, 28, 17, .14);
  --tile-ink: var(--ink);
  --tile-border: rgba(9, 11, 26, .1);
  /* Secondary text color on translucent tiles — bumped from 0.6 → 0.78
     navy so English translations / meanings / meta read clearly on the
     78%-alpha tile material. Dark-mode override below keeps the
     original 55% white for navy tiles. */
  --tile-muted: rgba(var(--brand-rgb), .78);
  --nav-ink: rgba(var(--brand-rgb), .55);
  --nav-ink-active: #1a1b26;
  /* --tile-blue is the ONE source of truth for the deep-indigo material used
     on every blue card / tile in the app.  Three layered levels for depth
     without per-tile decoration (no circles, no stamps — the whole stack
     reads as one continuous sheet of indigo cloth):
       - TOP layer: procedural SVG noise at ~5% opacity — woven-cloth grain.
         Gives every tile a subtle tactility, like the weft of fabric. The
         noise is stitch-tiled so it repeats seamlessly across any tile size.
       - MIDDLE layer: radial vignette, transparent in the center, deepening
         toward the corners.  Fixes "washed out" look — edges read firmly
         blue while the center lets a sliver of the page background show.
       - BOTTOM layer: base linear 135° diagonal at 94% alpha — keeps the
         diagonal shading character and lets the page bg peek through
         subtly (so tiles feel connected to the page, not painted on top).
     Net effect: tiles read as ONE material across the whole stack. The
     1px hairline gaps between tiles are the "stitching" in the cloth.
     Change the alpha numbers or noise baseFrequency here to tune globally. */
  /* Light-theme tile material — "handmade paper" stack:
       - TOP: warm-ink fractal noise (~6%) — fine paper tooth.  The noise
         color is shifted warm (brown/umber tone) instead of cool gray so
         the grain reads as organic fibre, not printer noise. baseFrequency
         0.9 keeps the grain fine, numOctaves 2 adds a second wavelength
         so the grain doesn't feel uniform.
       - BOTTOM: bone/parchment at ~88% alpha — NOT pure white.  The 2%
         warmth shift lifts the page out of "login-form flat" without
         reading as off-white.  Slight bump in opacity (was 78%) makes
         tiles feel more like actual paper (less glassy) while still
         letting a whisper of the cowrie backdrop through.
     Tune grain density: raise alpha past 0.06 if you want more tooth,
     lower to 0.03 for near-invisible. Tune warmth: shift the
     feColorMatrix RGB triple or the bone alpha. */
  --tile-blue: rgba(255, 255, 255, 0.72);
  /* --tile-radius: the ONE corner-rounding for every large tile across the
     app (feed cards, hero blocks, thread bubbles, alert items, post detail,
     leaderboard rows, message bubbles, etc.).  Set to 0 because tiles extend
     to the viewport edges on mobile — any corner rounding there gets visually
     clipped by the screen edge and looks broken.  Square tiles merge cleanly
     into the viewport.  If we want gentle rounding back on desktop only, we
     can add a @media (min-width: 600px) override later. */
  --tile-radius: 0px;

  /* ═══════════════════════════════════════════════════════════════
     EDITORIAL ARCHIVE SCHEME  (2026-04-23) — light theme
     Stone + oxblood accents. These cascade to every page without
     per-page overrides. --tile-blue bumped to 78% for more vellum
     translucency is applied below in :root:not([data-theme="dark"]).
     ─────────────────────────────────────────────────────────────── */
  --tile-line: rgba(var(--brand-rgb), 0.08);
  --stone:     #9e8e7a;
  --oxblood:   #a8413a;
  /* Aso oke gold — a muted warm gold, not a loud yellow.  Reserved
     for "precious" moments (profile bookplate accent, approved
     contribution celebration).  Used very sparingly by design. */
  --gold:      #c9a96e;
}

/* Light-only vellum — cowrie background breathes through tiles.
   Dark-theme --tile-blue defined later keeps its indigo gradient. */
:root:not([data-theme="dark"]) {
  --tile-blue: rgba(255, 255, 255, 0.78);
}

/* ═══════════════════════════════════════════════════════════════════
   EDITORIAL SCHEME — tile treatment (light theme)
   Enumerated to avoid snagging button / avatar classes that also
   paint with var(--tile-blue). !important on box-shadow+border to
   override per-page inline styles that were tuned for the old
   indigo tile era; cleaner than touching every page's inline CSS.
   Dark-theme restoration block lives further down.
   ─────────────────────────────────────────────────────────────── */
.hero,
.messages-hero, .alerts-hero, .saved-hero, .ajoso-header,
.word-review-head,
.feed-tabs-bar, .profile-tab-bar, .contrib-bucket-tabs,
.lb-tabs, .saved-tabs, .awujo-tabs, .word-tabs-bar,
.feed-card, .thread-link, .item, .activity-item, .mini-link,
.comment-item, .saved-post-card, .saved-word-card, .word-review-item,
.row, .person-card, .post-card, .post-detail, .comments-title,
.add-comment, .inline-panel-section, .conversation-panel,
.prefs-panel, .leaderboard-rail, .compose, .bubble {
  border: 1px solid var(--tile-line) !important;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55) !important;
}

/* ── Sticky page headers ──────────────────────────────
   Mirrors the RN staging architecture (Samuel's 5/8 refactor): on
   mobile, the leaderboard rail / awujo top tabs / alerts title /
   word-page tabs / saved tabs all sit OUTSIDE the scroll container
   and stay anchored at the top while the feed scrolls underneath.
   Web equivalent: position: sticky; top: 0 on each fixed-region
   tile.  Z-index 30 so they layer above feed cards (default 0) but
   below the bottom nav (z-index 40) and modals (9000+).

   Only ONE element per page is sticky to avoid stacked-tile
   overlap.  Home picks the leaderboard-rail (the heavy "stays"
   element the founder called out); the feed-tabs-bar still scrolls
   away with the WotD hero, matching the RN staging behavior where
   stickyHeaderIndices was removed.  Awujo / alerts / word page /
   saved / leaderboard each have their own single sticky tile. */
.leaderboard-rail,
.awujo-tabs,
.alerts-hero,
.word-tabs-bar,
.saved-tabs,
.lb-tabs {
  position: sticky;
  top: 0;
  z-index: 30;
}

/* Profile pages are the exception to the "one sticky element per page"
   convention above.  Founder ask 2026-05-27: "on RN profile when I
   scroll, the top is frozen — the profile section AND the Activity /
   Contributions / Followers / Following tabs.  I want the same on web."
   On RN that's the CowrieHeader (outside ScrollView) + stickyHeaderIndices
   on the tabs.  Web equivalent: wrap .hero.profile-hero AND
   .profile-tab-bar inside a .profile-sticky-top div and make THAT
   sticky — both children scroll-pin together as one frozen region.

   background: var(--bg-fallback) is REQUIRED.  Without it, --tile-blue
   on the hero + tab-bar is translucent (rgba 78% white in light, 98%
   near-black in dark) so as content scrolls UNDER the sticky wrapper,
   the activity items show through the gaps in the tile material.
   Founder follow-up 2026-05-27: "it feels like the profile top is
   transparent — I can see things I'm scrolling behind it."  Solid bg
   below the translucent tiles blocks the scroll-through cleanly. */
.profile-sticky-top {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--bg-fallback);
}

/* Kicker → museum plaque: transparent bg + stone letterspaced caps.
   Per-page inline styles declare most of these with page-local values
   (tile-ink color, tinted bg, smaller size). !important on each one
   guarantees the editorial label wins across every page. */
.kicker {
  background: transparent !important;
  color: var(--stone) !important;
  letter-spacing: .14em !important;
  padding: 0 !important;
  font-size: 12px !important;
  font-weight: 700 !important;
  border: 0 !important;
}

/* Small-caps form / meta labels — warm stone + slight size bump.
   Same !important rationale: inline page styles set font-size / color
   on these and would otherwise clobber the editorial treatment. */
.field-label,
.field-label-button {
  color: var(--stone) !important;
  font-size: 12px !important;
  letter-spacing: .12em !important;
  font-weight: 600 !important;
}

/* ═══════════════════════════════════════════════════════════════════
   EDITORIAL SCHEME — per-page label classes
   Each page defines its own small-caps labels with different class
   names (tab labels, section labels, eyebrows, pill labels). Catch
   them all here so the stone color appears site-wide, not just on
   the home page. Sizes are left alone — changing tab label sizes
   would break tab layouts. Color is the single unifying treatment.
   ─────────────────────────────────────────────────────────────── */
.awujo-tab-label,
.lb-tab-label,
.saved-tab-label,
.profile-tab-label,
.feed-tab-btn span,
.tag-label,
.hero-action-caption,
.contributor-pill-label,
.score-label,
.word-review-head h2,
.word-review-diff-label,
.word-head .eyebrow,
.wotd-eyebrow,
.leaderboard-rail-head h2 {
  color: var(--stone) !important;
}
/* Active tab labels stay at tile-ink so the selected tab is obvious */
.awujo-tab.is-active .awujo-tab-label,
.lb-tab.is-active .lb-tab-label,
.saved-tab.is-active .saved-tab-label,
.profile-tab-btn.is-active .profile-tab-label,
.feed-tab-btn.is-active span {
  color: var(--tile-ink) !important;
}

/* Oxblood accent — the ONE warm CTA color. Used sparingly on the
   active like/support state; everything else stays as-is. */
.icon-action.is-active {
  color: var(--oxblood) !important;
}

/* ═══════════════════════════════════════════════════════════════════
   Profile container — bookplate treatment
   Applied on both account.php (self) and profile.php (others) so
   every profile page reads as a personal artifact.  Classic "bound
   book" editorial move: an inner hairline frame floats 6px inside
   the outer edge, so the card looks like a bookplate / ex libris
   rather than a flat tile.  Paired with:
     • Warmer, more opaque paper tint (92% warm cream vs 78% on tiles)
     • Deeper elevated shadow for presence (14px drop vs tile's flush)
     • Subtle top-edge highlight (light on paper)
   All achieved via layered box-shadows — no extra markup required.
   Compound selector .hero.profile-hero beats the site-wide .hero
   rule's specificity so !important on the same properties wins.
   ─────────────────────────────────────────────────────────────── */
.hero.profile-hero {
  /* Discontinued the gold bookplate / cream border treatment per
     user request: profile card on web should sit cleanly on the
     cowrie strip with no decorative frame.  Background is left
     transparent because the html[style] cowrie-strip override
     below sets it to transparent anyway; box-shadow / border
     reset here so the previous bookplate styling doesn't leak in. */
  background: transparent !important;
  border: 0 !important;
  box-shadow: none !important;
}

/* ═══════════════════════════════════════════════════════════════════
   EDITORIAL SCHEME — dark theme restoration
   Keep the original stitch-seam aesthetic in dark mode; the hairline
   + inset highlight were tuned for the cream ground. On the indigo
   canvas, the hairline reads as a bright seam and the inset highlight
   turns into a visible white line. Restore the dark box-shadow stitch
   and drop the border.
   ─────────────────────────────────────────────────────────────── */
[data-theme="dark"] .hero,
[data-theme="dark"] .messages-hero,
[data-theme="dark"] .alerts-hero,
[data-theme="dark"] .saved-hero,
[data-theme="dark"] .ajoso-header,
[data-theme="dark"] .word-review-head,
[data-theme="dark"] .feed-tabs-bar,
[data-theme="dark"] .profile-tab-bar,
[data-theme="dark"] .contrib-bucket-tabs,
[data-theme="dark"] .lb-tabs,
[data-theme="dark"] .saved-tabs,
[data-theme="dark"] .awujo-tabs,
[data-theme="dark"] .word-tabs-bar,
[data-theme="dark"] .feed-card,
[data-theme="dark"] .thread-link,
[data-theme="dark"] .item,
[data-theme="dark"] .activity-item,
[data-theme="dark"] .mini-link,
[data-theme="dark"] .comment-item,
[data-theme="dark"] .saved-post-card,
[data-theme="dark"] .saved-word-card,
[data-theme="dark"] .word-review-item,
[data-theme="dark"] .row,
[data-theme="dark"] .person-card,
[data-theme="dark"] .post-card,
[data-theme="dark"] .post-detail,
[data-theme="dark"] .comments-title,
[data-theme="dark"] .add-comment,
[data-theme="dark"] .inline-panel-section,
[data-theme="dark"] .conversation-panel,
[data-theme="dark"] .prefs-panel,
[data-theme="dark"] .leaderboard-rail,
[data-theme="dark"] .compose,
[data-theme="dark"] .bubble {
  border: 0 !important;
  /* White-frost stitch — was indigo near-black (invisible on the new
     neutral near-black canvas).  Mirrors mobile's heroStitch. */
  box-shadow: 0 1px 0 0 rgba(255, 255, 255, .10) !important;
}
/* Dark-theme kicker: restore a subtle background tile + tile-ink color */
[data-theme="dark"] .kicker {
  background: rgba(255, 255, 255, .06) !important;
  color: var(--tile-ink);
  padding: 6px 10px;
  border-radius: 10px !important;
}
[data-theme="dark"] .field-label,
[data-theme="dark"] .field-label-button {
  color: var(--tile-muted);
}
[data-theme="dark"] .icon-action.is-active {
  color: #7dd3fc !important;
}
/* Dark-theme profile-hero: also discontinued the bookplate frame
   per user request.  Card sits cleanly on the cowrie strip; no
   border, no inset, no drop shadow. */
[data-theme="dark"] .hero.profile-hero {
  background: transparent !important;
  border: 0 !important;
  box-shadow: none !important;
}
/* Dark-theme: restore tile-muted on per-page labels (stone clashes
   with the indigo canvas; the dark aesthetic predates the editorial
   scheme). */
[data-theme="dark"] .awujo-tab-label,
[data-theme="dark"] .lb-tab-label,
[data-theme="dark"] .saved-tab-label,
[data-theme="dark"] .profile-tab-label,
[data-theme="dark"] .feed-tab-btn span,
[data-theme="dark"] .tag-label,
[data-theme="dark"] .hero-action-caption,
[data-theme="dark"] .contributor-pill-label,
[data-theme="dark"] .score-label,
[data-theme="dark"] .word-review-head h2,
[data-theme="dark"] .word-review-diff-label,
[data-theme="dark"] .word-head .eyebrow,
[data-theme="dark"] .wotd-eyebrow,
[data-theme="dark"] .leaderboard-rail-head h2 {
  color: var(--tile-muted) !important;
}
/* END EDITORIAL ARCHIVE SCHEME */

* { box-sizing: border-box; }

/* Root canvas — default theme is WHITE so the browser's canvas (iOS
   status-bar sample area, brief repaint window) matches the light
   background art. Dark mode below restores the indigo canvas. */
html { background: var(--bg-fallback); }

/* ── Tile stitching ──────────────────────────────────────
   The 1px gap between stacked tiles reads as a deep-indigo
   stitch (darker than the tile blue) instead of a cream seam.
   IMPORTANT: we can't put a bg color on the list container
   — tiles are 94% translucent, so they'd pick up the
   container's dark color as "what shows through" instead of
   the warm page background, which the user explicitly wants
   visible through the tile.
   Technique: box-shadow on each tile (except the last in its
   stack).  The shadow is 0px offset-x, +1px offset-y, 0 blur —
   a crisp 1px line that sits BELOW each tile.  Since grid
   gap:1px leaves exactly 1px of space between tiles, the
   shadow fills that gap.  Tiles themselves stay transparent-
   bg'd, so page bg still shines through their material. */
/* Inset hairline divider — drawn via ::after pseudo-element so it
   sits INSIDE the tile's box at the bottom edge.  Founder ask
   2026-05-27: "all those lines that are running across the screen,
   maybe they should actually not totally run across the screen —
   they should end just before they touch the two edges of the
   screen."  Refined 2026-05-28: "maybe we'll reduce it on one side.
   It touches the screen on one side and doesn't touch on the other"
   — asymmetric / leading-divider pattern (iOS Settings, Mail,
   Material 3).  Picked LEFT inset / RIGHT touches: most modern
   convention; the gutter reads as a list-item indent.
   Faintness: "the line needs to faint, like it's just a
   psychological divide. It doesn't need to pop at all" — so the
   color drops from var(--line) (12% / 10%) to var(--divider-soft)
   (6% / 6%).  See the :root tokens for the full rationale.

   Why pseudo-element (not box-shadow):
     box-shadow at offset (0,1) is drawn OUTSIDE the box, can't be
     inset horizontally, and gets covered by the next sibling when
     gap:0 (which is the dominant pattern after recent fixes).
   Why ::after on the item (not on a wrapper):
     Items in this selector list already have `position: relative`
     set in their own per-page CSS (.feed-card, .item, .comment-item,
     .activity-item, etc — verified).  The pseudo can absolute-position
     to the item's bottom edge with left inset.
   var(--divider-soft) flips with theme via the dark-theme override.
   pointer-events:none so the hairline never blocks clicks on the
   item underneath. */
.post-list > *:not(:last-child)::after,
.feed > *:not(:last-child)::after,
.list > *:not(:last-child)::after,
.thread-list > *:not(:last-child)::after,
.conversation > *:not(:last-child)::after,
.activity-list > *:not(:last-child)::after,
.follower-list > *:not(:last-child)::after,
.comment-list > *:not(:last-child)::after,
.word-posts-list > *:not(:last-child)::after,
.saved-post-list > *:not(:last-child)::after,
.saved-word-list > *:not(:last-child)::after,
.lb-swipe-panel > .list > *:not(:last-child)::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: var(--divider-inset);
  right: 0;
  height: 1px;
  background-color: var(--divider-soft);
  pointer-events: none;
}

/* Kill the double-up: tiles inside list containers get their
   border-top / border-bottom zeroed so they don't paint a second
   full-width line alongside the inset pseudo above.  The global
   tile-border rule earlier in this file paints 1px navy borders
   on all 4 sides of every tile class — fine for standalone tiles
   (hero, leaderboard-rail), but inside a stacked list it makes
   each item paint a full-width line at top AND bottom that runs
   right alongside our inset hairline.  Founder report 2026-05-28:
   "there's another line behind them... a line running full" —
   that's what they saw.  High-specificity + !important needed to
   beat the earlier global rule's own !important. */
.post-list > *,
.feed > *,
.list > *,
.thread-list > *,
.conversation > *,
.activity-list > *,
.follower-list > *,
.comment-list > *,
.word-posts-list > *,
.saved-post-list > *,
.saved-word-list > *,
.lb-swipe-panel > .list > * {
  border-top-width: 0 !important;
  border-bottom-width: 0 !important;
  /* Founder report 2026-05-28: "on web we're doubling up on those
     lines that are in between [Added / In Review] cards."  Diagnosis:
     the global tile-border rule earlier paints a 1px white inset
     top highlight (box-shadow: inset 0 1px 0 rgba(255,255,255,.55))
     designed for indigo tiles.  On the new paper background that
     white edge reads as a visible line — sitting right above each
     tile's bottom inset hairline (from the ::after pseudo).  Two
     close-together lines = perceived double-up.  Zeroing the
     box-shadow on list children kills the white edge; the single
     inset hairline below each item stays.  Stand-alone tiles
     (hero, leaderboard-rail) keep their highlight. */
  box-shadow: none !important;
}

/* ── Twitter / Threads-style post card layout ─────────────────
   Body, embedded-original, and tags sit UNDER the username instead
   of below the avatar — same indent pattern as RN PostCard
   (588b203).  Engagement bar (post-engagement) stays full-width.
   .post-head's grid is "auto 1fr auto" with gap:10px and the avatar
   is 40px, so margin-left:50px shifts each block to the start of
   the author column.

   IMAGES are deliberately NOT indented — they break out to full
   card width so detail isn't cropped by the narrower column.
   Mirrors the RN PostCard.imagesWrap.marginLeft: -56 trick
   (beef295) and how Threads renders 2- and 4-image grids: media
   spans wider than the text column.  Founder report 2026-05-27:
   "the graphics are being cut off because we moved things to the
   side — scale them so they fit." */
.post-card > .post-body,
.post-card > .embedded-original,
.post-card > .post-tags {
  margin-left: 50px;
}

/* Pull the body up into the empty space below the name (which sits
   beside the 40px avatar in .post-head).  Without this, the body
   starts BELOW the avatar's bottom edge — 25px of dead air between
   the name and the first text line.  Founder report 2026-05-27:
   "There's so much gap between the profile name and the text;
   the text needs to move up."
   -22px is calibrated to leave a tight Threads-style gap below
   the name without overlapping it.  Only applies to the body
   (the FIRST element after .post-head) — tags and embedded-original
   keep their normal stacking position below the body. */
.post-card > .post-body {
  margin-top: -22px;
}

/* ── Post-detail page Twitter/Threads layout ───────────────────
   Same indent + tight-gap pattern as the .post-card feed cards,
   adapted for post-detail's 44px avatar + 12px head gap (=56px
   indent).  Founder ask 2026-05-27: "On the post detail page,
   things still look like old times — apply the same fix."
   Images break out to full width (no indent) — same trick as
   .post-card images. */
.post-detail > .post-body,
.post-detail > .embedded-original,
.post-detail > .post-tags {
  margin-left: 56px;
}
.post-detail > .post-body {
  margin-top: -22px;
}

/* ── Profile activity posts: body uses the full vertical space ──
   On profile pages the author name is implicit (you're ON their
   profile), so .activity-head only carries the timestamp.  Without
   this rule, the body <p> starts BELOW that small time element —
   wasting the vertical space beside the avatar where the name
   would have lived.  Founder ask 2026-05-27: "the texts are moving
   down on profile — they should use that space and align with the
   top of the icon."
   Fix: pin .activity-head to the top-right of .activity-post-body
   so the body <p> starts at y=0 of the column.  padding-right on
   the first paragraph reserves room for the time so short bodies
   don't overlap it.  Applies to both account.php (own profile)
   and profile.php (other-user profile). */
.activity-post-body {
  position: relative;
}
.activity-post-body > .activity-head {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 2;
}
.activity-post-body > p:first-of-type {
  margin-top: 0;
  padding-right: 80px;
}

/* Restore visible seam between feed-cards on the home page.
   Founder report 2026-05-27: "you removed the faint line between
   tiles on Added / In Review." + follow-up: "lines aren't showing
   on dark theme, only light."
   Background: ajo.php closed .feed gap to 0 (the gap previously
   gave room for the box-shadow seam to render).
   In LIGHT: each feed-card has border:1px var(--tile-line) (8%
   navy) from the global tile-border rule — visible but faint.
   In DARK: the broad dark-theme rule
   (style.20260421b.css:316) strips ALL borders with
   `border: 0 !important;` so even setting border-top-color does
   nothing — width is 0.
   Fix: use the full longhand `border-top: 1px solid var(--line)
   !important` so the rule both sets the width (overriding dark's
   border:0) AND the color.  Specificity (0,0,3,0) + !important
   beats the dark rule's (0,0,2,0) + !important.  var(--line) flips
   correctly per theme:
     Light: rgba(42, 44, 69, .12)    — 12% navy
     Dark:  rgba(255, 255, 255, .10) — 10% white
   Same soft seam founder approved on the notifications page. */
.feed > .feed-card:not(:first-child) {
  border-top: 1px solid var(--line) !important;
}

/* ── User-posted clickable links in post bodies ────────────────
   Only rendered when the author is verified, Custodian (75+ badge),
   or has a 16+ week streak (see community_user_can_post_links).
   Uses the exact same color as the in-review word tag (#b8860b).

   Extra parent-scoped selectors are here to beat the per-page
   .activity-item a / .post-detail a / .comment-item a rules that
   otherwise force links to inherit the parent's white text color
   on blue tiles.  All selectors below share the same declarations. */
.post-link,
.activity-item .post-link,
.post-card .post-link,
.post-detail .post-link,
.comment-item .post-link {
  color: #b8860b;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  font-weight: 600;
  word-break: break-word;
  overflow-wrap: anywhere;
}
.post-link:hover,
.activity-item .post-link:hover,
.post-card .post-link:hover,
.post-detail .post-link:hover,
.comment-item .post-link:hover { text-decoration-thickness: 2px; }

body {
  margin: 0;
  min-height: 100vh;
  /* padding-top = iOS/Android safe-area inset so content starts below
     the native status bar (clock / battery / signal) without being
     hidden behind it.  In Capacitor wrap mode with overlaysWebView:true
     the WebView paints edge-to-edge, which means html::before (paper
     grain + cowrie backdrop) now flows under the status bar area —
     giving it real texture instead of a solid color, while this
     padding keeps real content comfortably below the clock.
     On regular desktop browsers env() returns 0, so nothing shifts. */
  padding: env(safe-area-inset-top) 0 96px;
  /* Body is transparent — html::before (cowries on white in light, navy
     cowrie cloth in dark) shows directly past the last tile on sparse
     pages.  No extra wash layer — user will provide a richer background
     image that looks good raw. */
  background: transparent;
  color: var(--ink);
  font-family: 'Gabarito', 'Noto Sans', sans-serif;
  font-size: 15px;
  line-height: 1.6;
}
[data-theme="dark"] body {
  background: transparent;
}

/* Hide scrollbars globally while keeping the element scrollable — per-user
   request.  Content still scrolls with mouse wheel, keyboard, and touch, just
   without the visible scroll-track chrome. */
html, body {
  scrollbar-width: none;          /* Firefox */
  -ms-overflow-style: none;       /* IE / legacy Edge */
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
  display: none;                  /* Chrome, Safari, Opera */
}

/* Fixed-position background layer — reliably stays put while content scrolls.
   Uses a pseudo-element on the html root so it always covers the full viewport,
   not just the body's layout box.
   Height uses 100lvh (large viewport height) so the element is locked to the
   LARGEST viewport size regardless of mobile URL-bar show/hide.  Otherwise
   `inset: 0` re-sizes during URL-bar toggles on Android Chrome, visible as
   the background "jumping" while the user scrolls. 100vh is the fallback for
   browsers that predate lvh units.

   BACKGROUND MATERIAL — SITE-WIDE:
   The page is painted with the EXACT same stack used for --tile-blue,
   plus a transparent PNG of white cowrie silhouettes on top.  Net effect:
   every page reads as one continuous sheet of indigo cloth, with tiles
   sitting flush against a background made of the same material.  The 94%
   translucent tiles let a hair's breadth of this material shine through
   them, tying the whole surface together.
     Layer order (top → bottom):
       1. white cowries PNG
       2. woven-cloth noise (SVG filter, 10% alpha)
       3. radial vignette — center light, corners dark
       4. 135° diagonal base blue
       5. #1e2046 fallback indigo */
/* Cowrie top strip — sits in document flow at the top of the page
   so it scrolls WITH content (matches the React <CowrieHeader/>
   wrapper).  Height is set per page via `--cowrie-strip-height`
   inline on `<html>` (defaults to 0).  Each page picks its own
   demarcation:
     ajo.php          → ~250px (covers the leaderboard rail)
     leaderboard.php  →  ~80px (just the topbar, ends at the tabs)
     notifications.php→ ~140px (covers the alerts hero)
     messages.php     → ~140px (covers the messages hero)
     profile.php      → ~280px (covers the profile hero)
     account.php      → ~280px (own profile hero)
     settings.php     → ~150px (covers the section tabs row)
   Implemented on `html::before` with `position: absolute` so the
   strip is anchored to the document (top:0 = page top, not viewport
   top) and scrolls along with the page rather than staying pinned
   while content moves under it.  Variable lives on <html> because
   pseudo-elements only see variables on their host element.
   `background-size: cover` preserves aspect ratio (same behaviour
   as the React `<Image resizeMode="cover">`).  Login + signup keep
   their own inline overrides for the splash canvas. */
[data-theme="dark"] html { background: #000000; }
/* Explicit reset so html has no UA-style margin / padding that
   would push html::before's `top: 0` below the actual top of the
   screen.  iOS Safari with viewport-fit=cover and PWA standalone
   mode honour env() — html top reaches into the safe-area inset
   only if html itself starts at 0. */
html { margin: 0; padding: 0; }
html::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--cowrie-strip-height, 0);
  background-image: var(--bg-image);
  background-position: center top;
  background-size: cover;
  background-repeat: no-repeat;
  /* Faded so the cowrie reads as a subtle backdrop and doesn't
     compete with hero text / avatars / tabs above it.  Matches the
     0.45 opacity the mobile <CowrieHeader/> uses on the React app. */
  opacity: 0.45;
  z-index: 0;
  pointer-events: none;
}
@media (min-width: 900px) {
  html::before {
    background-image: url('./assets/desktop-background.png?v=20260417b');
  }
}
[data-theme="dark"] html::before {
  background-image: url('./assets/feed-cowries.png?v=20260420d');
  /* Was 0.45 (inherited from light).  Mobile's CowrieHeader on
     staging uses 0.25 in dark — the feed-cowries PNG is more
     visible per pixel on a black canvas than mobile-background is
     on cream, so the lower value matches the same "ghost pattern"
     feel.  Without this, dark cowries read as too noisy and
     compete with hero text. */
  opacity: 0.25;
}
@media (min-width: 900px) {
  [data-theme="dark"] html::before {
    background-image: url('./assets/feed-cowries-desktop.png?v=20260420d');
  }
}

/* When a page sets `--cowrie-strip-height` on <html>, the first
   "hero" / tabs tile lives INSIDE the strip area and must be
   transparent so the cowrie canvas behind shows through.  Without
   this override each tile's `var(--tile-blue)` (72% white) would
   cover most of the strip and only the unfilled gap above the
   hero would show cowrie.  `position: relative; z-index: 1` lifts
   the tile above the strip's z-index: 0 so the tile content
   reliably paints on top. */
/* Note (2026-05-30): .alerts-hero, .messages-hero and .awujo-tabs
   removed from this list — they now self-style as sticky-pinned
   headers with their own opaque background-color + cowrie overlay
   (see "Sticky Community / Alerts / Messages headers" block below).
   The remaining tiles still ride the html::before cowrie strip
   pattern. */
html[style*="--cowrie-strip-height"] .saved-hero,
html[style*="--cowrie-strip-height"] .leaderboard-rail,
html[style*="--cowrie-strip-height"] .lb-tabs,
html[style*="--cowrie-strip-height"] .settings-tabs-bar,
html[style*="--cowrie-strip-height"] .hero.profile-hero {
  background: transparent !important;
  position: relative;
  z-index: 1;
}

/* ── Sticky Community / Alerts / Messages headers (2026-05-30) ────
   Founder ask: "messages, the word at the top should not be moving
   on the web. It should not be moving. ... And then when I'm
   scrolling behind it, it should be a solid background. ... back of
   that panel is translucent. We don't need it to be translucent."
   Plus: "calorie should extend all the way to the top and stop at
   the bottom right beneath messages."  And on Community: "you put a
   black line on top of search and the community. We don't need that
   line. If we need anything there, we're gonna need the calorie
   background."

   Implementation:
     • position: sticky pins the hero at viewport top:0 once the
       user scrolls past its natural position.
     • background-color: var(--bg-fallback) provides a SOLID backing
       (white in light, black in dark) so scrolled content can't
       bleed through — fixes the founder's "back of that panel is
       translucent" complaint (the old var(--tile-blue) was 92%
       indigo cloth = 8% see-through).
     • A ::before pseudo-element paints the cowrie pattern at
       opacity:0.45 (same mute as html::before) so the hero looks
       like cowrie-on-paper, matching the RN <CowrieHeader/> visual.
       Pseudo-element approach (instead of background-image directly)
       so we can opacity-mute the cowrie WITHOUT muting the title
       text + icons inside the hero.
     • z-index: 10 keeps the hero above the post / alert / thread
       tiles below it during scroll.
     • For continuity in the safe-area-top region (above the hero
       on iOS / PWA), html::before still renders the same cowrie
       pattern via --cowrie-strip-height — both use the same PNG
       at the same 0.45 opacity, so they read as one continuous
       cowrie field from viewport top down to the hero bottom. */
.awujo-tabs,
.alerts-hero,
.messages-hero {
  position: sticky;
  top: 0;
  z-index: 10;
  background-color: var(--bg-fallback);
}
.awujo-tabs::before,
.alerts-hero::before,
.messages-hero::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--bg-image);
  background-position: center top;
  background-size: cover;
  background-repeat: no-repeat;
  opacity: 0.45;
  pointer-events: none;
  z-index: 0;
}
@media (min-width: 900px) {
  .awujo-tabs::before,
  .alerts-hero::before,
  .messages-hero::before {
    background-image: url('./assets/desktop-background.png?v=20260417b');
  }
}
[data-theme="dark"] .awujo-tabs::before,
[data-theme="dark"] .alerts-hero::before,
[data-theme="dark"] .messages-hero::before {
  background-image: url('./assets/feed-cowries.png?v=20260420d');
  /* Dark cowrie PNG reads heavier per pixel on the near-black canvas;
     match html::before's dark opacity so the pattern density looks
     consistent above (html::before) and inside (this ::before) the
     hero. */
  opacity: 0.25;
}
@media (min-width: 900px) {
  [data-theme="dark"] .awujo-tabs::before,
  [data-theme="dark"] .alerts-hero::before,
  [data-theme="dark"] .messages-hero::before {
    background-image: url('./assets/feed-cowries-desktop.png?v=20260420d');
  }
}
/* Lift the hero's direct children above the ::before cowrie overlay
   (which sits at z-index: 0 inside the hero). */
.awujo-tabs > *,
.alerts-hero > *,
.messages-hero > * {
  position: relative;
  z-index: 1;
}

/* ── Type scale ──────────────────────────────────────── */
/*
  Page titles  (h1 on ajo, awujo, messages, alerts, leaderboard): 28px
  Person names (h1 on profile pages): clamp(22px, 3.5vw, 28px)
  Section hdrs (h2 inside panels, panel-head): 18px
  Content word (h2 in ÀJỌ feed): 26px  ← set per-page
  Body copy:   15px
  Meta/secondary: 13–14px
  Labels/caps: 11–12px
*/
h1 {
  margin: 0 0 4px;
  font-family: 'Literata', 'Noto Serif', serif;
  font-size: 28px;
  font-weight: 700;
  line-height: 1.15;
  color: var(--ink);
}

h2 {
  margin: 0;
  font-family: inherit;
  font-size: 18px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--ink);
}

p { margin: 0; }

.page {
  width: min(980px, 100%);
  margin: 0 auto;
  /* Flex column so a .tile-filler at the end grows to cover the empty
     space between the last real tile and the bottom nav, so the user
     never sees the raw page backdrop poking through on short pages
     (messages with 1 thread, post with 0 comments, etc.).  On long
     pages the filler collapses to 0 — no extra paper. */
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - 96px);
}

/* Paper filler that takes whatever vertical space is left in a .page
   AND extends past body's 96px bottom padding (the nav reserve) so
   paper paints all the way to the viewport bottom, behind and past
   the floating nav.
   - flex:1 0 0 lets it collapse to 0 on long pages (no extra paper past
     content) and grow to fill remaining space on short pages.
   - padding-bottom:96px + margin-bottom:-96px is the "extend past parent
     padding" trick: the padding paints 96px of extra tile material into
     body's bottom-padding area while the negative margin keeps the
     parent .page's layout size unchanged. */
.tile-filler {
  flex: 1 0 0;
  background: var(--tile-blue);
  box-shadow: inset 0 1px 0 0 rgba(9, 11, 26, .94);
  margin-bottom: -96px;
  padding-bottom: 96px;
}
/* Dark-theme tile-filler: near-black inner edge would disappear on the
   neutral near-black canvas — flip to white-frost so the top seam still
   reads (matches mobile's listStitch / heroStitch convention). */
[data-theme="dark"] .tile-filler {
  box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.10);
}

/* ── Topbar ─────────────────────────────────────────── */
.topbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 18px;
}

.brand, .link { text-decoration: none; color: inherit; }

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

.brand-logo {
  width: 56px;
  height: 56px;
  display: block;
  object-fit: contain;
  border-radius: 10px;
}

.topbar-right {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-left: auto;
}

.topbar-right .link { display: none; }

.link {
  padding: 10px 14px;
  border-radius: 10px;
  background: rgba(255, 255, 255, .7);
  border: 1px solid var(--line);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  transition: background 160ms ease, box-shadow 160ms ease, opacity 160ms ease;
}

.link:hover  { background: rgba(255, 255, 255, .92); box-shadow: 0 2px 8px rgba(33,28,17,.08); }
.link:active { opacity: .72; }

/* ── Unread dot ──────────────────────────────────────── */
.link.has-unread-dot,
.app-nav-item.has-unread-dot { position: relative; }

.link.has-unread-dot::after,
.app-nav-item.has-unread-dot::after {
  content: '';
  position: absolute;
  top: 9px;
  right: 11px;
  width: 9px;
  height: 9px;
  border-radius: 999px;
  background: #ff6b6b;
  /* Default light theme: warm-cream ring against the light nav. */
  box-shadow: 0 0 0 2px rgba(255, 253, 250, .9);
}
[data-theme="dark"] .link.has-unread-dot::after,
[data-theme="dark"] .app-nav-item.has-unread-dot::after {
  box-shadow: 0 0 0 2px rgba(var(--brand-rgb), .6);
}

/* ── User pill ───────────────────────────────────────── */
.user-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border-radius: 10px;
  text-decoration: none;
  overflow: hidden;
}
.user-pill img {
  width: 42px;
  height: 42px;
  border-radius: 10px;
  object-fit: cover;
  display: block;
  flex: 0 0 auto;
}

.user-pill-badge {
  width: 42px;
  height: 42px;
  border-radius: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, #1a1c3a 0%, #252a6e 100%);
  color: #fff;
  font-weight: 700;
  letter-spacing: .04em;
  flex: 0 0 auto;
}

/* ── Panel / hero base ───────────────────────────────── */
/* Border color changed from translucent-white to a dark navy-tinted
   alpha so:
   (a) On navy tile-blue heroes (profile / account), the outline reads
       as a defined dark edge instead of a faint white ghost line.
   (b) On cream .panel tiles (settings, word), the outline is still
       present but now reads as a proper muted dark border rather than
       the barely-visible white-on-cream it was before. */
.hero, .panel {
  background: var(--panel);
  border: 1px solid rgba(9, 11, 26, .35);
  box-shadow: var(--shadow);
  border-radius:var(--tile-radius);
}
/* Dark-theme override — the navy-ink .35 border above disappears on
   the new neutral near-black canvas.  White-frost border at 8%
   matches --tile-border in dark and reads as a thin white edge. */
[data-theme="dark"] .hero,
[data-theme="dark"] .panel {
  border-color: rgba(255, 255, 255, 0.08);
}

/* ── Page entrance animation ─────────────────────────── */
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.page { animation: fadeUp 260ms cubic-bezier(.25,.8,.35,1) both; }

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

/* ── Focus-visible ring ──────────────────────────────── */
:focus-visible {
  outline: 3px solid rgba(42, 46, 165, .45);
  outline-offset: 2px;
}
[data-theme="dark"] :focus-visible {
  outline-color: rgba(129, 140, 248, .65);
}

/* Input-specific focus (replaces outline with glow) */
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: none;
  border-color: rgba(42, 46, 165, .4) !important;
  box-shadow: 0 0 0 4px rgba(42, 46, 165, .12) !important;
}
[data-theme="dark"] input:focus-visible,
[data-theme="dark"] textarea:focus-visible,
[data-theme="dark"] select:focus-visible {
  border-color: rgba(129, 140, 248, .5) !important;
  box-shadow: 0 0 0 4px rgba(129, 140, 248, .14) !important;
}

/* ── Common utility ──────────────────────────────────── */
.subline { color: var(--muted); line-height: 1.7; margin: 0; }

.empty {
  padding: 28px 20px;
  border-radius: 10px;
  border: 1px dashed var(--line);
  color: var(--muted);
  background: rgba(255, 255, 255, .58);
  text-align: center;
  line-height: 1.6;
}

/* ── Splash screen ───────────────────────────────────── */
/* Fills the entire viewport edge-to-edge. Background matches the app's
   theme-color (#1e2046 navy) instead of the earlier beige/cream gradient
   — before this fix, the beige bg leaked through as a visible flash
   at load time on both first visit (before the splash video's first
   frame paints) and on any frame where the video letterboxes against
   an odd aspect ratio. Navy = same color as the <meta theme-color>
   tag, so there's no perceptible transition between the browser
   chrome and the splash. */
#appSplash {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  height: 100lvh;
  display: grid;
  place-items: stretch;
  /* White behind the splash video so nothing navy-ish ever peeks
     during the fade-in/out.  The video covers the full viewport
     edge-to-edge, but if it briefly fails to paint (codec init lag,
     cold decoder), the user sees white instead of navy. */
  background: #ffffff;
  opacity: 1;
  visibility: visible;
  pointer-events: none;
  transition: opacity .34s ease;
  z-index: 9999;
  overflow: hidden;
}
#appSplash > video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Strip the WebView's default media chrome so no play button, loading
   spinner, or centered play-arrow overlay flashes during the ~100ms
   between element mount and autoplay start. iOS Safari and Android
   WebView both honor these pseudo-elements. */
#appSplash > video::-webkit-media-controls,
#appSplash > video::-webkit-media-controls-enclosure,
#appSplash > video::-webkit-media-controls-panel,
#appSplash > video::-webkit-media-controls-start-playback-button,
#appSplash > video::-webkit-media-controls-overlay-play-button {
  display: none !important;
  -webkit-appearance: none !important;
  appearance: none !important;
  opacity: 0 !important;
}
#appSplash.is-hidden {
  opacity: 0;
  visibility: hidden;
}

/* ── App navigation (bottom floating bar — blue treatment) ── */
/* Width leaves comfortable breathing room from the viewport edges (24px each
   side on small phones, plus respect iOS safe-area insets) so the bar doesn't
   feel pasted onto the screen edge.  Capped at 820px on wide screens. */
.app-nav {
  display: grid;
  position: fixed;
  left: 50%;
  bottom: max(10px, env(safe-area-inset-bottom));
  transform: translateX(-50%);
  width: min(820px, calc(100% - 48px - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px)));
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 6px;
  padding: 10px 8px;
  /* Nav keeps a small rounded radius of its own.  Tiles use --tile-radius: 0
     because they extend edge-to-edge and rounded corners would get clipped by
     the viewport — but the nav floats above content with its own gutter.  A
     subtle radius reads softer than hard square corners without looking
     like a pill. */
  border-radius: 10px;
  border: none;
  /* Same woven-cloth noise as the feed tiles on top of the nav's own
     dark-blue gradient.  Keeps the nav visually part of the material
     family (ÀJỌ cloth) rather than looking like a different surface.
     Decorative corner squares on .app-nav::before/::after removed —
     they were leftover from the old indigo aesthetic and don't fit
     the current palette on either theme. */
  /* Default LIGHT nav: white base with the same chunky warm paper grain
     as the tile material — keeps the nav visually part of the same
     "paper" family instead of reading as a flat chrome surface. Nav
     stays fully opaque (no cowrie bleed) so content underneath doesn't
     muddy the icon readability. */
  background: #ffffff;
  box-shadow: 0 0 0 1px rgba(var(--brand-rgb), 0.1), 0 20px 40px rgba(var(--brand-rgb), 0.12);
  z-index: 40;
  overflow: hidden;
}
[data-theme="dark"] .app-nav {
  /* Mobile flipped to pure black; same here for visual parity. */
  background: #000000;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .12), 0 -4px 16px rgba(0, 0, 0, .4);
}

/* Nav corner decorations removed on dark theme — they were leftover
   from the old heavy-indigo aesthetic and don't fit the current
   cleaner palette.  Light theme wouldn't see them anyway (white-on-
   white at 4% alpha is invisible), so removing outright is safe. */

.app-nav-item {
  min-height: 58px;
  border-radius: 10px;
  text-decoration: none;
  color: var(--nav-ink);
  /* Kill the default Android/WebKit blue tap flash.  Our own :active
     styling (grey tint) is what the user should see on press, not the
     OS-default accent-color overlay. */
  -webkit-tap-highlight-color: transparent;
  tap-highlight-color: transparent;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  font-size: 11px;
  letter-spacing: .02em;
  border: 0;
  background: transparent;
  font-family: inherit;
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease, transform 120ms ease;
  position: relative;
  z-index: 1;
}

.app-nav-item:hover  { background: rgba(var(--brand-rgb), .06); color: var(--nav-ink-active); }
.app-nav-item:active { transform: scale(.93); }
[data-theme="dark"] .app-nav-item:hover { background: rgba(255, 255, 255, .08); color: #fff; }

.app-nav-item svg {
  width: 21px;
  height: 21px;
  display: block;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.app-nav-item.is-active {
  background: rgba(var(--brand-rgb), .10);
  color: var(--nav-ink-active);
  font-weight: 700;
}
[data-theme="dark"] .app-nav-item.is-active {
  background: rgba(255, 255, 255, .12);
  color: #fff;
}

.app-nav {
  transition: transform 280ms cubic-bezier(.4,0,.2,1), opacity 280ms ease;
}

.app-nav.is-hidden {
  transform: translateX(-50%) translateY(calc(100% + 20px));
  opacity: 0;
  pointer-events: none;
}

/* ── Bilingual label utility ────────────────────────────
   Pairs a Yoruba primary label with a small English gloss
   stacked underneath.  Used in the bottom nav (Àwùjọ /
   Community, Àtẹ̀jíṣẹ́ / Messages) and the home-feed tab
   bar (Dájọ / Contribute, Ṣàwárí / Search) so users learn
   the Yoruba word from context without needing a settings
   toggle.  Keep the gloss visually subordinate — the
   primary identity is Yoruba; English is a learning aid.
   No JS, no preference state. */
.bilingual-en {
  display: block;
  font-size: 8.5px;
  font-weight: 500;
  letter-spacing: .04em;
  opacity: .62;
  margin-top: 1px;
  text-transform: none;
  line-height: 1.1;
  text-align: center;
}
/* Center both lines (Yoruba primary + English gloss) inside
   the label container.  Without this, the longer English line
   sets the span's width and the shorter Yoruba word above
   reads as left-aligned. */
.app-nav-item > span,
.feed-tab-btn > span,
.wotd-challenge > span,
.word-share-suggest > span {
  text-align: center;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
}
.app-nav-item.is-active .bilingual-en,
.feed-tab-btn.is-active .bilingual-en {
  opacity: .82;
}

/* ── Toast / snackbar ────────────────────────────────── */
.toast-shelf {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  z-index: 9000;
  pointer-events: none;
}

/* Editorial toast — cream panel + serif headline + inset top highlight
   for paper tactility.  Tone is carried by border + icon color only,
   not by flooding the whole bg: keeps the celebration feel consistent
   across success / info / error.  Old indigo-tinted palette replaced
   with oxblood (positive) / red (danger) / stone (neutral). */
.toast {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 22px 28px;
  border-radius: 10px;
  font-family: 'Literata', 'Noto Serif', serif;
  font-size: 16px;
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
  pointer-events: auto;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.6),
    0 20px 48px rgba(var(--brand-rgb), 0.18);
  background: var(--panel);
  border: 1px solid rgba(var(--brand-rgb), 0.12);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--ink);
  opacity: 0;
  transform: scale(.9);
  transition: opacity 250ms ease, transform 250ms ease;
  min-width: 220px;
  max-width: min(360px, calc(100vw - 48px));
}

.toast.is-visible {
  opacity: 1;
  transform: scale(1);
}

.toast.is-leaving {
  opacity: 0;
  transform: scale(.9);
}

/* Tone variants — single warm accent per state.  Oxblood for success
   ties into the rest of the editorial palette (active likes, CTAs).
   Error keeps a saturated red so danger still reads as danger. Info
   uses stone for neutral moments (copied link, saved draft). */
.toast--success { border-color: rgba(168, 65, 58, 0.32); }
.toast--success .toast-icon { color: var(--oxblood, #a8413a); }
.toast--error   { border-color: rgba(185, 43, 36, 0.42); }
.toast--error   .toast-icon { color: #a82923; }
.toast--info    { border-color: rgba(158, 142, 122, 0.38); }
.toast--info    .toast-icon { color: var(--stone, #9e8e7a); }

.toast-icon { flex-shrink: 0; width: 28px; height: 28px; color: var(--oxblood, #a8413a); }

/* Dark theme: restore the old indigo-accented toast — the editorial
   palette was tuned for the cream ground; oxblood on indigo reads
   muddy.  Dark keeps its blue-accent celebration aesthetic. */
[data-theme="dark"] .toast--success { border-color: rgba(129, 140, 248, .28); }
[data-theme="dark"] .toast--success .toast-icon { color: var(--accent); }
[data-theme="dark"] .toast--error   { border-color: rgba(220, 60, 50, .35); }
[data-theme="dark"] .toast--error   .toast-icon { color: #f87171; }
[data-theme="dark"] .toast--info    { border-color: rgba(129, 140, 248, .28); }
[data-theme="dark"] .toast--info    .toast-icon { color: var(--accent); }
[data-theme="dark"] .toast-icon     { color: var(--accent); }

/* ── Skeleton loading ────────────────────────────────── */
@keyframes shimmer {
  0%   { background-position: -400px 0; }
  100% { background-position: 400px 0; }
}

.skeleton {
  background: linear-gradient(90deg, rgba(0,0,0,.07) 25%, rgba(0,0,0,.13) 50%, rgba(0,0,0,.07) 75%);
  background-size: 800px 100%;
  animation: shimmer 1.4s infinite linear;
  border-radius: 10px;
}

/* ── Responsive ──────────────────────────────────────── */
@media (max-width: 720px) {
  /* No horizontal padding on mobile either — tiles extend fully edge-to-edge. */
  /* Top padding 0 so tiles start flush with the viewport edge. */
  body { padding: 0 0 96px; }
}

/* ── Streak ring (generic, works on ANY avatar) ───────
   Any element that gets the `streak-ring` class + CSS variables
   --ring-color / --ring-glow applied inline picks up the streak
   treatment automatically.  Set via community_streak_ring_attrs()
   in PHP (see api/community_auth.php).  Applies to feed author
   avatars, comment authors, message participants, leaderboard rows,
   search results — anywhere we render a user's face.
   Covers common avatar sizes: small (~28px), medium (~42-48px),
   large (~50-76px).  The ring is a 2-3px colored border + optional
   glow shadow.  Inside inset keeps the ring distinct from the
   avatar's own border. */
.streak-ring {
  border: 2px solid var(--ring-color, transparent) !important;
  border-radius: 999px;
}
.streak-glow {
  box-shadow: 0 0 10px 2px var(--ring-glow, transparent),
              inset 0 0 0 2px rgba(var(--brand-rgb), 0.12);
}
[data-theme="dark"] .streak-glow {
  box-shadow: 0 0 10px 2px var(--ring-glow, transparent),
              inset 0 0 0 2px rgba(255, 255, 255, 0.12);
}

/* ── Verified badge ────────────────────────────────────
   The SVG (see verified_badge_html in community_auth.php) is split:
     circle fill = currentColor  — takes .verified-badge's color
     path stroke = var(--verified-check) — inner check color
   Light theme: dark circle + white check (reads on white bg).
   Dark theme: white circle + navy check (original look). */
.verified-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  vertical-align: middle;
  margin-left: 4px;
  overflow: visible;
  color: #1a1b26;
  --verified-check: #ffffff;
}
[data-theme="dark"] .verified-badge {
  color: #ffffff;
  --verified-check: #252a6e;
}
.verified-badge svg {
  width: .52em;
  height: .52em;
  display: block;
  overflow: visible;
}

/* ── Dark mode ──────────────────────────────────────────────────────
   This block was historically a side-mode; post-theme-flip it's where
   the ORIGINAL navy cowrie experience lives.  Keeping the exact same
   --tile-blue formula + the same --ink/--muted values as before so no
   hand-tuned per-page styling has to change to look right in dark mode.
   Users who want "the old look" set data-theme="dark" and get it back. */
/* Dark palette ported from RN staging (Samuel's 5/8 refactor on
   feature/profile-feed-ads).  Old: indigo-navy + lavender-frost.
   New: neutral near-black + white-frost.  Variable names unchanged
   so per-page styling that consumes --panel / --line / --bg / etc.
   doesn't need a touch — values just resolve to neutral now. */
[data-theme="dark"] {
  --ink:        #e2e0f2;
  --muted:      #9ca7bf;
  --bg:         #000000;
  --bg-fallback: #000000;
  --panel:      rgba(18, 18, 18, .98);
  --line:       rgba(255, 255, 255, .10);
  --accent:     #818cf8;
  --forest:     #4ade80;
  --shadow:     0 24px 60px rgba(0, 0, 0, .55);
  --tile-ink:   #ffffff;
  --tile-border: rgba(255, 255, 255, 0.08);
  --tile-muted: rgba(255, 255, 255, 0.55);
  --nav-ink:    rgba(255, 255, 255, .55);
  --nav-ink-active: #ffffff;
  /* --tile-blue was a heavy indigo radial+linear gradient; now a flat
     neutral surface so anywhere that paints --tile-blue (hero,
     feed-tabs-bar, leaderboard rail, ad cards, etc.) reads as clean
     black paper instead of glossy navy. */
  --tile-blue: rgba(18, 18, 18, .98);
  /* Dark-theme override: brand-rgb flips to white channels so any
     rgba(var(--brand-rgb), X) tint reads as neutral white-frost
     against the near-black canvas instead of indigo-on-black. */
  --brand-rgb: 255, 255, 255;
  /* Dark-theme divider-soft override — 6% white reads as a barely-
     there separation on the near-black canvas, mirroring the 6% navy
     on light.  See the light :root for the full rationale. */
  --divider-soft: rgba(255, 255, 255, 0.06);
}

[data-theme="dark"] #appSplash {
  /* Was a 3-stop indigo gradient; flat black to match mobile. */
  background: #000000;
}

[data-theme="dark"] .app-nav {
  box-shadow: 0 20px 40px rgba(0, 0, 0, .5);
}

[data-theme="dark"] .link {
  background: rgba(255, 255, 255, .06);
  border-color: rgba(255, 255, 255, .12);
}

[data-theme="dark"] .link:hover {
  background: rgba(255, 255, 255, .10);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .25);
}

/* Cards — neutral near-black surface (mirrors mobile's `surface:
   rgba(18, 18, 18, 0.98)`).  Was indigo `rgba(28, 26, 50, .88)` with
   lavender borders; flipped to neutral white-frost throughout. */
[data-theme="dark"] .feed-card,
[data-theme="dark"] .comment-item,
[data-theme="dark"] .row,
[data-theme="dark"] .member-card,
[data-theme="dark"] .activity-item,
[data-theme="dark"] .item,
[data-theme="dark"] .thread-link,
[data-theme="dark"] .stat-card {
  background: rgba(18, 18, 18, .98) !important;
  border-color: rgba(255, 255, 255, .08) !important;
}

[data-theme="dark"] .thread-link:hover,
[data-theme="dark"] .thread-link:focus-visible,
[data-theme="dark"] .item:hover,
[data-theme="dark"] .item:focus-within {
  /* Slightly elevated neutral on hover — was indigo `rgba(40, 36, 68, .92)`. */
  background: rgba(28, 28, 28, .98) !important;
  border-color: rgba(255, 255, 255, .14) !important;
}

[data-theme="dark"] .comment-item.is-highlighted,
[data-theme="dark"] .feed-card.is-highlighted {
  border-color: var(--accent) !important;
  box-shadow: 0 0 0 3px rgba(129, 140, 248, .18) !important;
}

/* Buttons and pill inputs */
[data-theme="dark"] .tab-button,
[data-theme="dark"] .tab-link,
[data-theme="dark"] .toggle-button,
[data-theme="dark"] .word-link,
[data-theme="dark"] .badge-button {
  background: rgba(255, 255, 255, .06);
  border-color: rgba(255, 255, 255, .12);
}

[data-theme="dark"] .tab-button.is-active,
[data-theme="dark"] .tab-link.is-active {
  background: var(--accent) !important;
  border-color: var(--accent) !important;
  color: #fff !important;
}

/* Inputs and textareas — flat solid fill, mirrors mobile's
   DARK_INPUT_BG (`#252a38`) after the 5/8 refactor.  Was a
   translucent indigo (`rgba(28, 26, 50, .9)`); flipping to a single
   solid color matches Samuel's "no shadow, uniform border, solid bg"
   treatment on RN's Input component. */
[data-theme="dark"] textarea,
[data-theme="dark"] input[type="text"],
[data-theme="dark"] input[type="email"],
[data-theme="dark"] input[type="password"],
[data-theme="dark"] input[type="search"],
[data-theme="dark"] select {
  background: #252a38;
  border-color: rgba(255, 255, 255, .12);
  color: var(--ink);
  color-scheme: dark;
}

/* Empty states */
[data-theme="dark"] .empty,
[data-theme="dark"] .status {
  background: rgba(20, 20, 20, .6);
  border-color: rgba(255, 255, 255, .10);
}

/* Hardcoded dark text on cards */
[data-theme="dark"] .feed-gloss,
[data-theme="dark"] .section-copy { color: var(--muted); }

/* Skeleton in dark mode */
[data-theme="dark"] .skeleton {
  background: linear-gradient(90deg, rgba(255,255,255,.04) 25%, rgba(255,255,255,.08) 50%, rgba(255,255,255,.04) 75%);
  background-size: 800px 100%;
}

/* Theme toggle button icons */
.theme-toggle .icon-sun  { display: block; }
.theme-toggle .icon-moon { display: none; }
[data-theme="dark"] .theme-toggle .icon-sun  { display: none; }
[data-theme="dark"] .theme-toggle .icon-moon { display: block; }

/* ── Streak callout ──────────────────────────────────── */
.streak-callout {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 18px;
  border-radius: 10px;
  border: 1px solid rgba(245, 158, 11, .22);
  background: rgba(245, 158, 11, .07);
  font-size: 14px;
  line-height: 1.45;
}
.streak-callout svg { flex-shrink: 0; color: #d97706; }
.streak-callout strong { font-size: 15px; }
[data-theme="dark"] .streak-callout {
  border-color: rgba(251, 191, 36, .18);
  background: rgba(251, 191, 36, .05);
}

/* ── Word of the Day ─────────────────────────────────── */
.wotd-card {
  padding: 22px 24px;
  border-radius: 10px;
  border: 1px solid rgba(42, 46, 165, .15);
  background: rgba(42, 46, 165, .05);
  display: grid;
  gap: 3px;
}
.wotd-eyebrow {
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 700;
  margin: 0 0 8px;
}
.wotd-word {
  font-family: 'Literata', 'Noto Serif', serif;
  font-size: 26px;
  font-weight: 700;
  line-height: 1.1;
  color: var(--ink);
  margin: 0;
}
.wotd-gloss { font-size: 15px; color: var(--muted); margin: 3px 0 0; }
.wotd-link {
  margin-top: 12px;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 13px;
  color: var(--accent);
  text-decoration: none;
  font-weight: 600;
}
.wotd-link svg { width: 13px; height: 13px; stroke: currentColor; stroke-width: 2.4; fill: none; stroke-linecap: round; stroke-linejoin: round; }
[data-theme="dark"] .wotd-card {
  border-color: rgba(129, 140, 248, .16);
  background: rgba(129, 140, 248, .05);
}

/* ── Recent Additions strip ──────────────────────────── */
.recent-section { display: grid; gap: 8px; padding: 0 2px; }
.recent-label {
  font-size: 11px;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 700;
}
.recent-strip {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 4px;
  -webkit-overflow-scrolling: touch;
}
.recent-strip::-webkit-scrollbar { display: none; }
.recent-chip {
  flex: 0 0 auto;
  display: grid;
  gap: 2px;
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, .70);
  text-decoration: none;
  color: inherit;
  min-width: 100px;
  max-width: 160px;
  transition: border-color .16s ease;
}
.recent-chip:hover { border-color: rgba(42, 46, 165, .22); }
.recent-chip-word {
  font-family: 'Literata', 'Noto Serif', serif;
  font-size: 14px;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--ink);
}
.recent-chip-gloss {
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
[data-theme="dark"] .recent-chip {
  background: rgba(20, 20, 20, .85);
  border-color: rgba(255, 255, 255, .10);
}

/* ── Approval celebration overlay ───────────────────── */
.celebration-overlay {
  position: fixed;
  inset: 0;
  z-index: 9990;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(8, 6, 20, .68);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 360ms ease;
}
.celebration-overlay.is-visible {
  opacity: 1;
  pointer-events: auto;
}
.celebration-card {
  position: relative;
  background: var(--panel);
  /* Editorial palette: oxblood border tint instead of indigo — matches
     the toast + active-like accent.  Dark-theme override below keeps
     the original indigo border so the celebration stays celebratory
     on the navy canvas. */
  border: 1px solid rgba(168, 65, 58, 0.26);
  border-radius: 10px;
  padding: 36px 32px 28px;
  max-width: 400px;
  width: 100%;
  text-align: center;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.6),
    0 32px 80px rgba(var(--brand-rgb), 0.32);
  transform: scale(.92) translateY(18px);
  transition: transform 420ms cubic-bezier(.2,.8,.4,1);
  overflow: hidden;
}
[data-theme="dark"] .celebration-card {
  border-color: rgba(129, 140, 248, .26);
  box-shadow: 0 32px 80px rgba(0, 0, 0, .32);
}
.celebration-overlay.is-visible .celebration-card { transform: scale(1) translateY(0); }
.c-particle {
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: 10px;
  opacity: 0;
  animation: cParticle 1.1s cubic-bezier(.2,.8,.4,1) forwards;
}
@keyframes cParticle {
  0%   { opacity: 1; transform: translate(0, 0); }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)); }
}
.celebration-badge {
  width: 64px; height: 64px;
  border-radius: 10px;
  background: rgba(42, 46, 165, .10);
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto 16px;
  color: var(--accent);
}
.celebration-badge svg { width: 30px; height: 30px; stroke: currentColor; stroke-width: 1.8; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.celebration-title { font-family: 'Literata','Noto Serif',serif; font-size: 20px; font-weight: 700; margin: 0 0 8px; color: var(--ink); }
.celebration-word  { font-family: 'Literata','Noto Serif',serif; font-size: 32px; font-weight: 700; color: var(--accent); margin: 0 0 4px; }
.celebration-gloss { font-size: 16px; color: var(--muted); margin: 0 0 12px; }
.celebration-body  { font-size: 14px; color: var(--muted); margin: 0 0 22px; line-height: 1.6; }
.celebration-dismiss {
  padding: 11px 28px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  font: inherit; font-size: 14px;
  color: var(--ink); cursor: pointer;
}

/* ── Shareable word card ─────────────────────────────── */
.word-share-card {
  margin-top: 20px;
  padding: 28px 26px 22px;
  border-radius:var(--tile-radius);
  background: linear-gradient(135deg, #1a1c3a 0%, #252a6e 100%);
  color: #fff;
  position: relative;
  overflow: hidden;
}
.word-share-card::before {
  content: '';
  position: absolute;
  top: -60px; right: -60px;
  width: 200px; height: 200px;
  border-radius: 10px;
  background: rgba(255,255,255,.04);
  pointer-events: none;
}
.word-share-brand { font-size: 10px; letter-spacing: .17em; text-transform: uppercase; opacity: .5; margin: 0 0 14px; }
.word-share-yoruba { font-family: 'Literata','Noto Serif',serif; font-size: 34px; font-weight: 700; line-height: 1.1; margin: 0 0 8px; }
.word-share-english { font-size: 17px; opacity: .78; margin: 0 0 4px; }
.word-share-tone    { font-size: 13px; opacity: .52; margin: 0; }
.word-share-action  {
  margin-top: 18px;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 10px 20px;
  border-radius: 10px;
  border: 1px solid rgba(255,255,255,.22);
  background: rgba(255,255,255,.10);
  color: #fff; font: inherit; font-size: 14px;
  cursor: pointer; transition: background .18s ease;
}
.word-share-action:hover { background: rgba(255,255,255,.18); }
.word-share-action svg { width: 15px; height: 15px; stroke: currentColor; stroke-width: 2; fill: none; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
