/* ============================================================
   Small World — stylesheet for the real web client.

   Design tokens and component styles are carried over from the prototype
   (reference/small-world.html): Apple-style layered surfaces, terracotta as the
   single accent, system font everywhere except the serif wordmark, light/dark by
   system setting, and type in rem so it scales with the reader's text-size setting.

   The prototype is a look-and-behaviour reference only; this file is hand-built for
   the real app and deliberately free of inline styles so a strict CSP can ship.
   ============================================================ */

:root{
  color-scheme: light dark;

  /* Surfaces */
  --bg:            #ffffff;
  --bg-grouped:    #f4f4f6;
  --surface:       #ffffff;
  --surface-2:     #f0f0f3;
  --surface-hover: #f6f6f8;

  /* Text (all pass WCAG AA on their surfaces) */
  --text:    #1c1c1e;
  --text-2:  #5c5c63;
  --text-3:  #6e6e73;

  /* Lines */
  --separator:        rgba(60,60,67,.11);
  --separator-strong: rgba(60,60,67,.20);
  /* ⚠️ The edge of a control whose BORDER is the only thing saying "this is pressable" —
     currently the ghost button. Deliberately much stronger than --separator-strong, which is
     for dividers between things that are already distinguishable. WCAG's non-text floor is
     3:1 and that is what this is tuned to; --separator-strong measures 1.41 on white, which
     is why a ghost button was nearly invisible in light mode.
     (Form fields at line ~344 use --separator-strong and share the same latent weakness. Left
     alone on purpose — nobody has reported it and inputs also carry a fill and a label — but
     it's the same question if it ever comes up.) */
  --control-edge:     rgba(60,60,67,.58);
  --switch-off:       #e4e4e8;   /* off-state switch track (iOS-like neutral grey) */

  /* The brand accent lives in the THEME blocks below (three gemstone palettes, reader's
     choice). Everything here is theme-independent. */

  /* Sage — "verified" and "that worked". ⚠️ DELIBERATELY NOT THEMED (2026-07-25). Sage is a
     SEMANTIC colour, the same category as the danger red below: green means confirmed
     regardless of which gemstone the reader picked, exactly as red means careful. Theming it
     would make a trust signal drift with a decorative preference. One rule, no exceptions —
     which is also why the tourmaline theme is allowed to sit near it in hue (sage is a
     desaturated yellow-green at hue ~110; tourmaline is a saturated blue-green at ~158). */
  --sage:          #587753;
  /* Danger — used ONLY for the destructive safety actions (block/report), never as a
     second brand accent. Button fill is dark enough that white text clears 4.5:1. */
  --danger-btn:    #bf2d1e;   /* white text on this ≈ 5.4:1 */
  --danger-text:   #b3261e;   /* on light surfaces ≈ 5.9:1 */
  --danger-weak:   rgba(179,38,30,.10);

  --shadow:    0 1px 2px rgba(20,20,30,.05), 0 8px 24px rgba(20,20,30,.05);
  --shadow-lg: 0 2px 6px rgba(20,20,30,.08), 0 18px 46px rgba(20,20,30,.10);

  --radius:    17px;
  --radius-sm: 12px;

  --sans:"-apple-system",BlinkMacSystemFont,"SF Pro Text","Segoe UI",system-ui,Roboto,sans-serif;
  /* Nunito, the wordmark face — SELF-HOSTED, never from Google Fonts, which would hand
     every reader's IP to Google on every page load (CLAUDE.md hard constraint; the CSP's
     `font-src 'self'` enforces it).
     ⚠️ WORDMARK ONLY. "System font everywhere except the wordmark" is a settled legibility
     rule; body text and headings stay on the system stack. Widening Nunito's job is a
     separate decision, not a side effect of changing the logo. */
  --display:"Nunito",var(--sans);
}

@media (prefers-color-scheme: dark){
  :root{
    --bg:            #000000;
    --bg-grouped:    #000000;
    --surface:       #1c1c1e;
    --surface-2:     #2c2c2e;
    --surface-hover: #2a2a2c;

    --text:    #f5f5f7;
    --text-2:  rgba(235,235,245,.72);
    --text-3:  rgba(235,235,245,.55);

    --separator:        rgba(84,84,88,.55);
    --separator-strong: rgba(120,120,124,.65);
    --control-edge:     rgba(150,150,158,.75);   /* see the light-mode note; ~3:1 on black */
    --switch-off:       #48484a;   /* off-state switch track, dark */

    /* The accent's dark-mode values also live in the THEME blocks below. */
    --sage:          #86a880;
    /* Danger stays a real red in dark mode (white text still clears 4.5:1); the text
       tint lightens so it reads on near-black. */
    --danger-btn:    #c33322;
    --danger-text:   #ff6b5e;   /* on the dark surface ≈ 5.8:1 */
    --danger-weak:   rgba(255,107,94,.15);

    --shadow:    0 1px 2px rgba(0,0,0,.5), 0 8px 24px rgba(0,0,0,.4);
    --shadow-lg: 0 2px 6px rgba(0,0,0,.55), 0 18px 46px rgba(0,0,0,.5);
  }
}

/* ============================================================
   THEMES — three gemstones, the reader's choice ("Your settings").
   `data-theme` is set on <html> by src/theme.js before first paint.

   ⚠️ TWO PALETTES PER THEME, AND THEY ARE NOT THE SAME THING.
     • --sw-swirl-*  are the LOGO's three bands. They were chosen for how they look as
       adjacent fills in the marble swirl, and they keep fixed brightness steps
       (1.76 / 1.66) so the hidden W still reads when iOS strips colour in Tinted mode.
       ⚠️ Don't hand-pick new ones; match those steps or the mark stops working in mono.
     • --accent-*    are the INTERFACE tint. These are derived, not copied: measured
       against WCAG AA on real surfaces, hitting the same ratios the shipped terracotta
       met (text 5.32, button-with-white-text 5.09, structural 4.23, dark 6.68).

   ⚠️ DO NOT wire the swirl hexes into --accent-*. Only each theme's DEEPEST band clears
   4.5:1 as text, and NONE of them clears it in dark mode — ultramarine's deep band is
   1.97:1 on the dark surface. That is exactly the bug CLAUDE.md records as the worst the
   app ever had (a 2.55:1 dark-mode button, invisible in light-mode screenshots).

   Blocks are `:root[data-theme=…]`, which outranks the bare `:root` above no matter where
   they sit in the file — deliberate, because "CSS specificity, not source order" has
   bitten this project three times.
   ============================================================ */

/* The three logo bands, defined ONCE each. They're reachable two ways: on <html> as the
   active theme, and on any element as `data-theme-preview` — because the You-tab picker has
   to show all three swatches at the same time. Sharing the rule means the swatch you tap can
   never drift from the theme you get. (The picker only needs the swirl bands, so the
   --accent-* tokens below stay scoped to :root and don't leak into a preview subtree.) */
:root, :root[data-theme="ultramarine"], [data-theme-preview="ultramarine"]{
  --sw-swirl-deep:  #1B28E1;
  --sw-swirl-mid:   #354EF2;
  --sw-swirl-light: #5976EA;
}
:root[data-theme="citrine"], [data-theme-preview="citrine"]{
  --sw-swirl-deep:  #C1512C;
  --sw-swirl-mid:   #E07D2A;
  --sw-swirl-light: #F2B23F;
}
:root[data-theme="tourmaline"], [data-theme-preview="tourmaline"]{
  --sw-swirl-deep:  #077950;
  --sw-swirl-mid:   #139E5D;
  --sw-swirl-light: #37C577;
}

/* ---- ultramarine (default — the blue marble) ----
   ⚠️ Deliberately DARKER and MORE SATURATED than the other two, and it must stay that way.
   A first pass solved every theme to terracotta's exact ratios with a saturation cap, which
   for blue meant lightening it — and light blue is LAVENDER, a different colour name. (Green
   and orange survive lightening because "light green" is still green.) Founder flagged it;
   measured, and in light mode there is NO tradeoff at all: full saturation is available at
   every contrast level from 4.5 to 10. So this sits deeper than the shipped terracotta did,
   which buys BOTH more blue and more contrast. Don't "harmonise" it back to match the other
   themes' numbers — the point is that a hue needs its own lightness to keep its identity.

   ⚠️ RE-TUNED 2026-07-30 (founder picked this from four rendered candidates). Every token is
   now the MOST CHROMATIC colour sRGB has at its own lightness and at the true ultramarine hue
   (-92.5° in OKLCh), rather than a value picked by eye. Light gained ~9% chroma AND ~1.2 stops
   of contrast at the same time, because for blue the maximum-chroma point sits DARKER than
   where these tokens were — going deeper costs nothing here and buys both.
   ⚠️ The four tokens have DIFFERENT jobs and different floors (structural 3.0; the rest 4.5),
   so they are solved separately. Don't collapse them to one contrast number — that is what
   left the room unspent the first time. */
:root,
:root[data-theme="ultramarine"]{
  --accent:        #2b44ff;   /* structural: borders, focus ring, thread lines — 6.22 on white */
  --accent-text:   #2309ff;   /* links and tinted labels — 8.23 (4.5 floor) */
  --accent-strong: #1f00eb;   /* hover / emphasis — 8.95 */
  /* ⚠️ NOT the accent's own channels — deliberately PRE-COMPENSATED, and this is the second
     half of the same lesson as the Share button's opacity (see .c-share-inline.is-off).
     Compositing a saturated colour toward WHITE rotates its hue; toward BLACK it barely does.
     Measured: the accent's own rgba(43,68,255,.11) over white lands at hue -80.2° against
     ultramarine's true -92.5° — 12° adrift, which reads as lilac across a flat area like the
     dimmed Share button, the active nav squircle or a private-message card. (The identical
     alpha in DARK mode is only 4° off, which is exactly why the founder saw dark as correct
     and light as purple.) These channels are solved so that the COMPOSITE over white lands on
     the true hue: rgba(10,82,255,.11) over #ffffff = #e4ecff at -92.3°.
     ⚠️ Kept TRANSLUCENT on purpose rather than hardcoding the composite — ~20 rules consume
     this token and a few sit over something other than plain white (the poll bar, the blurred
     phone nav). Staying translucent means they all keep adapting; only the hue is corrected.
     If the accent ever changes, re-solve: channel = (target - 255×(1-a)) ÷ a. The correction
     holds at ANY alpha (measured: 0.1° error from .11 through .20), so the depth below can be
     tuned without re-solving.

     ⚠️ ALPHA .15, NOT the .11 the other two themes use — deliberate, founder-chosen 2026-07-30
     after seeing .11/.15/.20 side by side. Correcting the hue was necessary but not sufficient:
     a VERY pale blue still reads ambiguous, because blue is the hue the eye discriminates worst
     at low chroma (fewest S-cones) and the one we are most used to reading as plain white. Pale
     orange is unmistakably peach and pale green unmistakably mint at the same alpha, which is
     why citrine and tourmaline look right at .11 and blue did not. More chroma is what makes
     the hue legible: .15 carries 0.037 against .11's 0.027.
     ⚠️ Citrine and tourmaline CANNOT follow, and this was measured, not assumed: at .15 their
     labels fall to 4.38 and 4.41, both failing AA. Ultramarine can afford it only because the
     re-tune left its link colour at 8.22:1 on white where theirs sit near 5.3. A per-theme
     alpha is consistent with how these themes already work — ultramarine deliberately runs
     deeper than the other two, and this is the same fact showing up in the tint.
     ⚠️ Don't push it to .20+: the small controls keep improving, but the private-message card
     is a LARGE tinted area and starts to shout, which would make a DM look more important than
     a family post. That was rendered and rejected. */
  --accent-weak:   rgba(10,82,255,.15);
  --accent-btn:    #241fff;   /* fill under white text — 7.74 */
  --btn-text:      #ffffff;
}
/* ---- citrine (warm — closest to the terracotta this app shipped with) ---- */
:root[data-theme="citrine"]{
  --accent:        #cd562f;   /* 4.24 */
  --accent-text:   #b34b29;   /* 5.33 */
  --accent-strong: #a44525;   /* 6.10 */
  --accent-weak:   rgba(205,86,47,.11);
  --accent-btn:    #b84d2a;   /* 5.09 */
  --btn-text:      #ffffff;
}
/* ---- tourmaline (green) ---- */
:root[data-theme="tourmaline"]{
  --accent:        #178c62;   /* 4.24 */
  --accent-text:   #147a55;   /* 5.33 */
  --accent-strong: #12704e;   /* 6.09 */
  /* Pre-compensated the same way — see the ultramarine note. Green drifts 10° over white
     (172.6° against a true 162.5°); citrine is left alone because orange sits in a safer part
     of the gamut and measured only 1° off, i.e. nothing to correct. */
  --accent-weak:   rgba(37,137,82,.11);
  --accent-btn:    #147d58;   /* 5.09 */
  --btn-text:      #ffffff;
}

@media (prefers-color-scheme: dark){
  /* ⚠️ On black every accent flips LIGHT, so white button text would fail badly (terracotta
     measured 2.55:1 there). The fill becomes the light tint and --btn-text flips to near-black,
     which lands at ~6.7:1. Same reversal in all three themes. */
  /* ⚠️ Ultramarine sits LOWER here than the other two themes' 6.68 — bought deliberately, and
     it is the one place blue genuinely has to compromise. Reading on near-black needs a LIGHT
     colour, and every light blue drifts toward periwinkle; taking less contrast headroom is
     what buys back the saturation. Don't raise it to match citrine/tourmaline — that trade was
     made with the numbers in front of us.

     ⚠️ RE-TUNED 2026-07-30. The dark tokens had DRIFTED ~10° OFF-HUE (-81.6° vs the light
     tokens' -92.5° in OKLCh) — so dark mode wasn't a paler ultramarine, it was a genuinely
     different, more violet colour. That drift, not the contrast, is most of why it read as
     periwinkle, and correcting it costs NOTHING. On top of that the tokens now take the
     chroma that was going spare: +9.6% over what shipped.

     ⚠️ THE BINDING CONSTRAINT HERE IS THE TINT, NOT THE SURFACE. --accent-text also sits on
     --accent-weak (the active nav item, the "Followers" pill, the selected picker row), and
     that pair is the tightest in the whole theme system — 4.71 here. A bolder blue was
     measured and rejected: it only stayed legal by thinning the tint to alpha .08, and the
     founder correctly called the result too faint on exactly those three controls. So the
     tint's alpha is a real design variable, not a constant to tidy. */
  :root,
  :root[data-theme="ultramarine"]{
    --accent:        #6088ff;   /* 5.22 on the dark surface */
    --accent-text:   #688eff;   /* 5.62 on the surface, 4.71 on the weak tint — the tight one */
    --accent-strong: #7599ff;   /* 6.29 / 5.29 on the tint */
    --accent-weak:   rgba(96,136,255,.13);
    --accent-btn:    #6088ff;   /* dark text on it — 5.22 */
    --btn-text:      #1c1c1e;
  }
  :root[data-theme="citrine"]{
    --accent:        #de8e74;   /* 6.69 */
    --accent-text:   #e29a82;   /* 7.41 */
    --accent-strong: #e5a48e;   /* 8.10 */
    --accent-weak:   rgba(222,142,116,.16);
    --accent-btn:    #de8e74;
    --btn-text:      #1c1c1e;
  }
  :root[data-theme="tourmaline"]{
    --accent:        #2bb785;   /* 6.68 */
    --accent-text:   #2dc18c;   /* 7.41 */
    --accent-strong: #2fca92;   /* 8.09 */
    --accent-weak:   rgba(43,183,133,.16);
    --accent-btn:    #2bb785;
    --btn-text:      #1c1c1e;
  }
}

*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}
/* Stops the elastic rubber-band scroll at the top and bottom of the page.
   Not cosmetic — device-measured 2026-08-02, an installed home-screen app could get STUCK at
   `scrollY: -118` and stay there: the whole page drawn 118px too low, and every fixed element
   (the nav, the topbar) taking taps 118px away from where it appeared, so the bottom bar
   simply ignored you. `overscroll-behavior` is the property that exists to prevent that bounce.

   ⚠️ SCOPED TO THE INSTALLED APP, and that scoping is the whole point. A first version applied
   it to `html,body` unconditionally, with a comment claiming "there is no pull-to-refresh to
   lose here — a standalone app has none." True of standalone; FALSE of a Safari tab, which has
   pull-to-refresh and lost it. Founder noticed within the hour.
   ⚠️ So: only where the fault actually occurred. The stuck scroll was never seen in a browser
   tab — every report was from the home-screen app — and standalone genuinely has no
   pull-to-refresh, so nothing is taken away here. Don't widen this back to all contexts.
   (`guardAgainstStuckOverscroll()` in app.js still runs everywhere; it only ever repairs a
   NEGATIVE scroll, and never on the scroll event, so it can't fight a live pull gesture.) */
@media (display-mode: standalone){
  html,body{overscroll-behavior-y:none}
}
body{
  font-family:var(--sans);
  background:var(--bg-grouped);
  color:var(--text);
  font-size:1.0625rem;          /* ~17px default, but scales if the reader enlarges text */
  line-height:1.47;
  letter-spacing:-.01em;
  min-height:100vh;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
input{font-family:inherit}
[hidden]{display:none!important}
/* Selected text uses the BUTTON pair, not the plain accent + hardcoded white. The old rule
   (background:var(--accent); color:#fff) failed in dark mode, where the accent is a LIGHT
   tint — white on it measured ~1.9:1. The button pair is the one combination already tuned
   to carry text in both schemes (5.09 light / 6.7 dark), in every theme. */
::selection{background:var(--accent-btn);color:var(--btn-text)}
:focus-visible{outline:2.5px solid var(--accent);outline-offset:2px;border-radius:6px}
*:focus:not(:focus-visible){outline:none}
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}

/* ---------- Brand / wordmark ---------- */
/* Self-hosted Nunito. One variable file covers every weight we'd ever want.
   ⚠️ `format("woff2")`, NOT `format("woff2-variations")` — the latter is an old syntax that
   some parsers don't recognise, and an unrecognised format makes the browser skip the source
   entirely and silently fall back to the system font. The `font-weight` RANGE below is what
   declares this as variable; the format string doesn't need to.
   `font-display:swap` so the wordmark shows in the fallback immediately rather than sitting
   invisible while a 100KB file downloads. */
@font-face{
  font-family:"Nunito";
  src:url("assets/fonts/Nunito.woff2") format("woff2");
  font-weight:200 1000;
  font-style:normal;
  font-display:swap;
}

/* The <symbol> sprite holding the marble. Takes no space and is never painted.
   ⚠️ Must be a CLASS, not style="position:absolute" — the CSP is `style-src 'self'`,
   which blocks inline style attributes (that's why this whole file exists). */
.svg-sprite{position:absolute;width:0;height:0;overflow:hidden}

/* ---- The lockup: marble + wordmark ----
   ⚠️ THE LOCKUP SCALES AS ONE THING. The container carries the size; the wordmark is 1em of
   it and the mark is a fixed multiple. So making the logo bigger or smaller is ONE number in
   ONE place, and the two halves can never drift out of proportion.
   The reference lockup (`lockup-light.png`) measures: mark = 1.75x the wordmark's ink height
   = 1.27x its font-size, gap = 0.54x the mark's width. (HANDOFF.md §5 quotes the same 1.7 but
   calls the gap 40%; 54% is what the artwork actually does.)

   ⚠️ WE DELIBERATELY RUN THE MARK MUCH LARGER THAN THAT REFERENCE (1.95em, not 1.27em) —
   founder's call on device, 2026-07-25, and it should not be "corrected" back. The reference
   ratio was struck for a lockup sitting alone on a page. Ours sits in a topbar as small
   persistent chrome, and the marble carries INTERIOR DETAIL (three bands, a hidden W) that the
   old flat line-art globe didn't: at reference size the swirl collapses into a single blob and
   the mark reads smaller than the globe it replaced, even though it measured the same pixels.
   Detail needs size to survive. The wordmark is unchanged — only the mark grew.

   1.95em puts the mark at 40px in the topbar, the SAME height as the "Only us" toggle beside
   it, so the bar reads as one balanced row rather than a small logo adrift in space. Costs
   nothing: the row is already 40px tall because of that toggle, so anything up to 40px is free.
   (For the record, matching the post avatar's breathing-room ratio instead — 39% of its own
   diameter — would have been 36px; founder chose the toggle match over the avatar match.) */
.brand{display:flex;align-items:center;font-size:1.42rem;gap:.69em}
.brand-mark{flex:none;display:grid;place-items:center}
/* ⚠️ The wordmark is always LOWERCASE — "small world", never title case, never all caps.
   The source text stays "Small World" so the proper name is what gets copied and what a
   screen reader announces; text-transform does the visual half. */
.wordmark{
  font-family:var(--display);font-weight:600;font-size:1em;
  text-transform:lowercase;letter-spacing:-.015em;line-height:1;
}

/* The marble. Sized in em against the lockup, so it tracks the wordmark exactly and grows
   with the reader's text size — a mark pinned to px would shrink beside the words at 200%.
   The rim is currentColor: it takes the surrounding text colour, which is why one file works
   in light AND dark (near-black rim on white, near-white on black). The swirl reads the
   theme's --sw-swirl-* and needs nothing here. */
.marble{width:1.95em;height:1.95em;display:block;color:var(--text)}

/* ---------- Buttons ---------- */
.btn{
  padding:9px 16px;border-radius:11px;font-weight:600;font-size:.92rem;
  transition:transform .12s,background .15s,border-color .15s;
  border:1px solid transparent;white-space:nowrap;min-height:38px;letter-spacing:-.01em;
}
.btn:active{transform:scale(.97)}
.btn-primary{background:var(--accent-btn);color:var(--btn-text)}
.btn-primary:hover{background:var(--accent-strong);color:var(--btn-text)}
/* ⚠️ The border is load-bearing in LIGHT mode, not decoration. --surface-2 (#f0f0f3) on a
   --surface / --bg of #ffffff is a 1.06:1 difference — the LABEL is perfectly readable but the
   button SHAPE is all but invisible, so it doesn't read as something you can press. Founder
   found it on the You tab ("Your profile" / "Sign out"), where ghost buttons sit straight on
   the background with nothing else around them; the same latent problem affects every other
   ghost button (Cancel, Show more, Not now) and this fixes them all at once. Costs no layout:
   .btn already reserves a 1px transparent border. */
.btn-ghost{background:var(--surface-2);color:var(--text);border-color:var(--control-edge)}
.btn-ghost:hover{background:var(--surface-hover)}
.btn-danger{background:var(--danger-btn);color:#fff}
.btn-danger:hover{filter:brightness(.93)}
.btn-sm{padding:7px 15px;font-size:.85rem;min-height:34px;border-radius:10px}

/* ---------- Form fields ---------- */
.field{text-align:left;margin-bottom:16px}
.field label{display:block;font-size:.78rem;font-weight:650;color:var(--text-2);margin-bottom:6px}
.field input{
  width:100%;font-size:1.05rem;padding:13px 15px;border-radius:var(--radius-sm);
  border:1.5px solid var(--separator-strong);background:var(--bg);color:var(--text);
  transition:border-color .15s;
}
.field input:focus{outline:none;border-color:var(--accent)}

/* ---------- Sign-in screen ---------- */
.signin{
  min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:28px;padding:32px 20px;background:var(--bg-grouped);
}
.signin-card{
  width:100%;max-width:420px;text-align:center;
  background:var(--surface);border:1px solid var(--separator);border-radius:var(--radius);
  padding:34px 28px;box-shadow:var(--shadow-lg);
}
.signin-card .brand{justify-content:center;margin-bottom:20px}
.signin-lead{font-size:1rem;color:var(--text-2);line-height:1.5;margin-bottom:24px}
.signin-go{width:100%;min-height:48px;font-size:1rem;border-radius:var(--radius-sm)}
.signin-error{
  font-size:.88rem;color:var(--accent-strong);line-height:1.45;margin-top:14px;
  background:var(--accent-weak);border-radius:var(--radius-sm);padding:10px 13px;text-align:left;
}
.signin-note{font-size:.8rem;color:var(--text-3);line-height:1.45;margin-top:16px}
.signin-values{display:flex;gap:20px;flex-wrap:wrap;justify-content:center}
.signin-values .val{display:flex;align-items:center;gap:7px;font-size:.83rem;color:var(--text-2);font-weight:500}
.signin-values .val svg{flex:none;stroke:var(--sage)}

/* ---------- App shell (signed in) ---------- */
.app{min-height:100vh;display:flex;flex-direction:column;background:var(--bg-grouped)}
/* flex-wrap: at 100% text this is one row. When a reader enlarges text far enough that the
   wordmark and the feed controls can't share a line, the controls drop to a second row rather
   than being pushed off-screen. Same accepted behaviour as the composer's toolbar at zoom —
   graceful two rows beats cramming, and beats hiding the brand. */
.topbar{
  position:sticky;top:0;z-index:5;
  display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;
  /* ⚠️ The top inset is 13px PLUS the safe area, not the safe area alone — same shape as the
     nav's bottom padding, and for the same reason: env() is 0 in a browser tab and on any
     phone without a notch, where this must still be the plain 13px it always was.
     It is load-bearing ONLY in the installed app: `black-translucent` (index.html) stops iOS
     reserving the status-bar strip and hands it to us, so without this the wordmark would sit
     under the clock. In Safari the inset resolves to 0 and nothing moves. */
  padding:calc(13px + env(safe-area-inset-top)) 20px 13px;
  border-bottom:1px solid var(--separator);
  background:color-mix(in srgb,var(--bg) 78%,transparent);
  backdrop-filter:saturate(180%) blur(20px);
  -webkit-backdrop-filter:saturate(180%) blur(20px);
}
/* Branding, not a control — it was a "home" button until the Home tab took that job. */
/* The topbar lockup. ONE number sets both halves (see .brand above) — the mark and the
   wordmark are locked in proportion, so this can't drift.
   ⚠️ Grown from 1.16rem (2026-07-25, founder): moving the feed controls into the topbar left
   the wordmark more room than it had, and the marble carries interior detail the old line-art
   globe didn't, so it earns the size. Deliberately still smaller than the sign-in lockup —
   this one is persistent chrome you see on every screen, not a title. */
.topbar-left{display:flex;align-items:center;font-size:1.28rem;gap:.69em;min-width:0}
.topbar-right{display:flex;align-items:center;gap:8px;flex:none}

.feedwrap{width:100%;max-width:640px;margin:0 auto;padding:22px 18px 60px}
.feed{display:flex;flex-direction:column}

/* Visually hidden, still read aloud. Used for the nav's "(new)" marker, where the dot is
   the sighted cue and the word is the screen-reader one. */
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0;
}

/* ---------- Main navigation (nav.js) ----------
   One component, two shapes. Phone: a bar fixed to the bottom, in thumb reach. Tablet and
   desktop (>820px): a rail down the left, laid out by the grid rule further down. Labels are
   always visible at both sizes — see the note in nav.js. */
.nav{display:flex;background:var(--surface)}
.nav-tab{
  flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
  padding:7px 4px;min-height:52px;color:var(--text-3);
  background:none;border:none;font-family:inherit;cursor:pointer;border-radius:14px;
  -webkit-tap-highlight-color:transparent;transition:color .15s,background .15s;
}
.nav-tab:hover{color:var(--text-2)}
/* The tab you're on carries the brand accent — the single strong colour in the chrome.
   Deliberately --accent-strong, NOT --accent-text: the active tab also gets an --accent-weak
   tint behind it, and --accent-text on that tint fails AA. --accent-strong measures 7.83 light
   / 5.26 dark on the tint. Don't "unify" this back to --accent-text.
   ⚠️ The tinted squircle is on BOTH shapes now (founder, 2026-07-30). It was desktop-only, so
   on a phone the only cue for "where am I" was the accent colour itself — a colour difference
   alone is a weak signal, and the weakest for anyone who doesn't see colour strongly. The
   filled shape is the non-colour cue, and it makes the bar and the rail read the same way. */
.nav-tab.is-on{color:var(--accent-strong);background:var(--accent-weak)}
.nav-icon{position:relative;display:flex;align-items:center;justify-content:center}
.nav-icon svg{display:block;width:1.5em;height:1.5em}   /* em, so it grows with reader text */
.nav-label{font-size:.72rem;font-weight:600;letter-spacing:-.01em;line-height:1.1}
/* Unread marker: a dot, never a count (see nav.js). Sits on the glyph's top-right corner. */
.nav-dot{
  position:absolute;top:-1px;right:-3px;width:9px;height:9px;border-radius:50%;
  background:var(--danger-btn);border:2px solid var(--surface);
}
.nav-dot[hidden]{display:none}

/* --- Phone: a bar fixed to the bottom, in thumb reach. --- */
@media(max-width:820px){
  .nav{
    position:fixed;left:0;right:0;bottom:0;z-index:6;
    border-top:1px solid var(--separator);
    background:color-mix(in srgb,var(--bg) 82%,transparent);
    backdrop-filter:saturate(180%) blur(20px);
    -webkit-backdrop-filter:saturate(180%) blur(20px);
    /* Clear the home indicator on a modern iPhone; 0 everywhere else.
       The 4px top/side padding insets the active tab's tinted squircle so it reads as a
       shape sitting IN the bar rather than a block filling it — without the inset the tab is
       52px inside a 53px bar and the tint runs edge to edge. */
    /* ⚠️ The bottom inset is 4px PLUS the safe area, not the safe area alone: on a phone with
       no home indicator (and in any test viewport) env() resolves to 0, which would sit the
       squircle flush against the bottom edge while the top kept its 4px. */
    padding:4px 6px calc(4px + env(safe-area-inset-bottom));
  }
  /* 46px + the 4px inset keeps the bar the height it already was, and stays comfortably above
     the 44px touch-target floor. */
  .nav-tab{min-height:46px}
  /* ⚠️ The bottom clearance for the nav is NOT set here — it lives with the phone .feedwrap
     shorthand further down this file (search "scroll clear of the bar"). A padding-bottom
     here was silently overridden by that later, equally-specific shorthand, which is the
     "specificity, not source order" trap this file already warns about on .topbar. Keeping
     the value in ONE place is the only reliable fix; this note exists so nobody adds a
     second one back. */
}

/* --- Tablet & desktop: a rail down the left. The shell becomes a two-column grid with the
   topbar spanning both, so the rail sits under it and the reading column stays centred in the
   space that's left. --topbar-h is measured in app.js (the bar grows with reader text size),
   with a sane fallback if that hasn't run yet. --- */
@media(min-width:821px){
  .app{display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr}
  .topbar{grid-column:1 / -1}
  #navMount{grid-column:1;grid-row:2;position:sticky;top:var(--topbar-h,60px);align-self:start}
  .nav{
    flex-direction:column;gap:2px;width:212px;padding:18px 12px;background:none;
  }
  .nav-tab{
    flex:none;flex-direction:row;justify-content:flex-start;gap:13px;
    padding:11px 14px;border-radius:12px;min-height:46px;
  }
  .nav-tab:hover{background:var(--surface-hover)}
  .nav-label{font-size:.98rem}
  .nav-dot{top:0;right:-4px}
  .feedwrap{grid-column:2;grid-row:2}
}

/* A tab's own region (Photos / Notifications / You), shown in place of the feed. */
.tab-view{display:block}
/* First-run / not-yet-built copy inside a tab. Calm and honest — never a fake screen. */
.tab-empty{text-align:center;padding:52px 22px;color:var(--text-2)}
.tab-empty h2{font-size:1.12rem;font-weight:650;color:var(--text);margin-bottom:8px}
.tab-empty p{font-size:.95rem;line-height:1.5;max-width:34ch;margin:0 auto}

/* ---------- Notifications ----------
   A row is one tappable line: who, what they did, when, and a plain-text excerpt of the post
   it's about. Deliberately not a full post card — this is a list you scan, and tapping opens
   the real thing. */
.note-list{display:flex;flex-direction:column}
/* The row is a container; the TAPPABLE part is .note-main inside it. That split exists so a
   Follows row can carry Approve / Follow back buttons — a button cannot contain buttons. */
.note{
  display:flex;flex-direction:column;width:100%;
  border-bottom:1px solid var(--separator);
  /* The leading edge is reserved on every row (transparent here, accent on a private one) so
     tinting a row can't nudge its text sideways. */
  border-left:3px solid transparent;
  padding:14px 4px 14px 11px;
  transition:background .15s;
}
.note-main{
  display:flex;align-items:flex-start;gap:13px;width:100%;text-align:left;
  background:none;border:none;padding:0;cursor:pointer;font-family:inherit;color:inherit;
}
/* Trailing controls sit under the text, lined up with it (avatar 44 + gap 13). */
.note-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:10px 0 0 57px}
/* These are decision buttons for an older reader — hold them to the 44px touch-target rule,
   which btn-sm (34px) is under. */
.note-actions .btn{min-height:44px;padding-left:18px;padding-right:18px}
.note-actions:empty{display:none}
.note-chip{
  font-size:.82rem;font-weight:650;color:var(--text-2);
  background:var(--surface-2);border-radius:999px;padding:5px 11px;
}
.note-actions-note{font-size:.85rem;color:var(--text-3)}
.note-actions-err{font-size:.85rem;color:var(--danger-text)}
/* A private message — same warm tint and leading edge as its card in the feed (.post.is-direct),
   so "this one is private" looks identical wherever you meet it. */
.note.is-direct{background:var(--accent-weak);border-left-color:var(--accent)}
.note.is-direct:hover{background:var(--accent-weak)}
/* The tint darkens the row enough that --text-3 drops to 4.05:1 in light mode and fails AA.
   Measured: --text-2 gives 5.30 on the same tint. Only the timestamp is affected (the name,
   verb and excerpt all clear it comfortably). Don't "unify" this back to --text-3. */
.note.is-direct .note-meta{color:var(--text-2)}
.note:last-child{border-bottom:none}
.note:hover{background:var(--surface-hover)}
/* Nothing to open (the post it referred to is gone) — still readable, just not a button. */
.note.is-flat .note-main{cursor:default}
.note.is-flat:hover{background:none}
.note-body{flex:1;min-width:0}
.note-line{font-size:1rem;line-height:1.4;color:var(--text)}
.note-who{font-weight:650}
.note-meta{font-size:.82rem;color:var(--text-3);margin-top:2px}
/* One line of the post, clipped — enough to recognise which post it was. */
.note-excerpt{
  font-size:.95rem;line-height:1.45;color:var(--text-2);margin-top:6px;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
}
.note-loading,.note-error{padding:28px 4px;color:var(--text-3);font-size:.95rem}
.note-error{color:var(--danger-text)}

/* The You tab: who you are · three places to go · Sign out. The pushed "Your settings" pane
   shares the same column layout — one rule, two names, so they cannot drift apart. */
.you-pane,.settings-pane{display:flex;flex-direction:column;gap:10px;padding:8px 0}
/* Avatar + handle on one row. Identity, not a control — see renderYou. */
.you-id{display:flex;align-items:center;gap:12px;padding:6px 2px 10px;min-width:0}
.you-handle{font-size:1.02rem;font-weight:600;color:var(--text-2);margin:0;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.you-action{justify-content:flex-start;text-align:left;min-height:48px;font-size:1rem}
/* ⚠️ TWO GROUPS, AND THE GAP IS THE POINT. Sign out is destructive and sits alone at the end;
   without the gap it reads as one more row of the list above it, which is exactly how it went
   wrong before ("just another preference"). Whitespace rather than a rule line — the ghost
   buttons already have their own edges, and a divider would be chrome doing a job the space
   already does. */
.you-group{display:flex;flex-direction:column;gap:10px}
/* ⚠️ In rem, not px, so the separation grows with the reader's text size. Measured at 200%:
   a fixed 34px gap sat between 58px-tall buttons and had visibly lost its authority as "these
   are different groups". The gap is doing legibility work here, not decoration. */
.you-group + .you-group{margin-top:2.1rem}
/* "Your settings" has TWO heading levels, and they must not look alike.
   • .set-group   — what a setting BELONGS to ("On this device", "Your account"). Sentence
                    case and full-size, because it is a real heading, not a label.
   • .set-section — the setting itself (Color, Notifications). The small uppercase label the
                    app already used before the grouping existed.
   ⚠️ If these two ever end up the same size and colour, the grouping stops reading as a
   grouping and the pane just looks like four headings in a row. */
.set-group{font-size:1.05rem;font-weight:700;color:var(--text);letter-spacing:-.01em;
  padding:22px 2px 0;margin:0}
.set-group:first-of-type{padding-top:8px}
.set-section{font-size:.82rem;font-weight:650;color:var(--text-3);letter-spacing:.02em;
  text-transform:uppercase;padding:16px 2px 2px;margin:0}

/* ---------- Settings switches ("Your settings") ----------
   Reuses the .switch / .switch-knob pill from the feed filter, so a switch means the same
   thing and looks the same wherever it appears. The whole row is the control — one big,
   legible target for a reader with large text, which is why the pill is aria-hidden and the
   row carries role=switch. */
.set-switch{
  display:flex;align-items:center;justify-content:space-between;gap:14px;width:100%;
  padding:12px 13px;border-radius:var(--radius-sm);background:var(--surface);
  border:1px solid var(--control-edge);text-align:left;
  /* 56px, not the usual 44: these rows carry two lines of text and are read as much as
     tapped, so they need the room. */
  min-height:56px;
}
.set-switch:hover{background:var(--surface-hover)}
.set-switch-text{display:flex;flex-direction:column;gap:2px;min-width:0}
.set-switch-label{font-size:1rem;font-weight:600;letter-spacing:-.01em}
.set-switch-hint{font-size:.85rem;color:var(--text-2);line-height:1.35}
/* The per-kind list, revealed only once notifications are on. Indented a little so it reads
   as belonging to the master switch above it rather than as six more equal choices. */
.set-sublist{display:flex;flex-direction:column;gap:8px;margin-top:8px;padding-left:12px}
.set-switch-sub{background:var(--surface-2);border-color:transparent}
.set-switch-sub:hover{background:var(--surface-hover)}
.set-switch.is-on .switch{background:var(--accent-btn)}
.set-switch.is-on .switch-knob{transform:translateX(18px)}
/* A short explanation, or an error. Sits under the switch it belongs to. */
.set-note{font-size:.9rem;line-height:1.45;color:var(--text-2);padding:8px 2px 0;margin:0}

/* ---------- Theme picker ("Your settings") ----------
   Three rows, all visible at once, because the whole job is comparing them. Each carries the
   swirl's three bands as a swatch, so you pick a picture rather than a word. */
.theme-picker{display:flex;flex-direction:column;gap:8px}
.theme-opt{
  display:flex;align-items:center;gap:13px;width:100%;text-align:left;
  min-height:60px;padding:10px 13px;border-radius:var(--radius-sm);
  background:var(--surface);border:1.5px solid var(--separator);
  transition:border-color .15s,background .15s;
}
.theme-opt:hover{background:var(--surface-hover)}
.theme-opt.is-on{border-color:var(--accent);background:var(--accent-weak)}
/* The swatch reads the bands from `data-theme-preview`, so every row shows its OWN theme's
   colours no matter which theme is currently active. */
.theme-swatch{
  flex:none;display:flex;width:44px;height:44px;border-radius:50%;overflow:hidden;
  border:1.5px solid var(--separator-strong);
}
.theme-swatch i{flex:1;display:block}
.theme-band-deep{background:var(--sw-swirl-deep)}
.theme-band-mid{background:var(--sw-swirl-mid)}
.theme-band-light{background:var(--sw-swirl-light)}
.theme-opt-text{flex:1;min-width:0;display:block}
.theme-opt-text b{display:block;font-weight:650;font-size:1rem;color:var(--text)}
.theme-opt-text span{display:block;font-size:.85rem;color:var(--text-3);line-height:1.35;margin-top:1px}
/* The tick is the confirmation; the border tint alone would be a colour-only cue.
   Uses --accent-strong, not --accent-text: this tick always sits on the selected row's
   tinted wash, and on that background the plain text tint was the tightest pair in the
   whole system (4.89 in dark ultramarine). The stronger tint clears it comfortably. */
.theme-tick{flex:none;font-weight:800;color:var(--accent-strong);opacity:0;font-size:1.05rem}
.theme-opt.is-on .theme-tick{opacity:1}

/* ---------- "3 new posts" ----------
   The feed NEVER moves on its own; the quiet poll (live-feed.js) offers what's arrived here
   and the reader taps to see it. Sticky under the topbar so it stays reachable once you've
   scrolled down — being told about new posts is useless if the telling scrolls away.

   The slot collapses to nothing when there's no pill ([hidden] is display:none), so it costs
   the feed no space. pointer-events are off on the slot and back on for the button, so an
   empty sticky strip can never swallow a tap meant for a post underneath. */
.new-posts-slot{
  position:sticky;top:var(--topbar-h,60px);z-index:4;
  display:flex;justify-content:center;pointer-events:none;
}
.new-posts{
  pointer-events:auto;display:inline-flex;align-items:center;gap:7px;
  margin:8px 0 2px;min-height:40px;padding:9px 18px;
  border:none;border-radius:999px;cursor:pointer;
  background:var(--accent-btn);color:var(--btn-text);
  font-family:inherit;font-size:.95rem;font-weight:650;letter-spacing:-.01em;
  box-shadow:var(--shadow-lg);
  transition:transform .12s,background .15s;
}
.new-posts:hover{background:var(--accent-strong)}
.new-posts:active{transform:scale(.97)}
.new-posts-arrow{font-weight:800;line-height:1}

/* ---------- Feed tools: source-view selector + filter ----------
   These live in the TOPBAR (see index.html), not in the scrolling column, so they stay put
   while you read and cost the feed no vertical space of their own. */
.feed-tools{display:flex;justify-content:flex-end;align-items:center;gap:8px}
.feed-tools[hidden]{display:none}
.feed-filter{display:flex}

/* Source-view selector ("Following" / "Only Us") */
.feed-view-wrap{position:relative}
.feed-view-btn{
  display:inline-flex;align-items:center;gap:6px;font-family:inherit;cursor:pointer;
  background:var(--surface-2);color:var(--text-2);border:none;border-radius:11px;
  padding:0 11px 0 12px;min-height:40px;font-size:.9rem;font-weight:600;
  transition:background .15s,color .15s;
}
.feed-view-btn:hover{background:var(--surface-hover);color:var(--text)}
.feed-view-icon{display:inline-flex}
/* Size here as well as in the glyph, so an attribute-less SVG can't collapse to nothing. */
.feed-view-icon svg{stroke:currentColor;width:17px;height:17px}
.feed-view-caret{font-size:.7rem;opacity:.7}
/* Accent when the view reaches past your own server (Following) — matching the composer, where
   Only us is calm and anything wider lights orange. Only Us (the default) stays neutral. */
.feed-view-btn.is-wide{background:var(--accent-weak);color:var(--accent-text)}
.feed-view-btn.is-wide:hover{background:var(--accent-weak);color:var(--accent-text)}

/* Phones: collapse to an icon squircle, exactly like the composer's visibility control — the
   label + caret drop, the full text stays in the aria-label. Tablet/desktop keep the label.
   Same 820px breakpoint as the composer. */
@media(max-width:820px){
  .feed-view-btn{width:40px;padding:0;justify-content:center;gap:0}
  .feed-view-btn .feed-view-label,
  .feed-view-btn .feed-view-caret{display:none}

  /* …EXCEPT in the topbar, where it keeps its words. Moving these controls out of the feed
     freed enough room to say which view you're in, and that name is the thing a reader
     actually wants ("am I looking at Only us or Following?") — a bare glyph can't answer it.
     The topbar's own labels also make this the screen's "where am I" line. */
  .topbar .feed-view-btn{width:auto;padding:8px 11px;justify-content:flex-start;gap:6px}
  .topbar .feed-view-btn .feed-view-label,
  .topbar .feed-view-btn .feed-view-caret{display:inline}
}

.feed-view-menu{
  position:absolute;top:calc(100% + 7px);right:0;z-index:20;min-width:250px;
  background:var(--surface);border:1px solid var(--separator);border-radius:14px;
  box-shadow:var(--shadow-lg);padding:5px;
}
.feed-view-menu[hidden]{display:none}
.feed-view-opt{
  display:flex;align-items:flex-start;gap:11px;width:100%;text-align:left;cursor:pointer;
  background:none;border:none;font-family:inherit;padding:11px 12px;border-radius:10px;
  transition:background .15s;
}
.feed-view-opt:hover{background:var(--surface-hover)}
.feed-view-opt-icon{display:inline-flex;margin-top:1px;color:var(--text-3)}
.feed-view-opt-icon svg{width:17px;height:17px}
/* Sets "Private messages" apart, matching the composer's visibility menu. */
.feed-view-sep{height:1px;background:var(--separator);margin:5px 10px}
.feed-view-opt.on .feed-view-opt-icon{color:var(--accent)}
.feed-view-opt-text{display:flex;flex-direction:column;gap:2px;min-width:0}
.feed-view-opt-text b{font-size:.98rem;font-weight:650;color:var(--text)}
.feed-view-opt.on .feed-view-opt-text b{color:var(--accent-text)}
.feed-view-opt-text span{font-size:.82rem;color:var(--text-3);line-height:1.35}

/* ---------- Reply/boost filter (squircle button → popover of switches) ---------- */
.filter-wrap{position:relative}
/* .filter-btn reuses .c-tool (the composer's 40x40 squircle) for a matching shape. */
.filter-btn{cursor:pointer}
.filter-btn svg{stroke:var(--text-2)}
.filter-btn.on{background:var(--accent-weak)}
.filter-btn.on svg{stroke:var(--accent)}

.filter-menu{
  position:absolute;top:calc(100% + 7px);right:0;z-index:20;min-width:210px;
  background:var(--surface);border:1px solid var(--separator);border-radius:14px;
  box-shadow:var(--shadow-lg);padding:5px;
}
.filter-menu[hidden]{display:none}
.filter-switch{
  display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;
  background:none;border:none;cursor:pointer;font-family:inherit;text-align:left;
  padding:10px 12px;border-radius:10px;min-height:44px;color:var(--text);
  font-size:.98rem;font-weight:500;transition:background .15s;
}
.filter-switch:hover{background:var(--surface-hover)}
/* The pill switch (decorative; the row carries role=switch + aria-checked) */
.switch{
  position:relative;flex:none;width:46px;height:28px;border-radius:999px;
  background:var(--switch-off);transition:background .18s;
}
.switch-knob{
  position:absolute;top:3px;left:3px;width:22px;height:22px;border-radius:50%;
  background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.28);transition:transform .18s;
}
.filter-switch.is-on .switch{background:var(--accent-btn)}
.filter-switch.is-on .switch-knob{transform:translateX(18px)}

/* ---------- Feed states (loading / empty / error) ---------- */
.feed-state{
  background:var(--surface);border:1px solid var(--separator);border-radius:var(--radius);
  padding:32px 26px;text-align:center;box-shadow:var(--shadow);color:var(--text-2);
}
.feed-state h3{font-size:1.16rem;font-weight:700;letter-spacing:-.03em;color:var(--text);margin-bottom:7px}
.feed-state p{font-size:.95rem;line-height:1.55;max-width:360px;margin:0 auto}
.feed-state .btn{margin-top:18px}

/* ---------- Post ---------- */
.post{
  position:relative;
  background:var(--surface);border:1px solid var(--separator);border-radius:var(--radius);
  padding:17px 19px;margin-bottom:14px;box-shadow:var(--shadow);transition:box-shadow .18s;
}
.post:hover{box-shadow:var(--shadow-lg)}
.post-boost{font-size:.8rem;color:var(--text-3);font-weight:600;margin-bottom:10px;display:flex;align-items:center;gap:6px}
.post-pin{font-size:.8rem;color:var(--accent-text);font-weight:600;margin-bottom:10px;display:flex;align-items:center;gap:6px}
.post-pin-icon{display:inline-flex}
/* Private message (direct): a warm tinted card + a labelled header, so it never reads as a
   normal, wider-audience post. */
.post.is-direct{background:var(--accent-weak);border-color:transparent;border-left:3px solid var(--accent)}
.post-dm{font-size:.8rem;color:var(--accent-text);font-weight:650;margin-bottom:10px;display:flex;align-items:center;gap:6px}
.post-dm-icon{display:inline-flex}
/* Unread marker on a Messages card. A DOT, never a count — same rule as the notifications
   bell (a number turns "someone wrote to you" into a score to clear).
   ⚠️ Sized in `em`, not px, so it grows with the reader's text like every other glyph here —
   at 200% text a fixed 9px dot would shrink to a speck beside its own label.
   ⚠️ Uses --danger-btn, the same red as the bell's dot, NOT the accent: it sits on the
   private-message card whose tint and label are ALREADY the accent colour, so an accent dot
   would disappear into its own background. This is the one place "unread" is shown twice in
   the app, and both places should look the same. */
.post-unread{
  width:.62em;height:.62em;border-radius:50%;flex:none;
  background:var(--danger-btn);margin-left:1px;
}
.post-head{display:flex;align-items:center;gap:12px;margin-bottom:12px}
.avatar{
  width:44px;height:44px;border-radius:50%;flex:none;display:grid;place-items:center;overflow:hidden;
  font-weight:600;color:#fff;font-size:1.06rem;letter-spacing:-.02em;background:var(--surface-2);
}
.avatar img{width:100%;height:100%;object-fit:cover;display:block}
.post-who{line-height:1.3;min-width:0}
.post-who .name{font-weight:650;font-size:1rem;letter-spacing:-.02em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.post-who .meta{font-size:.8rem;color:var(--text-3);font-weight:450;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* Custom emoji rendered inside a name — sized to the surrounding text so it scales
   with the reader's text-size setting. */
.emoji{height:1.2em;width:auto;vertical-align:-.2em;margin:0 .02em;object-fit:contain}

/* When a post header is tappable (opens the poster's profile), it's a button: reset
   the button chrome, keep it full-width and left-aligned, and give a gentle hover. */
/* Reserve the top-right corner for the "..." button so the header's hover highlight never
   slides under it (the "..." is absolutely positioned at right:11px, ~36px wide). */
/* ⚠️ The header's hover highlight must STOP SHORT of the corner cluster, or hovering a name
   on a desktop draws a box straight through the audience badge and the "…" — founder saw it
   cutting the badge off. The 42px reserved the "…" alone; the badge added ~31px more, and it
   grows with the reader's text size, so the badge's share is expressed in em. Measured: 17px
   of overlap before, none after, at both 100% and 200% text. If anything else joins that
   corner, this reservation has to grow with it.
   ⚠️ REM, NOT EM — this is a <button>, and a button carries the browser's own default
   font-size (13.33px here) which does NOT inherit from the card. An `em` reservation resolves
   against that and silently never grows, so at 200% text the badge grew and the hover box
   didn't, putting the overlap straight back. The badge itself is `em` of the CARD, which
   tracks the root, so `rem` is what matches it. */
.post-head-btn{width:calc(100% - 42px - 1.9rem);text-align:left;border-radius:11px;padding:4px;margin:-4px -4px 8px;transition:background .15s}
.post-head-btn:hover{background:var(--surface-hover)}
.post-head-btn .name{color:var(--text)}
.post-head-btn:hover .name{color:var(--accent-text)}

/* Heads-up (content warning) */
.post-cw{
  display:flex;align-items:flex-start;gap:8px;background:var(--surface-2);border-radius:var(--radius-sm);
  padding:11px 13px;margin-bottom:10px;font-size:.92rem;color:var(--text-2);line-height:1.45;
}

.post-body{font-size:1.05rem;line-height:1.55;overflow-wrap:break-word}
/* Tapping the text opens the post's conversation. A gentle hover highlight (matching the
   tappable header) plus a pointer cursor signal it — links inside keep their own cursor. */
.post-body.tappable{cursor:pointer;border-radius:11px;margin:0 -8px;padding:2px 8px;transition:background .15s}
.post-body.tappable:hover{background:var(--surface-hover)}
.post-body.tappable a{cursor:pointer}
/* Styling for the sanitized remote-content subset we allow through */
.post-body p{margin-bottom:8px}
.post-body p:last-child{margin-bottom:0}
.post-body a{color:var(--accent-text);text-decoration:none}
.post-body a:hover{text-decoration:underline}
.post-body ul,.post-body ol{margin:0 0 8px 1.3em}
.post-body blockquote{border-left:3px solid var(--separator-strong);padding-left:12px;color:var(--text-2);margin-bottom:8px}
.post-body pre{background:var(--surface-2);border-radius:10px;padding:11px 13px;overflow-x:auto;margin-bottom:8px}
.post-body code{font-size:.92em}

/* Media attachments */
.post-media{display:grid;gap:6px;margin-top:13px}
.post-media.g1{grid-template-columns:1fr}
.post-media.g2,.post-media.g3,.post-media.g4,.post-media.g5,.post-media.g6{grid-template-columns:1fr 1fr}
.post-media-item{position:relative}
.post-media-item img{
  width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:11px;display:block;background:var(--surface-2);
}
.post-media.g1 .post-media-item img{aspect-ratio:16/10}

/* Post actions (the Boost control lives here) */
.post-actions{display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;margin-top:14px}
.post-action{
  display:inline-flex;align-items:center;gap:8px;
  background:none;border:none;cursor:pointer;
  color:var(--text-3);font-size:.9rem;font-weight:600;font-family:inherit;
  padding:8px 12px;border-radius:11px;min-height:40px;
  transition:background .15s,color .15s;
}
.post-action:hover{background:var(--surface-hover);color:var(--text)}
/* Action-row glyphs are sized here, not in each SVG's attributes, so the shared heart (also
   used at a smaller size in the notifications view selector) matches boost and reply. */
.post-action-icon{display:inline-flex}
.post-action-icon svg{width:19px;height:19px}
.post-action.is-on{color:var(--accent-text)}
.post-action.is-on:hover{background:var(--accent-weak)}
/* A liked heart fills with the accent (CSS `fill` overrides the SVG's fill="none"). */
.post-action.love.is-on .post-action-icon svg{fill:currentColor}
.post-action-count{font-variant-numeric:tabular-nums}
.post-action-count[hidden]{display:none}
.post-action:disabled{opacity:.55;cursor:default}
.post-action-error{font-size:.84rem;color:var(--danger-text);line-height:1.35}
/* With "..." moved to the corner, the row holds just Like · Reply · Boost — three items fit
   comfortably, so a mild phone tightening is plenty (keeps roomy tap targets). */
@media (max-width:520px){
  .post-actions{gap:6px 5px}
  .post-action{padding:8px 8px}
}

/* The "..." overflow button + its popover menu, in the card's top-right corner — clear of the
   reaction row (so it never wraps) and clear of the tappable profile header. */
/* Holds the audience badge and the "…" side by side in the corner. */
.post-menu-wrap{position:absolute;top:9px;right:11px;z-index:3;display:flex;align-items:center;gap:1px}
/* While open, lift above sibling posts (all z:3) so a lower post's "..." can't bleed through. */
.post-menu-wrap.is-open{z-index:30}
.post-more{padding:8px 10px}
.post-more svg{display:block}
.post-menu{
  position:absolute;top:calc(100% + 7px);right:0;z-index:20;min-width:200px;
  background:var(--surface);border:1px solid var(--separator);border-radius:14px;
  box-shadow:var(--shadow-lg);padding:5px;
}
.post-menu[hidden]{display:none}
/* A row is [glyph][label], Apple's popup-menu shape. The glyph box is a fixed width so every
   label starts on the same vertical line, and the icon inherits the row's colour — so Delete's
   trash goes danger-red and "Link copied" tints to the accent with no extra rules. */
.post-menu-item{
  display:flex;align-items:center;gap:11px;width:100%;text-align:left;
  background:none;border:none;cursor:pointer;
  font-family:inherit;font-size:.98rem;font-weight:500;color:var(--text);
  padding:11px 13px;border-radius:10px;min-height:44px;transition:background .15s;
}
.post-menu-icon{flex:none;width:1.2em;display:flex;align-items:center;justify-content:center}
/* Sized here, not in the SVG's own attributes, so one glyph string can serve two sizes
   (the pin is shared with the "Pinned" marker above the post). In em, not px, so the glyph
   grows with the row's text when a reader enlarges type — same reason all type is in rem. */
.post-menu-icon svg{display:block;width:1.08em;height:1.08em}
.post-menu-item:hover{background:var(--surface-hover)}
.post-menu-item.done{color:var(--accent-text);font-weight:650}
/* Destructive item reads in the danger tint (matches the profile block/report menu). */
.post-menu-item.danger{color:var(--danger-text)}
.post-menu-item:disabled{opacity:.5;cursor:default}
/* Thin separators grouping safe → author → destructive. */
.post-menu-sep{height:1px;background:var(--separator);margin:5px 8px}
/* Delete confirm face — swaps in over the item list, popover stays put. */
.post-menu-confirm{padding:6px 6px 4px}
.post-menu-confirm-msg{font-size:.92rem;line-height:1.4;color:var(--text);margin:6px 7px 12px}
.post-menu-confirm-row{display:flex;gap:8px;justify-content:flex-end}
.post-menu-confirm-err{font-size:.84rem;color:var(--danger-text);line-height:1.35;margin:10px 7px 2px}

/* Poll (read-only display in the feed) */
.post-poll{margin-top:13px;display:flex;flex-direction:column;gap:8px}
.poll-opt{
  position:relative;border:1px solid var(--separator);border-radius:10px;
  overflow:hidden;background:var(--surface-2);
}
.poll-bar{
  position:absolute;inset:0 auto 0 0;background:var(--accent-weak);
  border-right:2px solid var(--accent);min-width:2px;
}
.poll-opt-label{
  position:relative;display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:11px 13px;font-size:.98rem;
}
.poll-opt-title{overflow-wrap:break-word;min-width:0}
.poll-opt-pct{font-variant-numeric:tabular-nums;color:var(--text-2);font-weight:650;flex:none}
.poll-foot{display:flex;gap:8px;font-size:.83rem;color:var(--text-3);padding:2px 2px 0}
.poll-foot span:first-child::after{content:"·";margin-left:8px}
.poll-opt.chosen{border-color:var(--accent)}
.poll-check{color:var(--accent-text);font-weight:700;margin-right:6px}

/* Poll voting (before you've voted) */
.post-poll .poll-vote{display:flex;flex-direction:column;gap:8px}
.poll-choice{
  display:flex;align-items:center;gap:10px;padding:11px 13px;border-radius:10px;
  border:1px solid var(--separator);background:var(--surface-2);font-size:.98rem;cursor:pointer;
}
.poll-choice:hover{border-color:var(--separator-strong)}
.poll-choice input{width:18px;height:18px;flex:none;accent-color:var(--accent)}
.poll-choice-title{overflow-wrap:break-word;min-width:0}
.poll-vote-btn{align-self:flex-start;margin-top:2px}
.poll-vote-btn:disabled{opacity:.5;cursor:default}
.poll-error{font-size:.86rem;color:var(--accent-strong);margin-top:2px}

/* "ALT" badge on a posted image + the description panel it reveals */
.media-alt{
  position:absolute;bottom:8px;left:8px;padding:4px 9px;border-radius:7px;
  background:rgba(0,0,0,.62);color:#fff;font-size:.7rem;font-weight:700;letter-spacing:.04em;
  backdrop-filter:blur(3px);
}
.media-alt:hover{background:rgba(0,0,0,.8)}
.media-alt-panel{
  position:absolute;inset:0;border-radius:11px;overflow-y:auto;
  background:rgba(0,0,0,.82);backdrop-filter:blur(4px);color:#fff;
  padding:13px 15px;display:flex;flex-direction:column;gap:8px;
}
.media-alt-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
.media-alt-head span{font-size:.82rem;font-weight:700;letter-spacing:.02em;color:rgba(255,255,255,.85)}
.media-alt-close{width:28px;height:28px;border-radius:50%;background:rgba(255,255,255,.16);color:#fff;display:grid;place-items:center;flex:none}
.media-alt-close:hover{background:rgba(255,255,255,.28)}
.media-alt-close svg{display:block}
.media-alt-text{font-size:.95rem;line-height:1.5;white-space:pre-wrap;overflow-wrap:break-word}

/* ---------- Composer ---------- */
.composer{
  background:var(--surface);border:1px solid var(--separator);border-radius:var(--radius);
  padding:15px 17px;margin-bottom:22px;box-shadow:var(--shadow);
  display:flex;gap:13px;align-items:flex-start;
  scroll-margin-top:76px; /* clears the sticky topbar when scrolled into view */
}
/* Full-window drop target, shown only while dragging image files anywhere over the
   page. pointer-events:none so the drag/drop events pass through to the document
   handlers beneath it. Dashed accent border frames the whole window. */
.c-dropzone{
  position:fixed;inset:0;z-index:1000;pointer-events:none;
  background:rgba(0,0,0,.55);display:grid;place-items:center;
}
/* Inset, rounded dashed frame — the dim above stays full-window, this just draws the
   border a little in from the edges with the same corner rounding as the posts. */
.c-dropzone::before{
  content:"";position:absolute;inset:12px;border-radius:calc(var(--radius) + 4px);
  border:3px dashed var(--accent);
}
.c-dropzone span{
  color:#fff;font-weight:650;font-size:1.2rem;letter-spacing:-.01em;
  background:rgba(0,0,0,.6);padding:15px 24px;border-radius:var(--radius-sm);
  backdrop-filter:blur(4px);
}
.c-col{flex:1;min-width:0;display:flex;flex-direction:column}
/* At rest this is ONE line tall, so the divider under it lands exactly on the bottom edge of
   the 44px avatar and the resting composer stays out of the feed's way (2026-07-24: it used to
   reserve 72px of empty box, which cost ~40px of every phone screen for nothing).
   It grows as you type — see growText() in compose.js — up to max-height, then scrolls.
   `resize:none` because auto-grow and a manual drag-handle fight each other; the box now sizes
   itself. min-height must stay in step with the avatar: 44px avatar − 14px padding = 30px. */
.c-text{
  width:100%;font-family:inherit;font-size:1.05rem;line-height:1.5;color:var(--text);
  background:transparent;border:none;resize:none;padding:7px 0;
  min-height:30px;max-height:44vh;overflow-y:auto;
}
.c-text:focus{outline:none}
.c-text::placeholder{color:var(--text-3)}

.c-bar{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding-top:12px;border-top:1px solid var(--separator);flex-wrap:wrap;
}
.c-bar-left{display:flex;align-items:center;gap:8px;min-width:0}
.c-bar-right{display:flex;align-items:center;gap:12px;margin-left:auto}

/* Toolbar buttons (photo; poll/heads-up land in later slices) */
.c-tool{
  width:40px;height:40px;border-radius:11px;background:var(--surface-2);flex:none;
  display:grid;place-items:center;transition:background .15s;
}
.c-tool:hover{background:var(--surface-hover)}
.c-tool svg{stroke:var(--text-2)}
.c-tool:disabled{opacity:.4;cursor:default}
.c-tool.on{background:var(--accent-weak)}
.c-tool.on svg{stroke:var(--accent)}

/* Heads-up (content warning) note */
/* Caution shown at the top of the composer when Private message is selected. */
.c-dm-note{font-size:.8rem;color:var(--text-3);line-height:1.4;margin:0 0 10px}
.c-dm-note[hidden]{display:none}
/* "Add a recipient" hint: shown only when a dimmed Share is tapped on a recipient-less PM. */
.c-dm-hint{font-size:.85rem;color:var(--text-2);line-height:1.4;margin:10px 0 0}
.c-dm-hint[hidden]{display:none}
/* Share looks dimmed (like disabled) but stays tappable, so tapping can explain what's
   missing (a private message with no recipient yet).
   ⚠️ NOT `opacity`, and this is a real finding rather than a style preference. Fading a
   saturated blue toward white ROTATES ITS HUE ~15° INTO VIOLET: the blue channel is already
   pegged at 255 so it cannot climb, while red and green both do, so the blend gains
   proportionally more red than blue. Measured — `opacity:.5` on the light-mode Share fill
   produced #928fff at hue -77.6°, against ultramarine's true -92.5°. That is exactly the
   "lavender" the founder saw, and it is UNFIXABLE BY PICKING A DIFFERENT COLOUR: the washed
   result already sits within 3.5% of the most chromatic blue available at that lightness. The
   mechanism had to change, not the value. So the off state now uses an explicit pale tint with
   a FULL-STRENGTH accent label — the one saturated element holds the true hue, and the wash is
   pale enough that any rotation in it is imperceptible. (It also reads better: --accent-text on
   --accent-weak measures 6.92 light / 4.68 dark, where the old 50%-faded white-on-wash was
   about 2.4.) */
.c-share-inline.is-off{
  background:var(--accent-weak);color:var(--accent-text);cursor:pointer;
}

/* @mention autocomplete dropdown, anchored beneath the textarea. */
.c-text-wrap{position:relative}
.c-mentions{
  position:absolute;top:calc(100% - 2px);left:0;z-index:30;min-width:250px;max-width:min(360px,90%);
  background:var(--surface);border:1px solid var(--separator);border-radius:12px;
  box-shadow:var(--shadow-lg);padding:4px;
}
.c-mentions[hidden]{display:none}
.c-mention{
  display:flex;align-items:center;gap:10px;width:100%;text-align:left;cursor:pointer;
  background:none;border:none;font-family:inherit;padding:7px 9px;border-radius:9px;
}
.c-mention.active,.c-mention:hover{background:var(--surface-hover)}
.c-mention-av{width:32px;height:32px;border-radius:50%;overflow:hidden;flex:none;background:var(--surface-2)}
.c-mention-av img{width:100%;height:100%;object-fit:cover;display:block}
.c-mention-who{display:flex;flex-direction:column;min-width:0;line-height:1.25}
.c-mention-who b{font-size:.92rem;font-weight:650;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.c-mention-who span{font-size:.8rem;color:var(--text-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

.c-headsup{margin:2px 0 11px}
/* ⚠️ 1rem (16px) IS A FLOOR ON EVERY TEXT CONTROL — never go below it.
   iOS Safari AUTO-ZOOMS the whole page when you focus an input whose font-size is under 16px.
   The zoom widens the layout viewport, which on a real iPhone looked like two unrelated bugs:
   the page shifted sideways, and the fixed bottom nav dropped below the visible area and
   stayed there after the keyboard closed. Found on device 2026-07-24 via the alt-text box;
   the heads-up field, poll options and poll duration all had it too.
   Keep these in rem (they scale with the reader's text size) and never under 1rem. Do NOT
   "fix" this instead with maximum-scale=1 on the viewport meta — that disables pinch-zoom,
   which an older reader may genuinely need. */
.c-headsup input{
  width:100%;font-family:inherit;font-size:1rem;padding:10px 12px;border-radius:10px;
  border:1.5px solid var(--separator-strong);background:var(--bg);color:var(--text);
}
.c-headsup input:focus{outline:none;border-color:var(--accent)}

/* Poll editor */
.c-poll{margin:13px 0 2px;display:flex;flex-direction:column;gap:8px}
.c-poll-opts{display:flex;flex-direction:column;gap:8px}
.c-poll-opt{display:flex;align-items:center;gap:8px}
.c-poll-opt input{
  flex:1;min-width:0;font-family:inherit;font-size:1rem;padding:10px 12px;border-radius:10px;
  border:1.5px solid var(--separator-strong);background:var(--bg);color:var(--text);
}
.c-poll-opt input:focus{outline:none;border-color:var(--accent)}
.c-poll-x{
  width:30px;height:30px;flex:none;border-radius:8px;color:var(--text-3);
  display:grid;place-items:center;background:var(--surface-2);
}
.c-poll-x:hover{background:var(--surface-hover);color:var(--text-2)}
.c-poll-x svg{display:block}
.c-poll-add{align-self:flex-start;font-size:.88rem;font-weight:600;color:var(--accent-text);padding:4px 2px}
.c-poll-settings{display:flex;align-items:center;gap:18px;flex-wrap:wrap;margin-top:2px}
.c-poll-dur{display:flex;align-items:center;gap:6px;font-size:.88rem;font-weight:500;color:var(--text-2)}
.c-poll-dur select{
  font-family:inherit;font-size:1rem;padding:7px 10px;border-radius:9px;
  border:1.5px solid var(--separator-strong);background:var(--bg);color:var(--text);
}
.c-poll-dur select:focus{outline:none;border-color:var(--accent)}
.c-poll-multi{display:flex;align-items:center;gap:7px;font-size:.88rem;font-weight:500;color:var(--text-2)}
.c-poll-multi input{width:17px;height:17px;accent-color:var(--accent)}

/* Photo previews */
.c-media{display:flex;gap:8px;margin:13px 0 2px;flex-wrap:wrap}
.c-thumb{position:relative;width:calc(50% - 4px);max-width:190px}
.c-thumb img{
  width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:11px;display:block;
  background:var(--surface-2);
}
.c-x{
  position:absolute;top:6px;right:6px;width:28px;height:28px;border-radius:50%;
  background:rgba(0,0,0,.6);color:#fff;
  display:grid;place-items:center;backdrop-filter:blur(3px);
}
.c-x:hover{background:rgba(0,0,0,.78)}
.c-x svg{display:block}

/* Per-photo alt-text (description) badge + editor */
.c-alt{
  position:absolute;bottom:6px;left:6px;padding:4px 9px;border-radius:7px;
  background:rgba(0,0,0,.6);color:#fff;font-size:.7rem;font-weight:700;letter-spacing:.04em;
  backdrop-filter:blur(3px);border:1.5px solid transparent;
}
.c-alt:hover{background:rgba(0,0,0,.78)}
.c-alt.set{background:var(--accent);border-color:rgba(255,255,255,.5)}
.c-alt.open{border-color:#fff}
.c-altbox{margin:11px 0 2px;padding:12px 13px;border-radius:var(--radius-sm);background:var(--surface-2)}
.c-altbox label{display:block;font-size:.82rem;font-weight:650;color:var(--text-2);margin-bottom:6px}
.c-altbox input{
  width:100%;font-family:inherit;font-size:1rem;padding:10px 12px;border-radius:10px;
  border:1.5px solid var(--separator-strong);background:var(--bg);color:var(--text);
}
.c-altbox input:focus{outline:none;border-color:var(--accent)}
.c-althint{display:block;font-size:.76rem;color:var(--text-3);margin-top:7px;line-height:1.4}

.c-vis-wrap{position:relative;flex:none}
/* CALM BY DEFAULT: at "Only us" the control is neutral (matches the tool buttons) — the safe
   private default has nothing to flag. `.is-wide` (any audience past Only us) turns it accent:
   a gentle "this reaches beyond your neighborhood". Set in setVisibility. */
.c-vis-pill{
  display:flex;align-items:center;gap:6px;padding:0 12px;height:40px;border-radius:11px;
  background:var(--surface-2);color:var(--text-2);font-size:.86rem;font-weight:650;
  letter-spacing:-.01em;white-space:nowrap;
}
.c-vis-pill.is-wide{background:var(--accent-weak);color:var(--accent-text)}
.c-vis-icon{display:flex}
.c-vis-icon svg{stroke:var(--text-2);flex:none}
.c-vis-pill.is-wide .c-vis-icon svg{stroke:var(--accent)}

/* ---- Phones: the visibility control collapses to an icon squircle ----
   The composer's toolbar and Share must fit ONE row on a phone. Two rows make the
   composer eat roughly 40% of the feed screen, which is too much for a control most
   people use a few times a day.

   This deliberately supersedes the old "labeled pill, never an icon" rule. That rule
   existed to stop someone posting family photos publicly by misreading a glyph; what
   makes an icon acceptable now is that the composer only ever REMEMBERS the two
   private tiers (Only us / Followers) — "Anyone" and "Private message" are never
   persisted, so the state you can be silently restored into is always a safe one.
   Choosing a wide audience is therefore always a deliberate act in the open menu,
   where every level still shows its full name and explanation.

   Three things carry the meaning once the word is gone:
     - the icon itself, matching the feed selector's glyphs for the same ideas;
     - the accent tint, which the neutral .c-tool buttons don't have, so this still
       reads as a different KIND of control (a state, not an action);
     - the button's aria-label, rewritten with the full level name on every change.
   The caret goes too — the other squircles have none, and the accent already marks
   this one as special.

   Breakpoint is the app-wide 820px, so iPad portrait (834pt) keeps the full labels
   along with the rest of the desktop layout. Worth revisiting for a folding phone,
   whose unfolded width may land on the "desktop" side while still being a phone. */
@media(max-width:820px){
  .c-vis-pill{
    width:40px;padding:0;justify-content:center;gap:0;
  }
  .c-vis-pill .c-vis-label,
  .c-vis-pill .c-caret{display:none}

  /* Reclaiming the last few pixels so the four squircles and Share share one row.
     Measured on a 375px phone: the bar gets 258px and needed 274px. The shortfall
     comes out of GAPS and Share's side padding — deliberately NOT out of the 40px
     buttons, which are already under the 44px touch-target guideline; shrinking
     those would trade legibility for tidiness, which is the trade this project
     keeps refusing to make.
     Specificity note: `.c-share-inline` is declared again LATER in this file, and a
     media query adds no specificity — so a bare `.c-share-inline` here loses to it on
     source order and silently does nothing. Qualified with `.c-bar` to win. This is
     the same trap the brief records ("CSS specificity, not source order"); it cost a
     measurement round-trip here too. */
  .c-bar-left{gap:6px}
  .c-bar{gap:6px}
  .c-bar .c-share-inline{padding:10px 12px}

  /* ---- Reply composer: reclaim the avatar gutter for the toolbar ----
     A reply composer is indented by the thread nesting (up to 4 levels = 60px), which eats
     the horizontal room the one-row toolbar needs. Measured at the deepest nest on a 375px
     phone: the bar was 58px short → it wrapped Share to a second row. So the toolbar breaks
     out LEFT under the avatar to span the composer's full content width (the avatar is only
     44px tall at the top; the bar sits well below it, over empty gutter). The text above
     keeps its normal alignment beside the avatar — this is the "keep the avatar, icons under
     it" layout. The outdent = avatar width (44px) + composer gap (13px); if either changes,
     change it here. A small padding trim (17→12) buys the last few px of headroom (+9 slack
     at the deepest nest). Post composer (no .is-reply) is untouched. */
  .composer.is-reply{padding-left:12px;padding-right:12px}
  /* The toolbar breaks out LEFT, under the avatar, spanning the composer's full width. The
     avatar is only 44px tall at the top, so the bar sits below it over empty gutter and buys
     back avatar(44) + gap(13) = 57px. Applied to BOTH composers (2026-07-24, founder) so the
     post and reply composers look alike — it started life as a reply-only fix for the room
     lost to thread nesting. ⚠️ The 57px is hardcoded here; if the avatar size or the composer
     gap changes, change it here. */
  .composer .c-bar{
    margin-left:calc(-44px - 13px);
    width:calc(100% + 44px + 13px);
    /* The divider stops at the text column instead of running under the avatar. Its job is
       "what you're writing goes above this", so lining it up with the text is what it means —
       and it's the iOS list idiom, where separators inset to the text, not the leading avatar.
       Founder's call over the alternative (a full-width line pushed down by a 12px gutter),
       which read fine but cost 12px of every phone screen for nothing.

       ⚠️ It has to stay on .c-bar, NOT on the text wrapper. Photo thumbnails, alt text and
       poll fields are appended between the text and the toolbar, so a line on the text would
       sit ABOVE your attached photos — putting them below the divider with the tools, which
       inverts what it means. The line belongs above the TOOLS. */
    border-top:none;
    position:relative;
  }
  .composer .c-bar::before{
    content:"";position:absolute;top:0;left:calc(44px + 13px);right:0;height:1px;
    background:var(--separator);
  }
}
.c-caret{font-size:.7rem;opacity:.75}

.c-count{font-size:.82rem;color:var(--text-3);font-variant-numeric:tabular-nums}
.c-count.over{color:var(--accent-text);font-weight:650}

.c-share-inline{min-height:40px;padding:10px 20px;border-radius:11px}
/* The everyday inactive Share — nothing typed yet. (The OTHER inactive state is
   `.c-share-inline.is-off`, for a private message with no recipient; that one stays tappable.
   This is the one a reader sees every time they open the app.)

   ⚠️ `opacity` fades a colour toward the BACKGROUND IN sRGB, which ROTATES ITS HUE. Measured
   on the shipped buttons: ultramarine swung **+14.8°** (to #928fff, visibly lavender) and
   tourmaline **+7.8°**; citrine only 0.7°, which is why orange always looked right and blue
   never did. Founder found it on device.
   ⚠️ But opacity has one real virtue that any fix must keep: it fades the FILL AND THE TEXT
   together, so their contrast with each other holds. Fading only the fill is not an option —
   measured, citrine and tourmaline can lighten just 6% before white text fails, which is no
   visible fade at all. So this does exactly what opacity does, in OKLab, where hue survives.
   At 35% every theme beats what shipped on all three axes at once: hue error under 0.5°
   instead of up to 15°, MORE chroma (ultramarine .184 vs .161), and better text contrast
   (3.30 vs 2.77). Mixing toward --surface means it works in both schemes with no new tokens.
   ⚠️ A genuinely disabled control is exempt from WCAG's contrast minimum, which is why a
   ratio near 3 is acceptable here and only here — it is also strictly better than the ~2.8
   that shipped. Don't "restore" plain opacity. */
.c-share-inline:disabled{opacity:.5;cursor:default}
@supports (background: color-mix(in oklab, red 50%, white)) {
  .c-share-inline:disabled{
    opacity:1;
    background:color-mix(in oklab, var(--accent-btn) 65%, var(--surface));
    color:color-mix(in oklab, var(--btn-text) 65%, var(--surface));
  }
}

.c-menu{
  position:absolute;top:48px;left:0;z-index:10;
  background:var(--surface);border:1px solid var(--separator);border-radius:var(--radius-sm);
  box-shadow:var(--shadow-lg);padding:5px;display:flex;flex-direction:column;
}
/* Right-aligned, NOT left. The visibility control sits at the right end of the
   toolbar row, so the inherited `.c-menu{left:0}` throws the menu off the right
   edge of a phone. Specificity is deliberately raised to `.c-menu.c-vismenu` so a
   later, equally-specific rule can't silently win on source order — this exact
   regression has happened once before (see the brief's mobile lessons). */
.c-menu.c-vismenu{left:auto;right:0;width:262px;max-width:calc(100vw - 52px)}

/* ⚠️ …EXCEPT on a phone wide enough to hold the words, where the label comes BACK.
   (2026-07-24, founder.) This partly undoes the icon-only rule above, and that is the right
   direction: the icon was always a space compromise, and the original design rule — "a
   labelled pill, never an icon" — exists because visibility is the one control where a wrong
   guess posts family photos publicly. Given the room, the words win.

   Why 390px, measured with the break-out applied: at 393px all four labels fit on one row
   WITH the caret; at 375px every one of them wraps Share onto a second line (fitting there
   would need the caret dropped AND the padding cut to 7px — too much to pay). So phones from
   390px up get words, and a 375px SE/mini keeps the icon squircle it has today, which is
   safe for the reasons in the note above and is not a regression.

   NOT applied to a reply composer: it's indented by thread nesting (up to 60px), so the room
   isn't there — founder's call, and the parent's audience is inherited anyway. */
@media(min-width:390px) and (max-width:820px){
  .composer:not(.is-reply) .c-vis-pill{
    width:auto;padding:8px 11px;justify-content:flex-start;gap:6px;
  }
  .composer:not(.is-reply) .c-vis-pill .c-vis-label,
  .composer:not(.is-reply) .c-vis-pill .c-caret{display:inline}
}
.c-vismenu button{
  display:flex;gap:11px;align-items:flex-start;text-align:left;
  padding:10px 12px;border-radius:8px;transition:background .15s;
}
.c-vismenu button:hover{background:var(--surface-2)}
/* The row glyph — same picture as the collapsed squircle, so the menu teaches the icon.
   Accent-tinted and nudged down to sit on the label's cap height. */
.c-vismenu-icon{display:flex;flex:none;margin-top:1px}
.c-vismenu-icon svg{stroke:var(--accent)}
.c-vismenu-text{display:block;min-width:0}
.c-vismenu-text b{display:block;font-weight:650;font-size:.92rem;color:var(--text)}
.c-vismenu-text span{display:block;font-size:.78rem;color:var(--text-3);line-height:1.35;margin-top:2px;white-space:normal}
.c-vismenu button.on .c-vismenu-text b{color:var(--accent-text)}
/* Separates Private message from the three broadcast levels — different axis, not
   just another step on the gradient. See the VISIBILITY comment in compose.js. */
.c-vismenu-sep{height:1px;background:var(--separator);margin:5px 12px}

.c-error{
  font-size:.86rem;color:var(--accent-strong);line-height:1.45;margin-top:11px;
  background:var(--accent-weak);border-radius:var(--radius-sm);padding:9px 12px;
}

/* ---------- Profile view (display only) ---------- */
.profile-view{display:block}

/* ---------- Single-post ("View") view ---------- */
/* Reuses the profile Back bar (.profile-bar / .profile-back). The focal post — the one you
   tapped "View" on — gets a calm accent border so it's clear which post the
   conversation is about, with any parent above it and replies below. */
.post-view{display:block}
.post-thread{margin-top:14px}
/* Nesting connector: one thin accent line just left of an indented post, running up into the
   gap to link it to the post it's replying to. Same-indent siblings share one continuous run;
   the stepped ancestor spine gets one short line per level. Kept subtle (thin, low opacity). */
.nest-line{position:relative}
.nest-line::before{
  content:"";position:absolute;left:-9px;top:-14px;bottom:0;width:2px;
  background:var(--accent);opacity:.4;border-radius:2px;
}
/* The highlighted (opened) post. scroll-margin-top clears the sticky topbar (57px) + Back bar
   (~44px) so scrolling to a mid-thread reply lands it just below them, not hidden underneath. */
.post.is-focal{border-color:var(--accent);scroll-margin-top:108px}
.thread-heading{
  font-size:.85rem;font-weight:650;color:var(--text-3);
  margin:2px 2px 12px;
}
.thread-heading[hidden]{display:none}

/* "View N replies" — the drill-in link on a reply that has its own replies (nested replies
   live on their own page). Reads as a quiet link, in the brand accent. */
.post-replies-link{
  display:inline-flex;align-items:center;gap:6px;margin-top:10px;
  background:none;border:none;cursor:pointer;font-family:inherit;
  color:var(--accent-text);font-size:.92rem;font-weight:600;
  padding:6px 8px;margin-left:-8px;border-radius:9px;min-height:36px;transition:background .15s;
}
.post-replies-link::before{content:"↳";font-weight:400;opacity:.8}
.post-replies-link:hover{background:var(--accent-weak)}

/* The reply composer at the foot of a conversation is the SAME component as the top-of-feed
   composer (`.composer`, in reply mode). `.reply-composer` is just its indent wrapper; it
   trims the composer's default bottom margin so it sits snugly at the end of the thread. */
.reply-composer{margin-top:2px}
.reply-composer .composer{margin-bottom:0}

/* Sticky Back bar — always reachable while reading a long profile. */
.profile-bar{
  position:sticky;top:57px;z-index:4;margin:-22px -18px 0;padding:9px 18px;
  background:color-mix(in srgb,var(--bg-grouped) 82%,transparent);
  backdrop-filter:saturate(180%) blur(16px);-webkit-backdrop-filter:saturate(180%) blur(16px);
}
.profile-back{
  display:inline-flex;align-items:center;gap:4px;font-weight:600;font-size:.95rem;
  color:var(--accent-text);padding:6px 10px 6px 6px;border-radius:10px;min-height:38px;
}
.profile-back:hover{background:var(--accent-weak)}
.profile-back-icon{display:flex}

/* Header banner + avatar that overlaps it, like a familiar profile page. */
.profile-header{
  height:150px;border-radius:var(--radius);overflow:hidden;background:var(--surface-2);
  margin-top:12px;
}
.profile-header-img{width:100%;height:100%;object-fit:cover;display:block}
.profile-id{padding:0 4px;margin-top:-42px}
.profile-avatar{
  width:84px;height:84px;border-radius:50%;overflow:hidden;display:grid;place-items:center;
  background:var(--surface-2);border:4px solid var(--bg-grouped);
  font-weight:600;color:#fff;font-size:2rem;letter-spacing:-.02em;
}
.profile-avatar img{width:100%;height:100%;object-fit:cover;display:block}
.profile-namerow{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-top:10px}
.profile-name{font-size:1.5rem;font-weight:750;letter-spacing:-.03em;line-height:1.15;overflow-wrap:break-word}
.profile-badge{
  font-size:.72rem;font-weight:650;letter-spacing:.02em;text-transform:uppercase;
  color:var(--text-3);background:var(--surface-2);border-radius:7px;padding:3px 8px;
}
.profile-handle{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:3px;font-size:.95rem;color:var(--text-3)}
.profile-lock{display:inline-flex;align-items:center;gap:5px;font-size:.83rem;font-weight:550;color:var(--text-2)}
.profile-lock-icon{display:flex}

.profile-bio{
  margin-top:14px;font-size:1.05rem;line-height:1.55;overflow-wrap:break-word;color:var(--text);
}
.profile-bio p{margin-bottom:8px}
.profile-bio p:last-child{margin-bottom:0}
.profile-bio a{color:var(--accent-text);text-decoration:none}
.profile-bio a:hover{text-decoration:underline}

.profile-stats{display:flex;gap:26px;margin-top:16px}
.profile-stat{display:flex;flex-direction:column;line-height:1.2}
.profile-stat-n{font-weight:700;font-size:1.12rem;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.profile-stat-l{font-size:.82rem;color:var(--text-3)}
.profile-joined{margin-top:12px;font-size:.88rem;color:var(--text-3)}

/* Custom fields (Mastodon-style), with a green verified check when the server
   confirmed the linked page. Field values are sanitized remote HTML. */
.profile-fields{margin-top:16px;border:1px solid var(--separator);border-radius:var(--radius-sm);overflow:hidden}
.profile-field{display:grid;grid-template-columns:minmax(90px,32%) 1fr;border-top:1px solid var(--separator)}
.profile-field:first-child{border-top:none}
.profile-field-name{padding:10px 13px;font-size:.86rem;font-weight:650;color:var(--text-2);background:var(--surface-2);overflow-wrap:break-word}
.profile-field-value{padding:10px 13px;font-size:.92rem;overflow-wrap:break-word;display:flex;align-items:center;gap:7px}
.profile-field-value a{color:var(--accent-text);text-decoration:none}
.profile-field-value a:hover{text-decoration:underline}
.profile-field.verified .profile-field-value{color:var(--sage)}
.profile-field.verified .profile-field-name{color:var(--sage)}
.profile-field-check{display:inline-flex;color:var(--sage);flex:none}

/* Timeline toolbar: the Posts / Photos tab bar. Tabs sit on a bottom border (the active one
   gets a terracotta underline over it); the reply/boost filter is pinned right and shows on
   the Posts tab only. margin-bottom gives even breathing room down to the first post. */
.profile-timeline-bar{
  display:flex;align-items:stretch;justify-content:space-between;gap:12px;
  margin-top:20px;margin-bottom:14px;min-height:46px;
  border-bottom:1px solid var(--separator);
}
.profile-timeline-bar[hidden]{display:none}
.profile-tabs-slot{display:flex;align-items:stretch;gap:2px;min-width:0}
.profile-tab{
  position:relative;display:flex;align-items:center;
  background:none;border:none;cursor:pointer;font-family:inherit;
  font-size:1rem;font-weight:600;color:var(--text-3);letter-spacing:-.01em;
  padding:0 12px;border-radius:9px 9px 0 0;transition:color .15s;
}
.profile-tab:hover{color:var(--text-2)}
.profile-tab.is-active{color:var(--text);font-weight:700}
/* Active-tab underline, overlapping the toolbar's bottom border. */
.profile-tab.is-active::after{
  content:"";position:absolute;left:8px;right:8px;bottom:-1px;height:2.5px;
  background:var(--accent);border-radius:2px;
}
.profile-filter-slot{flex:none;margin-left:auto;display:flex;align-items:center}

/* Photos tab: a square-thumbnail grid (a view over their media posts). */
.photo-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:4px}
.photo-cell{
  position:relative;padding:0;border:none;cursor:pointer;background:var(--surface-2);
  aspect-ratio:1;border-radius:8px;overflow:hidden;
}
.photo-cell img{width:100%;height:100%;object-fit:cover;display:block}
.photo-play{
  position:absolute;inset:0;display:grid;place-items:center;color:#fff;
  text-shadow:0 1px 4px rgba(0,0,0,.6);pointer-events:none;
}
.photo-alt{
  position:absolute;left:6px;bottom:6px;font-size:.62rem;font-weight:700;letter-spacing:.03em;
  color:#fff;background:rgba(0,0,0,.6);border-radius:5px;padding:2px 5px;pointer-events:none;
}

/* Full-size photo viewer (lightbox). Appended to <body>, above everything. */
.lightbox{
  position:fixed;inset:0;z-index:100;background:rgba(0,0,0,.9);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  padding:24px;gap:14px;
}
.lightbox-inner{display:flex;flex-direction:column;align-items:center;gap:12px;max-width:100%;max-height:100%}
.lightbox-img{max-width:100%;max-height:80vh;object-fit:contain;border-radius:8px}
.lightbox-alt{
  color:rgba(255,255,255,.85);font-size:.9rem;line-height:1.45;max-width:640px;text-align:center;
  overflow-y:auto;max-height:14vh;
}
.lightbox-close{
  position:absolute;top:16px;right:16px;width:44px;height:44px;border:none;cursor:pointer;
  display:grid;place-items:center;color:#fff;background:rgba(255,255,255,.15);border-radius:50%;
}
.lightbox-close:hover{background:rgba(255,255,255,.28)}
/* "View post" — the control that keeps Photos honest as a VIEW OVER POSTS rather than an
   album. Its own colours, not the light/dark button tokens: the lightbox is always a dark
   overlay in both schemes, so a themed button would go invisible in one of them. White on
   this backdrop measures well past AA. */
.lightbox-view{
  min-height:44px;padding:11px 22px;border:none;cursor:pointer;
  color:#fff;background:rgba(255,255,255,.18);border-radius:11px;
  font-size:1rem;font-weight:600;
}
.lightbox-view:hover{background:rgba(255,255,255,.3)}

/* The app-level Photos tab. "Show more" is a BUTTON on purpose — the feed ends, and reaching
   for more is the reader's decision, never an infinite scroll that pulls them onward. */
.photos-pane{display:flex;flex-direction:column;gap:16px}
.photos-more{align-self:center;min-height:44px;padding:11px 24px}

/* Actions: follow / hide-boosts / mute (display-only slice adds no block/report yet) */
.profile-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:15px}
.profile-follow{min-height:40px;padding:9px 22px}
.profile-follow.is-following{display:inline-flex;align-items:center}
.profile-follow-check{margin-right:6px;color:var(--sage);font-weight:800}
.profile-followsyou{
  font-size:.76rem;font-weight:650;color:var(--text-2);
  background:var(--surface-2);border-radius:7px;padding:5px 9px;
}
.profile-more{position:relative}
.profile-more-btn{min-height:40px;padding:9px 16px}
.profile-follow:disabled,.profile-more-btn:disabled,.profile-menu-item:disabled{opacity:.55;cursor:default}
.profile-menu{
  position:absolute;top:48px;right:0;z-index:10;width:288px;max-width:calc(100vw - 40px);
  background:var(--surface);border:1px solid var(--separator);border-radius:var(--radius-sm);
  box-shadow:var(--shadow-lg);padding:5px;display:flex;flex-direction:column;
}
.profile-menu-item{display:block;text-align:left;padding:10px 12px;border-radius:8px;transition:background .15s}
.profile-menu-item:hover{background:var(--surface-2)}
.profile-menu-item b{display:block;font-weight:650;font-size:.95rem;color:var(--text)}
.profile-menu-item span{display:block;font-size:.8rem;color:var(--text-3);line-height:1.35;margin-top:2px;white-space:normal}
.profile-actions-error{
  font-size:.86rem;color:var(--accent-strong);background:var(--accent-weak);
  border-radius:var(--radius-sm);padding:9px 12px;margin-top:10px;
}
.profile-actions-note{
  font-size:.86rem;color:var(--sage);background:color-mix(in srgb,var(--sage) 13%,transparent);
  border-radius:var(--radius-sm);padding:9px 12px;margin-top:10px;
}
.profile-actions-loading{color:var(--text-3);font-size:1.3rem;letter-spacing:2px;padding:2px 10px}
.profile-blocked-tag{
  font-size:.8rem;font-weight:650;color:var(--danger-text);
  background:var(--danger-weak);border-radius:7px;padding:6px 11px;
}

/* Destructive actions sit below a divider and read in the danger tint. */
.profile-menu-sep{height:1px;background:var(--separator);margin:5px 8px}
.profile-menu-item.danger b{color:var(--danger-text)}

/* In-place confirm panel / report form (the menu morphs into these, no modal). */
.profile-menu-panel{padding:12px 13px}
.profile-panel-title{font-size:1rem;font-weight:700;letter-spacing:-.01em;color:var(--text);margin-bottom:6px}
.profile-panel-text{font-size:.88rem;color:var(--text-2);line-height:1.45;margin-bottom:13px}
.profile-panel-actions{display:flex;gap:8px;justify-content:flex-end}
.profile-report-reason{display:flex;gap:9px;align-items:flex-start;padding:8px;border-radius:8px;cursor:pointer}
.profile-report-reason:hover{background:var(--surface-2)}
.profile-report-reason input{margin-top:3px;width:17px;height:17px;flex:none;accent-color:var(--accent)}
.profile-report-reason-text b{display:block;font-weight:600;font-size:.9rem;color:var(--text)}
.profile-report-reason-text span{display:block;font-size:.78rem;color:var(--text-3);line-height:1.3;margin-top:1px}
.profile-report-comment{
  width:100%;margin:8px 0 4px;font-family:inherit;font-size:.9rem;padding:9px 11px;border-radius:9px;
  border:1.5px solid var(--separator-strong);background:var(--bg);color:var(--text);resize:vertical;
}
.profile-report-comment:focus{outline:none;border-color:var(--accent)}
.profile-report-forward{display:flex;gap:8px;align-items:flex-start;font-size:.82rem;color:var(--text-2);line-height:1.35;padding:6px 2px 12px;cursor:pointer}
.profile-report-forward input{margin-top:2px;width:16px;height:16px;flex:none;accent-color:var(--accent)}

/* ---------- Image cropper (avatar / header editing) ---------- */
.cropper{display:flex;flex-direction:column;gap:14px}
.cropper-title{font-size:.95rem;font-weight:650;color:var(--text-2);text-align:center}
.cropper-stage{
  position:relative;align-self:center;max-width:100%;overflow:hidden;
  border-radius:var(--radius-sm);background:var(--surface-2);touch-action:none;
}
.cropper-canvas{display:block;touch-action:none;cursor:grab;background:#000}
.cropper-canvas:active{cursor:grabbing}
/* Circular preview for avatars (stored square, shown round like everywhere else). */
.cropper-round-mask{position:absolute;inset:0;border-radius:50%;box-shadow:0 0 0 9999px rgba(0,0,0,.5);pointer-events:none}
.cropper-zoom{display:flex;align-items:center;gap:12px;width:100%;max-width:360px;align-self:center}
.cropper-zoom-ic{color:var(--text-3);font-size:1.1rem;flex:none;width:18px;text-align:center}
.cropper-slider{flex:1;accent-color:var(--accent)}
.cropper-actions{display:flex;gap:10px;justify-content:flex-end}
.cropper-loading,.cropper-error{padding:24px;text-align:center;color:var(--text-2)}
.cropper-error{display:flex;flex-direction:column;gap:12px;align-items:center}
.cropper-error-line{font-size:.85rem;color:var(--danger-text);text-align:center;line-height:1.4}

/* ---------- Edit profile ---------- */
.edit-view{display:block}
.edit-bar{
  position:sticky;top:57px;z-index:4;display:flex;align-items:center;justify-content:space-between;gap:12px;
  margin:-22px -18px 0;padding:9px 14px;
  background:color-mix(in srgb,var(--bg-grouped) 82%,transparent);
  backdrop-filter:saturate(180%) blur(16px);-webkit-backdrop-filter:saturate(180%) blur(16px);
}
.edit-cancel{display:inline-flex;align-items:center;gap:3px;font-weight:600;font-size:.95rem;color:var(--accent-text);padding:6px 8px;border-radius:10px;min-height:38px}
.edit-cancel:hover{background:var(--accent-weak)}
.edit-cancel-icon{display:flex}
.edit-title{font-weight:700;font-size:1rem;letter-spacing:-.01em}
.edit-save{min-height:38px}
.edit-save:disabled{opacity:.5;cursor:default}
.edit-loading{padding:40px;text-align:center;color:var(--text-2)}

.edit-form{display:flex;flex-direction:column;margin-top:14px}
.edit-header{position:relative;height:150px;border-radius:var(--radius);overflow:hidden;background:var(--surface-2)}
.edit-header-img{width:100%;height:100%;object-fit:cover;display:block}
.edit-photo-btns-header{position:absolute;bottom:10px;right:10px;display:flex;gap:8px}
.edit-change-photo{
  background:rgba(0,0,0,.6);color:#fff;font-size:.82rem;font-weight:600;
  padding:8px 14px;min-height:36px;backdrop-filter:blur(3px);
}
.edit-change-photo:hover{background:rgba(0,0,0,.78)}
/* The avatar overlaps the header, so it must paint ABOVE it — the header is a
   positioned element, so the avatar row needs its own stacking to sit on top. But the
   row spans the full width, so its empty right-hand area would otherwise cover (and
   swallow clicks meant for) the "Change header" button beneath it — hence
   pointer-events:none on the row, re-enabled on its actual controls. */
.edit-idrow{position:relative;z-index:1;display:flex;align-items:center;gap:14px;padding:0 4px;margin-top:-42px;pointer-events:none}
.edit-idrow > *{pointer-events:auto}
.edit-avatar{
  position:relative;width:84px;height:84px;border-radius:50%;overflow:hidden;flex:none;
  border:4px solid var(--bg-grouped);background:var(--surface-2);display:grid;place-items:center;
}
.edit-avatar img{width:100%;height:100%;object-fit:cover;display:block}
.edit-avatar-letter{font-weight:600;font-size:2rem;color:var(--text-2);letter-spacing:-.02em}
.edit-photo-btns{display:flex;gap:8px;flex-wrap:wrap;align-self:flex-end;margin-bottom:8px}
.edit-change-avatar{background:var(--surface-2);color:var(--text)}
.edit-change-avatar:hover{background:var(--surface-hover)}
/* "Use default" is a consistent solid pill like the others, just visually secondary
   (muted text) — not transparent, which let the page show through unevenly. */
.edit-use-default.edit-change-avatar{background:var(--surface-2);color:var(--text-2)}
.edit-use-default.edit-change-avatar:hover{background:var(--surface-hover);color:var(--text)}

.edit-field{display:flex;flex-direction:column;margin-top:20px;padding:0 2px}
.edit-label{font-size:.8rem;font-weight:650;color:var(--text-2);margin-bottom:7px}
.edit-input{
  width:100%;font-family:inherit;font-size:1.02rem;padding:12px 14px;border-radius:var(--radius-sm);
  border:1.5px solid var(--separator-strong);background:var(--bg);color:var(--text);
}
.edit-input:focus{outline:none;border-color:var(--accent)}
.edit-textarea{resize:vertical;line-height:1.5;min-height:96px}
.edit-count{align-self:flex-end;font-size:.78rem;color:var(--text-3);margin-top:6px;font-variant-numeric:tabular-nums}
.edit-count.over{color:var(--accent-text);font-weight:650}
.edit-hint{font-size:.78rem;color:var(--accent-text);margin-top:6px;line-height:1.35}
.edit-crop{margin-top:20px}

/* ---------- Mobile ---------- */
@media(max-width:820px){
  body{font-size:1rem}
  /* ⚠️ The GAP is what buys room here; the padding does not. Measured at 393px, holding the
     label fixed: gap 16→10px moves the wrap threshold up one step (Only us 125%→130%,
     Following 115%→120%), while padding 16→14px changes it by NOTHING. The 14px is kept only
     because it hands 4px back to content. Don't "reclaim" more by trimming padding — measure
     the gap instead.

     Where the topbar actually wraps to two rows (393px, current spacing), by label:
       Following / Messages  ~120%      Only us  ~130%
       Saved                 ~138%      Liked    ~150%
     Wrapping is graceful and accepted; this is just so nobody re-measures from scratch.

     ⚠️ This is the ONLY place the phone topbar's padding is set. An earlier attempt added a
     second `.topbar` rule higher up the file; being equally specific, this later shorthand
     silently won and that trim never applied at all — the "specificity, not source order"
     trap the brief already records. Keep it in one place. */
  /* ⚠️ The safe-area inset has to be repeated HERE, not just on the base .topbar rule — this
     shorthand is equally specific and later, so it wins, and a fix applied only up there
     would work on tablet and desktop and silently do nothing on a phone, which is the only
     place the installed-app status bar actually matters. See the note above. */
  .topbar{padding:calc(12px + env(safe-area-inset-top)) 14px 12px;gap:10px}
  /* ⚠️ THE ONLY PLACE the phone feedwrap's padding is set — see the note up beside .nav.
     The bottom must scroll clear of the fixed nav bar or the last thing on the page hides
     under it. An earlier `padding-bottom` higher in this file was overridden by this
     shorthand and never applied, leaving 60px against a bar that is ~88px tall once
     `viewport-fit=cover` woke the safe-area inset up — which showed as the You tab's
     "Sign out" button being cut in half (founder, on device, 2026-08-02).
     ⚠️ It has to cover the TALLEST bar: 4px top inset + 46px tab + 4px + the home-indicator
     inset. 78px + the inset leaves room to spare on every phone, notch or no notch. */
  .feedwrap{padding:16px 14px calc(78px + env(safe-area-inset-bottom))}
}

/* ---------- "Your people" — the people-finder (people.js) ----------
   One component in two homes: the You tab (your own connections + everyone on your server +
   search) and a profile's Followers / Following counts. Rows deliberately echo .note from
   the notifications list — avatar, name, handle, one control on the right — so there is one
   row shape to learn across the app. */
.people{display:flex;flex-direction:column;gap:14px}

.people-search{display:flex;flex-direction:column;gap:7px}
/* ⚠️ 1rem is a HARD FLOOR on every text control: iOS Safari auto-zooms the whole page when
   you focus an input under 16px, which shifts the layout sideways and drops the fixed bottom
   nav out of view. Kept in rem so it still scales with the reader's text size. */
.people-search-input{
  width:100%;font-family:inherit;font-size:1rem;min-height:44px;
  padding:11px 14px;border-radius:12px;
  border:1.5px solid var(--separator-strong);background:var(--bg);color:var(--text);
}
.people-search-input:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.people-search-hint{font-size:.82rem;color:var(--text-3);line-height:1.4}

/* The segmented pill bar. CLAUDE.md records this as the agreed mechanism when a set of
   views needs to be visible rather than hidden behind a menu — it was designed for the
   notifications tab, rejected there for lack of volume, and kept for exactly this case. */
.people-pills{
  display:flex;gap:4px;background:var(--surface-2);border-radius:12px;padding:4px;
}
.people-pill{
  flex:1;min-height:40px;padding:8px 10px;border:none;border-radius:9px;
  background:none;color:var(--text-2);font-family:inherit;font-weight:650;font-size:.92rem;
  cursor:pointer;white-space:nowrap;transition:background .15s,color .15s;
}
.people-pill:hover{color:var(--text)}
.people-pill.is-on{background:var(--surface);color:var(--accent-strong)}

.people-list{display:flex;flex-direction:column}
.person{
  display:flex;align-items:center;gap:10px;
  border-bottom:1px solid var(--separator);padding:11px 2px;
  transition:background .15s;
}
.person:last-child{border-bottom:none}
.person-main{
  flex:1;min-width:0;display:flex;align-items:center;gap:12px;text-align:left;
  background:none;border:none;padding:6px 4px;cursor:pointer;font-family:inherit;color:inherit;
  border-radius:10px;
}
.person-main:hover{background:var(--surface-hover)}
.person-text{display:flex;flex-direction:column;min-width:0}
.person-name{font-weight:650;font-size:1rem;color:var(--text);line-height:1.25}
/* The handle must be able to shrink — a long remote address would otherwise push the follow
   control off the row on a phone. */
.person-handle{
  font-size:.86rem;color:var(--text-3);line-height:1.3;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
/* 44px, not btn-sm's 34 — this is a decision button for an older reader (same rule as the
   Approve / Follow back controls in the notifications list). */
.person-action{flex:none;min-height:44px;padding-left:16px;padding-right:16px}
/* A settled state, not a control: "Following" / "Asked to follow" / "You". Same treatment as
   the notifications list's chips. */
.person-chip{
  flex:none;font-size:.84rem;font-weight:650;color:var(--text-2);
  background:var(--surface-2);border-radius:999px;padding:7px 13px;white-space:nowrap;
}
.person.is-failed{background:var(--danger-weak,rgba(200,40,40,.08))}

.people-state{padding:26px 8px;text-align:center}
.people-state-title{font-weight:650;font-size:1rem;color:var(--text);margin-bottom:6px}
.people-state-body{font-size:.9rem;color:var(--text-3);line-height:1.45}

/* The profile's Followers / Following counts are buttons now — they open this view for that
   person. Keep them looking like the plain stat they were; only the affordance changed. */
.profile-stat-btn{
  background:none;border:none;font-family:inherit;cursor:pointer;color:inherit;
  padding:2px 6px;border-radius:9px;
}
.profile-stat-btn:hover{background:var(--surface-hover)}

/* The Back bar of a pushed view that also carries a TITLE — "Your people" and "Your
   settings". Both reuse .profile-bar / .profile-back so there is one way back to learn; this
   adds the row layout and the heading beside it. The title matters because these views can be
   reached several levels down (feed → post → profile → their followers), where the Back
   chevron alone wouldn't say where you are.
   ⚠️ One rule, two class names — never a second copy of these declarations. */
.people-bar,.settings-bar{display:flex;align-items:center;gap:10px;margin-bottom:4px}
.people-title,.settings-title{
  font-size:1.02rem;font-weight:700;color:var(--text);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
}

/* The foot of the feed. ⚠️ A BUTTON, never infinite scroll — "chronological feeds that end"
   is the ethos, and a feed that grows as you fall down it is exactly what this app isn't.
   It removes itself once the timeline is exhausted, so the end stays an end. */
.feed-more{display:flex;justify-content:center;padding:18px 0 6px}
.feed-more-btn{min-height:44px;padding-left:22px;padding-right:22px}

/* The per-post audience badge — a glyph in the card's corner, left of the "…". It REPORTS,
   it doesn't act, so it is deliberately quieter than the menu button beside it.
   Accent rule is the app-wide one: calm at "Only us", accent once the audience is wider. */
/* ⚠️ Sized in em, not px. The glyph inside is em too, so a fixed box would be overflowed by
   its own contents once the reader enlarges text — measured: at 200% the box stayed 30px while
   the glyph grew to ~37px. Same rule as the "…" menu glyphs and the nav icons. */
.post-vis{
  display:inline-flex;align-items:center;justify-content:center;
  width:1.9em;height:1.9em;border-radius:9px;color:var(--text-3);flex:none;
}
.post-vis.is-wide{color:var(--accent-text)}
/* Sized in em so it grows with the reader's text size — same rule as the "…" menu glyphs. */
.post-vis svg{display:block;width:1.15em;height:1.15em}
