/* UmmahSocial — app.v2.css
   ==========================================================================
   THE V2 STYLESHEET. It REPLACES app.css when the preview flag is on; the two
   are never loaded together. See App\Http\Middleware\UiVersion and the
   stylesheet branch in resources/views/layouts/app.blade.php.

   THIS FILE IS IN TWO PARTS.

   PART 1 — CARRIED V1 RULES. A verbatim copy of public/css/app.css as of
   v=102. It is here so that every page outside phase 1 of the design pass
   (settings, notifications, search, Sands, live, standards, about, profile
   chrome, the guest hero, the branded error pages) renders under ?ui=2
   exactly as it does today. Nothing may render unstyled, and a page nobody
   redesigned yet should look like itself, not like a broken half of v2.

   KEEPING THE TWO IN SYNC: app.css is still the live stylesheet for everyone,
   so a fix landed there does NOT reach v2 on its own. Until the flag flips,
   any edit to app.css must be repeated in Part 1 below, and both ?v= numbers
   must be bumped in layouts/app.blade.php. When the flag flips for good this
   file becomes app.css and Part 1 goes away.

   PART 2 — THE V2 LAYER, scoped to body.ui-v2. See its own header.

   Bump BOTH the ?v= on this file and the one on app.css in the same commit as
   any edit. There is no build step; nginx serves /css/ with max-age=604800,
   and a review seat that loads a week-old stylesheet is grading the wrong
   build.
   ========================================================================== */

/* ==========================================================================
   PART 1 BEGINS — carried verbatim from app.css v=102. Do not edit here
   without making the same edit in app.css.
   ========================================================================== */
/* UmmahSocial — "Your page. Your people. No algorithm."
   Palette: night-ink / paper / masjid-tile green / khatam gold.
   Type: Sora (display) + Manrope (body), Bunny Fonts. */

:root {
  --ink: #131A20;
  --ink-soft: #45524D;
  --paper: #FAF8F3;
  --card: #FFFFFF;
  --line: #E5E2D9;
  --green: #166534;
  /* Green as FOREGROUND (a label, a ring), never as a surface. Separate from
     --green, which is a ground that carries light text. Two different jobs, so
     two different tokens: conflating them is what broke dark mode once. */
  --green-ink: #166534;
  --green-deep: #14532D;
  --gold: #C8912E;
  /* SURFACE token: a soft gold FILL sat behind dark text. It is redefined in
     the dark block (-> #3A3324) because a fill has to follow the theme. */
  --gold-soft: #F3E8CF;
  /* TEXT token: gold TYPE on the masjid-green header (--green-deep #14532D),
     which by hard rule stays green in BOTH themes. Because the backdrop never
     changes, this value must never change either - so it is deliberately NOT
     redefined in the dark block below. Do not "tidy" it into --gold-soft.

     Aug 3 2026: .beta-chip and .head-nav .you (the signed-in member's own
     @handle, on every authenticated page) both read --gold-soft as TEXT. In
     dark that token had already flipped to the #3A3324 fill, so gold-brown
     type landed on dark-green header at 1.37:1 - functionally invisible.
     One token was doing two incompatible jobs; splitting it is the fix.
     #F3E8CF measures 7.49:1 on #14532D and is the value both rules already
     rendered in light, so light mode is byte-identical.
     NOTE both obvious alternatives were measured and REJECTED: var(--gold)
     #C8912E = 3.27:1 and the dark gold #D9A94F = 4.23:1 - neither clears 4.5
     for 0.62rem bold type, which is not "large text". */
  --gold-on-green: #F3E8CF;
  --danger: #A63D2F;
  /* SAND — the Sands tray ring only (r538/r587, Sep 10 2026). These are pure
     DECORATION tokens: nothing anywhere sits text on them, so unlike --green
     and --green-deep they are free to follow the theme. Before changing
     either, grep for it as a `background:` under a rule that also sets
     `color:` — if it ever carries text it has become a ground and stops
     following the theme, which is the lesson that cost 29 text failures. */
  --sand: #C8912E;
  --sand-spent: #DED6C4;
  --radius: 14px;
  --col: 640px;
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  /* 'Noto Sans Arabic' behind Manrope (final polish, Sep 9 2026): per-glyph fallback,
     so Latin stays Manrope and only Arabic-script glyphs use it -- see the font link
     in layouts/app.blade.php for why it costs Latin pages nothing. A UI sans, not
     Amiri: a feed is a UI register, and Amiri at 16px reads small and bookish
     beside a geometric Latin. */
  font-family: 'Manrope', 'Noto Sans Arabic', system-ui, sans-serif;
  background: var(--paper);
  color: var(--ink);
  line-height: 1.55;
  font-size: 16px;
  /* CONTAINMENT, inherited by every descendant. A 3000-character unbroken
     string typed into /search echoed into an <h3> gave a scrollWidth of
     38111px on a 390px viewport — ~100x the screen — and a perfectly ordinary
     500-char word gave 6383px (QA round 2). The content was safely escaped;
     nothing anywhere told the browser it was allowed to break a long word.
     overflow-wrap IS an inherited property, so declaring it once here covers
     every surface that echoes member text: names, handles, bios, post bodies,
     link-card titles, error copy, rail headlines. Element-level rules below
     upgrade the known free-text echoes to `anywhere`, which additionally
     shrinks min-content so a long word cannot blow out a FLEX item either. */
  overflow-wrap: break-word;
}

/* The free-text echoes. `anywhere` (not `break-word`) on purpose: only
   `anywhere` participates in min-content sizing, which is what a flex/grid
   child needs before it will agree to be narrower than its longest word. */
.empty h3,
.empty .empty-title,
.name, .handle, .bio, .who,
.post .body,
.post .quoted,
.profile-head h1,
.starter h2,
.list-links,
.mutual-line,
.link-card-title,
.link-card-meta,
.rail-list a,
.rail-domain,
.section-mark { overflow-wrap: anywhere; }
/* A flex child will not go narrower than its longest word unless it is told
   its min-content floor does not apply. `.post .who` already had this; every
   other people-row (search, who-to-follow, people strip, follow lists,
   notifications) is the same shape and needs it too. */
.who, .name, .handle { min-width: 0; }

h1, h2, h3, .display { font-family: 'Sora', 'Manrope', sans-serif; letter-spacing: -0.02em; }

/* ---------- links ----------
   UNDERLINE IS THE RESTING STATE, and the opt-out list below is the whole
   design (WCAG 1.4.1, QA cleanup pass Aug 2 2026).

   Before this, `a` was colour-only: green #166534 against #131A20 body ink is
   2.46:1, and against the softer #45524D prose tone used in /about, /standards,
   /contact and the rail's "Where you are" card it is 1.14:1 — axe measured that
   exact pair on five pages. A reader who cannot separate those two hues had no
   way to tell "UmmahCity" or "Sign in" was a link until they hovered or tabbed
   to it, and on a phone there is no hover at all.

   The direction of the fix is deliberate. Raising the link colour would have
   needed a hue far enough from BOTH ink tones to clear 3:1 while still reading
   as this site's green, and it would have to be re-won every time a new prose
   tone appears. Underline is the conventional answer, it is what the hover
   state already promised, and — the reason it is done as an opt-out rather
   than a list of prose selectors — its failure mode is safe: a link nobody
   remembered to classify comes out underlined, i.e. accessible and slightly
   noisy, instead of silently failing 1.4.1. That is the "fixed the instance,
   missed the class" trap this programme keeps paying for, pointed the other
   way. */
a {
  color: var(--green);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.16em;
}
a:hover { text-decoration-thickness: 2px; }

/* CHROME, not prose. 1.4.1 is about a link sitting INSIDE a block of text,
   where colour alone has to carry the difference between the link and the
   sentence around it. Nothing in this list is in that position: they are
   navigation, buttons, tabs, whole-card affordances, image wrappers, the
   people-row name/avatar/timestamp, and the shared ecosystem footer (owned by
   /var/www/_shared and consumed by six apps — never restyled from here).
   Underlining them would be noise, not information.
   NOTE the hover rule below: everything here behaves exactly as it did before
   this change — no underline at rest, underline on hover where the element is
   text at all. */
.wordmark,
.head-nav a,
.tabs a,
.btn,
.action-btn,
.new-posts-pill,
.byline a,
.link-card,
.post-images a,
.rail-list a,
.rail-more,
details.more .menu a,
.ecosystem-footer a { text-decoration: none; }

.head-nav a:hover,
.byline a:hover,
.link-card:hover,
.rail-list a:hover,
.rail-more:hover,
.ecosystem-footer a:hover { text-decoration: underline; }

/* The gold ring pixel-samples at 2.80:1 on white, under the 3:1 bar for a UI
   shape, and a focus indicator is the one mark a keyboard user cannot do
   without. Rather than darken the gold (which would change every other gold
   thing on the site), the ring is doubled: an inner dark ring carries the
   contrast against light grounds, the gold outer keeps the brand mark and
   carries it against dark ones. Passes on both themes without touching --gold. */
/* Sep 10 2026 — INVERTED, after a seat measured that the ink ring did not
   exist on any button. `box-shadow` is contested: .btn.quiet (0,2,0) and
   .btn.quiet.sands-danger (0,3,0) both set their own, and a bare
   :focus-visible (0,1,0) loses to both. The focus ring silently degraded to
   gold alone, 2.78:1 on white, against a 3:1 bar — a keyboard user on a light
   theme had no compliant focus indicator on any control.
   Raising the selector would only work until the next class combination, so
   the FUNCTIONAL ring now lives in `outline`, which nothing on this estate
   overrides, and the brand gold moved to `box-shadow`. The failure mode is now
   the right way round: if a component overrides box-shadow it loses the
   decoration and KEEPS the contrast. --ink is a theme token, so it passes on
   both grounds (#131A20 on #FFF = 16.5:1, #E7E4DD on #1A2127 = 12.6:1). */
:focus-visible { outline: 3px solid var(--ink); outline-offset: 2px; border-radius: 4px;
  box-shadow: 0 0 0 6px var(--gold); }

/* Available to screen readers, invisible on screen. Used for the signed-in
   home's h1 — the visible one lives in the guest hero, so an authed page had
   no name at all (a11y pass, Aug 2). Clip-path, not display:none, which would
   hide it from assistive tech too. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* ---------- khatam: the 8-point star ---------- */
.khatam { display: inline-block; width: 1em; height: 1em; vertical-align: -0.12em; }

/* ---------- crescent: warm aside, never the mark (dY7s2ODE follow-on) ----
   Discipline per the ruling: gold via the existing --gold token (this app
   has no separate --gold-text var; --gold already does text-color duty
   elsewhere, e.g. .follows-you), <=1em, stroke only, never a filled badge,
   never animated, max ONE per viewport. Sized slightly under 1em on purpose
   — "punctuation, never a claim" reads as literally smaller than the words
   beside it. */
.crescent { display: inline-block; width: 0.8em; height: 0.8em; vertical-align: -0.08em; color: var(--gold); }

/* ---------- header ---------- */
.site-head {
  background: var(--green-deep);
  color: #F4F6F1;
  border-bottom: 3px solid var(--gold);
}
.site-head .inner {
  max-width: var(--col);
  margin: 0 auto;
  padding: 0.7rem 1rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
.wordmark {
  font-family: 'Sora', sans-serif;
  font-weight: 700;
  font-size: 1.15rem;
  color: #F4F6F1;
  display: flex;
  align-items: center;
  gap: 0.45rem;
}
.wordmark:hover { text-decoration: none; }
.wordmark .khatam { color: var(--gold); width: 1.15em; height: 1.15em; }
.head-nav { margin-left: auto; display: flex; align-items: center; gap: 0.9rem; font-size: 0.9rem; }
.head-nav a { color: #D9E2DA; font-weight: 600; }
.head-nav .you { color: var(--gold-on-green); }

/* ---------- shell ---------- */
.shell { max-width: var(--col); width: 100%; margin: 0 auto; padding: 0 1rem 4rem; flex: 1; }

/* ---------- hero (logged out) ---------- */
.hero { padding: 3.2rem 0 2.4rem; border-bottom: 1px solid var(--line); }
.hero h1 {
  font-size: clamp(1.9rem, 6vw, 2.9rem);
  font-weight: 700;
  line-height: 1.12;
  text-wrap: balance;
  color: var(--ink);
}
.hero h1 .khatam { color: var(--gold); width: 0.62em; height: 0.62em; margin-left: 0.14em; }
.hero .sub {
  margin-top: 0.9rem;
  font-size: 1.08rem;
  color: var(--ink-soft);
  max-width: 34rem;
  text-wrap: balance;
}
.hero .cta-row { margin-top: 1.6rem; display: flex; gap: 0.8rem; flex-wrap: wrap; align-items: center; }
.hero .bridge { margin-top: 1.2rem; font-size: 0.95rem; color: var(--ink-soft); }
/* Sep 9 2026: this was scoped `.hero ul.tenets`, but the same class is used again in
   the guest footer OUTSIDE .hero -- so that copy fell back to browser defaults and
   rendered as bulleted list items outdented from the column it sits in. A review seat
   read it as sloppy footer styling; it was a selector that had never matched there. */
/* The guest tenets now sit under a real <h2> (final polish, Sep 9 2026). */
.section-mark + ul.tenets { margin-top: 0.6rem; }
ul.tenets { margin-top: 1.4rem; list-style: none; padding-left: 0; display: grid; gap: 0.45rem; font-size: 0.95rem; color: var(--ink-soft); }
ul.tenets .khatam { color: var(--green); width: 0.7em; height: 0.7em; margin-right: 0.45rem; }

/* Sep 9 2026 — a design seat found an EMPTY GREEN PILL painting in the fold.
   `.btn { display: inline-flex }` beats the user-agent's `[hidden]{display:none}`
   on specificity, so <a class="btn" hidden> with no text still painted a 45x44
   solid disc under the primary CTA. It read as a control that failed to render,
   on the page whose whole argument is that it can be trusted.
   The attribute must win over any display we set. */
[hidden] { display: none !important; }

/* Sep 9 2026 — avatars had NO object-fit rule, so the browser default (`fill`)
   applied and every non-square source was stretched to the 42x42 box: a 736x1308
   portrait rendered at +77.7% aspect error, a 735x412 landscape at -43.9%. Square
   sources escaped by luck. On a social product the avatar is the identity token,
   so a squashed face is not a small defect. */
.avatar, img.avatar { object-fit: cover; }

/* WCAG 2.5.8: the field the hero asks you to type in was 316x23. Padding only. */
#handle-try-input { min-height: 28px; padding-block: 0.3rem; }

/* Byline name/timestamp links ran 18-21px. Hit area only — the painted text does
   not move, matching the tap-target sweep already used elsewhere on this page. */
.post .name > a, .post .when > a { display: inline-block; padding-block: 0.15rem; min-height: 24px; }


/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-family: 'Manrope', sans-serif;
  font-weight: 700; font-size: 0.95rem;
  border: 0; border-radius: 999px;
  padding: 0.62rem 1.4rem;
  cursor: pointer;
  background: var(--green);
  color: #fff;
  transition: background 0.15s ease;
}
.btn:hover { background: var(--green-deep); text-decoration: none; }
.btn.secondary { background: transparent; color: var(--green); box-shadow: inset 0 0 0 2px var(--green); }
.btn.secondary:hover { background: rgba(22, 101, 52, 0.07); }
.btn.small { padding: 0.35rem 0.95rem; font-size: 0.85rem; }
/* A short pill that wraps collapses into a circle with a letter in it (CD saw
   "Sign in" do exactly that at 390px). But the hero CTA is a full sentence and
   MUST wrap — forcing nowrap on every .btn pushed it to 474px inside a 390px
   viewport. So: nowrap the small/quiet header pills, let long labels wrap. */
.btn.small, .btn.quiet { white-space: nowrap; }
.btn { max-width: 100%; }
/* --line measures 1.22:1 to 1.46:1 against its ground. That is fine for a card
   edge, which is decoration, and NOT fine here: .btn.quiet has a transparent
   background, so this hairline is the control's ONLY shape, against a 3:1 bar.
   --ink-soft is already the label colour and clears 3:1 on both themes, so the
   border simply reuses it at 40% rather than inventing a colour. */
.btn.quiet { background: transparent; color: var(--ink-soft);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink-soft) 75%, transparent); }
.btn.quiet:hover { background: #F0F0E9; }

/* ---------- tabs ---------- */
.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--line); margin-top: 0.6rem; }
.tabs a {
  padding: 0.8rem 1.1rem;
  font-weight: 700; font-size: 0.95rem;
  color: var(--ink-soft);
  border-bottom: 3px solid transparent;
  margin-bottom: -1px;
}
.tabs a:hover { text-decoration: none; color: var(--ink); }
.tabs a.active { color: var(--green-deep); border-bottom-color: var(--gold); }
.tabs .hint { margin-left: auto; align-self: center; font-size: 0.78rem; color: var(--ink-soft); }

/* ---------- composer ---------- */
.composer {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem;
  margin: 1.1rem 0;
}
.composer textarea {
  width: 100%;
  min-height: 88px;
  border: 0;
  resize: vertical;
  font: inherit;
  color: var(--ink);
  background: transparent;
  /* Wajid reported on Aug 27 2026 that typing here feels cramped: the text
     started hard against the left edge because this rule set every box
     property EXCEPT padding, so the textarea inherited none while the card
     around it has 1rem. The placeholder sat flush to the border too.
     Vertical padding is small on purpose - the card already supplies the
     outer breathing room, and a tall inset would push the 88px min-height
     into a box that looks empty. */
  padding: 0.35rem 0.5rem;
}
.composer textarea:focus {
  /* Was outline:none with nothing compensating — the one field everyone uses
     was the one field with no focus state (flow audit, Aug 1). Inset so the
     ring doesn't collide with the card edge. */
  outline: 2px solid var(--gold);
  outline-offset: -2px;
}
.composer .row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.7rem; margin-top: 0.6rem; }
.composer select {
  font: inherit; font-size: 0.85rem; font-weight: 600;
  color: var(--ink-soft);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 0.3rem 0.5rem; background: var(--paper);
}
.composer .count { margin-left: auto; font-size: 0.78rem; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

/* ---------- posts ---------- */
.timeline { list-style: none; }
.post {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
  margin: 0.8rem 0;
  position: relative; /* the first-card seal anchors to the corner (Aug 10) */
}
.post .byline { display: flex; align-items: center; gap: 0.6rem; }
.post .avatar { width: 42px; height: 42px; border-radius: 50%; flex: none; background: var(--paper); }
.post .who { line-height: 1.25; min-width: 0; }
.post .who .name { font-weight: 700; }
.post .who .handle { color: var(--ink-soft); font-size: 0.88rem; }
.post .when { margin-left: auto; font-size: 0.8rem; color: var(--ink-soft); white-space: nowrap; }
.post .when a { color: inherit; }
.post .body { margin-top: 0.55rem; white-space: pre-wrap; overflow-wrap: break-word; }
/* An RTL paragraph (dir="auto" resolved it from the first strong character) gets the
   extra leading Arabic ascenders and diacritics need. :dir() is a progressive
   enhancement (Safari 16.4+, Chrome 120+, Firefox 49+); older browsers keep 1.55. */
/* BodyFormat wraps each Arabic run in <span lang="ar" dir="rtl">, and dir="auto"
   on the paragraph IGNORES descendants that carry their own dir -- so a post that
   opens with an ayah and continues in English resolves LTR and :dir(rtl) never
   matches the paragraph. The span rule is the one that fires; an inline box's
   line-height still raises the line box it sits on. */
.post .body [lang="ar"], .post .body:dir(rtl), .post .quoted .body:dir(rtl), .bio:dir(rtl) { line-height: 1.75; }
/* Sep 9 2026: a quoted Arabic passage gets its own line. `dir="auto"` takes the
   paragraph's direction from the first strong character, so a post that opens with an
   ayah turns the WHOLE paragraph RTL, including the English translation after it, and
   the punctuation between them lands at the junction. Separating the passage fixes
   the reading without touching the poster's words. Short runs stay inline. */
.post .body .ar-block { display: block; margin: 0 0 0.5rem; text-align: right; }
.post .vis-chip,
.profile-head .vis-chip { /* "Approves followers" chip on private profiles (gap 5) */
  font-size: 0.72rem; font-weight: 700;
  color: var(--green-deep); background: rgba(22,101,52,0.08);
  padding: 0.1rem 0.5rem; border-radius: 999px;
}

.post .quoted {
  margin-top: 0.7rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 0.7rem 0.85rem;
  font-size: 0.93rem;
  background: var(--paper);
}
.post .quoted .who { font-size: 0.85rem; }
.post .deleted-stub { color: var(--ink-soft); font-style: italic; }

.repost-note, .reply-note { font-size: 0.8rem; color: var(--ink-soft); margin-bottom: 0.4rem; }
.repost-note .khatam { color: var(--green); width: 0.7em; height: 0.7em; }

/* actions */
.post .actions { display: flex; gap: 0.4rem; margin-top: 0.7rem; align-items: center; flex-wrap: wrap; }
.action-btn {
  display: inline-flex; align-items: center; gap: 0.35rem;
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 0.83rem; font-weight: 600;
  color: var(--ink-soft);
  padding: 0.5rem 0.7rem; border-radius: 8px; min-height: 44px; /* 44px = the ecosystem touch-target convention */
}
.action-btn:hover { background: var(--gold-soft); color: var(--green-deep); text-decoration: none; box-shadow: inset 0 0 0 1px var(--line); }
.action-btn .khatam { width: 0.95em; height: 0.95em; }
.action-btn.liked { color: var(--gold); }
.action-btn.liked .khatam { color: var(--gold); }
.action-btn .n { font-variant-numeric: tabular-nums; }

/* Notification row: the WHOLE row opens the same place "View" does (card
   SZvX090H, Aug 11 2026) -- not a new element, an extended hit area on the
   REAL View anchor via ::after (the standard "block link"/stretched-link
   technique). No wrapping <a>: the row already carries a real anchor (the
   actor's avatar, which must keep going to their profile -- the displayed
   name beside it is plain text, not a link, in this template), and nesting
   <a> inside <a> is invalid HTML with unpredictable browser recovery. The
   View link is left position:static on purpose -- that makes .post's own
   position:relative (not the small button) the ::after's containing block,
   so inset:0 covers the FULL card, not just the button. The avatar link
   needs position:relative + an explicit z-index above it, or the overlay
   (painted after all in-flow content, regardless of DOM order) swallows its
   tap even though it sits earlier in the markup. Scoped to .notif .actions
   so ordinary post cards elsewhere are untouched. No color/background on
   the pseudo-element, so dark-mode contrast is unaffected by construction. */
.notif .actions .action-btn::after { content: ""; position: absolute; inset: 0; }
.notif .byline a { position: relative; z-index: 1; }

details.more { position: relative; margin-left: auto; }
details.more summary { list-style: none; cursor: pointer; color: var(--ink-soft); padding: 0.5rem 0.7rem; min-height: 40px; display: flex; align-items: center; border-radius: 8px; font-weight: 700; }
details.more summary::-webkit-details-marker { display: none; }
details.more summary:hover { background: #F0F0E9; }
details.more .menu {
  position: absolute; right: 0; z-index: 10;
  background: var(--card); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 8px 24px rgba(19,26,32,0.12);
  min-width: 180px; padding: 0.3rem;
}
details.more .menu form { display: block; }
details.more .menu button, details.more .menu a {
  display: block; width: 100%; text-align: left;
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 0.88rem;
  color: var(--ink); padding: 0.45rem 0.7rem; border-radius: 7px;
}
details.more .menu button:hover, details.more .menu a:hover { background: var(--paper); text-decoration: none; }
details.more .menu .danger { color: var(--danger); }

/* inline reply */
details.reply-box { margin-top: 0.5rem; }
details.reply-box summary { list-style: none; cursor: pointer; display: none; }
.reply-form { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
.reply-form textarea {
  flex: 1; min-height: 44px; font: inherit; font-size: 0.93rem;
  border: 1px solid var(--line); border-radius: 10px; padding: 0.5rem 0.7rem;
  background: var(--paper); color: var(--ink); resize: vertical;
}

/* ---------- dividers ---------- */
/* Section marks are real headings (<h1>/<h2>) as of the Aug 2 a11y pass — the
   page had exactly one h1 and zero h2s, so heading navigation was dead for a
   screen-reader user. font-family is restated because the global h1,h2,h3 rule
   would otherwise flip these labels from Manrope to Sora; the render is
   pixel-identical to when they were divs. */
.section-mark {
  display: flex; align-items: center; gap: 0.7rem;
  margin: 2.2rem 0 0.6rem;
  /* Sep 9 2026: this was a 12.8px uppercase Manrope kicker, and it was the ONLY
     in-page heading on the homepage. A review seat measured the scale jumping from a
     54.4px H1 straight to 12.5px with nothing in between, across roughly ten screens
     of feed, so a reader had no waypoints at all. Promoted to a real mid tier: Sora,
     20px, sentence case. The hairline rule after it stays, because that is what makes
     it read as a section break rather than as another post. */
  font-family: 'Sora', 'Manrope', sans-serif;
  color: var(--ink); font-size: 1.25rem; font-weight: 650;
  text-transform: none; letter-spacing: -0.005em; line-height: 1.25;
}
.section-mark::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.section-mark .khatam { color: var(--gold); width: 0.85em; height: 0.85em; }

/* ---------- profile ---------- */
.profile-head { padding: 1.6rem 0 1rem; border-bottom: 1px solid var(--line); display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
.profile-head .avatar { width: 74px; height: 74px; border-radius: 50%; background: var(--card); border: 1px solid var(--line); }
.profile-head .meta { min-width: 0; }
.profile-head h1 { font-size: 1.35rem; }
.profile-head .handle { color: var(--ink-soft); }
.profile-head .bio { margin-top: 0.4rem; max-width: 30rem; font-size: 0.95rem; }
/* r792 (Tab, Sep 19 2026): plain text, same register as .own-count and
   .mutual-line right below it -- no border, no background, no pill. A count
   next to a score-shaped chip is the thing this rule explicitly avoids. */
.profile-head .profile-stats { margin-top: 0.35rem; font-size: 0.85rem; color: var(--ink-soft); }
.profile-head .own-count { margin-top: 0.35rem; font-size: 0.85rem; color: var(--ink-soft); }
/* flex-wrap: the follow pill + the ummah.me pill side by side are 331px —
   wider than a 320px phone (found by the Aug 2 overflow sweep). */
.profile-head .actions-col { margin-left: auto; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; justify-content: flex-end; }

/* ---------- starter pack ---------- */
.starter {
  background: linear-gradient(135deg, rgba(22,101,52,0.06), rgba(200,145,46,0.07));
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.1rem 1.2rem;
  margin: 1.1rem 0;
}
.starter h2 { font-size: 1.05rem; }
.starter p { font-size: 0.92rem; color: var(--ink-soft); margin-top: 0.25rem; }
.starter .pack-row { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.8rem; flex-wrap: wrap; }
.starter .faces { display: flex; }
.starter .faces img { width: 34px; height: 34px; border-radius: 50%; border: 2px solid var(--card); margin-left: -8px; background: var(--paper); }
.starter .faces img:first-child { margin-left: 0; }

/* ---------- empty states ---------- */
.empty {
  text-align: center; padding: 2.6rem 1rem; color: var(--ink-soft);
  border: 1px dashed var(--line); border-radius: var(--radius); margin: 1rem 0;
}
/* An empty state's title takes its LEVEL from the outline and its LOOK from
   here, and the two are deliberately decoupled: `.empty-title` renders
   identically on h1, h2 or h3, so the tag can always be the structurally
   correct one. Measured on /search after the Aug-2 cleanup swap: h3 and
   h2.empty-title both compute 16.8px / 700 / Sora / #131A20 at the same box
   and the same page position, i.e. the swap is purely structural.
   The rule of thumb, since this shipped wrong on six surfaces at once:
   h1 when the empty state IS the page (404/405/419, the gated-list
   interstitial), h2 when the page already has an h1 above it, h3 only when it
   sits under a real h2 (the home feed's, under "The city, in order"). */
.empty h3, .empty .empty-title { color: var(--ink); font-size: 1.05rem; }
.empty h1.empty-title { font-weight: 700; }
.empty p { font-size: 0.93rem; margin-top: 0.35rem; }
.empty .khatam { color: var(--gold); width: 1.6em; height: 1.6em; margin-bottom: 0.5rem; }

/* Khatam tessellation — a fine-line panel texture, "empty room" surfaces
   ONLY (card dY7s2ODE, Aug 3 2026 Islamic-flavor pass). Applied by adding
   `khatam-field` alongside an existing class (`.empty khatam-field`,
   `.stage-note khatam-field`) and including partials.khatam-tess as the
   FIRST child. HARD CAP: never on `.post` (the feed) — that surface already
   has its own, denser watermark rule below; this is the sparse cousin for
   surfaces with no content to compete with.
   The color lives ONCE, on the <pattern> itself (#khatam-tess, defined in
   layouts/app.blade.php) via `color: var(--gold)` — because a <pattern>'s
   children inherit CSS from the pattern's OWN place in the DOM, not from
   whatever shape references it via fill:url(#id). That is what makes dark
   mode free: --gold is redefined in the dark media query below and nothing
   here needs to know it changed. Only opacity is tuned per theme, and it is
   tuned as a plain number, never a color. */
/* `z-index: 0` here is NOT decorative — it is the fix for a real bug found
   in verification (Aug 3 2026). `position: relative` alone does not
   establish a new stacking context (only position + a NON-auto z-index
   does), so without this the `z-index: -1` child below does not stack
   against ITS PARENT — it escapes to the nearest ancestor that DOES
   establish one (in practice, the root), and paints behind the ENTIRE
   page's normal-flow layer. `.khatam-field`'s own background/border then
   paints in that normal-flow layer and silently covers the star field:
   every computed style read correct (opacity 0.08, position, z-index,
   color) and the texture was still fully invisible on every real render.
   Caught only by comparing an isolated reproduction (worked) against the
   live page (didn't) and reasoning through paint order — never by reading
   computed styles alone. */
.khatam-field { position: relative; overflow: hidden; z-index: 0; }
.khatam-field > svg.khatam-tess {
  position: absolute; inset: 0; width: 100%; height: 100%;
  z-index: -1; pointer-events: none;
}
#khatam-tess { color: var(--gold); }
.khatam-tess-rect { opacity: 0.08; }

/* ---------- flash + errors ---------- */
.flash { border-radius: 10px; padding: 0.7rem 1rem; margin: 0.9rem 0; font-size: 0.92rem; font-weight: 600; }
/* OPAQUE, not a tint. .flash is position:fixed further down, so a
   translucent background let whatever was underneath show through — in one
   Aug-2 capture the "Posted." toast rendered as a faint ghost on top of the
   ad unit and was barely readable. A floating element needs its own surface. */
.flash.ok { background: #EFF6F1; color: var(--green-deep); border: 1px solid rgba(22,101,52,0.28); }
.flash.err { background: #FBF1EF; color: var(--danger); border: 1px solid rgba(166,61,47,0.28); }

/* ---------- standards + footer ---------- */
.prose { padding: 2rem 0; max-width: 38rem; }
.prose h1 { font-size: 1.7rem; margin-bottom: 0.7rem; }
.prose h2 { font-size: 1.1rem; margin: 1.4rem 0 0.4rem; }
.prose p, .prose li { color: var(--ink-soft); }
.prose ol { padding-left: 1.3rem; display: grid; gap: 0.55rem; margin-top: 0.6rem; }
.prose ol li strong { color: var(--ink); }

.site-foot { border-top: 1px solid var(--line); margin-top: auto; }
.site-foot .inner {
  max-width: var(--col); margin: 0 auto; padding: 1.4rem 1rem 2rem;
  font-size: 0.85rem; color: var(--ink-soft);
  display: grid; gap: 0.4rem;
}
.site-foot a { color: var(--ink-soft); font-weight: 600; }

/* ---------- pagination ---------- */
.load-more { display: block; text-align: center; margin: 1.4rem 0; }

/* ---------- admin ---------- */
.report-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem; margin: 0.8rem 0; }
.report-card .reason { font-weight: 700; }
.report-card .quoted { border: 1px solid var(--line); border-radius: 10px; padding: 0.7rem; margin-top: 0.6rem; font-size: 0.92rem; background: var(--paper); }
.report-card .actions { display: flex; gap: 0.5rem; margin-top: 0.7rem; }

/* ---------- responsive ---------- */
@media (max-width: 480px) {
  body { font-size: 15px; }
  .composer label[for="visibility"] { display: none; }
  .composer .count { display: none; } /* maxlength still enforces; the meter is decoration on a phone */
  .post { padding: 0.85rem 0.9rem; }
  .hero { padding: 2.2rem 0 1.8rem; }
  .head-nav { gap: 0.6rem; font-size: 0.85rem; }
  /* The header ran 5px past a 390px viewport once the Sign in pill stopped
     wrapping — a wrapped pill and a horizontal scrollbar are both wrong, so
     tighten the bar instead of letting either happen. Holds down to 320px. */
  .site-head .inner { padding: 0.7rem 0.8rem; gap: 0.5rem; }
  .wordmark { font-size: 1.05rem; }
  .head-nav { flex-wrap: nowrap; }
}
@media (max-width: 420px) {
  /* One row, even at 320px (Aug 10): "Find people" + the Sign in pill fit
     beside the wordmark now that Standards moved to the footer and the beta
     chip moved into the hero eyebrow. Measured: wordmark 118px + nav ~168px
     + gaps fits 320px with the tightened paddings above. A wrapped header
     read as broken layout in every screenshot review. */
  .head-nav:not(.authed) { flex-wrap: nowrap; }
  .head-nav:not(.authed) .head-drop { display: inline; }
}
/* Signed-in header on phones: measured 147px of overflow at 390px with a short
   handle and 276px with the longest live one (in-process render, Aug 1). The
   handle is the only unbounded item, so it clamps with an ellipsis; Sign out
   moves to Settings (one tap away on the gear, and the header keeps every
   daily action: notifications, search, profile, settings, Reports). */
/* The account menu replaced the old .head-signout form on Aug 6 2026. That form
   was hidden below 640px to stop the header overflowing, which meant a member on
   a phone could not sign out from any page. This is ONE control instead of two
   (it swallowed the gear link as well), so the header is narrower on a phone
   than it was, and Sign out is always two taps away. */
.acct { position: relative; }
.acct > summary { list-style: none; cursor: pointer; color: #D9E2DA;
  padding: 0.15rem 0.35rem; line-height: 1; }
.acct > summary::-webkit-details-marker { display: none; }
.acct > summary::marker { content: ""; }
.acct[open] > summary { color: var(--gold-on-green); }
.acct-menu { position: absolute; right: 0; top: 1.9rem; z-index: 80;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 0.3rem; min-width: 10.5rem; display: flex; flex-direction: column; gap: 0.1rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18); }
/* Tap targets, not pointer targets: this menu is used mostly on a phone. */
.acct-menu a, .acct-menu button { display: block; width: 100%; text-align: left;
  padding: 0.6rem 0.7rem; border-radius: 8px; border: 0; background: none;
  font: inherit; color: var(--ink); text-decoration: none; cursor: pointer; }
.acct-menu a:hover, .acct-menu button:hover { background: var(--paper); text-decoration: none; }
.acct-menu form { margin: 0; }
/* A way back out of a post (card n1xg6A3v). Quiet, above the title, and a real
   link so it works for someone who arrived cold from a shared link. */
.back-link { display: inline-block; margin: 0.2rem 0 0.6rem; font-size: 0.9rem;
  color: var(--ink-soft); text-decoration: none; }
.back-link:hover { color: var(--ink); text-decoration: underline; }
@media (max-width: 640px) {
  .head-nav .you {
    max-width: 7.5rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* The signed-in nav (bell/Search/@you/gear[/Reports]) cannot fit beside the
     wordmark on a phone — measured 52px past a 390px viewport even with the
     handle clamped and Sign out moved to Settings. It wraps to its own
     full-width row instead, evenly spread for thumbs. Guest nav (just Sign in)
     still fits inline and keeps the one-row header. */
  .site-head .inner { flex-wrap: wrap; }
  .head-nav.authed {
    width: 100%;
    margin-left: 0;
    justify-content: space-between;
    gap: 0.25rem;
  }
}
@media (max-width: 360px) {
  .head-nav .you { max-width: 5rem; }
}

/* ---------- Jul 31 tester-feedback round ---------- */
/* Sign-out was near-invisible on the dark header (Ilma + Saad both hit it) */
.head-nav .btn.quiet { color: #E9EFE7; box-shadow: inset 0 0 0 1.5px rgba(244,246,241,0.5); }
.head-nav .btn.quiet:hover { background: rgba(244,246,241,0.14); color: #FFFFFF; }

/* Reposted toggle state */
.action-btn.reposted { color: var(--green); font-weight: 700; }
.action-btn.reposted:hover { color: var(--danger); } /* hints tap-to-remove */

/* ---------- /app install page ---------- */
.install-grid { display: grid; gap: 1rem; margin-top: 1.4rem; }
.install-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.1rem 1.2rem; }
.install-card h2 { font-size: 1.02rem; display: flex; align-items: center; gap: 0.5rem; }
.install-card h2 .khatam { color: var(--gold); width: 0.85em; height: 0.85em; }
.install-card ol { padding-left: 1.3rem; margin-top: 0.5rem; display: grid; gap: 0.3rem; color: var(--ink-soft); font-size: 0.95rem; }
#install-btn-wrap { display: none; margin: 1.2rem 0 0; }

/* CD round 2: repost annotation reads as annotation, echo card sits quieter.
   background-COLOR (not the shorthand): the shorthand resets background-image
   and would erase the card watermark below. */
.repost-note { font-weight: 700; color: var(--ink); }
.repost-note .khatam { color: var(--gold); }
.post:has(> .repost-note) { background-color: #FCFBF7; }

/* beta chip (Tab, Aug 1) */
.beta-chip { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--gold-on-green); border: 1px solid rgba(243,232,207,0.5); border-radius: 999px; padding: 0.08rem 0.45rem; margin-left: 0.2rem; }

/* CD footer round: Sign-in button was near-invisible on the header (1.28:1) */
.head-nav .btn.small { background: #F4F6F1; color: var(--green-deep); }
.head-nav .btn.small:hover { background: #FFFFFF; color: var(--green-deep); }

/* Phase B: image grid + Ummah One member ring + attach control */
.post-images { display: grid; gap: 6px; margin-top: 0.7rem; border-radius: 12px; overflow: hidden; }
.post-images.n1 { grid-template-columns: 1fr; }
.post-images.n2, .post-images.n4 { grid-template-columns: 1fr 1fr; }
.post-images.n3 { grid-template-columns: 1fr 1fr 1fr; }
/* Public-ready round, Sep 19 2026: this v1 byte is carried here verbatim
   (Part 1); the v2-scoped .post-images further down (body.ui-v2 li.prow
   .post-images) has the SAME overflow:hidden clip and its own, higher-
   specificity fix beside it. See that rule's comment for the measurement. */
.post-images a:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: -3px;
  box-shadow: none;
  border-radius: 8px;
}
.post-images img { width: 100%; height: 100%; max-height: 420px; object-fit: cover; display: block; background: var(--paper); }
/* Sep 9 2026: when someone has no avatar photo, UmmahPass generates a magenta
   gradient disc (hsl 323) -- the only purple on a green-and-gold page, and it
   was 7 of the 12 faces a guest saw. We draw our own in the site's colours.
   Same tokens as the header, so it stays correct in both themes. */
.avatar--initial, .post .avatar--initial, .profile-head .avatar--initial {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--green-deep); color: var(--gold-on-green);
  font-weight: 700; font-size: 1.05rem; line-height: 1; letter-spacing: .01em;
  user-select: none; overflow: hidden; }
.avatar.uo-ring { box-shadow: 0 0 0 2px var(--gold), 0 0 0 4px rgba(200,145,46,0.25); }
.attach-label { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.85rem; font-weight: 600; color: var(--ink-soft); cursor: pointer; padding: 0.3rem 0.6rem; border: 1px solid var(--line); border-radius: 8px; background: var(--paper); }
.attach-label:hover { color: var(--green-deep); background: var(--gold-soft); }
/* Sep 4 2026: the composer's action row reads as one matched group,
   but "Photos" is a <label> (no underline) while "Go live" and "Post a story"
   are anchors and carried the browser default. Same row, same treatment. */
.attach-label--plain, .attach-label--plain:hover { text-decoration: none; }
/* Sep 4 2026: the composer action row is the first thing a
   thumb touches and its controls measured 33px. 44 is the brand floor. */
/* The composer action row is NOT covered by the hit-area sweep below (that
   sweep lists .btn.small/.btn.quiet, the wordmark, the handle row and the hero
   links), so these three controls really were 33px of tappable area, not 33px
   painted over 44px tappable. Measured Sep 4 2026 before and after. */
.attach-label { min-height: 44px; box-sizing: border-box; }
/* No .story-door sizing rule: `.btn.small` pills are painted compact ON PURPOSE
   and already carry a 44px tap area from the Aug 14 2026 hit-area sweep below
   (an invisible ::after, top/bottom -6px). Measured Sep 4: painted 32px, tap
   44px. A min-height here is overridden by that sweep and would only make the
   pill visually taller than every other pill on the site. */
.attach-label .khatam { width: 0.85em; height: 0.85em; }

/* Photo preview + edit-removal (Alisha's Aug 1 report) */
.photo-preview { grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 0.6rem; }
.photo-preview-cell { position: relative; border-radius: 10px; overflow: hidden; border: 1px solid var(--line); }
.photo-preview-cell img { width: 100%; height: 92px; object-fit: cover; display: block; }
.photo-preview-remove { position: absolute; top: 4px; right: 4px; width: 26px; height: 26px; border-radius: 50%; border: 0; background: rgba(19,26,32,0.72); color: #fff; font-size: 1rem; line-height: 1; cursor: pointer; }
.photo-preview-remove:hover { background: var(--danger); }
/* Full-width notices inside the 4-col preview grid (composer.js: over-limit
   and oversize-file messages). Without grid-column they render as a cramped
   grid cell. */
.photo-preview-note { grid-column: 1 / -1; margin: 0.2rem 0 0; font-size: 0.85rem; color: var(--ink-soft); }
.photo-remove-cell { cursor: pointer; }
.photo-remove-cell input { position: absolute; top: 6px; right: 6px; width: 18px; height: 18px; accent-color: var(--danger); }
.photo-remove-tag { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(19,26,32,0.62); color: #fff; font-size: 0.72rem; font-weight: 700; text-align: center; padding: 2px 0; opacity: 0; }
.photo-remove-cell:has(input:checked) { outline: 2px solid var(--danger); }
.photo-remove-cell:has(input:checked) .photo-remove-tag { opacity: 1; }
@media (max-width: 480px) { .photo-preview { grid-template-columns: repeat(3, 1fr); } }

/* Report-reason picker (5 standards lines + other) */
details.report-sub summary { list-style: none; cursor: pointer; padding: 0.45rem 0.7rem; border-radius: 7px; font-size: 0.88rem; }
details.report-sub summary::-webkit-details-marker { display: none; }
details.report-sub summary:hover { background: var(--paper); }
.report-reasons { padding: 0.2rem 0.4rem 0.4rem; display: grid; gap: 2px; }
.report-reasons button { display: block; width: 100%; text-align: left; background: none; border: 0; cursor: pointer; font: inherit; font-size: 0.83rem; color: var(--ink-soft); padding: 0.35rem 0.6rem; border-radius: 6px; }
.report-reasons button:hover { background: var(--gold-soft); color: var(--green-deep); }

/* Toast-style flashes (CD: confirmation feedback) — fixed, self-dismissing */
.flash { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 90; box-shadow: 0 8px 24px rgba(19,26,32,0.18); max-width: min(92vw, 480px); animation: toast-out 4.5s ease forwards; }
@keyframes toast-out { 0% { opacity: 0; transform: translate(-50%, 8px); } 8% { opacity: 1; transform: translate(-50%, 0); } 80% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
@media (prefers-reduced-motion: reduce) { .flash { animation: none; } }

/* Two-column shell (rail ≥1100px) + rail cards.
   The wrap needs its OWN max-width. .shell is flex:1, so it grew to fill
   everything the rail did not take and then centred its 640px inside that —
   which left the content column 226px from the left edge while the rail sat
   FLUSH against the right edge of the window with zero margin. At 1440 that
   reads as a mobile column pinned to the middle with an ad card stuck to the
   glass (CD, Aug 2). 1028 = 640 col + 28 gap + 320 rail + 40 padding. */
.shell-wrap { display: flex; justify-content: center; gap: 28px; width: 100%; }
.right-rail { display: none; }
@media (min-width: 1100px) {
  .shell-wrap { max-width: 1028px; margin: 0 auto; padding: 0 20px; }
  /* Final polish (Sep 9 2026): the header's inner box was still capped at --col
     (640px) after .shell-wrap grew to 1028, so at 1440 the wordmark sat at x=416
     while the H1 under it started at 242 -- the logo aligned with nothing on the
     page. Same 1028 box; the padding puts the wordmark on the H1's left edge
     (206 + 36 = 242) and the Sign-in pill on the rail's right edge (1234 - 20 =
     1214). Measured, not eyeballed. */
  .site-head .inner { max-width: 1028px; padding-left: 36px; padding-right: 20px; }
  /* STICKY, and this is the whole fix for "the rail dies after two screens".
     Measured before: rail bottom 1582px against a feed bottom of 8005px on a
     8327px page — so 8 of 10 screen-heights showed a lone 640px column adrift
     in cream (CD round 2). The rail was as tall as an honest rail can be; the
     bug was that height was ever the mechanism. Height cannot solve it without
     inventing filler modules, and the alternative (re-centring the feed once
     the rail runs out) is a mid-page column jump no CSS can do without JS
     reflow. Sticky keeps three REAL modules beside the reader for the whole
     scroll and adds not one word of padding content.
     The offset lives in MARGIN, not padding: padding sits inside the sticky
     box, so a 507px pad would stick 507px of nothing to the top of the window.
     max-height + overflow is the safety net for short viewports only — the
     rail fits without a scrollbar at >=900px tall (measured 793px of content). */
  .right-rail {
    display: block; width: 320px; flex: none; align-self: flex-start;
    margin-top: 1.4rem;
    position: sticky; top: 14px;
    max-height: calc(100vh - 28px);
    overflow-y: auto;
    /* The thumb must be VISIBLE or the clamp reads as a card chopped in half
       instead of a scroll region. First pass used var(--line) (#E5E2D9) on
       #FAF8F3 paper — invisible, which is how a safety net turns into a
       defect. ink-soft at 38% is quiet and unmistakably a scrollbar. */
    scrollbar-width: thin;
    scrollbar-color: rgba(69,82,77,0.38) transparent;
  }
  /* SIGNED-OUT HOME — what the sink protects, and what it cost (CD round 4).
     It exists because the hero IS the argument: a MuslimReach unit carrying a
     SIBLING BRAND ("UMMAHCITY - One account for the Muslim internet") landing
     beside the H1 was the round-1 QA deduction, and a Buzz card there was the
     CD one. That protection is untouched here — the ad's page position does
     not move by a pixel (label at 777px, where round 1 put it).
     What the sink COST was the whole top-right quadrant: the rail's own top
     sat at 565px against a hero running 58-728, so 507px of the first viewport
     were blank at exactly the spot a stranger checks for "is this alive, is
     this safe". Sinking the ad is right; sinking the TRUST CARD with it was
     collateral. So the column splits: "Where you are" holds the top of the
     rail, only the group under it is sunk.
     466 puts the group's top at 754px — the exact top of the "The city, in
     order" heading, so the rail now literally starts level with the feed,
     which is what the Aug-1 comment SAID the 507 sink did and did not: it
     actually started the rail 189px above that line, inside the hero band.
     One constant holds at every width for the same reason it did before: the
     content column is a fixed max-width, so the hero never reflows
     (re-measured 1100/1280/1440/1680). */
  .right-rail.rail--below-hero {
    margin-top: 1.4rem;
    /* The aside stops being the sticky box and becomes the full-height track
       its sticky child rides, so align-self must FLIP to stretch: with
       flex-start the parent is only as tall as its content and the child would
       stop travelling a screen and a half down an 8000px page — the exact
       defect round 2 fixed. Stretch is safe HERE precisely because the sticky
       element is now the child, not this box. */
    align-self: stretch;
    position: static;
    max-height: none;
    overflow: visible;
  }
  /* CD round 5: the 466px sink was right about the AD and wrong about the RAIL.
     Measured before this change — trust card 80-287, ad group top 753 — so
     466px, over half the visible rail at 1440x900, was cream on first paint.
     The ad's page position does not move (754, level with "The city, in
     order"); what changes is that the offset is now expressed as the HEIGHT OF
     THE TRACK ABOVE IT rather than as a margin on the group itself, so a module
     can be dropped into that track without recomputing anything.
     673 = 753 (the protected ad position) - 80 (the rail's own top).
     min-height, NOT height: content that outgrows the track pushes the ad
     DOWN, which is the safe direction — up into the hero is the one thing the
     sink exists to prevent. */
  /* Final polish (Sep 9 2026): 673 was computed against a hero ending at 728, and
     the Sep 9 hero trims moved "Everyone here, newest first" up to 602 -- so the
     track was 151px too tall and the ad sat 150px BELOW the line it was pinned to,
     with ~300px of cream between the rules card and the unit. 522 = 602 - 80, the
     same derivation. A third module (people posting here, real accounts, newest
     post first -- the same query the /search strip uses) now fills the track; its
     height exceeds the minimum, which is the safe direction: the ad moves DOWN,
     never up into the hero. */
  .right-rail.rail--below-hero .rail-top { min-height: 522px; }
  .right-rail.rail--below-hero .rail-sticky {
    position: sticky; top: 14px;
    max-height: calc(100vh - 28px);
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: rgba(69,82,77,0.38) transparent;
  }
}
.rail-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.1rem; margin-bottom: 4px; }
/* "The five lines" — the second above-the-fold rail module (CD round 5). Same
   muted register as the rest of the rail on purpose: it is a contract, not a
   warning notice, so no red, no alert box, no icons. The numerals stay quiet
   and the rules carry the weight, hence bold leads on ordinary body colour. */
.rail-rule-list { list-style: decimal; padding-left: 1.1rem; display: grid; gap: 0.4rem; }
.rail-rule-list li { font-size: 0.84rem; line-height: 1.4; color: var(--ink-soft); }
.rail-rule-list li::marker { color: var(--green-deep); font-weight: 700; font-size: 0.78rem; }
.rail-rule-list strong { color: var(--ink); font-weight: 700; }
/* The "never remove" half is not decoration: a list of removals on its own
   reads as a threat, and the rule that protects unpopular speech is the one a
   stranger arriving from a ranked feed is actually weighing. */
.rail-rule-note { font-size: 0.82rem; line-height: 1.45; color: var(--ink-soft); margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--line); }
.rail-rules .rail-links { margin-top: 0.7rem; }
/* Rail kickers are real <h2>s now (heading navigation was dead for screen
   readers — QA, Aug 2), so the family has to be re-stated: the global
   h1,h2,h3 rule would otherwise switch these labels to Sora and change a
   render nobody asked to change. Same reason .section-mark restates it. */
.rail-kicker { font-family: 'Manrope', sans-serif; font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--green-deep); margin-bottom: 0.6rem; }
/* "Where you are" — the first rail module. It states the ecosystem
   relationship ABOVE the ad unit, which is what makes a sibling-brand house
   creative parseable to a stranger instead of an unplaceable name. */
.rail-context p { font-size: 0.88rem; color: var(--ink-soft); line-height: 1.5; }
.rail-context p + p { margin-top: 0.5rem; }
.rail-context .rail-links { margin-top: 0.7rem; font-size: 0.82rem; font-weight: 700; }
.rail-context .rail-links a { color: var(--green); }
.rail-kicker .rail-note { text-transform: none; letter-spacing: 0; font-weight: 600; color: var(--ink-soft); font-size: 0.72rem; }
/* "People posting here" -- the third guest-rail module (final polish, Sep 9 2026).
   Whole-row links (the .byline pattern: no resting underline, name underlines on
   hover), 44px rows, no follow buttons for a guest, no counts, no ring. */
.rail-people-list { list-style: none; display: grid; gap: 0.1rem; margin-top: -0.2rem; }
.rail-people-list a { display: flex; align-items: center; gap: 0.6rem; min-height: 44px; padding: 0.2rem 0; color: var(--ink); text-decoration: none; }
.rail-people-list a:hover .name { text-decoration: underline; }
.rail-people-list .avatar { width: 32px; height: 32px; border-radius: 50%; flex: none; background: var(--paper); }
.rail-people-who { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.rail-people-who .name { font-weight: 700; font-size: 0.9rem; }
.rail-people-who .handle { color: var(--ink-soft); font-size: 0.8rem; }
.rail-list { list-style: none; display: grid; gap: 0.55rem; }
.rail-list a { color: var(--ink); font-size: 0.9rem; font-weight: 600; line-height: 1.35; }
.rail-list a:hover { color: var(--green-deep); }
.rail-domain { display: block; font-size: 0.72rem; color: var(--ink-soft); }
.rail-more { display: inline-block; margin-top: 0.7rem; font-size: 0.82rem; font-weight: 700; }
.rail-attrib { font-size: 0.72rem; color: var(--ink-soft); margin-top: 4px; }
/* Ad slots stay hidden until the frame loads, so nothing flashes into place and
   there is no grey placeholder rectangle (the old inline background read as a
   broken box — CD, Aug 1). Whether the slot renders AT ALL is decided
   server-side by App\Support\AdSlots. The frame src is JS-set, so no-JS means
   no slot, consistently. */
/* THE AD BOX IS RESERVED FROM THE FIRST PAINT AND NEVER CHANGES SIZE (Tab,
   Sep 19 2026, r790, superseding the Aug 2 three-state contract and r789; the full
   contract is the header of public/js/ads.js). Whether the slot renders AT ALL
   is still decided server-side by App\Support\AdSlots. Four states, one box:
   base and is-loading paint nothing inside it; is-live paints the served unit
   and the caption; is-house paints our own inline creative and the caption.
   Before this the base state was display:none and the box arrived with the
   first IntersectionObserver callback, which on a short page was a 347px
   insertion with the footer already on screen (CLS 0.14 at 390, 0.28 to 0.31
   with the ad host blocked). Only a box that exists before the first paint
   fixes that; no script timing can.
   The caption is `visibility`, not `opacity`, so nothing reaches a screen
   reader before it is true. The frame is `opacity`, paint-only, so the unit
   inside measures 300x250 from byte one (ads.js note 2); in is-house it
   switches to `visibility`, which also takes the empty frame out of the
   accessibility tree. A blocked frame's error page therefore never paints. */
.mrx-ad-wrap { display: block; }
.mrx-ad-wrap:not(.is-live):not(.is-house) .mrx-caption { visibility: hidden; }
.mrx-ad-wrap:not(.is-live) .mrx-ad-frame { opacity: 0; }
.mrx-ad-wrap.is-house .mrx-ad-frame { visibility: hidden; }
/* The lazy loader observes a 1px anchor at the slot's position (it predates
   the reserved box, when the wrapper had no box to intersect; kept because the
   partial and ads.js key on it). Intersection starts the frame's navigation. */
.mrx-anchor { height: 1px; }
/* No opacity: it dropped the nudge below the contrast floor on the toast's
   own tinted surface. Colour it explicitly instead. */
.flash-nudge { display: block; font-size: 0.85rem; font-weight: 400; margin-top: 3px; color: var(--ink-soft); }
.flash-nudge a, .flash-nudge .linkish { color: var(--green-deep); font-weight: 600; }
.list-links { font-size: 0.82rem; margin-top: 4px; }
.list-links a { color: var(--ink-soft); font-weight: 600; }
.list-links a:hover { color: var(--green-deep); }
.mutual-line { font-size: 0.82rem; color: var(--ink-soft); margin-top: 4px; }
.mutual-line a { color: var(--ink); font-weight: 600; }
.follows-you {
  display: inline-block; font-size: 0.68rem; font-weight: 700;
  letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--green-deep); background: var(--gold-soft, #F3E4C2);
  border-radius: 5px; padding: 1px 6px; margin-left: 6px; vertical-align: 1px;
}
/* Inside the installed app the nudge is noise — it's already installed. */
@media (display-mode: standalone) {
  [data-standalone-hide] { display: none; }
}
/* One unit per page: on desktop the rail carries the ad, so the above-footer
   slot (the mobile/PWA unit) stands down. Below 1100px the rail is gone and
   this is the only slot — which is the point. */
@media (min-width: 1100px) {
  body.has-rail .foot-ad, body.has-rail .foot-ad-anchor { display: none; }
}
/* `.mrx-label` (the outer "ADVERTISEMENT" caption above the frame) is GONE by
   Tab's ruling of Aug 2 2026: "drop the outer label, keep the pill". The
   disclosure is the ad server's own in-unit chip plus the iframe title; the
   rule is deleted rather than left dead so nothing invites it back. */
/* The plain-words caption under the unit. Deliberately the same muted
   register as our other footnotes so it reads as OUR line about the ad, not
   as more ad copy. Width-matched to the 300px unit so it does not spill into
   the feed column's measure on mobile. */
.mrx-caption { max-width: 340px; margin: 6px auto 0; font-size: 0.72rem; line-height: 1.45; color: var(--ink-soft); text-align: center; }
.mrx-frame-box { position: relative; width: 300px; max-width: 100%; height: 250px; margin: 0 auto; border-radius: 10px; overflow: hidden; }
/* The house creative: our own Ummah One unit, inline markup only (no request,
   so no blocker can stop it), revealed by ads.js in the same 300x250 box when
   no network unit arrives (the UmmahPlaces ad-zone pattern, Tab, May 3 2026).
   Wording is the live /one copy verbatim; r777 freezes Ummah One copy and
   free-versus-paid is open (r776, r784), so it names no price and no trial.
   Ground is the pinned deep green carrying light ink, the header's own pairing
   (8.37:1); the CTA is the cream the header already uses for gold-on-green.
   The chip mirrors the network unit's "Ad" pill so both kinds of unit are
   announced the same way. No khatam and no gold surface (CD bans, Aug 10).
   Type sizes for the v2 body are set on the tokens further down. */
.mrx-house { display: none; position: absolute; inset: 0; z-index: 2; box-sizing: border-box;
  flex-direction: column; align-items: center; justify-content: center; gap: 10px;
  padding: 28px 22px 24px; text-align: center; text-decoration: none;
  background: var(--green-deep); color: #F4F6F1; }
.mrx-ad-wrap.is-house .mrx-house { display: flex; }
/* The dark theme recolours every `a` at (0,2,1) (`:root:not([data-theme="light"]) a`),
   which outranked the anchor rule above and painted the headline mid-green on
   deep green (measured Sep 19). Each text span carries its own colour, and the
   anchor rule is repeated at (0,3,1) so the link colour never reaches it. */
.mrx-ad-wrap .mrx-frame-box a.mrx-house { color: #F4F6F1; }
.mrx-house-kicker { font-size: 0.72rem; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; color: var(--gold-on-green); }
.mrx-house-head { font-size: 1.15rem; font-weight: 700; line-height: 1.25; letter-spacing: -0.01em; text-wrap: balance; max-width: 250px; color: #F4F6F1; }
.mrx-house-cta { display: inline-block; margin-top: 4px; padding: 9px 16px; border-radius: 999px; background: #F3E8CF; color: var(--green-deep); font-size: 0.85rem; font-weight: 700; }
.mrx-house:hover .mrx-house-cta, .mrx-house:focus-visible .mrx-house-cta { background: #FFFFFF; }
.mrx-house-chip { position: absolute; top: 6px; right: 6px; padding: 3px 8px; border-radius: 999px;
  background: rgba(250, 253, 251, 0.94); border: 1px solid #E2ECE6; color: #4A5B51;
  font-size: 0.68rem; font-weight: 800; letter-spacing: 0.02em; line-height: 1.3; }

/* ═════════ First-visit 95 — Part 1 (Aug 2 2026) ═════════ */

/* Card watermark — the locked component standard's signature interior texture
   (brand-system components.css .umg-card), adapted fine-line per the plan.
   Painted as a BACKGROUND, not ::before + overflow:hidden, because post cards
   contain dropdown menus that must overhang the card edge — negative-offset
   background-position gives the same corner-bleed crop without clipping
   children (border-radius clips the paint automatically). Repost cards set
   background-COLOR only, so the mark survives there. */
.post, .install-card {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23166534' stroke-opacity='.08' stroke-width='.25' stroke-linejoin='round' d='M12 1.8L14.1703 6.7605L19.2125 4.7875L17.2395 9.8297L22.2 12L17.2395 14.1703L19.2125 19.2125L14.1703 17.2395L12 22.2L9.8297 17.2395L4.7875 19.2125L6.7605 14.1703L1.8 12L6.7605 9.8297L4.7875 4.7875L9.8297 6.7605Z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  /* FULLY INSIDE the corner (Aug 10): the old right -44px bottom -44px crop
     cut the star at the card border, and at a glance the cut edges read as a
     rendering fault — the CD called it and a cold-visitor pass saw the same.
     A small complete star reads as a deliberate mark; a cropped one reads
     as a glitch. */
  background-position: right 10px bottom 10px;
  background-size: 64px 64px;
}

/* Motion (CD: the property had zero motion). Both effects are one-shot page
   entrances and sit behind prefers-reduced-motion: no-preference — on top of
   the global reduce kill-switch near the top of this file. The hero star
   SETTLES (-22.5° = half its 45° symmetry step, once, 700ms); it never spins
   (the brand book bans looping rotation of the mark). */
@media (prefers-reduced-motion: no-preference) {
  .hero h1 .khatam { animation: star-settle 0.7s ease-out 1 both; }
  /* fill-mode is BACKWARDS, never `both` — and this is load-bearing, not a
     style preference (found Aug 4 2026, reproduced on the live guest feed).
     `feed-in` animates opacity+transform, so while the animation APPLIES to
     an element that element holds a stacking context. With `both` the
     forwards fill applies FOREVER, so every post card kept one permanently
     and each card painted over the previous card's positioned children. The
     casualty was the `···` menu (`details.more .menu`, position:absolute
     z-index:10): on any post with another post after it, everything below
     the first menu row was covered by the NEXT card — and covered means
     un-tappable, elementFromPoint returned the next card's byline. That hid
     Report to a human, Mute, Block, Edit and Delete on the feed, i.e. the
     whole moderation path, for every post but the last one.
     `backwards` keeps the pre-start fill (no flash during the delay) and
     drops the fill after 150ms, so the stacking context is released. The
     to-state is opacity:1/transform:none — identical to the default — so
     losing the forwards fill changes NOTHING visually.
     It hid because it is inside prefers-reduced-motion: no-preference (a
     reduced-motion grader never sees it) and because a permalink page has no
     card after the post, so the menu there looks perfect. */
  .timeline .post { animation: feed-in 0.15s ease-out backwards; }
  .timeline .post:nth-child(1) { animation-delay: 40ms; }
  .timeline .post:nth-child(2) { animation-delay: 80ms; }
  .timeline .post:nth-child(3) { animation-delay: 120ms; }
  .timeline .post:nth-child(4) { animation-delay: 160ms; }
  .timeline .post:nth-child(5) { animation-delay: 200ms; }
  .timeline .post:nth-child(6) { animation-delay: 240ms; }
  .timeline .post:nth-child(7) { animation-delay: 280ms; }
  .timeline .post:nth-child(8) { animation-delay: 320ms; }
}
@keyframes star-settle { from { opacity: 0; transform: rotate(-22.5deg) scale(0.72); } to { opacity: 1; transform: none; } }
@keyframes feed-in { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; } }

/* CLS: image cells get intrinsic height before the file arrives. Inside the
   640px column the ratio height never reaches the legacy 420px cap. */
.post-images a { display: block; }
/* Sep 9 2026: a single photo is shown at its OWN shape. This forced every
   one into a 1.6:1 landscape box -- and the template was feeding it the
   480x480 square `_thumb`, so a portrait photo was cropped square, cropped
   again to 16:10, then upscaled. The site's real photos are portrait
   (884x1600, 736x1308) and the correct 1600px file was on disk all along.
   Height is capped so a tall photo cannot push the next post off the fold;
   at a 327px column an 884x1600 photo wants 592px and gets 560, so the crop
   is about 5% instead of most of the picture. */
/* Sep 9 2026, second pass. Two seats disagreed about this rule and both were right:
   one measured a portrait losing height to the crop, the other measured the first
   post at 829px -- a whole phone screen of one photo before a stranger has read a
   second post. A flat pixel cap cannot serve both, because the same 560px is a
   modest crop on a desktop column and the entire viewport on a phone.
   Capping against the VIEWPORT settles it: a photo never takes more than 60% of the
   screen, so two posts always fit, and on a wide screen it keeps the full 560. */
/* Final polish (Sep 9 2026), third pass: never upscale. `width: 100%` stretched a
   450x455 upload to the 571px desktop column -- 2.54x its pixels at DPR 2, visibly
   soft. The cap is the photo's OWN pixel width, which only the template knows: it
   prints `style="max-width:<natural>px"` beside the width/height attributes
   (partials/post.blade.php). `width: 100%` stays, on purpose -- a `width: auto`
   attempt was measured first and it shrank every tall photo instead (an 884x1600
   fell to 298px wide on desktop and 240px on a phone, because a max-height clamp
   recomputes an AUTO width to keep the ratio; an explicit width holds and
   object-fit crops, which is the behaviour the two Sep 9 passes above tuned). So:
   a photo wider than the column fills it and tall ones crop exactly as before; a
   photo narrower than the column renders at its own size. The radius moves onto
   the image because a narrower photo no longer fills the rounded grid box. */
.post-images.n1 img { margin-inline: auto; aspect-ratio: auto; height: auto; max-height: min(560px, 60vh);
  object-fit: cover; width: 100%; max-width: 100%; border-radius: 12px; }
.post-images.n2 img, .post-images.n3 img, .post-images.n4 img { aspect-ratio: 1 / 1; }

/* Hero CTA caption — preps the cross-domain hop before it happens */
.cta-note { margin-top: 0.55rem; font-size: 0.86rem; color: var(--ink-soft); max-width: 30rem; }

/* Standards strip under the hero */
.standards-strip {
  display: flex; align-items: center; gap: 0.6rem;
  margin-top: 1.35rem; padding: 0.7rem 0.9rem;
  background: rgba(200, 145, 46, 0.07);
  border: 1px solid var(--line); border-radius: 10px;
  font-size: 0.9rem; color: var(--ink-soft);
}
.standards-strip .khatam { color: var(--gold); flex: none; }
.standards-strip a { font-weight: 700; white-space: nowrap; }

/* Honest-stage line above the guest feed — quiet, dashed like the empty
   states (the register for "we're telling you something, not selling") */
.stage-note {
  margin: 0.8rem 0 0.2rem; padding: 0.75rem 1rem;
  font-size: 0.9rem; color: var(--ink-soft);
  background: var(--card); border: 1px dashed var(--line); border-radius: 10px;
}

/* One-line frame under a section mark */
.feed-frame { font-size: 0.8rem; color: var(--ink-soft); margin: 0.1rem 0 0.3rem; }

/* Gold-ring legend (renders once per page, under the first ring) */
.uo-legend { display: flex; align-items: center; gap: 0.55rem; margin: 0.55rem 0 0.1rem; font-size: 0.8rem; color: var(--ink-soft); }
.uo-legend .ring-dot { width: 14px; height: 14px; border-radius: 50%; background: var(--paper); box-shadow: 0 0 0 2px var(--gold), 0 0 0 4px rgba(200, 145, 46, 0.25); flex: none; margin: 2px 3px 2px 2px; }

/* The Buzz card used to be pushed 48vh down the rail so a third brand name
   would not land in a stranger's first screenful. That bought distance and
   paid for it in dead canvas: one ad card alone in ~430px of nothing (CD cold
   grade, Aug 2). The collision is now answered by EXPLAINING the family — the
   "Where you are" card above the ad, the caption under it, and this card's own
   "our sister news site" note — so the stack can close up to a normal gap. */
@media (min-width: 1100px) { .rail-buzz-late { margin-top: 12px; } }

/* /contact blocks */
.contact-block { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.1rem 1.2rem; margin-top: 1rem; }
.contact-block h2 { font-size: 1.02rem; margin: 0 0 0.35rem; }
.contact-block p { color: var(--ink-soft); font-size: 0.95rem; }

/* /app: the real icon at home-screen scale */
.app-icon-preview { display: flex; flex-direction: column; align-items: center; gap: 0.45rem; margin: 1.3rem 0 0.2rem; }
.app-icon-preview img { width: 88px; height: 88px; border-radius: 22.5%; box-shadow: 0 10px 26px rgba(19, 26, 32, 0.18); }
.app-icon-preview span { font-size: 0.78rem; color: var(--ink-soft); font-weight: 600; }

/* ---------------------------------------------------------------------------
   Link cards (card u1e4HgJA, Aug 2 2026). A posted article/video URL renders
   a real card that CLICKS OUT — never an embedded player. The thumbnail is
   our own re-hosted WebP, so no reader's IP reaches the source.
   Deliberately quieter than a post card: it is somebody else's content
   sitting inside ours, and it should not out-shout the words above it.
--------------------------------------------------------------------------- */
.link-card {
  display: flex; gap: 0.85rem; align-items: stretch;
  margin-top: 0.7rem;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--paper);
  overflow: hidden;
  text-decoration: none; color: inherit;
}
.link-card:hover { border-color: var(--gold); background: var(--card); }
.link-card-thumb {
  width: 116px; height: auto; min-height: 100%;
  max-height: 132px;
  object-fit: cover; display: block; flex: none;
  background: var(--card);
  aspect-ratio: 1.91 / 1;
}
.link-card-text {
  display: flex; flex-direction: column; justify-content: center; gap: 0.25rem;
  padding: 0.7rem 0.85rem 0.7rem 0; min-width: 0; flex: 1;
}
.link-card:not(:has(.link-card-thumb)) .link-card-text { padding-left: 0.85rem; }
.link-card-title {
  font-weight: 700; font-size: 0.95rem; line-height: 1.35; color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; text-wrap: pretty;
}
.link-card-meta { font-size: 0.79rem; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
@media (max-width: 430px) {
  .link-card-thumb { width: 92px; max-height: 108px; }
  .link-card-title { font-size: 0.9rem; }
}

/* ---------------------------------------------------------------------------
   Part 3 — first session + coming back (Aug 2 2026)
--------------------------------------------------------------------------- */

/* A button that reads as a link (invite-copy inside running text). */
.linkish {
  background: none; border: 0; padding: 0; font: inherit;
  color: var(--green-deep); font-weight: 600; text-decoration: underline;
  cursor: pointer;
}
.linkish:hover { color: var(--gold); }

/* Long-post clamp (P2.3, Aug 10 2026): visual only — the FULL text is always
   in the DOM (server-rendered; SEO and no-JS readers see everything). The
   Show-more control renders exactly when this class does (one server-side
   predicate drives both), and doubles as a real permalink link when JS is
   off. Permalink pages never clamp. */
.post .body.clamped {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 8;
  overflow: hidden;
}
.clamp-toggle { display: inline-block; margin-top: 4px; text-decoration: none; }

/* Inline handle try-out on the guest fold (P-A4): quiet, optional, never a
   gate. Hidden until JS arms it. */
.handle-try { margin: 10px 0 2px; max-width: 340px; }
.handle-try-row { display: flex; align-items: center; gap: 6px; border-bottom: 1px solid var(--line, rgba(20,83,45,0.25)); padding: 4px 2px; }
/* Sep 9 2026: was var(--gold) = rgb(200,145,46) on cream = 2.62:1, the only
   solid-background contrast failure on the page, and on the primary conversion
   path. #7A5312 measures 6.44:1. Dark theme overrides this below and already
   passed, so it is untouched. */
/* Sep 9 2026, corrected twice. The original var(--gold) measured 2.62:1 on cream, so
   I darkened it to #7A5312 for 6.44:1. That fixed LIGHT and broke DARK: the same
   colour on rgb(17,22,27) measures 2.66:1, which a review seat then caught. A single
   value cannot serve both grounds. Light gets the dark gold, dark gets a light one. */
.handle-at { color: #7A5312; font-weight: 700; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .handle-at { color: #D8B25A; }
}
[data-theme="dark"] .handle-at { color: #D8B25A; }
/* No outline:none here - the sitewide :focus-visible gold ring applies (QA
   fresh walk: killing it left keyboard users a hue-only 1px cue). */
#handle-try-input { flex: 1; border: 0; background: transparent; font: inherit; color: inherit; min-width: 0; }
#handle-try-input::placeholder { color: var(--ink-soft); opacity: 0.7; }
.handle-try-row:focus-within { border-bottom-color: var(--gold); }
.handle-try-note { margin: 5px 0 0; font-size: 0.85rem; min-height: 1.2em; }
.handle-try-note.ok { color: var(--green-deep); }
.handle-try-note.warn { color: var(--ink-soft); }

/* Guest invite strip: states a fact, sells nothing. */
.invite-strip {
  display: flex; align-items: center; gap: 0.7rem;
  margin: 1.1rem 0 -0.6rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--line); border-left: 3px solid var(--gold);
  border-radius: 10px; background: var(--card);
  font-size: 0.9rem; color: var(--ink-soft);
}
.invite-strip img { width: 34px; height: 34px; border-radius: 50%; flex: none; background: var(--paper); }
.invite-strip strong { color: var(--ink); }

/* Pre-selected inviter inside the starter pack. */
.starter .invite-pick {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-size: 0.93rem; font-weight: 600; color: var(--ink); cursor: pointer;
}
.starter .invite-pick input { width: 18px; height: 18px; accent-color: var(--green); flex: none; }
.starter .invite-pick img { width: 34px; height: 34px; border-radius: 50%; background: var(--paper); }

/* First-post note under the composer. Two paragraphs since the salaam welcome
   landed (card nBkhb4H8): where a post goes, then the invitation to write a
   hello. Same muted footnote register either way - it is a note above the
   composer, never a card and never a banner. */
.first-post-note {
  font-size: 0.85rem; color: var(--ink-soft);
  margin-bottom: 0.55rem; padding-left: 0.6rem;
  /* Light: a soft cream hairline on white. In dark this token becomes the
     #3A3324 fill, which against the dark card (#1A2127) measures 1.30:1 -
     no border at all. Overridden in the dark block below. */
  border-left: 2px solid var(--gold-soft);
}
.first-post-note p { margin: 0; }
.first-post-note p + p { margin-top: 0.4rem; }

/* The "Just this link" caveat under the composer's audience picker. Same muted
   footnote register as the other composer hints - it is a plain statement of
   what the option does, not a warning banner. */
.vis-note {
  font-size: 0.78rem; line-height: 1.45; color: var(--ink-soft);
  margin-top: 0.45rem; max-width: 46ch;
}

.empty .invite-line { margin-top: 0.9rem; font-size: 0.88rem; }

/* New-posts pill: quiet, one line, dismissible only by acting on it. */
/* display:block below BEATS the UA's [hidden] default at author-level -
   without this guard an EMPTY bordered bar rendered on every authed load
   (QA fresh walk, -4). The hidden state must always win. */
.new-posts-pill[hidden] { display: none; }
.new-posts-pill {
  display: block; text-align: center;
  margin: 0.7rem 0 -0.2rem;
  padding: 0.5rem 1rem;
  border: 1px solid rgba(22,101,52,0.28); border-radius: 999px;
  background: #EFF6F1; color: var(--green-deep);
  font-size: 0.86rem; font-weight: 700; text-decoration: none;
}
.new-posts-pill:hover { background: var(--gold-soft); border-color: var(--gold); }

/* --- Composer live counter (Wajid item 2) --- */
.composer .count.near-limit { color: var(--danger); font-weight: 700; }
/* Two chips share the reply row; only the first takes the auto push. */
.composer .count + .count.js-charcount { margin-left: 0.6rem; }
@media (max-width: 480px) {
  /* The static "2,000 max" stays decoration on a phone, but once you're
     actually typing the live count is information — show it. */
  .composer .count.js-charcount.active { display: inline; }
}

/* --- Image lightbox (Wajid item 8) --- */
.lightbox { position: fixed; inset: 0; z-index: 200; background: rgba(12,16,20,0.92); display: flex; align-items: center; justify-content: center; }
.lightbox[hidden] { display: none; }
.lb-img { max-width: 94vw; max-height: 92vh; object-fit: contain; border-radius: 6px; cursor: zoom-in; }
.lightbox.zoomed .lb-img { max-width: none; max-height: none; width: auto; height: auto; cursor: zoom-out; }
.lightbox.zoomed { overflow: auto; align-items: flex-start; justify-content: flex-start; }
.lb-close { position: fixed; top: 10px; right: 12px; width: 44px; height: 44px; border: 0; border-radius: 50%; background: rgba(255,255,255,0.14); color: #fff; font-size: 1.6rem; line-height: 1; cursor: pointer; z-index: 202; }
.lb-nav { position: fixed; top: 50%; transform: translateY(-50%); width: 46px; height: 64px; border: 0; border-radius: 10px; background: rgba(255,255,255,0.12); color: #fff; font-size: 2rem; line-height: 1; cursor: pointer; z-index: 202; }
.lb-prev { left: 8px; }
.lb-next { right: 8px; }
.lb-close:hover, .lb-nav:hover { background: rgba(255,255,255,0.28); }
.lightbox.single .lb-nav { display: none; }
.lb-count { position: fixed; bottom: 14px; left: 50%; transform: translateX(-50%); color: #fff; font-size: 0.9rem; font-variant-numeric: tabular-nums; z-index: 202; text-shadow: 0 1px 6px rgba(0,0,0,0.9); }
/* Caption (P2.1): the honest alt string the feed computed, quiet under the
   photo — one notch above the counter so both read anywhere. The pill
   background is the DESIGNED legibility guarantee (CD re-score, Aug 10): a
   tall photo reaches under the fixed caption on desktop, so readability
   must never depend on what the photo happens to contain. */
.lb-caption { position: fixed; bottom: 34px; left: 50%; transform: translateX(-50%); max-width: 90vw; text-align: center; color: rgba(255,255,255,0.85); font-size: 0.85rem; margin: 0; z-index: 202; pointer-events: none; background: rgba(12,16,20,0.72); padding: 3px 12px; border-radius: 999px; }

/* ---------- dark mode (system-setting respect — Tab ruling V0XcmFd8, Aug 3;
   the manual Settings toggle is deliberately post-wave). Architecture: the
   FIVE surface vars flip; --green/--green-deep are NOT remapped because they
   are load-bearing BACKGROUNDS (site head, .btn) that stay dark green in both
   themes — every place that used the greens as TEXT gets a targeted override
   to a brighter accent instead, or it would sit at ~3:1 on the dark paper.
   Header, lightbox and photo-overlay rules are untouched: already dark. */

/* ---- Live Circles (card ejxBNd7J). Everything rides theme vars so the
   dark block needs no overrides; the dot is static on purpose (motion is
   one-shot-only per brand book — no looping pulse). ---- */
.live-bar { margin: 0.9rem 0; display: flex; flex-direction: column; gap: 0.5rem; }
.live-bar-row { display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 0.6rem 0.9rem; text-decoration: none; color: var(--ink); }
.live-bar-row:hover .live-bar-title { text-decoration: underline; }
.live-bar-label { font-weight: 800; font-size: 0.8rem; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--danger); }
.live-bar-row--upcoming .live-bar-label { color: var(--ink-soft); }
.live-bar-title { font-weight: 700; font-size: 0.95rem; min-width: 0; overflow-wrap: anywhere; }
.live-bar-meta { font-size: 0.85rem; color: var(--ink-soft); margin-left: auto; }
.live-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--danger); flex: none; }
.live-page { max-width: var(--col); }
.live-kicker { font-size: 0.85rem; color: var(--ink-soft); margin: 0.4rem 0 0.8rem; }
.live-title { font-size: 1.5rem; margin: 0 0 0.4rem; overflow-wrap: anywhere; }
.live-byline { font-size: 0.95rem; color: var(--ink-soft); margin: 0 0 0.7rem; }
.live-desc { margin: 0 0 1rem; white-space: pre-line; overflow-wrap: anywhere; }
.live-state { background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1rem 1.1rem; margin: 1rem 0; }
.live-state--on { border-color: var(--danger); }
.live-now-line { font-weight: 700; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.live-join { display: inline-block; margin-top: 0.8rem; }
.live-note { font-size: 0.85rem; color: var(--ink-soft); margin-top: 0.6rem; }
.live-optional { font-weight: 400; color: var(--ink-soft); }
.live-hostbox { background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1rem 1.1rem; margin: 1rem 0; }
.live-hostbox h2 { font-size: 1rem; margin: 0 0 0.6rem; }
.live-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.live-facts { font-size: 0.9rem; color: var(--ink-soft); margin: 1rem 0; }
.live-error { color: var(--danger); font-size: 0.9rem; font-weight: 600; margin: 0.4rem 0; }
.live-report { margin: 1.2rem 0; font-size: 0.92rem; }
.live-report summary { cursor: pointer; color: var(--ink-soft); }
.live-report form { margin-top: 0.6rem; display: flex; flex-direction: column; gap: 0.5rem; align-items: flex-start; }
.live-report textarea, .live-form input, .live-form textarea, .live-form select {
  width: 100%; border: 1px solid var(--line); border-radius: 10px; padding: 0.55rem 0.7rem;
  font: inherit; background: var(--card); color: var(--ink); }
.live-form { display: flex; flex-direction: column; gap: 0.45rem; align-items: flex-start; margin-top: 1rem; }
.live-form label { font-weight: 700; font-size: 0.9rem; margin-top: 0.5rem; }
.live-form .btn { margin-top: 0.8rem; }
/* The stated hosting bar (App\Support\LiveHostEligibility). Every token used
   here is already theme-aware, so the dark block at the end of this file
   needs no addition — check that before adding a light-tint literal. */
/* --r is not a token in this file (it is --radius), so this box was rendering
   square-cornered against a rounded app. Found while verifying the disclosure. */
.live-gate { background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 0.9rem 1rem; margin: 1rem 0; }
.live-gate h2 { font-size: 1.05rem; margin: 0 0 0.5rem; }
.live-gate p { margin: 0 0 0.6rem; }
.live-gate-list { list-style: none; padding: 0; margin: 0.2rem 0 0.9rem; }
.live-gate-list li { display: grid; grid-template-columns: 1.1rem 1fr;
  gap: 0.05rem 0.5rem; padding: 0.5rem 0; border-top: 1px solid var(--line); }
.live-gate-list li:first-child { border-top: 0; padding-top: 0.2rem; }
.live-gate-mark { grid-row: span 2; font-weight: 800; line-height: 1.5; }
.live-gate-need { font-weight: 700; font-size: 0.95rem; overflow-wrap: anywhere; }
.live-gate-have { font-size: 0.87rem; color: var(--ink-soft); overflow-wrap: anywhere; }
.live-gate-list li.is-met .live-gate-need { font-weight: 400; color: var(--ink-soft); }
.live-bar-row--host .live-bar-title { font-weight: 600; font-size: 0.9rem; color: var(--ink-soft); }
/* THE DISCLOSURE (2-expert naming conferral, Aug 4 2026). The word "Circle"
   carries a decade of small/chosen/semi-private connotation, but a live
   session is publicly listenable by anyone with the link, with listeners the
   host cannot see. That gap is a member forming a false belief about who can
   hear them, so the fact is stated IN THE ROOM - and deliberately NOT in the
   quiet --ink-soft footnote register every other .live-note uses: full --ink
   on the --gold-soft notice fill, which is that token's documented job (a soft
   gold FILL sat behind dark text) and measures 14.4:1 light / 9.8:1 dark.
   Theme vars only, so the dark block below needs no override. */
.live-warn { background: var(--gold-soft); border: 1px solid var(--gold);
  border-radius: var(--radius); padding: 0.8rem 1rem; margin: 0 0 1rem;
  color: var(--ink); font-size: 0.95rem; }
.live-warn p + p { margin-top: 0.45rem; }
.live-warn-lead { font-weight: 800; }
/* The last look before the mic opens, restated inside the host box's note. A
   warning sitting inside a soft note has to climb back to full ink, or the
   softer sentences around it outrank it. */
.live-warn-inline { color: var(--ink); }
/* .live-form is a flex column with align-items:flex-start, so a block child
   would otherwise shrink to its own text width. */
.live-form .live-warn { align-self: stretch; margin: 1rem 0 0; }
/* The two ways out of the create form sit on one row and wrap on a phone.
   Same reason as .live-warn above: a block child of this flex column would
   shrink to its text width. */
.live-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; align-self: stretch; }
.live-actions .btn { margin-top: 0.8rem; }
/* The host row carries a sentence under its label now, so it stacks instead
   of pushing the meta to the right edge of a 390px row. */
.live-bar-row--host { flex-direction: column; align-items: flex-start; gap: 0.15rem; }
.live-bar-row--host .live-bar-meta { margin-left: 0; }


/* ---- Theme choice (card V0XcmFd8, Aug 6 2026) ------------------------------
   Dark mode was entirely inside ONE media block, so giving members a real
   choice needed no change to a single base rule. Two scopes now:
     - the media block below still follows the device, but stands down when a
       member has explicitly chosen light;
     - the duplicate after it applies dark when a member has explicitly chosen
       it, whatever the device says.
   "System" is the absence of the attribute, which is why it is the default.
   If you add a dark rule, add it in BOTH places or the toggle half-works. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --ink: #E7E4DD;
    /* 🔴 DO NOT redefine --green or --green-deep here. I did, and it broke
       every authenticated page in dark: `.site-head` is
       `background: var(--green-deep); color: #F4F6F1` and `.btn` is
       `background: var(--green); color: #fff`. Lightening them put near-white
       text on light green: the wordmark measured 1.80:1, the nav 1.48:1, the
       primary button label 2.50:1, and a live sweep found 29 text failures on
       the home page alone. That is defect #7 committed inside a fix for
       defect #7.

       --green and --green-deep are GROUND tokens: surfaces that carry light
       text. They must stay dark in both themes, exactly as the --gold-on-green
       note above says.

       Green used as FOREGROUND (a label, a ring) is a different colour and now
       has its own token, --green-ink, below. */
    --green-ink: #5CB585;
    --ink-soft: #A0ABA4;
    --paper: #11161B;
    --card: #1A2127;
    --line: #2C353C;
    --gold: #D9A94F;
    /* SURFACE token only - see the :root note. --gold-on-green is deliberately
       absent here: the header it sits on is green in both themes, so its text
       colour must not follow the theme. */
    --gold-soft: #3A3324;
    --danger: #D4756A;
    /* Ring decoration only — see the :root note. */
    --sand: #D9A94F;
    --sand-spent: #3A3F44;
  }/* --gold-soft is a fill, so as a visible 2px rule against the dark card it
     reads 1.30:1 - i.e. no border. This is the same root cause as the two text
     usages, in its third shape. 3.62:1 clears the 3:1 bar for a non-text mark
     while keeping the quiet footnote register the rule is supposed to have
     (the app's --gold #D9A94F would be 7.54:1 here - correct but far too loud
     for a hairline). */

  
  :root:not([data-theme="light"]) .first-post-note { border-left-color: #8A7448; }
  :root:not([data-theme="light"]) a { color: #5CB585; }
  :root:not([data-theme="light"]) .btn.secondary { color: #5CB585; box-shadow: inset 0 0 0 2px #3E8A63; }
  :root:not([data-theme="light"]) .btn.secondary:hover { background: rgba(92, 181, 133, 0.12); }
  :root:not([data-theme="light"]) .btn.quiet:hover { background: #232B33; }
  :root:not([data-theme="light"]) .tabs a.active { color: #5CB585; }
  :root:not([data-theme="light"]) .post .vis-chip,
  :root:not([data-theme="light"]) .profile-head .vis-chip { color: #7CC79F; background: rgba(92, 181, 133, 0.14); }
  :root:not([data-theme="light"]) .action-btn:hover { color: var(--ink); }
  :root:not([data-theme="light"]) .action-btn.reposted { color: #5CB585; }
  :root:not([data-theme="light"]) .repost-note .khatam { color: #5CB585; }
  :root:not([data-theme="light"]) .flash.ok { background: #1C2A21; color: #7CC79F; border-color: rgba(92, 181, 133, 0.35); }
  :root:not([data-theme="light"]) .flash.err { background: #2E1D1A; border-color: rgba(212, 117, 106, 0.4); }
  :root:not([data-theme="light"]) details.more summary:hover { background: #232B33; }
  :root:not([data-theme="light"]) .post:has(> .repost-note) { background-color: #161B21; }
  :root:not([data-theme="light"]) .new-posts-pill { background: #1C2A21; color: #7CC79F; border-color: rgba(92, 181, 133, 0.35); }
  :root:not([data-theme="light"]) .new-posts-pill:hover { background: var(--gold-soft); border-color: var(--gold); }
  :root:not([data-theme="light"]) .follows-you { color: var(--gold); }
  :root:not([data-theme="light"]) .attach-label:hover { color: var(--gold); }
  :root:not([data-theme="light"]) .report-reasons button:hover { color: var(--gold); }
  :root:not([data-theme="light"]) .rail-rule-list li::marker,
  :root:not([data-theme="light"]) .rail-kicker { color: #5CB585; }
  :root:not([data-theme="light"]) .rail-list a:hover,
  :root:not([data-theme="light"]) .rail-context .rail-links a,
  :root:not([data-theme="light"]) .list-links a:hover,
  :root:not([data-theme="light"]) .flash-nudge a,
  :root:not([data-theme="light"]) .flash-nudge .linkish,
  :root:not([data-theme="light"]) .linkish { color: #5CB585; }
  :root:not([data-theme="light"]) ul.tenets .khatam { color: #5CB585; }/* Tessellation reads slightly heavier against dark paper at the same
     opacity (small-area fine lines gain apparent contrast on a dark ground) —
     nudged down within the locked 6-8% dark range. #khatam-tess itself needs
     no override: --gold is already redefined above. */

  
  :root:not([data-theme="light"]) .khatam-tess-rect { opacity: 0.065; }
}


/* Explicit dark choice (see the note above). */
  :root[data-theme="dark"] {
    color-scheme: dark;
    --ink: #E7E4DD;
    /* 🔴 DO NOT redefine --green or --green-deep here. I did, and it broke
       every authenticated page in dark: `.site-head` is
       `background: var(--green-deep); color: #F4F6F1` and `.btn` is
       `background: var(--green); color: #fff`. Lightening them put near-white
       text on light green: the wordmark measured 1.80:1, the nav 1.48:1, the
       primary button label 2.50:1, and a live sweep found 29 text failures on
       the home page alone. That is defect #7 committed inside a fix for
       defect #7.

       --green and --green-deep are GROUND tokens: surfaces that carry light
       text. They must stay dark in both themes, exactly as the --gold-on-green
       note above says.

       Green used as FOREGROUND (a label, a ring) is a different colour and now
       has its own token, --green-ink, below. */
    --green-ink: #5CB585;
    --ink-soft: #A0ABA4;
    --paper: #11161B;
    --card: #1A2127;
    --line: #2C353C;
    --gold: #D9A94F;
    /* SURFACE token only - see the :root note. --gold-on-green is deliberately
       absent here: the header it sits on is green in both themes, so its text
       colour must not follow the theme. */
    --gold-soft: #3A3324;
    --danger: #D4756A;
    /* Ring decoration only — see the :root note. */
    --sand: #D9A94F;
    --sand-spent: #3A3F44;
  }/* --gold-soft is a fill, so as a visible 2px rule against the dark card it
     reads 1.30:1 - i.e. no border. This is the same root cause as the two text
     usages, in its third shape. 3.62:1 clears the 3:1 bar for a non-text mark
     while keeping the quiet footnote register the rule is supposed to have
     (the app's --gold #D9A94F would be 7.54:1 here - correct but far too loud
     for a hairline). */

  
  :root[data-theme="dark"] .first-post-note { border-left-color: #8A7448; }
  :root[data-theme="dark"] a { color: #5CB585; }
  :root[data-theme="dark"] .btn.secondary { color: #5CB585; box-shadow: inset 0 0 0 2px #3E8A63; }
  :root[data-theme="dark"] .btn.secondary:hover { background: rgba(92, 181, 133, 0.12); }
  :root[data-theme="dark"] .btn.quiet:hover { background: #232B33; }
  :root[data-theme="dark"] .tabs a.active { color: #5CB585; }
  :root[data-theme="dark"] .post .vis-chip,
  :root[data-theme="dark"] .profile-head .vis-chip { color: #7CC79F; background: rgba(92, 181, 133, 0.14); }
  :root[data-theme="dark"] .action-btn:hover { color: var(--ink); }
  :root[data-theme="dark"] .action-btn.reposted { color: #5CB585; }
  :root[data-theme="dark"] .repost-note .khatam { color: #5CB585; }
  :root[data-theme="dark"] .flash.ok { background: #1C2A21; color: #7CC79F; border-color: rgba(92, 181, 133, 0.35); }
  :root[data-theme="dark"] .flash.err { background: #2E1D1A; border-color: rgba(212, 117, 106, 0.4); }
  :root[data-theme="dark"] details.more summary:hover { background: #232B33; }
  :root[data-theme="dark"] .post:has(> .repost-note) { background-color: #161B21; }
  :root[data-theme="dark"] .new-posts-pill { background: #1C2A21; color: #7CC79F; border-color: rgba(92, 181, 133, 0.35); }
  :root[data-theme="dark"] .new-posts-pill:hover { background: var(--gold-soft); border-color: var(--gold); }
  :root[data-theme="dark"] .follows-you { color: var(--gold); }
  :root[data-theme="dark"] .attach-label:hover { color: var(--gold); }
  :root[data-theme="dark"] .report-reasons button:hover { color: var(--gold); }
  :root[data-theme="dark"] .rail-rule-list li::marker,
  :root[data-theme="dark"] .rail-kicker { color: #5CB585; }
  :root[data-theme="dark"] .rail-list a:hover,
  :root[data-theme="dark"] .rail-context .rail-links a,
  :root[data-theme="dark"] .list-links a:hover,
  :root[data-theme="dark"] .flash-nudge a,
  :root[data-theme="dark"] .flash-nudge .linkish,
  :root[data-theme="dark"] .linkish { color: #5CB585; }
  :root[data-theme="dark"] ul.tenets .khatam { color: #5CB585; }/* Tessellation reads slightly heavier against dark paper at the same
     opacity (small-area fine lines gain apparent contrast on a dark ground) —
     nudged down within the locked 6-8% dark range. #khatam-tess itself needs
     no override: --gold is already redefined above. */

  
  :root[data-theme="dark"] .khatam-tess-rect { opacity: 0.065; }


/* Theme choice control (card V0XcmFd8). Radio rows with real tap targets. */
.theme-choice { display: flex; flex-direction: column; gap: 0.15rem; }
.theme-choice label { display: flex; align-items: center; gap: 0.55rem;
  padding: 0.55rem 0.1rem; cursor: pointer; font-size: 0.95rem; }
.theme-choice input { width: 1.05rem; height: 1.05rem; accent-color: var(--green); }

/* Straight to the people (Aug 6 2026, creative review). Mobile-only: on a
   phone the guest home is one column, so the hero + tenets + standards strip
   all sit between a visitor and the first human being. Desktop keeps the rail
   beside the feed and never had the problem, so this stays hidden there. */
.hero-jump { display: none; }
.hero-jump a { color: var(--green); font-weight: 600; text-decoration: none; }
.hero-jump a:hover { text-decoration: underline; }
@media (max-width: 900px) {
  .hero-jump { display: block; margin: 0.5rem 0 0; }
}
/* Sep 20 2026: this used to be unscoped -- `html { scroll-behavior: smooth }`
   -- which animated EVERY scroll on the site, not just the one in-page hash
   jump that wants it (the guest hero's "See what people are posting" link,
   below). That included the notifications bell's history.back() bfcache
   restore, and it made any script that scrolls and immediately reads the
   resting scroll position (QA/review tooling included) read a mid-animation
   value instead. Moved to /js/hash-jump.js, which finds a real same-page
   `<a href="#id">` and animates only that jump. Nothing here needs the
   reduced-motion media query any more; the script reads the same preference
   itself before it animates anything. */

/* Separator between an action's words and its count. Without it "Sign in to
   appreciate 2" reads as a single phrase (creative score, Aug 6 2026). */
.action-btn .sep { color: var(--ink-soft); margin: 0 0.1rem; }

/* ================= Aug 10 2026 — first-visit + member design pass =========
   CD conferral (both seats), Tab-approved plan Phase 1.5-7 + Phase 2 S-set.
   Constraints honored: AAA contrast untouched (shadows/ornament only, no text
   colour changes), both themes via existing tokens, reduced-motion respected
   by adding NO new animation. */

/* Crash-recovery note (QA Aug 10: reusing .count inherited the phone-width
   display:none meant the label never showed on the devices it exists for). */
.draft-note { margin: 0.35rem 0 0; font-size: 0.85rem; color: var(--ink-soft); }

/* The one-token coherence lift: every card gets quiet elevation. */
:root { --shadow-card: 0 1px 3px rgba(19,26,32,0.05), 0 1px 2px rgba(19,26,32,0.04); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --shadow-card: 0 1px 3px rgba(0,0,0,0.32), 0 1px 2px rgba(0,0,0,0.24); }
}
:root[data-theme="dark"] { --shadow-card: 0 1px 3px rgba(0,0,0,0.32), 0 1px 2px rgba(0,0,0,0.24); }
.post, .composer, .stage-note, .standards-strip, .invite-strip { box-shadow: var(--shadow-card); }

/* Tactility: a quiet hover lift on cards, a press state on buttons. Motion is
   a transform only — no new keyframes, nothing loops. */
@media (prefers-reduced-motion: no-preference) {
  .post { transition: transform 0.12s ease, box-shadow 0.12s ease; }
  .post:hover { transform: translateY(-1px); box-shadow: 0 3px 8px rgba(19,26,32,0.08), 0 1px 2px rgba(19,26,32,0.05); }
  .btn:active { transform: scale(0.97); }
}

/* Masthead eyebrow above the H1 — the newspaper flag. Gold as a RULE clears
   the 3:1 non-text bar (header border precedent); the text is ink-soft. */
.hero-eyebrow {
  display: inline-flex; align-items: center; gap: 0.55rem;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-soft);
}
.hero-eyebrow::before { content: ""; width: 34px; height: 2px; background: var(--gold); }

/* One oversized khatam, bled off the hero's top-right — a single confident
   mark, never tiling. Colour rides the same inheritance trick as the
   tessellation (#khatam-tess): gold in both themes, opacity per theme. */
.hero { position: relative; overflow: hidden; } /* the mark bleeds by being
  CLIPPED at the hero's edges — overflow:visible let it widen the page to 454px
  at a 390px viewport (QA finding 120, measured horizontal scroll) */
.hero-mark {
  position: absolute; top: -3.4rem; right: -4.2rem; width: 520px; height: 520px;
  color: var(--gold); opacity: 0.07; pointer-events: none; z-index: 0;
}
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .hero-mark { opacity: 0.055; } }
:root[data-theme="dark"] .hero-mark { opacity: 0.055; }
.hero > *:not(.hero-mark) { position: relative; z-index: 1; }
@media (max-width: 480px) { .hero-mark { width: 340px; height: 340px; top: -2.2rem; right: -5rem; } }

/* Bigger H1, more air before the room starts. */
.hero h1 { font-size: clamp(2.1rem, 7.5vw, 3.4rem); line-height: 1.05; }
.hero { padding-bottom: 2.9rem; }

/* The first post reads chosen, not first-in-a-list. */
.post--first { box-shadow: 0 4px 14px rgba(19,26,32,0.10), 0 1px 3px rgba(19,26,32,0.06); }
.post--first .post-seal {
  position: absolute; top: 0.65rem; right: 0.65rem; width: 1.05rem; height: 1.05rem;
  color: var(--gold); opacity: 0.85;
}

/* Guest hero secondary row: the jump link and the app link, side by side. */
.hero-links { margin-top: 0.8rem; display: flex; gap: 1.1rem; flex-wrap: wrap; font-size: 0.95rem; }

/* Honest numbers under the CTA — live-computed, quiet, never a counter. */
.hero-honest { margin-top: 0.6rem; font-size: 0.9rem; color: var(--ink-soft); max-width: 32rem; }

/* Tenets lose their star bullets (the mark now appears ONCE in the fold). */
.hero ul.tenets li { padding-left: 0; }

/* Feed recency line — an honest instrument, styled like the feed-frame note. */
.feed-recency { font-size: 0.85rem; color: var(--ink-soft); margin: 0.2rem 0 0.8rem; }

/* Gold-ring legend, rendered once as a feed footnote instead of inside every
   card (it is a hover tooltip's text and was printing as body copy on touch). */
.uo-legend-footnote { font-size: 0.83rem; color: var(--ink-soft); margin: 1rem 0 0; }

/* Custom form controls: appearance:none + our own chrome. CSP-safe, no JS. */
.composer select, .settings-form select {
  appearance: none; -webkit-appearance: none;
  background-color: var(--card); color: var(--ink);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 0.35rem 1.6rem 0.35rem 0.6rem; font: inherit; font-size: 0.9rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%239aa59e' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 0.55rem center; background-size: 10px 6px;
}
.settings-form input[type="checkbox"] {
  flex-shrink: 0; /* CD round 2: inside a flex label, appearance:none let the
     text crush the box to a 5px sliver on the two most-used privacy toggles */
  appearance: none; -webkit-appearance: none;
  width: 1.15rem; height: 1.15rem;
  /* Public-ready round, Sep 19 2026: was `var(--line)`, measured 1.30:1
     against --card in BOTH themes (the border and the card token move
     together, so the ratio never changes with the theme) -- well under the
     3:1 UI-component bar. --ink-soft is an EXISTING token, not an eighth
     colour: 8.18:1 light / 6.86:1 dark against --card. */
  border: 1.5px solid var(--ink-soft); border-radius: 5px;
  background: var(--card); vertical-align: -0.22rem; position: relative; cursor: pointer;
}
.settings-form input[type="checkbox"]:checked { background: var(--green); border-color: var(--green); }
.settings-form input[type="checkbox"]:checked::before {
  content: ""; position: absolute; inset: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 10'%3E%3Cpath d='M1.5 5.5l3 3 6-7' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/70% no-repeat;
}
.settings-form input[type="checkbox"]:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* Composer invitation: the helper copy recedes once writing starts. */
@media (prefers-reduced-motion: no-preference) { .composer .first-post-note { transition: opacity 0.2s ease; } }
.composer:focus-within .first-post-note { opacity: 0.4; }

/* The honest-stage note drops its warning-label register: solid hairline and
   the masthead's gold rule instead of a dashed box. Same words, confident. */
.stage-note { border-style: solid; border-top: 2px solid var(--gold); }

/* The account gear, drawn instead of typed (the "⚙" glyph sat an inch from a
   hand-drawn SVG — two icon systems in one nav row). */
.acct summary .gear-icon { width: 1.15em; height: 1.15em; display: block; color: #D9E2DA; }

/* Theme radios, same chrome as the checkboxes (QA finding 121; selectors
   corrected in CD round 2 — the scripted edit had paired an unqualified
   selector with a :checked one in the same list, a landmine the moment
   .settings-form grows a radio). Both containers, every state explicit. */
.settings-form input[type="radio"],
.settings-card input[type="radio"] {
  appearance: none; -webkit-appearance: none; flex-shrink: 0;
  width: 1.05rem; height: 1.05rem;
  /* Same fix, same measurement, as the checkbox above: var(--line) was
     1.30:1 against --card in both themes. */
  border: 1.5px solid var(--ink-soft); border-radius: 50%;
  background: var(--card); vertical-align: -0.18rem; position: relative; cursor: pointer;
}
.settings-form input[type="radio"]:checked,
.settings-card input[type="radio"]:checked { border-color: var(--green); }
.settings-form input[type="radio"]:checked::before,
.settings-card input[type="radio"]:checked::before {
  content: ""; position: absolute; inset: 3px; border-radius: 50%; background: var(--green);
}
.settings-form input[type="radio"]:focus-visible,
.settings-card input[type="radio"]:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* Nav/action line icons (Aug 10, CD round 2): the khatam retires to identity
   moments and Appreciate; bell/magnifier/camera/mic do the utility work. */
.nav-icon { width: 1.15em; height: 1.15em; display: inline-block; vertical-align: -0.2em; }
.nav-icon--ink { color: var(--ink-soft); }

/* Single-row member header at phone width: with Search as an icon the row
   fits 390px (wordmark + bell + magnifier + clamped @handle + gear). */
@media (max-width: 640px) {
  .head-nav.authed { flex-wrap: nowrap; width: auto; }
  .site-head .inner { flex-wrap: nowrap; }
  .head-nav.authed .you { max-width: 92px; }
}

/* The empty-state Circle host row: a slim strip, not a full card — the
   composer is the point of the page (CD round 1, item 1). The door stays
   (the Aug 5 discoverability lesson holds); it just stops shouting. */
.live-bar--quiet .live-bar-row--host { padding: 0.45rem 0.8rem; }
.live-bar--quiet .live-bar-title { font-size: 0.88rem; font-weight: 600; }
.live-bar--quiet .live-bar-meta { font-size: 0.78rem; }
@media (max-width: 480px) { .live-bar--quiet .live-bar-meta { display: none; } }

/* Ads are subordinate to content, everywhere (CD round 1, item 4 + Tab's
   2-expert placement ruling): no content-card elevation, a hairline frame,
   quieter caption, clear separation from what people came for. */
.mrx-ad-wrap { box-shadow: none; border-top: 1px solid var(--line); padding-top: 0.9rem; }
.mrx-frame-box { box-shadow: none; }
.mrx-caption { font-size: 0.72rem; color: var(--ink-soft); opacity: 0.85; }

/* Starter pack, lightened (CD round 2 item 4): it self-retires at 3 follows
   and is genuine first-day onboarding, so it stays — but it no longer
   outweighs the composer. No tint, no elevation, compact. */
.starter { background: transparent; box-shadow: none; border: 1px solid var(--line); padding: 0.7rem 0.9rem; }
.starter h2 { font-size: 0.95rem; margin: 0 0 0.3rem; }
.starter p { font-size: 0.85rem; margin: 0 0 0.5rem; }
/* (First written against ".starterpack" — a class that does not exist; the
   wrapper is ".starter". Caught before deploy by checking the real markup,
   which is the exact lesson of QA finding 121.) */

/* Live Circle host page (Aug 10): the share link leads, the enter button is
   the primary action, both sit inside the host box. */
.live-share { margin: 0 0 0.7rem; font-size: 0.92rem; }
.live-share strong { display: block; margin-bottom: 0.3rem; }
.btn.live-enter { display: block; text-align: center; margin-bottom: 0.6rem; }

/* Header text is theme-PROOF (Aug 10, cold-visitor walk): the site-head bar is
   forest green in light AND dark, but the dark-mode `a { color:#5CB585 }` rule
   was bleeding onto the wordmark and "Find people", rendering green-on-green.
   The bar never darkens, so its text is pinned light in both themes. Wins over
   the dark `a` rule by carrying the same or higher specificity here, placed
   after it in source order. */
.site-head .wordmark { color: #F4F6F1; }
.site-head .head-nav a { color: #D9E2DA; }
.site-head .head-nav .btn { color: var(--green); }
:root:not([data-theme="light"]) .site-head .head-nav a,
:root[data-theme="dark"] .site-head .head-nav a { color: #D9E2DA; }
:root:not([data-theme="light"]) .site-head .wordmark,
:root[data-theme="dark"] .site-head .wordmark { color: #F4F6F1; }
/* Aug 21 2026 (card RGwajf3b item 2): the dark-mode `a { color:#5CB585 }` rule
   also bled onto solid primary buttons -- green-tint on the green fill,
   measured 2.85:1 on the live dark render ("Claim your @handle"). Primary
   buttons keep white ink in every theme state; white on --green is 7.5:1. */
:root:not([data-theme="light"]) a.btn:not(.secondary):not(.quiet),
:root[data-theme="dark"] a.btn:not(.secondary):not(.quiet) { color: #fff; }

/* Sep 10 2026 — the :not(.quiet) exclusion above left quiet ANCHORS to the
   dark `a { color:#5CB585 }` rule, so `Open` and `Post to Sands` painted green
   while `Keep it`, identical classes but a <button>, painted grey. Contrast
   passes either way (6.51:1), so no contrast sweep could see it; the cost is
   the colour language. #5CB585 is --green-ink, which on that same row already
   means "Kept". Two controls in one row must not differ only by tag name. */
:root:not([data-theme="light"]) a.btn.quiet,
:root[data-theme="dark"] a.btn.quiet { color: var(--ink-soft); }

/* NOTE (same day): the white rule above ties the pill rule below at 0-5-1, so
   SOURCE ORDER is load-bearing here -- the header pill (light fill, dark ink)
   must come LAST to win. Measured after ordering: pill 8.37:1, CTA 7.13:1. */
/* Aug 21 2026 (card RGwajf3b): the theme-proof armour above out-specifies the
   Sign-in pill's own rule (.head-nav .btn.small, 0-3-0 vs 0-4-1), so the pill
   rendered nav-link mint (#D9E2DA) on its near-white fill: 1.22:1, both
   themes. The pill's background is pinned light like the bar, so its ink is
   pinned dark the same way -- literal, not a token that darkens. Measured
   #14532D on #F4F6F1 = 9.7:1. */
:root:not([data-theme="light"]) .site-head .head-nav a.btn,
:root[data-theme="dark"] .site-head .head-nav a.btn,
.site-head .head-nav a.btn { color: #14532D; }

/* Sep 10 2026 — the same bleed as the pill above, one component further on.
   `.site-head .head-nav a` (0,2,1) out-specifies `.acct-menu a` (0,2,0), so
   the mint ink written for the GREEN bar followed the account dropdown onto
   its white panel: "Settings" measured 1.33:1 while its sibling "Sign out"
   rendered 17.55:1, because that one is a <button> and the `a` rule missed it.
   The panel is var(--card), which FLIPS with the theme, and the rule works
   because var(--ink) flips with it. Do not "fix" this by hardcoding a light
   value: an earlier version of this comment claimed the panel was light in
   both themes, which is false and would have led exactly there. */
.site-head .acct-menu a,
:root:not([data-theme="light"]) .site-head .acct-menu a,
:root[data-theme="dark"] .site-head .acct-menu a { color: var(--ink); }

/* Metered soft wall (Tab ruling Aug 10 2026). A quiet card, not a hard block;
   the posts beyond it are dimmed and un-tappable until dismissed, never
   removed (so "Not now" restores them and a crawler with JS off sees them
   normally). Both themes via tokens; no new motion beyond a soft fade. */
.guest-gate {
  /* Server-rendered in place, styled from first paint — no JS reveal dance
     (the reveal-after-move version tripped a headless-WebKit tile artifact;
     Aug 10). list-style none because it is a real <li> in the timeline. */
  list-style: none; margin: 1rem 0; padding: 1.1rem 1.2rem;
  background: var(--card); border: 1px solid var(--gold);
  border-radius: var(--radius); box-shadow: var(--shadow-card); text-align: center;
}
/* r467 (Sep 9 2026): .gated-hidden and .gated-hidden-link DELETED. They blurred
   the feed beneath a card promising nothing is hidden. Do not reintroduce them. */
.guest-gate[hidden] { display: none; }
.guest-gate-lead { font-weight: 600; margin: 0 0 0.8rem; }
.guest-gate-actions { display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; }
.guest-gate-note { font-size: 0.8rem; color: var(--ink-soft); margin: 0.7rem 0 0; }
/* Paid line (Aug 10 2026, Tab ruling): quieter than the lead, one notch above
   the note — present, never shouting. Link inherits the standard link color. */
.guest-gate-paid { font-size: 0.85rem; color: var(--ink-soft); margin: 0.55rem 0 0; }

/* ---------- Tier 1d: 44px tap-target sweep (Aug 14 2026) ----------
   Hit-area-only expansion: transparent ::after overlays + min-heights.
   Zero visible change - the pills/links keep their painted size; only the
   tappable area grows to the 44px floor (WCAG 2.5.8 / estate harness).
   Expansions are VERTICAL only (left/right stay at the box edge) so
   adjacent controls in the same row can never overlap targets. */
.btn { min-height: 44px; }
/* Small/quiet pills keep their compact visual; hit area grows invisibly. */
.btn.small, .btn.quiet { min-height: 0; position: relative; }
.btn.small::after, .btn.quiet::after {
  content: ""; position: absolute; top: -6px; bottom: -6px; left: 0; right: 0;
}
/* Header icon links (bell, search): a 16x21px painted glyph in a tall bar,
   and NOT covered by the pill rule above because they are bare anchors, so
   their tap area really was 21px. Found Sep 4 2026. Vertical-only expansion
   like every other entry here; the row has ample horizontal spacing. */
.head-nav > a { position: relative; }
.head-nav > a::after {
  content: ""; position: absolute; top: -12px; bottom: -12px; left: -6px; right: -6px;
}
/* Sep 10 2026. Three more that the Aug 14 sweep never covered, measured
   independently by two seats at 20.0/26.8px, 21.7px and 40.3px. The account
   trigger matters most: at 390px it is the ONLY route to Settings and Sign
   out, on every authenticated page, and it was the smallest control on the
   page. `.head-nav > a` above does not reach `.you` because a neighbour's
   overlay was winning the hit test, so it gets its own strip and a real
   min-height rather than relying on the shared rule. */
.site-head .acct > summary { position: relative; min-height: 44px; min-width: 44px;
  display: inline-flex; align-items: center; justify-content: center; }
/* inline-BLOCK, not inline-flex. Flex makes the text an anonymous flex item,
   which kills the text-overflow: ellipsis that max-width: 92px depends on, so
   the handle hard-clipped mid-glyph ("@qacanarypas") instead of eliding. The
   44px tap area comes from the ::after strip below, not from the display mode.
   I introduced this regression fixing the tap target. */
.site-head .head-nav a.you { position: relative; min-height: 44px;
  display: inline-block; line-height: 44px; }
.site-head .head-nav a.you::after {
  content: ""; position: absolute; top: -11px; bottom: -11px; left: 0; right: 0;
}
/* The menu items are a real list on a phone; padding, not an overlay, because
   an overlay on stacked items overlaps its neighbour. 40.3 -> 44. */
.acct-menu a, .acct-menu button { min-height: 44px; display: flex; align-items: center; }

/* The focus ring on the GREEN BAR. --ink is #131A20 and the bar is pinned
   #14532D in both themes, so the functional ring measured 1.93:1 there and the
   indicator was passing only on its gold flank at 3.27:1 - i.e. resting on the
   layer this design explicitly calls expendable. The bar never darkens, so a
   light ring always passes on it: #F4F6F1 on #14532D is 8.4:1. */
.site-head :focus-visible { outline-color: #F4F6F1; }

/* Header wordmark link (26px tall text in a tall bar). */
.site-head .wordmark { position: relative; }
.site-head .wordmark::after {
  content: ""; position: absolute; top: -9px; bottom: -9px; left: 0; right: 0;
}
/* Guest hero: the try-your-handle row. The <label> is the tap surface
   (tapping it focuses the input), so it gets top/bottom hit STRIPS - never
   a full overlay, which would steal precise caret taps from the input. */
.handle-try-row { position: relative; }
.handle-try-row::before {
  content: ""; position: absolute; top: -8px; height: 8px; left: 0; right: 0;
}
.handle-try-row::after {
  content: ""; position: absolute; bottom: -6px; height: 6px; left: 0; right: 0;
}
/* Guest hero links ("See what people are posting" / "Get the app"). */
.hero-links a { position: relative; }
.hero-links a::after {
  content: ""; position: absolute; top: -10px; bottom: -10px; left: 0; right: 0;
}

/* Sep 9 2026: with the rail card condensed, the standards strip below the feed
   repeats it on wide screens. Hidden there only -- on phones there is no rail,
   so the strip is the only place the five rules are named and it must stay. */
@media (min-width: 1100px) { body.has-rail .standards-strip { display: none; } }

/* DARK MODE: the primary CTA's SHAPE was dissolving into the page (Sep 9 2026).
   Fill rgb(22,101,52) measures 2.55:1 against the dark ground rgb(17,22,27), under
   the 3.0 required for a UI component. Brightening the fill would fix the shape and
   break the label: at #1E8A4B the white text drops to 4.38:1, under 4.5. So the fill
   stays (white label 7.13:1, comfortable) and the shape gets a ring instead, which
   measures 5.44:1 against the ground. Both requirements met, neither traded away. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn:not(.quiet) { border: 1px solid #2AA05C; }
}
[data-theme="dark"] .btn:not(.quiet) { border: 1px solid #2AA05C; }

/* WCAG 2.5.8: two primary controls sat under the 44px minimum. The handle input at
   316x33 is the page's main conversion affordance, and Sign in was 74x32. Padding
   and min-height only, so no painted text moves. */
#handle-try-input { min-height: 44px; }
.head-nav .btn.small { min-height: 44px; display: inline-flex; align-items: center; }

/* Phone trust fold-out under the guest hero (Sep 9 2026). Below 1100px the right rail
   is display:none, so its three trust modules did not exist on the device 72% of
   visits use, and the only phone substitute sat at 84% page depth. This is a closed
   details element one row tall, in the standards-strip register (same tint, same
   khatam), holding the rail's own cards. Closed on purpose: the first post starts at
   the fold (721 against 724) and an open module would push it a screen down. The
   chevron is drawn with borders, not a glyph, so it cannot fall back to a font. Every
   colour is a theme token, so dark mode needs no second rule. */
.trust-fold {
  margin-top: 0.75rem;
  background: rgba(200, 145, 46, 0.07);
  border: 1px solid var(--line); border-radius: 10px;
}
/* The feed heading keeps its 2.2rem top margin everywhere else; directly under the
   fold-out that read as double spacing and cost the fold 13px. Adjacent-sibling on
   purpose: if the live bar ever renders between them, the heading simply keeps its
   default margin. Scoped below 1100px: a display:none fold-out is still a sibling, and
   unscoped this moved the desktop heading up 13px, which the rail's ad-track height
   (522 = heading top 602 - 80) is derived from. Measured: first card 797 -> 767 at 390. */
@media (max-width: 1099.98px) { .trust-fold + .section-mark { margin-top: 1.4rem; } }
.trust-fold summary {
  display: flex; align-items: center; gap: 0.6rem;
  min-height: 44px; padding: 0.55rem 0.9rem;
  font-size: 0.9rem; line-height: 1.4; color: var(--ink-soft);
  cursor: pointer; list-style: none;
}
.trust-fold summary::-webkit-details-marker { display: none; }
.trust-fold summary strong { color: var(--ink); font-weight: 700; }
.trust-fold summary .khatam { color: var(--gold); flex: none; }
.trust-fold summary::after {
  content: ""; flex: none; margin-left: auto;
  width: 0.55rem; height: 0.55rem;
  border-right: 2px solid var(--ink-soft); border-bottom: 2px solid var(--ink-soft);
  transform: translateY(-2px) rotate(45deg);
  transition: transform 150ms ease;
}
.trust-fold[open] summary::after { transform: translateY(2px) rotate(-135deg); }
@media (prefers-reduced-motion: reduce) { .trust-fold summary::after { transition: none; } }
.trust-fold-body { padding: 0 0.5rem 0.5rem; display: grid; gap: 4px; }
.trust-fold-body .rail-card { margin-bottom: 0; }
.trust-fold-body .rail-buzz-late { margin-top: 0; }
@media (min-width: 1100px) { .trust-fold { display: none; } }

/* ---------------------------------------------------------------------------
   Sands: a member's own posts (r542, Sep 9 2026)
   ---------------------------------------------------------------------------
   Wajid, 27 Aug: "the story image currently feels quite large and horizontal.
   I think a more compact, vertical story-style presentation would look cleaner."
   So: a list of compact rows, portrait thumbnail, never a wide hero.

   TOKENS ONLY. Every colour here is a var(), so the dark block at the top of
   this file covers this component too. A hardcoded hex is defect #8 in the
   design standard: a shared component with no dark theme shipped greens at
   1.66:1 across seven properties.

   No counters, no shimmer, no presence dots, no autoplay. Nothing here
   fabricates busyness.
--------------------------------------------------------------------------- */
.sands-page {
  /* NO horizontal padding here. `.shell` already supplies `0 1rem` and both
     set the same max-width, so this element was charging the row a SECOND
     32px gutter it did not need. That cost the caption 32px, which is the
     whole reason the earlier note claimed 45 characters per line was
     unreachable at 390px. It was reachable; the padding was doubled.
     Measured after removing it: caption 305.2px -> 337.2px, 47 characters on
     every full line, scrollWidth still 390. */
  max-width: var(--col); padding-bottom: 3rem; }
.sands-head { padding: 1.5rem 0 0.75rem; }
.sands-head h1 { font-size: 1.5rem; line-height: 1.25; margin: 0 0 0.35rem; }
.sands-head p { margin: 0; font-size: 0.95rem; }

.sands-flash { border-radius: var(--radius); padding: 0.75rem 1rem; margin: 0 0 1rem; font-size: 0.95rem; }
.sands-flash--ok { background: var(--card); color: var(--ink); box-shadow: inset 0 0 0 2px var(--green-ink); }
.sands-flash--bad { background: var(--card); color: var(--ink); box-shadow: inset 0 0 0 2px var(--danger); }

.sands-note { background: var(--card); border-radius: var(--radius); padding: 0.75rem 1rem;
  margin: 0 0 1.25rem; font-size: 0.9rem; color: var(--ink-soft);
  /* A 2px inset ring rather than a tinted fill: brightening a fill to clear the
     3:1 UI-shape bar can push the text under 4.5:1. The standard says add a ring
     instead, so this adds a ring. */
  box-shadow: inset 0 0 0 1px var(--line); }
.sands-note a { color: var(--green-ink); }

.sands-group { font-size: 1.05rem; margin: 1.5rem 0 0.5rem; }
.sands-groupnote { font-size: 0.85rem; margin: 0 0 0.75rem; }

.sands-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.75rem; }

.sands-row { display: flex; gap: 0.85rem; background: var(--card); border-radius: var(--radius);
  padding: 0.85rem; box-shadow: inset 0 0 0 1px var(--line);
  /* align-items:flex-start, not stretch: a grid or flex row that stretches sizes
     every cell to the tallest, which is defect #5 (four cards of wildly
     different length all became 352px). */
  align-items: flex-start; }

.sands-thumb { flex: 0 0 56px; width: 56px; height: 56px; border-radius: 10px; overflow: hidden;
  background: var(--paper); box-shadow: inset 0 0 0 1px var(--line);
  display: flex; align-items: center; justify-content: center; }
/* object-fit is explicit and load-bearing. The default is `fill`, which
   STRETCHES; a portrait story in a square box is a 77% aspect error that looks
   fine in review whenever the test image happens to be square. */
.sands-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sands-thumb__none { font-size: 0.7rem; color: var(--ink-soft); }

.sands-body { flex: 1 1 auto; min-width: 0; }
/* min-width:0 is load-bearing on a flex child: without it a long unbroken
   caption sets the min-content width and pushes the row past the viewport,
   which is the horizontal-scroll defect the bar forbids outright. */

.sands-meta { margin: 0 0 0.25rem; font-size: 0.85rem; color: var(--ink-soft); }
.sands-kind { font-weight: 600; color: var(--ink); }
.sands-dot { margin: 0 0.35rem; }

/* white-space and unicode-bidi are BOTH load-bearing, and neither was here.
   Without pre-wrap the member's own line breaks are discarded, so a caption
   written as Arabic / transliteration / translation collapses into one run.
   Then, because the first strong character is Arabic, dir="auto" resolves the
   WHOLE paragraph to rtl, which right-aligns the English and mirrors its
   paired punctuation: a member's closing ")" rendered as "(" at the far left,
   and her Qur'anic line was spliced into the middle of the English sentence.
   unicode-bidi: plaintext resolves direction PER LINE instead, which is what
   dir="auto" already promises per element.
   The feed has carried pre-wrap since app.css:390 (.post .body); this partial
   never inherited it. Four of one member's six captions are affected today. */
.sands-caption { margin: 0 0 0.4rem; font-size: 0.95rem; line-height: 1.45;
  overflow-wrap: anywhere; white-space: pre-wrap; unicode-bidi: plaintext; }

.sands-state { margin: 0 0 0.6rem; display: flex; flex-wrap: wrap; gap: 0.4rem; }
.sands-tag { font-size: 0.78rem; color: var(--ink-soft); background: var(--paper);
  border-radius: 999px; padding: 0.2rem 0.6rem; box-shadow: inset 0 0 0 1px var(--line); }
.sands-tag--kept { color: var(--green-ink); box-shadow: inset 0 0 0 1px var(--green-ink); }
.sands-tag--held { color: var(--danger); box-shadow: inset 0 0 0 1px var(--danger); }

.sands-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
.sands-actions form { margin: 0; }
/* 44px is the platform HIG floor the estate holds, not the 24px WCAG minimum.
   .btn.small is 0.35rem padding on a 0.85rem font, which is under it, so these
   are raised deliberately rather than inheriting a too-small control. */
.sands-actions .btn { min-height: 44px; display: inline-flex; align-items: center; }
.sands-danger { color: var(--danger); }
.sands-danger:hover { background: var(--paper); }

.sands-empty { background: var(--card); border-radius: var(--radius); padding: 1.5rem;
  box-shadow: inset 0 0 0 1px var(--line); text-align: center; }
.sands-empty p { margin: 0 0 0.9rem; color: var(--ink-soft); }

.sands-foot { margin: 1.5rem 0 0; }

/* `.muted` was used in two places on this page and DEFINED NOWHERE. Every
   occurrence of the word in this stylesheet was inside a comment, so both
   elements rendered at full --ink: the page had no quiet register at all, the
   H1 subtitle competed with the heading above it, and the group note read as a
   repetition of its own heading rather than as a footnote. Measured 7.71:1
   light and 7.67:1 dark, so it is quiet without dropping under the text bar. */
.muted { color: var(--ink-soft); }

/* A third real heading tier. H1 21.6 -> H2 16.8 -> body 15.2 gave steps of
   1.29x and 1.11x, which is not a hierarchy: "Today" barely read as a
   waypoint. 28 / 18.4 / 15.2 gives 1.52x and 1.21x. */
.sands-head h1 { font-size: 1.75rem; }
.sands-group { font-size: 1.15rem; }

/* 465, not 430: below the flex layout the caption shares the row with the
   thumbnail, and between 431 and 465 that left it at 270px and 31 cpl, worse
   than the 390px case this fix was written for. The grid has to cover the
   whole range where the row is too narrow to share. */
@media (max-width: 465px) {
  .sands-head h1 { font-size: 1.5rem; }
  /* Sep 10 2026 — the caption measured 234.8px, about 30 characters per line,
     against a 45-75 bar. The thumbnail plus two gaps was taking 40% of a 390px
     viewport away from the one thing on the row a member came to read.
     `display: contents` promotes .sands-body's children to grid items of the
     row itself, so the caption can span the full card while the thumbnail
     stays beside the meta line. No markup change, and the row still sizes to
     its own content rather than stretching (defect #5).
     Measured after: 337px, about 43 cpl. 45 is not reachable by any single
     column at 390px CSS with a readable body size, so this goes as close as
     the viewport allows rather than shrinking the type to win a number. */
  .sands-row { padding: 0.65rem; gap: 0.6rem 0.7rem;
    display: grid; grid-template-columns: 44px 1fr;
    grid-template-areas: "thumb meta" "caption caption" "state state" "actions actions"; }
  .sands-body { display: contents; }
  .sands-thumb { grid-area: thumb; flex-basis: 44px; width: 44px; height: 44px; }
  .sands-meta { grid-area: meta; align-self: center; margin: 0; }
  /* min-width:0 moves WITH the caption: it was on .sands-body, which no longer
     generates a box, and without it a long unbroken word sets the column's
     min-content width and reintroduces horizontal scroll. */
  .sands-caption { grid-area: caption; min-width: 0; margin: 0; }
  .sands-state { grid-area: state; margin: 0; }
  .sands-actions { grid-area: actions; }
}

/* Sands: the take-down confirmation, and the two tap targets a sweep missed.
   ---------------------------------------------------------------------------
   The confirmation is a native <details>, NOT JavaScript. The first version
   used onsubmit="return confirm(...)" and it never ran: this page's CSP is
   script-src 'self' with no 'unsafe-inline', so the browser strips inline
   handlers. It was in the HTML, it grepped clean, and an irreversible delete
   fired on one tap. <details> cannot be stripped by any content policy and is
   keyboard accessible with no script at all. */
.sands-confirm { display: inline-block; }
.sands-confirm > summary { list-style: none; cursor: pointer; }
.sands-confirm > summary::-webkit-details-marker { display: none; }
.sands-confirm > summary::marker { content: ''; }
/* Placed late and at (0,3,1) deliberately: the plain `.sands-confirm[open] >
   summary` form is (0,2,0) and lost to `.btn.quiet.sands-danger` (0,3,0),
   which is also later in this file, so the open state was byte-identical to
   the closed one. The same specificity trap this file already documents. */
.sands-confirm[open] > summary.sands-danger { box-shadow: inset 0 0 0 2px var(--danger); }
.sands-confirm__body { margin-top: 0.5rem; padding: 0.75rem; border-radius: var(--radius);
  background: var(--paper); box-shadow: inset 0 0 0 1px var(--danger); }
.sands-confirm__body p { margin: 0 0 0.6rem; font-size: 0.85rem; color: var(--ink); }
.sands-confirm__body form { margin: 0; }
.sands-danger-solid { background: var(--danger); color: var(--paper); min-height: 44px;
  display: inline-flex; align-items: center; }
.sands-danger-solid:hover { background: var(--danger); filter: brightness(0.92); }

/* The note's link was a bare inline <a> at 19px tall. The estate's 44px sweep
   expands .btn.small and .head-nav > a via a transparent ::after, and covers
   neither a bare anchor in a <p>. It is 25px under the bar and it is the ONLY
   route to fixing the state the note is complaining about. */
.sands-note a { display: inline-block; min-height: 44px; line-height: 44px; }

/* The thumbnail is a link now: on a page that exists because members could not
   see their own post, tapping the picture of the post must open the post.
   tabindex=-1 and aria-hidden keep it out of the tab order and off the screen
   reader, because the row's "Open" button already carries that action with a
   distinct accessible name. */
a.sands-thumb { text-decoration: none; }
a.sands-thumb:hover { box-shadow: inset 0 0 0 2px var(--green-ink); }

/* .sands-danger is specificity (0,1,0) and lost to .btn.quiet (0,2,0), so the
   danger colour never painted and all three row controls rendered identically
   grey. A seat measured the computed colour as rgb(69,82,77) on all three. The
   destructive control now carries its own ring and label colour, so the one
   button that removes a post does not look like the two that do not. */
.btn.quiet.sands-danger,
.sands-confirm > summary.sands-danger {
  color: var(--danger);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--danger) 85%, transparent);
}
.btn.quiet.sands-danger:hover,
.sands-confirm > summary.sands-danger:hover { background: color-mix(in srgb, var(--danger) 8%, transparent); }

/* The footer link is .btn.quiet.small and sits outside both .sands-actions and
   .sands-empty, so it inherited neither 44px rule. A seat measured 32.3px. It is
   the third control the estate's tap sweep did not reach. */
.sands-foot .btn { min-height: 44px; display: inline-flex; align-items: center; }

/* The 44px fix on the note's link used line-height:44px, which orphaned the word
   "free" onto its own line and grew the note from 113px to 135px, taking 35% of
   the fold on the majority path. Padding gives the same tap area without
   breaking the sentence. */
.sands-note a { display: inline; min-height: 0; line-height: inherit;
  /* 0.75rem gave 43px: one pixel under, which is still under. Measured, not
     estimated. 0.85rem clears it without changing the line box, so the sentence
     stays whole. */
  padding: 0.85rem 0; }

/* Member-supplied text, shared definition (r570). Mirrors
   /var/www/_shared/css/member-text.css; the <x-member-text> component adds this
   class. pre-wrap keeps the member's own line breaks, and unicode-bidi:
   plaintext resolves direction per LINE so one Arabic word at the start does
   not flip a whole paragraph to rtl and mirror the English punctuation. */
.umg-member-text { white-space: pre-wrap; unicode-bidi: plaintext; overflow-wrap: anywhere; }

/* ---------------------------------------------------------------------------
   The Sands tray on the signed-in home (r538 / r565 / r587, Sep 10 2026).
   Below the composer, above the tabs. Guests never render it.

   NO horizontal page scroll: the strip bleeds to the shell's own 1rem padding
   with a matching inner padding, so its box spans exactly the viewport at
   390px and never a pixel more. `.sands-page` learned this the other way
   round -- it set its own padding on top of .shell's and cost the caption 17
   characters a line -- so the rule here is the same one: know what .shell
   already gives you before adding any inline space.

   Nothing in this block animates on a loop, no shimmer, no dot, and there is
   no counter anywhere in the markup it styles.
   --------------------------------------------------------------------------- */
.sands-tray { margin: 0.9rem 0 0; }

.sands-tray-strip {
  display: flex;
  gap: 12px;
  list-style: none;
  margin: 0 -1rem;
  padding: 0.15rem 1rem 0.35rem;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  /* The thumb has to be VISIBLE. The right rail shipped once with the
     scrollbar painted in --line on --paper, i.e. invisible, which turns a
     safety net into what reads as a card chopped in half. */
  scrollbar-color: var(--sand-spent) transparent;
}
.sands-tray-strip::-webkit-scrollbar { height: 6px; }
.sands-tray-strip::-webkit-scrollbar-thumb { background: var(--sand-spent); border-radius: 3px; }
.sands-tray-strip::-webkit-scrollbar-track { background: transparent; }

.sands-tile { flex: 0 0 72px; width: 72px; position: relative; }

.sands-tile-link {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  text-decoration: none;      /* opt-out: a whole-tile affordance, not prose */
  color: var(--ink-soft);
  /* 72 wide x (64 ring + 5 gap + ~15 label) — comfortably past the 44px bar
     in both directions, so no invisible ::after hit-area extension is needed
     here and getBoundingClientRect() reads the true target. */
  min-height: 44px;
}
.sands-tile-link:hover .sands-tile-name { text-decoration: underline; }

.sands-ring {
  box-sizing: border-box;
  width: 64px; height: 64px; border-radius: 50%;
  display: grid; place-items: center;
  /* The drain. --drain is set per tile as a percentage of the story's life
     that is LEFT, floored at 4% so a story with minutes on it still shows a
     sliver of sand rather than reading as the seen state. It is a clock, not
     a metric: nothing here counts anything a person did. */
  background: conic-gradient(var(--sand) var(--drain, 100%), var(--sand-spent) 0);
}
.sands-ring > img {
  width: 52px; height: 52px; border-radius: 50%;
  /* EXPLICIT. The browser default is `fill`, which stretches a portrait into
     the circle, and it survives review whenever the sample happens to be
     square. A 736x1308 source measured +77.7% aspect error under the default. */
  object-fit: cover;
  display: block;
  box-shadow: 0 0 0 3px var(--paper);
  background: var(--line);
}
/* Kept: solid, no drain, because there is no clock left to draw. */
.sands-ring--kept { background: var(--green-ink); }
/* The member's own live post carries the same sand language as everyone's. */
.sands-ring--own { background: var(--sand); }
/* No live post: a dashed outline and the word "Post". box-sizing keeps the
   ring 64px so the row does not shift when the state changes. */
.sands-ring--none { background: none; border: 2px dashed var(--line); }
.sands-ring--preparing { background: var(--sand-spent); }
.sands-ring-glyph { font-size: 1.5rem; line-height: 1; color: var(--ink-soft); }

/* Seen: flat tan, and the tile DOES NOT MOVE. Added by sands-tray.js from
   this browser's own localStorage; it is never sent anywhere and no author
   can ever see it. */
.sands-tile.is-seen .sands-ring { background: var(--sand-spent); }

.sands-tile-name {
  font-size: 0.7rem; line-height: 1.25;
  max-width: 72px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: center;
}

/* "Post another" — a real control with a real destination, not a status dot.
   Sized to 44px through an invisible ::after rather than by painting a big
   circle, the same technique the Aug 14 tap-target sweep used. Measure it
   with getComputedStyle(el, '::after') and subtract the offsets; a plain
   getBoundingClientRect() understates it by 12px. */
.sands-tile-add {
  position: absolute; right: 4px; top: 44px;
  width: 20px; height: 20px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--green); color: #fff;
  font-size: 0.85rem; line-height: 1; font-weight: 700;
  text-decoration: none;
  box-shadow: 0 0 0 2px var(--paper);
}
.sands-tile-add::after {
  content: ""; position: absolute; top: -12px; right: -12px; bottom: -12px; left: -12px;
}

.sands-tray-empty,
.sands-tray-note {
  font-size: 0.8rem; color: var(--ink-soft); margin: 0;
}
.sands-tray-empty { margin-top: 0.35rem; }
/* 20px below the strip, as ruled. */
.sands-tray-note { margin-top: 20px; }

/* The focus ring lives in `outline`, never in box-shadow. A box-shadow
   indicator is (0,1,0) against :focus-visible and loses to any (0,2,0) rule
   such as .btn.quiet, which degraded the whole property's indicator to gold
   alone at 2.78:1. Failure mode this way round is safe: override it and you
   lose decoration, not compliance. */
.sands-tile-link:focus-visible,
.sands-tile-add:focus-visible {
  outline: 3px solid var(--green-ink);
  outline-offset: 2px;
  border-radius: 8px;
}

/* ---------------------------------------------------------------------------
   Story report queue (r565, Sep 10 2026) — /admin/story-reports.
   Moderator-only, four named people. Nothing here is a member surface.
   --------------------------------------------------------------------------- */
.muted-note { color: var(--ink-soft); font-size: 0.9rem; line-height: 1.5; margin: 0.4rem 0 1rem; }

.story-review { display: flex; gap: 14px; align-items: flex-start; }
.story-review-media { flex: 0 0 120px; }
.story-review-media img {
  width: 120px; height: 150px; border-radius: 10px; display: block;
  /* EXPLICIT: the default is `fill`, which stretches a portrait. A moderator
     judging a photo must see its real proportions. */
  object-fit: cover;
  background: var(--line);
  border: 1px solid var(--line);
}
.story-review-gone {
  display: grid; place-items: center; text-align: center;
  width: 120px; min-height: 150px; padding: 0.5rem;
  border: 1px dashed var(--line); border-radius: 10px;
  font-size: 0.78rem; color: var(--ink-soft);
}
.story-review-body { flex: 1; min-width: 0; }
.story-review-reports { list-style: none; margin: 0.5rem 0 0; padding: 0; }
.story-review-reports li { margin: 0 0 0.4rem; font-size: 0.9rem; display: flex; gap: 0.5rem; align-items: baseline; flex-wrap: wrap; }
.story-review-reason { margin: 0; font-size: 0.9rem; }
.story-review-notes { font-size: 0.85rem; color: var(--ink-soft); margin-top: 0.5rem; }
.story-review-notes span { display: block; white-space: pre-wrap; }
.story-review-note {
  flex: 1; min-width: 180px; font: inherit; font-size: 0.88rem;
  border: 1px solid var(--line); border-radius: 8px; padding: 0.4rem 0.6rem;
  background: var(--paper); color: var(--ink);
  min-height: 44px;
}
.report-card .actions { flex-wrap: wrap; align-items: center; }

/* At 390 the media and the body stack, or the caption gets ~14 characters a
   line. `.shell` already supplies the horizontal padding — nothing here adds
   any, which is the mistake `.sands-page` made and paid 32px for. */
@media (max-width: 560px) {
  .story-review { flex-direction: column; }
  .story-review-media, .story-review-media img, .story-review-gone { width: 100%; flex: 0 0 auto; }
  .story-review-media img { height: 220px; }
}

/* Sep 10 2026 — the tray's "+" badge, and the SAME trap the two rules above
   record. `.sands-tile-add` sets `color:#fff` at (0,1,0) and lost to the
   dark-mode `:root:not([data-theme="light"]) a { color:#5CB585 }` at (0,2,1),
   so the plus painted --green-ink on the pinned green disc: measured 2.85:1 in
   dark, exactly the number the primary-button version of this bug measured in
   August. Light was 7.13:1 and passed, which is why one theme's sweep could
   never have found it.

   --green is a GROUND that is pinned in both themes, so the ink on it is
   pinned too, literally, and never a token that follows the theme. Same
   reasoning as --gold-on-green. Written at (0,3,0) so it out-specifies the
   theme rule rather than relying on source order. */
:root:not([data-theme="light"]) .sands-tile-add,
:root[data-theme="dark"] .sands-tile-add { color: #fff; }

/* The author link in a queue card's meta line was 145x18 painted — a real
   target, under both the 44px bar and the 24px minimum, measured on the live
   page. Extended with an invisible ::after rather than by growing the printed
   text, which is the technique the Aug 14 sweep established; remember that
   getBoundingClientRect() understates these by the ::after's own offsets, so
   measure with getComputedStyle(el, '::after') and subtract them. */
.story-review-body .sands-meta { position: relative; }
.story-review-body .sands-meta a { position: relative; display: inline-block; }
.story-review-body .sands-meta a::after {
  content: ""; position: absolute; top: -13px; bottom: -13px; left: -6px; right: -6px;
}

/* The queue's decision buttons take a real 44px PAINTED box rather than the
   estate's compact-pill-plus-invisible-::after treatment.
   Measured on the live page: `.btn.small` is deliberately reset to
   `min-height: 0` by the Aug 14 sweep and grows only 12px through its
   ::after, landing at 41.2px — 2.8px under the bar, everywhere on the site.
   That is a standing estate-wide finding and is NOT fixed here: widening the
   shared rule is a whole-estate change, and this row wraps at 390px, where a
   taller invisible overlay on two stacked buttons would overlap their targets,
   which is the exact hazard that sweep's own comment warns about.
   So this page takes the height for real. Nobody but a moderator sees it, and
   an irreversible-ish decision is the wrong control to make small. */
.report-card .actions .btn { min-height: 44px; }
.report-card .actions { row-gap: 0.6rem; }

/* ---------------------------------------------------------------------------
   Download my UmmahSocial data (r739, Sep 16 2026, card fZQ7SMOm).

   `.composer input[type="text"|"password"]` did not exist ANYWHERE in this
   file before today -- checked, not assumed, because `.muted` was used on the
   Sands page and defined nowhere for a week. The export page is the first
   surface here with a free-text field outside the composer textarea, so the
   tokens are stated once and reused.

   The focus ring lives in `outline`, never in `box-shadow`: :focus-visible is
   (0,1,0) and loses to any two-class selector, which on Sep 10 2026 degraded
   every button's indicator to gold alone at 2.78:1. Gold is decoration here
   and the contrast comes from the outline, so overriding it costs decoration
   and keeps compliance.
--------------------------------------------------------------------------- */
.settings-form input[type="text"],
.settings-form input[type="password"] {
  font: inherit;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* 44px minimum tap target, painted -- this field is NOT in the Aug 14 2026
     invisible-::after sweep, so its box is its real hit area. */
  min-height: 44px;
  box-sizing: border-box;
}
.settings-form input[type="text"]:focus-visible,
.settings-form input[type="password"]:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-color: var(--green-ink);
}

/* ==========================================================================
   PART 1 ENDS.
   ========================================================================== */

/* ==========================================================================
   THE V2 LAYER — everything below is scoped to body.ui-v2
   ==========================================================================

   Phase 1 of the design pass (plan of record:
   /root/.claude/plans/ummahsocial-design-pass-sep17.md, FINAL after two seats).

   WHY EVERY SELECTOR CARRIES .ui-v2, AND WHY THAT IS NOT A SPECIFICITY WAR.
   The carried v1 rules above are untouched so that a page out of phase-1 scope
   (settings, notifications, search, Sands, live, standards, about) renders
   exactly as it does today under ?ui=2. The v2 layer reuses a handful of v1
   class names on purpose (.timeline, .avatar, .body, .btn), because the JS
   hooks and the blade partials depend on them. The .ui-v2 prefix is what makes
   the v2 rule win, deterministically, at (0,2,0) against v1's (0,1,0).
   The failure mode is the safe one: a v2 rule I forgot leaves the element with
   its v1 styling, which is styled and readable, never unstyled.

   Sep 9 2026's lesson is honoured here: --v-green-deep and --v-green-btn are
   GROUNDS carrying light text and do not follow the theme. --v-green-ink is
   green as a FOREGROUND and it does.
   ========================================================================== */

/* ---------- tokens: six colour roles, seven type steps, four radii -------- */

body.ui-v2 {
  /* Colour. Six roles, exactly as the plan states them. */
  --v-green-deep: #14532D;   /* GROUND: shell chrome. Pinned in both themes.  */
  --v-green-btn:  #166534;   /* GROUND: the Post button. Pinned both themes.  */
  --v-ink:        #131A20;
  --v-ink-soft:   #45524D;
  --v-paper:      #FAF8F3;
  --v-card:       #FFFFFF;   /* raised things only: sheets, menus, the ad     */
  --v-gold:       #C8912E;
  --v-line:       #E5E2D9;   /* hairline                                      */
  /* Green as a FOREGROUND is a different colour from green as a surface.     */
  --v-green-ink:  #166534;
  /* A shade of the gold role, not a seventh role: the appreciated star needs
     an edge that clears 3:1 on paper, and the brand gold measures 2.62:1
     there. Fill stays brand gold; the stroke is this. */
  --v-gold-deep:  #8A6520;
  --v-danger:     #A63D2F;
  /* Light gets depth from a sheet shadow; dark gets it from a lighter ground
     (see the dark block). Never both. */
  --v-shadow: 0 1px 2px rgba(19,26,32,.06), 0 8px 24px rgba(19,26,32,.08);

  /* Type. Seven steps and no eighth. Sora at 28 and above only; names in the
     feed are Manrope 700. */
  --t-meta:    13px;
  --t-ui:      15px;
  --t-body:    17px;
  --t-section: 21px;
  --t-title:   28px;
  --t-name:    34px;
  --t-hero:    54px;

  /* Shape. Four radii. */
  --r-ctl:   8px;    /* controls        */
  --r-media: 12px;   /* media in a feed */
  --r-sheet: 16px;   /* sheets          */
  --r-full:  999px;  /* avatars, pills  */

  /* Spacing. A 4px scale, as tokens. Nothing in this layer is hand-written. */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px;

  /* THE STICKY STACK, published as two numbers (round 1.1).
     --v-head-h  the height of the sticky top bar, or 0 where it is not there.
     --v-stick   the bottom edge of ALL the sticky chrome above the timeline:
                 the bar plus the tabs row.
     They exist because four separate things need the same answer and were
     each carrying their own copy of it: where the tabs row pins, where an
     anchored post must stop, where the day chip's line sits, and how far a
     permalink has to be pushed down.

     The bar's own padding-top is the notch inset, so the token carries it:
     without it an anchored post on a notched iPhone stops 47px too high.
     public/js/v2.js does NOT parse this value -- getComputedStyle hands an
     unregistered custom property back as the literal text that was written,
     so `calc(...)` there would read as NaN. It MEASURES the two elements
     instead, which is the observed answer rather than the declared one.

     ROUND 1.2 -- THIS BLOCK WAS UNTERMINATED IN THE MIDDLE OF IT, and those two
     characters broke the tabs row on every phone. The comment closed early, six
     lines of English prose became raw CSS, and a CSS declaration block error-
     recovers by discarding everything up to the next semicolon -- which was the
     one at the end of `--v-head-h`. So the token was never defined below 1100px,
     `top: var(--v-head-h)` was invalid at computed-value time, and `top` fell
     back to its initial value, `auto`. Measured live before the fix:
     getComputedStyle(body).getPropertyValue('--v-head-h') === '' at 390 and
     '0px' at 1440, and nav.tabs computed `position: sticky; top: auto`, so the
     row scrolled away to -1178px and the day chip left the screen at every
     position past the first marker.

     Two lessons, both already in this repo's memory in another form. A comment
     that does not nest is a code defect, not a typo (Blade comments, Sep 10).
     And a custom property that fails to parse is SILENT: every rule reading it
     keeps rendering, just wrong. The declaration below therefore carries a
     literal fallback inside var(), so the same class of failure degrades to a
     working sticky bar instead of a broken one. */
  --v-head-h: calc(52px + env(safe-area-inset-top));
  --v-tabs-h: 48px;
  --v-stick: calc(var(--v-head-h) + var(--v-tabs-h));

  background: var(--v-paper);
  color: var(--v-ink);
  font-size: var(--t-ui);
  line-height: 1.5;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body.ui-v2 {
    --v-ink:       #E7E4DD;
    --v-ink-soft:  #A0ABA4;
    --v-paper:     #11161B;
    --v-card:      #1F272E;   /* elevation BY GROUND, never by shadow */
    --v-gold:      #D9A94F;
    --v-gold-deep: #D9A94F;   /* on a dark ground the brand gold is already 8.5:1 */
    --v-line:      #2C353C;
    --v-green-ink: #5CB585;
    --v-danger:    #D4756A;
    --v-shadow:    none;
  }
}
:root[data-theme="dark"] body.ui-v2 {
  --v-ink:       #E7E4DD;
  --v-ink-soft:  #A0ABA4;
  --v-paper:     #11161B;
  --v-card:      #1F272E;
  --v-gold:      #D9A94F;
  --v-gold-deep: #D9A94F;
  --v-line:      #2C353C;
  --v-green-ink: #5CB585;
  --v-danger:    #D4756A;
  --v-shadow:    none;
}

/* ---------- the focus indicator lives in `outline` ------------------------
   Sep 10 2026: a ring built with box-shadow is contested by any later
   box-shadow rule and degraded to gold alone at 2.78:1 on every button.
   Nothing overrides outline, so that is where the contrast ring lives. */
body.ui-v2 :focus-visible {
  outline: 2px solid var(--v-green-ink);
  outline-offset: 2px;
  border-radius: var(--r-ctl);
}
body.ui-v2 .v2-head :focus-visible,
body.ui-v2 .v2-rail-post:focus-visible,
body.ui-v2 .v2-tabbar-post:focus-visible { outline-color: #F4F6F1; }

/* ---------- the shell ---------------------------------------------------- */

body.ui-v2 { margin: 0; }

/* Phone: a slim top bar. Wordmark, and the live pill only when a Circle is
   live. Nothing else: the bottom bar is the navigation. */
body.ui-v2 .v2-head {
  position: sticky; top: 0; z-index: 40;
  background: var(--v-green-deep);
  color: #F4F6F1;
  display: flex; align-items: center; gap: var(--s3);
  padding: 0 var(--s4);
  min-height: 52px;
  padding-top: env(safe-area-inset-top);
}
body.ui-v2 .v2-wordmark {
  display: inline-flex; align-items: center; gap: var(--s2);
  color: #F4F6F1; text-decoration: none;
  font-family: 'Manrope', system-ui, sans-serif;
  font-weight: 700; font-size: var(--t-ui); letter-spacing: .01em;
  min-height: 44px;
}
body.ui-v2 .v2-wordmark .khatam { width: 20px; height: 20px; color: var(--v-gold); }
body.ui-v2 .v2-head-right { margin-left: auto; display: flex; align-items: center; gap: var(--s2); min-width: 0; }

/* Circles and Stories on a phone (round 1.1, item 8). 44x44 painted, the label
   visually hidden so the accessible name never depends on the width of the
   window -- the same idiom the collapsed left rail uses. Hidden from 1100px,
   where the left rail carries both and a second copy would be two doors to one
   room on one page. */
body.ui-v2 .v2-head-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; flex: none;
  border-radius: var(--r-full);
  color: #F4F6F1; text-decoration: none;
}
body.ui-v2 .v2-head-icon .v2-icon { width: 22px; height: 22px; }
body.ui-v2 .v2-head-icon:hover { background: rgba(244,246,241,.14); }
body.ui-v2 .v2-head-icon .t {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}
@media (min-width: 1100px) { body.ui-v2 .v2-head-icon { display: none; } }

/* The live pill. Renders only when a Circle is actually live — there is no
   "nothing live" state, because announcing absence is worse than silence. */
body.ui-v2 .v2-live-pill {
  display: inline-flex; align-items: center; gap: var(--s2);
  min-height: 44px; padding: 0 var(--s3);
  border-radius: var(--r-full);
  background: rgba(244,246,241,.14);
  color: #F4F6F1; text-decoration: none;
  font-size: var(--t-meta); font-weight: 700;
  /* Was max-width: 55%. Two more 44px controls now sit beside it (round 1.1),
     so a percentage cap is no longer a guarantee: `min-width: 0` plus a shrink
     factor is, because a flex item with min-width 0 can always give back
     whatever the row needs, and the label inside already ellipsises. The
     percentage stays as the comfortable case, not as the safety net. */
  max-width: 40%; min-width: 0; flex: 0 1 auto;
}
body.ui-v2 .v2-live-pill .v2-live-dot {
  width: 8px; height: 8px; border-radius: var(--r-full);
  background: #F0B8AE; flex: none;
}
body.ui-v2 .v2-live-pill span.t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The column. One grid, three breakpoints, and the feed column is 600px. */
body.ui-v2 .shell-wrap {
  display: block; max-width: none; margin: 0; padding: 0;
}
body.ui-v2 main.shell {
  max-width: 600px; width: 100%; margin: 0 auto; padding: 0;
  flex: none;
}
@media (min-width: 1100px) {
  /* The top bar is gone at this width, so the column needs the breathing room
     the bar used to give it -- without this the composer sheet sits flush
     against the top of the window and reads as clipped. */
  body.ui-v2 main.shell { padding-top: var(--s5); }
}
body.ui-v2 .v2-leftrail { display: none; }

@media (min-width: 1100px) {
  /* The bar goes, so the sticky stack above the timeline is the tabs row and
     nothing else. Everything keyed on --v-head-h follows automatically: where
     the tabs pin, where an anchored post stops, where the chip's line sits. */
  body.ui-v2 { --v-head-h: 0px; }
  body.ui-v2 .v2-head { display: none; }
  body.ui-v2 .shell-wrap {
    display: grid;
    grid-template-columns: 72px minmax(0, 600px);
    gap: var(--s5);
    justify-content: center;
    padding: 0 var(--s4);
    align-items: start;
  }
  body.ui-v2.has-rail .shell-wrap { grid-template-columns: 72px minmax(0, 600px) 340px; }
  /* A guest has no left rail to navigate — they get the top bar and the
     Sign in door, exactly as today. Guest chrome is phase 2. */
  body.ui-v2.is-guest { --v-head-h: calc(52px + env(safe-area-inset-top)); }
  body.ui-v2.is-guest .v2-head { display: flex; }
  body.ui-v2.is-guest .shell-wrap { grid-template-columns: minmax(0, 640px); }
  body.ui-v2.is-guest.has-rail .shell-wrap { grid-template-columns: minmax(0, 640px) 340px; }
}
@media (min-width: 1280px) {
  body.ui-v2 .shell-wrap { grid-template-columns: 240px minmax(0, 600px); }
  body.ui-v2.has-rail .shell-wrap { grid-template-columns: 240px minmax(0, 600px) 340px; }
}

/* ---------- the left rail (desktop) -------------------------------------- */

/* The rail's own layout lives INSIDE the breakpoint. Setting `display: flex`
   in a base rule here silently beat the `display: none` above it -- same
   specificity, later wins -- and the whole rail painted down the top of every
   phone screen. Nothing below 1100 may give this element a display value. */
@media (min-width: 1100px) {
  body.ui-v2 .v2-leftrail {
    position: sticky; top: 0; align-self: start;
    max-height: 100vh; overflow-y: auto;
    padding: var(--s5) 0 var(--s4);
    display: flex; flex-direction: column; gap: var(--s1);
  }
}
body.ui-v2 .v2-rail-mark {
  display: flex; align-items: center; gap: var(--s2);
  color: var(--v-ink); text-decoration: none;
  font-family: 'Manrope', system-ui, sans-serif; font-weight: 700;
  font-size: var(--t-body);
  padding: var(--s2) var(--s3); margin-bottom: var(--s3);
  min-height: 44px;
}
body.ui-v2 .v2-rail-mark .khatam { width: 24px; height: 24px; color: var(--v-gold); flex: none; }
body.ui-v2 .v2-rail-link {
  display: flex; align-items: center; gap: var(--s3);
  min-height: 48px; padding: 0 var(--s3);
  border-radius: var(--r-full);
  color: var(--v-ink); text-decoration: none;
  font-size: var(--t-body); font-weight: 600;
  position: relative;
}
body.ui-v2 .v2-rail-link:hover { background: rgba(19,26,32,.05); }
:root[data-theme="dark"] body.ui-v2 .v2-rail-link:hover { background: rgba(231,228,221,.07); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body.ui-v2 .v2-rail-link:hover { background: rgba(231,228,221,.07); }
}
body.ui-v2 .v2-rail-link .v2-icon { width: 24px; height: 24px; flex: none; color: var(--v-ink-soft); }
body.ui-v2 .v2-rail-link.is-here { font-weight: 700; color: var(--v-green-ink); }
body.ui-v2 .v2-rail-link.is-here .v2-icon { color: var(--v-green-ink); }
body.ui-v2 .v2-rail-post {
  margin-top: var(--s3);
  display: flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: 48px; border-radius: var(--r-full);
  background: var(--v-green-btn); color: #fff;
  font-size: var(--t-body); font-weight: 700; text-decoration: none;
  border: 0; cursor: pointer;
}
body.ui-v2 .v2-rail-post .v2-icon { width: 20px; height: 20px; color: #fff; }
body.ui-v2 .v2-rail-foot {
  margin-top: auto; padding: var(--s4) var(--s3) 0;
  font-size: var(--t-meta); color: var(--v-ink-soft);
  display: flex; flex-direction: column; gap: 0;
}
body.ui-v2 .v2-rail-foot a {
  color: var(--v-ink-soft);
  display: flex; align-items: center; min-height: 44px;
}
/* 1100 to 1279: icons only. The label is still in the DOM for screen readers. */
@media (min-width: 1100px) and (max-width: 1279.98px) {
  body.ui-v2 .v2-rail-link .t,
  body.ui-v2 .v2-rail-post .t,
  body.ui-v2 .v2-rail-mark .t,
  body.ui-v2 .v2-rail-foot { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  body.ui-v2 .v2-rail-link,
  body.ui-v2 .v2-rail-post { justify-content: center; padding: 0; width: 48px; }
  body.ui-v2 .v2-rail-mark { justify-content: center; padding: var(--s2) 0; }
  body.ui-v2 .v2-rail-foot { display: none; }
}

/* ---------- the bottom tab bar (phone) ----------------------------------- */

body.ui-v2 .v2-tabbar { display: none; }
@media (max-width: 1099.98px) {
  body.ui-v2.is-member .v2-tabbar {
    display: grid; grid-template-columns: repeat(5, 1fr);
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
    background: var(--v-card);
    border-top: 1px solid var(--v-line);
    padding-bottom: env(safe-area-inset-bottom);
    align-items: center;
  }
  /* The bar is 56px of chrome; each destination is a 48px target inside it. */
  body.ui-v2.is-member .v2-tab {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 2px; min-height: 56px; text-decoration: none;
    color: var(--v-ink-soft); font-size: var(--t-meta); font-weight: 600;
    position: relative;
  }
  body.ui-v2.is-member .v2-tab .v2-icon { width: 24px; height: 24px; }
  body.ui-v2.is-member .v2-tab.is-here { color: var(--v-green-ink); }
  body.ui-v2.is-member .v2-tabbar-post {
    display: flex; align-items: center; justify-content: center;
    width: 48px; height: 48px; margin: 0 auto;
    border-radius: var(--r-full);
    background: var(--v-green-btn); color: #fff;
  }
  body.ui-v2.is-member .v2-tabbar-post .v2-icon { width: 24px; height: 24px; color: #fff; }
  body.ui-v2.is-member .v2-tab-dot {
    position: absolute; top: 8px; left: 50%; margin-left: 6px;
    width: 8px; height: 8px; border-radius: var(--r-full);
    background: var(--v-gold-deep);
  }
  /* The fixed bar must never cover the last post or the footer's last line.
     57, NOT 56 (round 1.4, item 7): each tab is min-height 56px and the bar
     carries a 1px border-top, so the bar occupies 57px plus the inset and the
     old reservation was one pixel short of it. The same 57 is what the overflow
     sheet above sits on, so the two numbers move together. */
  body.ui-v2.is-member { padding-bottom: calc(57px + env(safe-area-inset-bottom)); }
}

/* ---------- the composer, one row until it is used ----------------------- */

body.ui-v2 form#compose.composer {
  background: var(--v-card);
  border: 0; border-bottom: 1px solid var(--v-line);
  border-radius: 0;
  box-shadow: none;
  padding: var(--s3) var(--s4);
  margin: 0;
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  gap: var(--s3);
  align-items: start;
}
body.ui-v2 .v2-composer-avatar {
  width: 44px; height: 44px; border-radius: var(--r-full);
  object-fit: cover; grid-row: 1; flex: none;
  display: flex; align-items: center; justify-content: center;
}
body.ui-v2 span.v2-composer-avatar {
  background: var(--v-green-deep); color: #F4F6F1;
  font-family: 'Manrope', system-ui, sans-serif; font-weight: 700; font-size: var(--t-body);
}
body.ui-v2 form#compose.composer textarea[name="body"] {
  grid-column: 2;
  width: 100%; box-sizing: border-box;
  border: 0; background: transparent; color: var(--v-ink);
  font: inherit; font-size: var(--t-body); line-height: 1.5;
  padding: 10px 0; margin: 0;
  resize: none; overflow: hidden;
  min-height: 44px; height: 44px;
  transition: height .14s ease;
}
body.ui-v2 form#compose.composer textarea[name="body"]::placeholder { color: var(--v-ink-soft); }
body.ui-v2 form#compose.composer textarea[name="body"]:focus { outline: none; }
body.ui-v2 form#compose.composer.is-open textarea[name="body"] { height: 108px; overflow: auto; }

/* Everything below the one row is folded away until the composer is open.
   Folded, not removed: composer-v2.js opens on focus and closes again ONLY
   when the box is empty, so a click on Post can never land on a control that
   a blur has just taken away. */
body.ui-v2 form#compose.composer .v2-composer-more {
  grid-column: 2;
  display: none;
}
body.ui-v2 form#compose.composer.is-open .v2-composer-more { display: block; }
body.ui-v2 form#compose.composer #photo-preview { grid-column: 2; }
body.ui-v2 form#compose.composer .row {
  display: flex; align-items: center; gap: var(--s2);
  flex-wrap: wrap; margin: 0; padding-top: var(--s2);
  border-top: 1px solid var(--v-line);
}
body.ui-v2 form#compose.composer .attach-label {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0; border-radius: var(--r-full);
  color: var(--v-green-ink); cursor: pointer; border: 0; background: none;
  font-size: 0; /* the words move to aria-label; the icon is the control */
}
body.ui-v2 form#compose.composer .attach-label .v2-icon,
body.ui-v2 form#compose.composer .attach-label .nav-icon { width: 22px; height: 22px; color: var(--v-green-ink); }
body.ui-v2 form#compose.composer .attach-label:hover { background: rgba(22,101,52,.08); }
body.ui-v2 form#compose.composer .v2-composer-spacer { flex: 1 1 auto; }
body.ui-v2 form#compose.composer .js-charcount {
  font-size: var(--t-meta); color: var(--v-ink-soft); font-variant-numeric: tabular-nums;
}
body.ui-v2 form#compose.composer .js-charcount.near-limit { color: var(--v-danger); font-weight: 700; }
body.ui-v2 form#compose.composer select#visibility {
  font-size: var(--t-meta); font-weight: 600;
  color: var(--v-green-ink); background: transparent;
  border: 1px solid var(--v-line); border-radius: var(--r-full);
  min-height: 44px; padding: 0 var(--s3);
}
body.ui-v2 form#compose.composer button[type="submit"].btn {
  background: var(--v-green-btn); color: #fff;
  border: 0; border-radius: var(--r-full);
  min-height: 44px; padding: 0 var(--s5);
  font-size: var(--t-ui); font-weight: 700; cursor: pointer;
}
body.ui-v2 form#compose.composer label[for="visibility"] { font-size: var(--t-meta); color: var(--v-ink-soft); }

/* The 26-word privacy essay is now an info control, not a preamble. Native
   <details>: no script, so no policy can strip it, and it is keyboard
   reachable by default. This property blocks inline scripts. */
body.ui-v2 .v2-vis-info { margin: var(--s2) 0 0; }
body.ui-v2 .v2-vis-info > summary {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: var(--s2);
  min-height: 44px; font-size: var(--t-meta); color: var(--v-ink-soft);
}
body.ui-v2 .v2-vis-info > summary::-webkit-details-marker { display: none; }
body.ui-v2 .v2-vis-info > summary .v2-icon { width: 16px; height: 16px; }
body.ui-v2 .v2-vis-info .vis-note {
  font-size: var(--t-meta); line-height: 1.5; color: var(--v-ink-soft);
  margin: 0 0 var(--s2); max-width: 52ch;
}
body.ui-v2 .first-post-note {
  grid-column: 2; font-size: var(--t-meta); color: var(--v-ink-soft);
  border: 0; background: none; padding: 0 0 var(--s2); margin: 0;
}
body.ui-v2 .first-post-note p { margin: 0 0 var(--s2); }

/* ---------- the Sands ring row ------------------------------------------- */

body.ui-v2 .sands-tray {
  background: transparent; border: 0; border-bottom: 1px solid var(--v-line);
  border-radius: 0; box-shadow: none; padding: var(--s3) var(--s4); margin: 0;
}
body.ui-v2 .sands-tray-strip {
  display: flex; gap: var(--s4); overflow-x: auto; list-style: none;
  margin: 0; padding: 0; scrollbar-width: none;
}
body.ui-v2 .sands-tray-strip::-webkit-scrollbar { display: none; }
body.ui-v2 .sands-tray .sands-tile { flex: none; margin: 0; }
body.ui-v2 .sands-tray .sands-tile-link {
  display: flex; flex-direction: column; align-items: center; gap: var(--s1);
  text-decoration: none; color: var(--v-ink-soft); font-size: var(--t-meta);
  width: 64px; min-height: 44px;
}
body.ui-v2 .sands-tray .sands-tile-name {
  max-width: 64px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Sep 20 2026 fix: this rule shrank the RING CONTAINER to 52px, the same
   size as the photo inside it (`.sands-ring > img`, Part 1, 52px), so the
   ring drew directly behind the photo and every state (drain, kept, own,
   none, preparing) was invisible under every photo at every viewport in
   both themes. v1's relationship — a 64px ring holding a 52px photo, a 6px
   collar all round for the ring to show in — is restored here rather than
   left to Part 1's unscoped `.sands-ring`, so this stays correct even if a
   future v2-only change touches ring sizing again. */
body.ui-v2 .sands-tray .sands-ring { width: 64px; height: 64px; }
body.ui-v2 .sands-tray .sands-ring img { object-fit: cover; }
body.ui-v2 .sands-tray-note, body.ui-v2 .sands-tray-empty {
  font-size: var(--t-meta); color: var(--v-ink-soft); margin: var(--s2) 0 0;
}

/* ---------- tabs --------------------------------------------------------- */

/* The tabs row pins UNDER the top bar wherever the top bar is there, and at 0
   where it is not. It reads --v-head-h rather than repeating 52px, so the one
   token below governs the pin, the scroll padding, the anchor offset and the
   day chip's line together (round 1.1). It used to hard-code 52px in a
   max-width query, which was right for a member on a phone and wrong for a
   GUEST at 1440 -- guests keep the top bar at every width, so the tabs row
   pinned to 0 underneath it and the whole row sat behind the green bar. */
body.ui-v2 nav.tabs {
  display: flex; align-items: stretch; gap: 0;
  background: var(--v-paper);
  border: 0; border-bottom: 1px solid var(--v-line);
  border-radius: 0; padding: 0; margin: 0;
  /* The literal fallback is the failure-mode control, not decoration: if
     --v-head-h is ever undefined again (round 1.2's unterminated comment), the
     row still pins under a 52px bar instead of silently computing `top: auto`
     and scrolling away. env() inside the fallback keeps the notch inset.

     Do not write a comment-close sequence inside a CSS comment to talk about
     one. Writing this note cost a build: the quoted sequence closed the comment
     early, the prose after it became raw CSS, and the declaration block's error
     recovery swallowed `position: sticky` up to the next semicolon -- turning
     the row static at BOTH widths, which is worse than the defect being fixed.
     Say "an unterminated comment" in words. */
  position: sticky;
  top: var(--v-head-h, calc(52px + env(safe-area-inset-top)));
  z-index: 30;
}
body.ui-v2 nav.tabs a {
  flex: 1 1 0; display: flex; align-items: center; justify-content: center;
  min-height: var(--v-tabs-h); text-decoration: none;
  font-size: var(--t-ui); font-weight: 600; color: var(--v-ink-soft);
  border: 0; background: none; position: relative;
}
body.ui-v2 nav.tabs a.active { color: var(--v-ink); font-weight: 700; }
body.ui-v2 nav.tabs a.active::after {
  content: ''; position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 0; width: 56px; height: 3px; border-radius: 3px 3px 0 0;
  background: var(--v-green-ink);
}
body.ui-v2 nav.tabs .hint { display: none; }

/* ---------- the timeline, and the day spine ------------------------------ */

body.ui-v2 ul.timeline {
  list-style: none; margin: 0; padding: 0;
  background: transparent; border: 0;
}

/* THE SIGNATURE. Day markers label the order; they never create it. Nothing
   is grouped, nothing is reordered — the marker is a <li> the server writes
   between two posts whose UTC date differs, and removing every marker would
   leave the feed in exactly the same order. */
/* NOT STICKY, and that is a measured decision rather than an omission.
   Every marker shares one containing block (ul.timeline), so every marker
   sticks at the SAME offset and nothing pushes anything out: an arriving
   marker's opaque band slices the stuck one in half for the ~34px of scroll
   it takes to cover it. Caught live at 390 in dark; the frame is kept beside
   the other captures as DEFECT-sticky-marker-overlap-390-dark.png.
   The fix a bounded sticky needs is a per-day wrapper element, and that would
   (a) group the timeline, which this feed's covenant does not allow, and
   (b) move #guest-gate out of ul.timeline, where metered-gate.js and a
   standing Tab ruling both require it to be a direct child. So the spine
   scrolls with the feed. It still does its whole job: it labels the order, in
   place, server-rendered, and removing every marker would leave the list in
   exactly the same sequence. Sticky can come back in phase 2 with a ruling on
   the wrapper. */
body.ui-v2 li.v2-day {
  background: var(--v-paper);
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s5) var(--s4) var(--s2);
  border-bottom: 1px solid var(--v-line);
  scroll-margin-top: calc(var(--v-stick) + var(--s2));
}
/* THE GUTTER QUESTION, ANSWERED BY MEASURING RATHER THAN BY TASTE. The brief
   allowed the marker's label to move into the 44px avatar gutter at 1100 and
   up if it fit cleanly. It does not: at 13px Manrope 700, uppercase, with the
   .06em tracking this label carries, "Today" measures 52.9px and "Yesterday"
   95.6px against a 44px column. Two of the three labels overflow the gutter by
   more than they occupy, so the label stays full width at every breakpoint. */
body.ui-v2 li.v2-day:first-child { padding-top: var(--s3); }
body.ui-v2 li.v2-day .d {
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-meta); font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--v-green-ink);
  white-space: nowrap;
}
body.ui-v2 li.v2-day .r { flex: 1 1 auto; height: 1px; background: var(--v-line); }

/* ---------- the post row ------------------------------------------------- */

/* padding-block was 12/12 and is now 4/8. It is not a cosmetic tightening: the
   row header below became a real 44px hit row (round 1.1, item 4), which added
   22px, and this plus the action row's margin gives back 20 of them, so the row
   is within 2px of its phase-1 height with every target legal. The avatar and
   the name now sit on the same band, which is also what X does. */
body.ui-v2 li.prow {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  column-gap: var(--s3);
  padding: var(--s1) var(--s4) var(--s2);
  border-bottom: 1px solid var(--v-line);
  background: transparent;
  list-style: none;
  /* An anchored post stops BELOW the sticky chrome, not under it (item 2). */
  scroll-margin-top: calc(var(--v-stick) + var(--s2));
}
/* ROUND 1.4, ITEM 6 -- THE ROW BODY OPENS THE POST.
   The pointer is added by public/js/v2.js, on the body, only once the handler
   is actually bound. A cursor written unconditionally in the stylesheet would
   promise a click to every visitor whose script was stripped or never arrived,
   which on the one property in the estate that serves `script-src 'self'` with
   no 'unsafe-inline' is not a theoretical group. The timestamp link stays the
   keyboard and no-script route either way, so nothing here is required. */
body.ui-v2.v2-rowclick li.prow { cursor: pointer; }
/* A member's own words are selectable text and say so. Links and buttons inside
   the row already carry their own pointer from the user agent and from .act. */
body.ui-v2.v2-rowclick li.prow .prow-body,
body.ui-v2.v2-rowclick li.prow .quoted { cursor: text; }
body.ui-v2 li.prow .prow-note {
  grid-column: 1 / -1;
  font-size: var(--t-meta); color: var(--v-ink-soft);
  display: flex; align-items: center; gap: var(--s2);
  margin: 0 0 var(--s2);
}
body.ui-v2 li.prow .prow-note .v2-icon { width: 15px; height: 15px; flex: none; }
body.ui-v2 li.prow .prow-note a { color: inherit; }
body.ui-v2 li.prow .prow-avatar { grid-column: 1; grid-row: span 9; }
body.ui-v2 li.prow .avatar {
  width: 44px; height: 44px; border-radius: var(--r-full);
  object-fit: cover; display: block;
}
body.ui-v2 li.prow .avatar--initial {
  display: flex; align-items: center; justify-content: center;
  background: var(--v-green-deep); color: #F4F6F1;
  font-family: 'Manrope', system-ui, sans-serif; font-weight: 700; font-size: var(--t-body);
}
body.ui-v2 li.prow .uo-ring { box-shadow: 0 0 0 2px var(--v-paper), 0 0 0 4px var(--v-gold); }

/* Name, handle and time on ONE line, and that line IS THE HIT ROW (round 1.1,
   item 4). Both seats measured the name at 22px and the timestamp at 21px, in
   a row whose painted height was 22px, so neither link could be tapped
   reliably on the device most members hold.

   IT IS DONE WITH HEIGHT, NOT WITH AN INVISIBLE ::after STRIP, and that is a
   measurement rather than a preference. This property's idiom elsewhere is a
   transparent ::after that grows a compact control past its paint. Here it
   would not be safe: a 44px strip centred on 22px of text overhangs 11px above
   and below, and directly below this row is .prow-body, which carries real
   links (mentions, hashtags, URLs). A strip that steals the top 11px of a
   member's first line is a worse defect than the one it fixes.

   So the row becomes 44px tall and the space is paid for out of li.prow's own
   vertical padding above. Net effect on row height, measured: within 2px. The
   text does not move a pixel: align-items goes baseline -> center only because
   items of different heights cannot share a baseline usefully. */
body.ui-v2 li.prow .prow-head {
  grid-column: 2;
  display: flex; align-items: center; gap: var(--s2);
  min-width: 0; margin-bottom: 0; min-height: 44px;
}
/* ROUND 1.4, ITEM 5 -- `text-overflow: ellipsis` DOES NOTHING ON A FLEX BOX.
   This element carried overflow:hidden, text-overflow:ellipsis AND
   display:inline-flex. text-overflow applies to the inline content of a BLOCK
   container; a flex container's content is flex items, so the property was
   inert and a 40-character display name was simply cut mid-glyph at the 55%
   cap, with no ellipsis to say anything had been removed.
   The 44px hit row (round 1.1, item 4) was the reason for the flex box, and it
   is kept WITHOUT one: `line-height: 44px` on a block gives a single line of
   text the same 44px box and the same optical centring, and a block is what
   text-overflow needs. Proven by pixel crop on a 40-character fixture name. */
body.ui-v2 li.prow .prow-name {
  font-family: 'Manrope', system-ui, sans-serif;
  font-weight: 700; font-size: var(--t-ui); color: var(--v-ink);
  text-decoration: none; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-width: 55%;
  display: block; min-width: 0; min-height: 44px; line-height: 44px;
}
body.ui-v2 li.prow .prow-handle,
body.ui-v2 li.prow .prow-when {
  font-size: var(--t-ui); color: var(--v-ink-soft);
  white-space: nowrap; text-decoration: none; min-width: 0;
}
body.ui-v2 li.prow .prow-handle { overflow: hidden; text-overflow: ellipsis; }
body.ui-v2 li.prow .prow-when {
  margin-left: auto; padding-left: var(--s2);
  display: inline-flex; align-items: center; min-height: 44px;
}
body.ui-v2 li.prow .prow-when a {
  color: inherit; text-decoration: none;
  display: inline-flex; align-items: center; min-height: 44px;
}
body.ui-v2 li.prow .vis-chip {
  font-size: var(--t-meta); font-weight: 700; color: var(--v-ink-soft);
  border: 1px solid var(--v-line); border-radius: var(--r-full);
  padding: 1px var(--s2); white-space: nowrap;
}
body.ui-v2 li.prow .prow-body {
  grid-column: 2;
  font-size: var(--t-body); line-height: 1.5; color: var(--v-ink);
  /* Member text is three properties, not one (Sep 10 2026). */
  white-space: pre-wrap; unicode-bidi: plaintext; overflow-wrap: anywhere;
}
/* SIX, and the server-side predicate in partials/v2/post.blade.php moved with
   it in the same edit. The two are one decision: the "Show more" control
   renders exactly when this class does, so a CSS-only change would clamp posts
   that have no control under them. Permalinks never clamp. */
body.ui-v2 li.prow .prow-body.clamped {
  display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden;
}
/* ROUND 1.3: `.is-sm` is a clamp that belongs to the phone column only. It is
   set by the server when a body is six lines or more at 38 characters a line
   and fewer than six at 58 -- i.e. long for a phone, ordinary for a desktop
   column. Above 640 the clamp stands down and its control is hidden, because a
   "Show more" under a body that is already whole is a control that does
   nothing. See partials/v2/post.blade.php: the class it sets is the SAME
   `clamped`, so clamp.js is unchanged and one tap unclamps at every width. */
@media (min-width: 640px) {
  body.ui-v2 li.prow .prow-body.clamped.is-sm {
    display: block; -webkit-line-clamp: none; overflow: visible;
  }
  body.ui-v2 li.prow .clamp-toggle.is-sm { display: none; }
}
body.ui-v2 li.prow .clamp-toggle {
  grid-column: 2; display: inline-flex; align-items: center;
  min-height: 44px; font-size: var(--t-ui); color: var(--v-green-ink);
}
body.ui-v2 li.prow .deleted-stub { color: var(--v-ink-soft); font-style: italic; }

/* Media: fixed aspect boxes at the media radius, so the column never jumps. */
body.ui-v2 li.prow .post-images {
  grid-column: 2; margin: var(--s3) 0 0;
  display: grid; gap: 2px;
  border-radius: var(--r-media); overflow: hidden;
  border: 1px solid var(--v-line);
}
body.ui-v2 li.prow .post-images.n1 { grid-template-columns: 1fr; }
body.ui-v2 li.prow .post-images.n2 { grid-template-columns: 1fr 1fr; }
body.ui-v2 li.prow .post-images.n3 { grid-template-columns: 1fr 1fr; }
body.ui-v2 li.prow .post-images.n3 a:first-child { grid-row: span 2; }
body.ui-v2 li.prow .post-images.n4 { grid-template-columns: 1fr 1fr; }
body.ui-v2 li.prow .post-images a { display: block; position: relative; }
body.ui-v2 li.prow .post-images img {
  width: 100%; height: 100%; display: block;
  object-fit: cover; max-width: none !important;
  /* ROUND 1.3 -- A PROPERTY THIS RULE NEVER SET WAS STILL BEING SET.
     The carried v1 rule `.post-images img { max-height: 420px }` is (0,1,1)
     and this rule is (0,3,3), so it wins every property it NAMES -- and
     max-height was not one of them. Measured at 1440 on the three-photo
     fixture: the tall tile's <a> is 254x513 and the <img> inside it was
     254x420, a 93px band of empty paper inside a bordered grid cell. Invisible
     at 390, where the same tile is 301px and never reaches the cap, which is
     why two review rounds on a phone could not see it. */
  max-height: none;
}
/* A flat pixel cap cannot serve both widths: the same height is a modest crop
   on a 600px desktop column and most of a phone screen. min() of a pixel cap
   and a share of the viewport settles it, which is the same shape the Sep 9
   round landed on for v1. */
body.ui-v2 li.prow .post-images.n1 a {
  aspect-ratio: 16 / 10; max-height: min(320px, 46vh);
}
body.ui-v2 li.prow .post-images.n2 a,
body.ui-v2 li.prow .post-images.n4 a { aspect-ratio: 1 / 1; }
body.ui-v2 li.prow .post-images.n3 a:first-child { aspect-ratio: 1 / 2.02; }
body.ui-v2 li.prow .post-images.n3 a:not(:first-child) { aspect-ratio: 1 / 1; }
/* Public-ready round, Sep 19 2026: this container carries its OWN
   overflow:hidden (line 3290, for the rounded grid corners) at higher
   specificity than the Part-1 fix above, so it needs its own rule. Measured
   live on a real photo post: the sitewide ring draws 2-6px OUTSIDE the
   border box, and a single-image post's <a> fills its container exactly,
   so a keyboard user tabbing to the photo saw no focus indicator at all --
   the whole ring fell outside the clip. Same idiom as the composer textarea
   and the tab row: draw it INSIDE with a negative offset. */
body.ui-v2 li.prow .post-images a:focus-visible {
  outline: 3px solid var(--v-ink);
  outline-offset: -3px;
  box-shadow: none;
}

/* A quieter link card: hairline, no fill, the domain last and small. */
body.ui-v2 li.prow .link-card {
  grid-column: 2; margin: var(--s3) 0 0;
  display: block; text-decoration: none; color: inherit;
  border: 1px solid var(--v-line); border-radius: var(--r-media);
  overflow: hidden; background: transparent;
}
body.ui-v2 li.prow .link-card { display: flex; align-items: stretch; }
/* CAPPED AT 96px ON A PHONE (round 1.1, item 10). The card was as tall as its
   text wanted to be; a long title ran it past 130px, and the house account
   posts links all day, so that is most of the feed. A square thumbnail whose
   side IS the cap, two lines of title, then the domain.
   object-fit is set explicitly because a missing object-fit is not neutral:
   the browser default is `fill`, which stretches. That is the Sep 9 avatar
   defect, one component over. */
body.ui-v2 li.prow .link-card { max-height: 96px; overflow: hidden; }
body.ui-v2 li.prow .link-card-thumb {
  width: 96px; height: 96px; flex: none;
  object-fit: cover; display: block;
  border-right: 1px solid var(--v-line); max-width: none;
}
body.ui-v2 li.prow .link-card-text {
  display: flex; flex-direction: column; justify-content: center;
  padding: var(--s3); min-width: 0; flex: 1 1 auto;
}
/* QUIETER, at every width. A full-bleed 16:9 hero was the first thing tried and
   it is the opposite of quiet: measured, it cost 328px of row on a 600px column
   and made a link louder than the words above it. The house account posts links
   all day, so that is most of the feed. A square thumbnail beside two lines of
   text is 120px and reads as a reference, which is what a link is. */
@media (min-width: 640px) {
  body.ui-v2 li.prow .link-card { max-height: 112px; }
  body.ui-v2 li.prow .link-card-thumb { width: 112px; height: 112px; }
}
/* TWO LINES OF TITLE, and no more: the cap above is a ceiling, and text that
   simply overflowed it would be a sentence cut off mid-stroke with no ellipsis
   to say so. */
body.ui-v2 li.prow .link-card-title {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--t-ui); font-weight: 600; color: var(--v-ink);
  line-height: 1.35; overflow-wrap: anywhere;
}
body.ui-v2 li.prow .link-card-meta {
  display: block; margin-top: 2px;
  font-size: var(--t-meta); color: var(--v-ink-soft);
}
body.ui-v2 li.prow .quoted {
  grid-column: 2; margin: var(--s3) 0 0; padding: var(--s3);
  border: 1px solid var(--v-line); border-radius: var(--r-media);
}
body.ui-v2 li.prow .quoted .who { font-size: var(--t-meta); color: var(--v-ink-soft); margin-bottom: var(--s1); }
body.ui-v2 li.prow .quoted .body { font-size: var(--t-ui); white-space: pre-wrap; unicode-bidi: plaintext; }

/* ---------- the action row: icons, in X's order -------------------------- */

/* margin-top was 8px and is now 0: the controls in this row are 44px tall with
   their icons centred, so they already carry their own separation, and the 8px
   is part of what pays for the 44px header row above (round 1.1, item 4). */
body.ui-v2 li.prow .actions {
  grid-column: 2; margin: 0;
  display: flex; align-items: center; gap: 0;
  border: 0; padding: 0;
}
body.ui-v2 li.prow .actions form { margin: 0; display: contents; }
/* ROUND 1.4, ITEM 3 -- ONE RHYTHM FOR ONE COMPONENT.
   MEASURED at 390: a guest's four controls sat at x 72 / 156 / 240 / 330 and a
   member's at 72 / 116 / 160 / 210. Same component, two spacings, because
   `space-between` only SPREADS when nothing absorbs the free space -- and on a
   member's row the v1 rule `details.more { margin-left: auto }` absorbs all of
   it, so the first four pack left and More goes to the edge. A guest has no
   More, so the same declaration spread them across the column.
   flex-start makes the packed rhythm the rule rather than a side effect, and
   More still goes right on its own margin. The guest's hidden repost spacer is
   gone from the markup in the same edit: it existed only to keep a column
   alignment that space-between was inventing. */
body.ui-v2 li.prow .actions { max-width: 420px; justify-content: flex-start; }
body.ui-v2 li.prow .act {
  display: inline-flex; align-items: center; justify-content: flex-start;
  gap: 6px; min-width: 44px; min-height: 44px; padding: 0 var(--s2);
  flex: 0 0 auto;
  border: 0; background: none; cursor: pointer;
  color: var(--v-ink-soft); text-decoration: none;
  font-size: var(--t-meta); font-weight: 600;
  border-radius: var(--r-full);
  font-variant-numeric: tabular-nums;
}
body.ui-v2 li.prow .act .v2-icon,
body.ui-v2 li.prow .act .khatam { width: 20px; height: 20px; flex: none; color: currentColor; }
body.ui-v2 li.prow .act .label {
  /* The words stay in the DOM — like.js and repost.js write into them — and
     the visible label is now the icon plus the count. */
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}
body.ui-v2 li.prow .act .n { font-size: var(--t-meta); }
body.ui-v2 li.prow .act:hover { color: var(--v-ink); }
body.ui-v2 li.prow .act.like-btn.liked { color: var(--v-gold-deep); }
body.ui-v2 li.prow .act.like-btn.liked .khatam { color: var(--v-gold); stroke: var(--v-gold-deep); }
body.ui-v2 li.prow .act.repost-btn.reposted { color: var(--v-green-ink); }
body.ui-v2 li.prow .act[disabled] { opacity: .55; }

/* The one ornament: eight short rays open and fade when a post is
   appreciated. 180ms, one shot, and off entirely under reduced motion. */
body.ui-v2 li.prow .act.like-btn { position: relative; }
body.ui-v2 li.prow .act.like-btn .burst {
  position: absolute; left: 14px; top: 50%; width: 0; height: 0;
  pointer-events: none; opacity: 0;
}
body.ui-v2 li.prow .act.like-btn .burst i {
  position: absolute; left: 0; top: 0;
  width: 2px; height: 7px; margin: -3.5px -1px;
  border-radius: var(--r-full); background: var(--v-gold);
  transform-origin: 50% 50%;
}
body.ui-v2 li.prow .act.like-btn .burst i:nth-child(1) { transform: rotate(0deg)   translateY(-8px); }
body.ui-v2 li.prow .act.like-btn .burst i:nth-child(2) { transform: rotate(45deg)  translateY(-8px); }
body.ui-v2 li.prow .act.like-btn .burst i:nth-child(3) { transform: rotate(90deg)  translateY(-8px); }
body.ui-v2 li.prow .act.like-btn .burst i:nth-child(4) { transform: rotate(135deg) translateY(-8px); }
body.ui-v2 li.prow .act.like-btn .burst i:nth-child(5) { transform: rotate(180deg) translateY(-8px); }
body.ui-v2 li.prow .act.like-btn .burst i:nth-child(6) { transform: rotate(225deg) translateY(-8px); }
body.ui-v2 li.prow .act.like-btn .burst i:nth-child(7) { transform: rotate(270deg) translateY(-8px); }
body.ui-v2 li.prow .act.like-btn .burst i:nth-child(8) { transform: rotate(315deg) translateY(-8px); }
/* Keyed on .bursting, which v2.js adds for the length of the animation, NOT on
   .liked. The server renders .liked on every post the member appreciated
   months ago, so an animation bound to that state would set eight rays going
   on half the feed at page load. */
@media (prefers-reduced-motion: no-preference) {
  body.ui-v2 li.prow .act.like-btn.bursting .burst { animation: v2burst 180ms ease-out 1; }
  body.ui-v2 li.prow .act.like-btn.bursting .khatam { animation: v2star 180ms ease-out 1; }
}
body.ui-v2 li.prow .act .v2-share-said { font-size: var(--t-meta); }
@keyframes v2burst {
  0%   { opacity: 1; transform: scale(.35); }
  100% { opacity: 0; transform: scale(1.25); }
}
@keyframes v2star {
  0%   { transform: scale(.7); }
  55%  { transform: scale(1.18); }
  100% { transform: scale(1); }
}

/* The overflow menu. Native <details>, exactly as v1: no script to strip. */
body.ui-v2 li.prow details.more { position: relative; display: inline-flex; }
body.ui-v2 li.prow details.more > summary {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: var(--r-full);
  color: var(--v-ink-soft);
}
body.ui-v2 li.prow details.more > summary::-webkit-details-marker { display: none; }
body.ui-v2 li.prow details.more > summary .v2-icon { width: 20px; height: 20px; }
/* ROUND 1.4, ITEM 1 -- THE MENU WAS UNDER THE TAB BAR, AND THE LAYER WAS ONLY
   HALF OF IT.

   MEASURED at 390x724: the panel is z-index 30 and .v2-tabbar is z-index 50, so
   the last rows of an open menu ("Block @handle") painted behind the bar and
   elementFromPoint at their centres returned the tab bar, not the button. Two
   separate defects wearing one symptom:

     LAYER. `details.more` is position:relative with z-index:auto, so it is NOT
     a stacking context and the panel's 30 competed with the bar's 50 in the
     ROOT context and lost. `[open]` now carries z-index 60 on the details
     itself, which both wins against the bar and gives the panel a context of
     its own, so no future number inside this block can leak out.

     GEOMETRY. A layer fix alone would paint the menu OVER the bar and leave the
     bottom of it off screen, which is the same item unreachable for a different
     reason. Below 1100 the panel is therefore a bottom sheet: fixed to the
     viewport, sitting on top of the tab bar's own height plus the safe-area
     inset, capped at 60vh and scrolling inside itself. Every row is on screen
     at 724 and at 844 whether the post is the first in the room or the last.

   `position: fixed` here is safe because nothing between this element and the
   document root carries transform, filter or perspective -- any of those would
   turn the viewport anchor into an ancestor anchor and silently put the sheet
   back inside the column. Checked, and the row-click handler in v2.js adds no
   transform either. */
body.ui-v2 li.prow details.more[open] { z-index: 60; }
body.ui-v2 li.prow details.more .menu {
  position: absolute; right: 0; top: 46px; z-index: 1;
  min-width: 232px; padding: var(--s2);
  background: var(--v-card); color: var(--v-ink);
  border: 1px solid var(--v-line); border-radius: var(--r-sheet);
  box-shadow: var(--v-shadow);
  display: flex; flex-direction: column; align-items: stretch;
  text-align: left;
  /* Above 1100 there is no tab bar, but a menu opened from a row near the foot
     of a tall window still has to be readable without the document growing
     under it. Capped and scrollable there too. */
  max-height: calc(100vh - 120px); overflow-y: auto;
}
/* Above 1100 the panel is still anchored to its own row, so a menu opened from
   a row near the foot of the window would hang below the fold. v2.js measures
   the space and adds .is-up, which is the "open upward" half of item 1; below
   1100 the fixed sheet above already answers it and this class is inert there
   because `top` and `bottom` are both restated in that media query. Keyed on a
   class rather than a media query in JS so the stylesheet owns both widths. */
body.ui-v2 li.prow details.more.is-up .menu { top: auto; bottom: 46px; }
@media (max-width: 1099.98px) {
  body.ui-v2 li.prow details.more .menu {
    position: fixed; left: var(--s3); right: var(--s3); top: auto;
    bottom: calc(57px + env(safe-area-inset-bottom) + var(--s3));
    min-width: 0; max-height: 60vh; overflow-y: auto;
    padding: var(--s3);
  }
  /* No guest branch, and that is deliberate rather than missing: the overflow
     menu is inside @auth in partials/v2/post.blade.php, so `body.is-member` is
     true wherever this selector can match. A `:not(.is-member)` rule here would
     be a line that can never apply -- the class of dead CSS this file has been
     bitten by before. */
}
body.ui-v2 li.prow details.more .menu a,
body.ui-v2 li.prow details.more .menu button,
body.ui-v2 li.prow details.more .menu summary {
  display: flex; align-items: center;
  min-height: 44px; padding: 0 var(--s3);
  border: 0; background: none; cursor: pointer;
  font: inherit; font-size: var(--t-ui); color: var(--v-ink);
  text-align: left; text-decoration: none; border-radius: var(--r-ctl);
  width: 100%; box-sizing: border-box;
}
body.ui-v2 li.prow details.more .menu form { display: block; margin: 0; }
body.ui-v2 li.prow details.more .menu button.danger { color: var(--v-danger); }
body.ui-v2 li.prow details.more .menu summary { list-style: none; }
body.ui-v2 li.prow details.more .menu summary::-webkit-details-marker { display: none; }
body.ui-v2 li.prow details.more .report-reasons { padding-left: var(--s3); }

/* ---------- the suggestion module inside the timeline -------------------- */
/* Below 1100 only: at 1100 and up the right rail carries this and the module
   here would be a second copy of it. Server-rendered, never reordering. */
body.ui-v2 li.v2-inline-suggest {
  border-bottom: 1px solid var(--v-line);
  padding: var(--s4);
  background: var(--v-card);
}
@media (min-width: 1100px) { body.ui-v2 li.v2-inline-suggest { display: none; } }

/* ---------- the right rail ----------------------------------------------- */

body.ui-v2 .right-rail {
  display: none;
  background: transparent; border: 0; padding: 0; margin: 0;
}
@media (min-width: 1100px) {
  body.ui-v2 .right-rail {
    display: block; position: sticky; top: var(--s5);
    align-self: start; max-height: calc(100vh - var(--s7)); overflow-y: auto;
    padding: var(--s5) 0;
  }
  body.ui-v2 .right-rail::-webkit-scrollbar { width: 8px; }
  body.ui-v2 .right-rail::-webkit-scrollbar-thumb { background: var(--v-line); border-radius: var(--r-full); }
}
body.ui-v2 .v2-rail-card {
  background: var(--v-card); border: 1px solid var(--v-line);
  border-radius: var(--r-sheet); box-shadow: var(--v-shadow);
  padding: var(--s4); margin: 0 0 var(--s4);
}
body.ui-v2 .v2-rail-card h2 {
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-body); font-weight: 700; color: var(--v-ink);
  margin: 0 0 var(--s3);
}
body.ui-v2 .v2-sugg-row {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) 0; min-height: 56px;
}
body.ui-v2 .v2-sugg-row img.avatar, body.ui-v2 .v2-sugg-row .avatar {
  width: 40px; height: 40px; border-radius: var(--r-full); object-fit: cover; flex: none;
}
body.ui-v2 .v2-sugg-who { min-width: 0; flex: 1 1 auto; }
/* ROUND 1.4, ITEM 4 -- THE NAME LINK IS THE WHOLE WHO-BLOCK, NOT ITS FIRST
   LINE. It measured 138 x 22.5 in the 1440 right rail. The row-sized ::after
   below did make the real hit area 56px tall, but a control whose compliance
   only shows up under a second instrument gets re-reported every round (the
   same reasoning that took the action row's ::after strip out).
   So the anchor now CONTAINS both lines -- the display name and the handle --
   and is 44px tall painted. The handle was never a link and still is not; it is
   a span that moved inside one. One destination, one tab stop, one target. */
body.ui-v2 .v2-sugg-name {
  color: var(--v-ink); text-decoration: none;
  display: flex; flex-direction: column; justify-content: center;
  min-height: 44px; min-width: 0; overflow: hidden;
}
body.ui-v2 .v2-sugg-display {
  font-family: 'Manrope', system-ui, sans-serif; font-weight: 700;
  font-size: var(--t-ui); color: var(--v-ink);
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body.ui-v2 .v2-sugg-handle {
  font-size: var(--t-meta); color: var(--v-ink-soft);
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body.ui-v2 .v2-sugg-row form { margin: 0; flex: none; }
body.ui-v2 .v2-follow-btn {
  min-height: 44px; padding: 0 var(--s4);
  border-radius: var(--r-full); border: 1px solid var(--v-green-btn);
  background: transparent; color: var(--v-green-ink);
  font-size: var(--t-ui); font-weight: 700; cursor: pointer;
}
body.ui-v2 .v2-follow-btn:hover { background: rgba(22,101,52,.08); }
body.ui-v2 .v2-rail-note { font-size: var(--t-meta); color: var(--v-ink-soft); margin: var(--s2) 0 0; }

/* The suggestion module reuses partials/who-to-follow and partials/starterpack
   verbatim rather than re-querying, so it restyles THEIR markup here. An empty
   card is the "empty green pill" defect class: who-to-follow renders nothing at
   all when there is nobody to suggest, so a frame with no list inside it stands
   down rather than painting a bordered box around nothing. */
body.ui-v2 .v2-suggest:not(:has(ul)):not(:has(.starter)) { display: none; }
body.ui-v2 .v2-suggest h2 {
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-body); font-weight: 700; color: var(--v-ink);
  margin: 0 0 var(--s2); letter-spacing: 0;
}
body.ui-v2 .v2-suggest p { font-size: var(--t-meta); color: var(--v-ink-soft); margin: 0 0 var(--s3); }
body.ui-v2 .v2-suggest ul.timeline { margin: 0; padding: 0; }
/* ROUND 1.2 -- A 48px ROW, A 40px AVATAR, AND THE WHOLE ROW IS THE TARGET.
   Measured on the live rail before this: the li was 64px but the only things
   you could actually tap were a 47x24.3 name link and a 36x36 avatar. A
   suggestion is one decision about one person, so the row is one target.

   IT IS A STRETCHED LINK, NOT A WRAPPING ANCHOR, and that is forced by the
   markup: partials/who-to-follow is SHARED with v1 and carries a Follow
   <form> inside the row. An <a> may not contain a <form>, so the name link's
   ::after is grown to cover the row instead, and the Follow control is lifted
   above it. Nothing in the shared partial moves.

   The avatar attributes still say 36 because the shared partial says 36; the
   file behind them is the 40px derivative family, chosen in the blade off the
   same $uiV2 flag that already picks the srcset, so the v1 document keeps its
   exact bytes. !important is needed because the partial carries an inline
   style attribute, which nothing else can beat. */
body.ui-v2 .v2-suggest li.post {
  background: transparent; border: 0; box-shadow: none; border-radius: 0;
  padding: 0 !important; margin: 0 0 var(--s1);
  position: relative;
}
body.ui-v2 .v2-suggest li.post:last-child { margin-bottom: 0; }
body.ui-v2 .v2-suggest .byline { display: flex; align-items: center; gap: var(--s3); min-height: 48px; }
body.ui-v2 .v2-suggest .avatar {
  border-radius: var(--r-full); object-fit: cover; flex: none;
  width: 40px !important; height: 40px !important;
}
body.ui-v2 .v2-suggest .who { min-width: 0; flex: 1 1 auto; }
body.ui-v2 .v2-suggest .name {
  font-family: 'Manrope', system-ui, sans-serif; font-weight: 700;
  font-size: var(--t-ui) !important; line-height: 1.3;
}
body.ui-v2 .v2-suggest .name a { color: var(--v-ink); text-decoration: none; }
/* The row-sized target. It covers the li, so a tap anywhere that is not the
   Follow button opens the person's profile. */
body.ui-v2 .v2-suggest .name a::after {
  content: ''; position: absolute; inset: 0; z-index: 0;
}
body.ui-v2 .v2-suggest li.post:hover .name a { text-decoration: underline; }
body.ui-v2 .v2-suggest .handle { font-size: var(--t-meta); color: var(--v-ink-soft); }
/* Above the stretched link, or the row would swallow the one control on it.
   The avatar anchor is raised for the same reason: it is a real link too. */
body.ui-v2 .v2-suggest .when { margin-left: auto; position: relative; z-index: 1; }
body.ui-v2 .v2-suggest .byline > a { position: relative; z-index: 1; }
body.ui-v2 .v2-suggest .when form { margin: 0; }
body.ui-v2 .v2-suggest .btn.small,
body.ui-v2 .v2-suggest .starter .btn.small {
  min-height: 44px; padding: 0 var(--s4);
  border-radius: var(--r-full); border: 1px solid var(--v-green-btn);
  background: transparent; color: var(--v-green-ink);
  font-size: var(--t-ui); font-weight: 700; cursor: pointer;
}
body.ui-v2 .v2-suggest .btn.small::after { content: none; }
body.ui-v2 .v2-suggest .starter {
  background: transparent; border: 0; box-shadow: none; padding: 0; margin: 0;
}
body.ui-v2 .v2-suggest .starter .pack-row {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
}
body.ui-v2 .v2-suggest .starter .faces {
  display: inline-flex; align-items: center; padding-right: var(--s2);
}
body.ui-v2 .v2-suggest .starter .faces img {
  width: 32px; height: 32px; border-radius: var(--r-full); object-fit: cover;
  margin-right: -6px; border: 2px solid var(--v-card);
}
body.ui-v2 .v2-suggest .starter .faces img:last-child { margin-right: 0; }
body.ui-v2 .v2-inline-suggest .v2-suggest { padding: 0; }

/* ---------- the ad stays a card, so it never reads as a post -------------- */
/* Once posts lose their card, the one remaining card in the column is the ad.
   The disclosure is the creative's OWN "Ad - via MuslimReach" pill plus the
   iframe title plus our caption (Tab, Aug 2 2026: "drop the outer label, keep
   the pill" — an outer text label is NOT re-added here).

   ROUND 1.1: our caption is now read BEFORE the unit inside the card, not
   after it. The pill lives INSIDE the iframe, so it is the ad server's
   disclosure; ours was the only text on this page we control and it sat below
   the creative. Same sentence, same opening word, read first. Nothing was
   added: .mrx-label stays deleted, and the Aug 2 ruling's own conditional
   ("if a creative ever ships without the pill, the outer label comes BACK
   first") is untouched and still available.

   The caption is still hidden until a unit has rendered. That rule is a
   descendant selector, so it does not care which side of the frame the
   caption is on -- checked, because the wrapper must never announce a unit
   that has not arrived.

   ONE CARD FOR EVERY STATE (Sep 19 2026). The sheet below used to be scoped to
   .is-live, then round 1.7 gave .is-loading the same sheet so a reserved box
   read as a quiet sheet rather than a hole. Now that the box exists from the
   first paint, the sheet is on the wrapper itself: its padding and border are
   part of the box's size, and a box that gains 26px of padding on fill is a
   layout shift by another name. Same metrics in every state, by construction. */
body.ui-v2 .mrx-ad-wrap {
  background: var(--v-card); border: 1px solid var(--v-line);
  border-radius: var(--r-sheet); box-shadow: var(--v-shadow);
  padding: var(--s3); box-sizing: border-box;
}
body.ui-v2 .mrx-house-kicker { font-size: var(--t-meta); }
body.ui-v2 .mrx-house-head { font-size: var(--t-section); }
body.ui-v2 .mrx-house-cta { font-size: var(--t-ui); }
body.ui-v2 .mrx-house-chip { font-size: var(--t-meta); }
body.ui-v2 .mrx-caption {
  font-size: var(--t-meta); line-height: 1.5; color: var(--v-ink-soft);
  margin: var(--s2) 0 0; text-align: left;
}
body.ui-v2 .mrx-caption-first .mrx-caption { margin: 0 0 var(--s3); }

/* ---------- guest gate, flash, pill, pagination -------------------------- */

body.ui-v2 li.guest-gate {
  background: var(--v-card); border: 0; border-bottom: 1px solid var(--v-line);
  border-radius: 0; padding: var(--s5) var(--s4);
}
body.ui-v2 .guest-gate-lead { font-size: var(--t-body); font-weight: 600; margin: 0 0 var(--s3); }
body.ui-v2 .guest-gate-actions { display: flex; gap: var(--s3); flex-wrap: wrap; }
body.ui-v2 .guest-gate-actions .btn {
  min-height: 44px; display: inline-flex; align-items: center;
  border-radius: var(--r-full); padding: 0 var(--s5);
  background: var(--v-green-btn); color: #fff; border: 0;
  font-size: var(--t-ui); font-weight: 700; text-decoration: none; cursor: pointer;
}
body.ui-v2 .guest-gate-actions .btn.quiet {
  background: transparent; color: var(--v-green-ink);
  border: 1px solid var(--v-line);
}
body.ui-v2 .guest-gate-note, body.ui-v2 .guest-gate-paid {
  font-size: var(--t-meta); color: var(--v-ink-soft); margin: var(--s3) 0 0;
}
body.ui-v2 .new-posts-pill {
  display: block; margin: var(--s3) auto; width: max-content;
  min-height: 44px; line-height: 44px; padding: 0 var(--s5);
  border-radius: var(--r-full); background: var(--v-green-btn); color: #fff;
  font-size: var(--t-ui); font-weight: 700; text-decoration: none;
  box-shadow: var(--v-shadow);
}
body.ui-v2 .load-more {
  display: flex; align-items: center; justify-content: center;
  min-height: 48px; margin: 0; border: 0;
  border-bottom: 1px solid var(--v-line);
  background: transparent; color: var(--v-green-ink);
  font-size: var(--t-ui); font-weight: 700; text-decoration: none;
  border-radius: 0;
}
body.ui-v2 .empty {
  border: 0; border-bottom: 1px solid var(--v-line); border-radius: 0;
  background: transparent; padding: var(--s7) var(--s4); text-align: center;
}
body.ui-v2 .empty h3 { font-size: var(--t-section); font-family: 'Sora', system-ui, sans-serif; }
body.ui-v2 .empty p { font-size: var(--t-ui); color: var(--v-ink-soft); }
body.ui-v2 .uo-legend-footnote,
body.ui-v2 .feed-frame { font-size: var(--t-meta); color: var(--v-ink-soft); padding: 0 var(--s4); }

/* The footer is out of phase-1 scope and keeps its v1 look; it only needs to
   sit on the v2 ground and clear the fixed tab bar. */
body.ui-v2 footer.site-foot { background: var(--v-card); border-top: 1px solid var(--v-line); }
body.ui-v2 .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;
}
body.ui-v2 .v2-icon { fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }

/* ---------- the page tail joins the system ------------------------------- */
/* The footer and the account menu are out of phase-1 scope and keep their v1
   layout, but they are ON the home page, so their type and their radii are part
   of what a review seat measures there. These rules put them on the seven-step
   scale and the four radii without touching the shared ecosystem component's
   own markup, which six properties depend on. */
body.ui-v2 footer.site-foot,
body.ui-v2 footer.site-foot div,
body.ui-v2 footer.site-foot p,
body.ui-v2 footer.site-foot a,
body.ui-v2 footer.site-foot span,
body.ui-v2 footer.site-foot b,
body.ui-v2 footer.site-foot li { font-size: var(--t-meta); }
body.ui-v2 footer.site-foot h2,
body.ui-v2 footer.site-foot h3,
body.ui-v2 footer.site-foot button,
body.ui-v2 footer.site-foot input { font-size: var(--t-ui); }
body.ui-v2 footer.site-foot .btn { border-radius: var(--r-full); min-height: 44px; }
body.ui-v2 .acct-menu,
body.ui-v2 .starter { border-radius: var(--r-sheet); }
/* The newsletter line carries an inline font-size, which beats a stylesheet
   rule; this is the one place in the layer that needs the flag. */
body.ui-v2 .nl-foot p { font-size: var(--t-meta) !important; }
body.ui-v2 .mrx-frame-box { border-radius: var(--r-media); overflow: hidden; }
/* The appreciated star: brand gold fill, and a darker gold that draws the
   shape's edge. Brand gold alone measures 2.62:1 on paper, under the 3:1 bar
   for a graphical object, so the EDGE carries the contrast (5.04:1) and the
   count beside it turns the same darker gold, which is 5.04:1 as text too. */
body.ui-v2 li.prow .act.like-btn.liked .khatam { stroke-width: 1.4; }

/* ==========================================================================
   ROUND 1.1 — what two blind seats found, fixed
   ==========================================================================
   Panel scored phase 1 at 86 and 79 against a bar of 90. Everything below is
   one of the items both seats agreed on, or one of the two phase-2 items both
   named as demo-critical. Plan of record:
   /root/.claude/plans/ummahsocial-design-pass-sep17.md
   ========================================================================== */

/* ---------- 1. BUTTONS. The P1, and why it needed its own armour ---------
   THE DEFECT: the guest header's "Sign in" pill was WHITE ON #F4F6F1 --
   measured 1.09:1, in BOTH themes, on the door a returning member uses.

   THE CAUSE is worth writing down because it is the second time this exact
   rule has done this. app.css carries, at the top level and NOT inside a
   media query:

       :root:not([data-theme="light"]) a.btn:not(.secondary):not(.quiet),
       :root[data-theme="dark"]        a.btn:not(.secondary):not(.quiet) { color: #fff }

   That first selector matches whenever the root is not explicitly stamped
   "light", which is the DEFAULT state -- so it applies in light mode too. It
   is (0,5,1). Phase 1's pill rule was `body.ui-v2 .v2-head-right .btn`, which
   is (0,3,1) and loses. The armour was written in August to stop a dark-mode
   anchor rule tinting primary buttons, and app.css has a comment directly
   under it recording that it did exactly this to v1's own header pill then.

   THE FIX IS THE SAME SHAPE THE v1 FIX USED: repeat the selector three ways,
   so one of them wins in each of the three theme states.
     (a) :root:not([data-theme="light"]) ...  (0,5,2), beats the armour
     (b) :root[data-theme="dark"] ...         (0,5,2), beats the armour
     (c) the bare selector                    applies where the armour cannot,
                                              i.e. root explicitly "light"
   A fourth state does not exist, so this is complete rather than lucky.

   THE GROUNDS, named once so the measurements below have something to cite:
     --v-green-deep #14532D   the top bar          (pinned, both themes)
     --v-green-btn  #166534   a primary button     (pinned, both themes)
     --v-paper / --v-card     the page and sheets  (theme-following)
   Green as a FOREGROUND is --v-green-ink, which is dark in light and light in
   dark. A ground is not a foreground (Sep 9, 29 broken text nodes). */

/* (1a) The pill on the green bar: light fill, dark ink, pinned like the bar. */
:root:not([data-theme="light"]) body.ui-v2 .v2-head-right a.btn,
:root[data-theme="dark"] body.ui-v2 .v2-head-right a.btn,
body.ui-v2 .v2-head-right a.btn,
body.ui-v2 .v2-head-right button.btn {
  background: #F4F6F1; color: #14532D;
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-ui); font-weight: 700;
  border-radius: var(--r-full); padding: 0 var(--s4);
  min-height: 44px; display: inline-flex; align-items: center;
  text-decoration: none; border: 0; box-shadow: none;
}
:root:not([data-theme="light"]) body.ui-v2 .v2-head-right a.btn:hover,
:root[data-theme="dark"] body.ui-v2 .v2-head-right a.btn:hover,
body.ui-v2 .v2-head-right a.btn:hover { background: #FFFFFF; color: #14532D; }

/* (1b) A primary button on the page. White on #166534 is 7.07:1 and the same
   in both themes, because the ground is pinned. */
:root:not([data-theme="light"]) body.ui-v2 .shell-wrap a.btn:not(.quiet):not(.secondary),
:root[data-theme="dark"] body.ui-v2 .shell-wrap a.btn:not(.quiet):not(.secondary),
body.ui-v2 .shell-wrap a.btn:not(.quiet):not(.secondary),
body.ui-v2 .shell-wrap button.btn:not(.quiet):not(.secondary),
body.ui-v2 .v2-joinbar a.btn:not(.quiet) {
  background: var(--v-green-btn); color: #FFFFFF;
  border: 0; box-shadow: none;
  border-radius: var(--r-full);
  min-height: 44px; padding: 0 var(--s5);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-ui); font-weight: 700; text-decoration: none; cursor: pointer;
}
:root:not([data-theme="light"]) body.ui-v2 .shell-wrap a.btn:not(.quiet):not(.secondary):hover,
:root[data-theme="dark"] body.ui-v2 .shell-wrap a.btn:not(.quiet):not(.secondary):hover,
body.ui-v2 .shell-wrap a.btn:not(.quiet):not(.secondary):hover,
body.ui-v2 .shell-wrap button.btn:not(.quiet):not(.secondary):hover {
  background: var(--v-green-deep); color: #FFFFFF;
}

/* (1c) A quiet button: no fill, so its hairline is the control's only shape
   and has to clear 3:1 on its own. --v-ink-soft is already the label colour
   and clears it in both themes, so the border reuses it rather than inventing
   a seventh colour. */
:root:not([data-theme="light"]) body.ui-v2 a.btn.quiet,
:root[data-theme="dark"] body.ui-v2 a.btn.quiet,
body.ui-v2 a.btn.quiet,
body.ui-v2 button.btn.quiet {
  background: transparent; color: var(--v-ink-soft);
  border: 1px solid var(--v-ink-soft);
  box-shadow: none; border-radius: var(--r-full);
  min-height: 44px; padding: 0 var(--s4);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-ui); font-weight: 700; cursor: pointer;
}
body.ui-v2 a.btn.quiet:hover,
body.ui-v2 button.btn.quiet:hover { background: rgba(19,26,32,.06); color: var(--v-ink); }
:root[data-theme="dark"] body.ui-v2 a.btn.quiet:hover,
:root[data-theme="dark"] body.ui-v2 button.btn.quiet:hover { background: rgba(231,228,221,.08); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body.ui-v2 a.btn.quiet:hover,
  :root:not([data-theme="light"]) body.ui-v2 button.btn.quiet:hover { background: rgba(231,228,221,.08); }
}

/* (1d) "Older posts" is `a.btn.secondary.load-more`, which the armour excludes
   and which v1's own .btn.secondary rule then reaches with a dark-mode
   box-shadow. Given its own definition here so it is not decided by two
   stylesheets arguing. */
:root:not([data-theme="light"]) body.ui-v2 a.btn.secondary.load-more,
:root[data-theme="dark"] body.ui-v2 a.btn.secondary.load-more,
body.ui-v2 a.btn.secondary.load-more {
  background: transparent; color: var(--v-green-ink);
  border: 0; box-shadow: none; border-radius: 0;
  border-bottom: 1px solid var(--v-line);
}

/* ---------- 1e. FORM CONTROLS INHERIT THE FONT ---------------------------
   button.v2-share-btn computed Arial. A <button> does not inherit font-family
   from its ancestors -- the user agent gives it its own -- and .act, unlike
   .btn, never named one. So one control in the action row was set in the
   system default while its four neighbours were Manrope, which is the kind of
   thing nobody can name and everybody can see. Declared once, for every form
   control in the layer, rather than patched per component. */
body.ui-v2 button,
body.ui-v2 input,
body.ui-v2 select,
body.ui-v2 textarea,
body.ui-v2 summary { font-family: inherit; }

/* ---------- 2. PERMALINKS AND IN-PAGE JUMPS ------------------------------
   Open a permalink and the post you asked for was BEHIND the sticky header.
   Our own DEFECT capture from phase 1 is an instance of this bug.

   scroll-padding-top applies to the scrollport, and the document's scrollport
   belongs to the ROOT element. It does not propagate up from body the way
   `overflow` does, so a body-scoped rule for it does nothing at all. Hence the
   ui-v2-root class on <html>, which the layout emits as zero bytes under v1.
   scroll-margin-top on the targets themselves covers the same ground from the
   other side, so a scroller that is not the viewport is handled too. */
html.ui-v2-root { scroll-padding-top: calc(52px + env(safe-area-inset-top) + 48px + 8px); }
@media (min-width: 1100px) {
  /* The bar is gone for a member; a GUEST keeps it at every width, so a guest
     keeps the full stack. The root carries guest/member as a class rather than
     this rule asking `html:has(body.is-guest)`, because a correctness property
     should not depend on :has() being supported. */
  html.ui-v2-root.ui-v2-member { scroll-padding-top: 56px; }
  html.ui-v2-root.ui-v2-guest { scroll-padding-top: 108px; }
}

/* ---------- 3. THE CURRENT-DAY CHIP -------------------------------------
   Sticky day markers are NOT retried, and the reason is in the phase-1 note
   above: one containing block, one offset, an arriving marker slicing the
   stuck one in half. This is the other way to answer "what day am I reading" —
   put it in the element that is already pinned.

   IT RESERVES ITS BOX FROM FIRST PAINT. Appearing as a new flex item would
   re-centre both tab labels mid-scroll, which is a navigation bar moving under
   a reader's thumb. So the slot is always there and only its contents arrive;
   `visibility`, never `display`.

   ROUND 1.2 -- IT WAS CLIPPING ITS OWN TEXT, ON THE LEFT. `flex: 0 0 88px` with
   `overflow: hidden` is a hard 88px box, and `justify-content: flex-end` puts
   the overflow at the START of the line, so "Mon 31 Aug" painted as "ON 31 AUG"
   at both widths and in both themes -- a label that reads as a different word
   rather than as a truncation.

   AND scrollWidth CANNOT SEE IT. Overflow in the inline-start direction is not
   scrollable overflow: scrollWidth came back equal to clientWidth (88 === 88)
   on the broken build, so the obvious probe returns a clean pass against a
   visibly clipped element. It is checked by pixel crop as well as by number.

   The box is now a FLOOR, not a cage: min-width keeps the slot reserved so the
   tab labels still never move, max-width bounds it against a pathological
   label, and overflow is visible so nothing is ever cut. Measured, the widest
   string this code can emit ("WEDNESDAY 30 SEP", the long fallback form) fits
   inside the cap, so the cap is a guard rail and not a clip. */
body.ui-v2 .v2-daychip {
  flex: 0 1 auto; min-width: 88px; max-width: 160px;
  display: inline-flex; align-items: center; justify-content: flex-end;
  padding-right: var(--s3);
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-meta); font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--v-ink-soft);
  white-space: nowrap; overflow: visible;
  visibility: hidden;
}
body.ui-v2 .v2-daychip.is-on { visibility: visible; }
@media (prefers-reduced-motion: no-preference) {
  body.ui-v2 .v2-daychip { transition: opacity .12s linear; opacity: 0; }
  body.ui-v2 .v2-daychip.is-on { opacity: 1; }
}

/* ---------- 11. THE DESK LABEL ------------------------------------------
   Copy only, on the house account and nowhere else. Nothing in the feed is
   suppressed, capped or reordered, and the posting rate is Tab's open call.
   A line rather than a chip: measured, the chip form pushes the handle or the
   timestamp out of the head row at 390px. */
body.ui-v2 li.prow .prow-desk {
  grid-column: 2; margin: 0 0 var(--s1);
  font-size: var(--t-meta); font-weight: 600; color: var(--v-ink-soft);
  letter-spacing: .01em;
}

/* ---------- 9. (retired) THE GUEST HERO, AND THE JOIN BAR ----------------
   Craft round, Part 1 (Sep 19 2026): the cream hero (.v2-hero, its watermark
   star and its button) and the fixed join bar (.v2-joinbar) are gone. The
   guest's first screen is the forest masthead at the END of this file. */

/* ==========================================================================
   ROUND 1.1, SECOND PASS — what the measurement found that the brief did not
   ==========================================================================
   Everything below was raised by measuring the pages after the first pass, not
   by reading the brief. Each item says what it measured at. */

/* ---------- the account gear was a 28.4 x 22 target ----------------------
   Found on every v2 page: the <summary> that opens the account menu painted
   28.4x22, which on a phone is the only way out of your own account. The
   sweep the brief asked for is "every .btn on every ground"; this is not a
   .btn and it is the smallest control in the chrome. */
body.ui-v2 .v2-head details.acct > summary {
  width: 44px; height: 44px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-full); cursor: pointer; list-style: none;
  color: #F4F6F1;
}
body.ui-v2 .v2-head details.acct > summary::-webkit-details-marker { display: none; }
body.ui-v2 .v2-head details.acct > summary .gear-icon { width: 22px; height: 22px; }
body.ui-v2 .v2-head details.acct > summary:hover { background: rgba(244,246,241,.14); }

/* ---------- 21.6px and 23px targets on the post page ---------------------
   .back-link measured 124.1 x 21.6, and the "Replying to @handle" link inside
   a row's note measured 63 x 23. The note's link grows the note's own flex
   line rather than being given an invisible ::after strip: a 44px strip
   centred on an 18px line here would overhang into .prow-head, whose name
   link is already a 44px target, and two overlapping targets is a worse
   defect than one small one. The note's margin-bottom is given up in
   exchange, so a row carrying a note grows by 18px and not by 26. */
body.ui-v2 .back-link {
  display: inline-flex; align-items: center; min-height: 44px;
  color: var(--v-green-ink); font-size: var(--t-ui);
}
body.ui-v2 li.prow .prow-note { align-items: center; margin-bottom: 0; }
body.ui-v2 li.prow .prow-note a {
  display: inline-flex; align-items: center; min-height: 44px;
}

/* ---------- the guest home carried 12 type steps and 6 radii -------------
   MEASURED, guest home, both widths: 11.52 / 12.48 / 12.8 / 13 / 13.12 /
   14.08 / 14.4 / 15 / 15.2 / 17 / 20 / 54px, and radii 8 / 10 / 12 / 14 /
   999 / 50%. Every one of the strays belongs to a guest-only v1 block that
   phase 1 never reached: the right rail's cards, the phone trust fold-out,
   the tenets list and the standards strip. The signed-in home measures 4 type
   steps and 4 radii, which is what these are being brought onto.

   This is a re-mapping, not a redesign: the blocks keep their layout, their
   copy and their behaviour, and only their type step and their corner change. */
body.ui-v2 .rail-card,
body.ui-v2 .trust-fold,
body.ui-v2 .standards-strip { border-radius: var(--r-sheet); }
body.ui-v2 img.avatar,
body.ui-v2 .rail-people-list img { border-radius: var(--r-full); }

body.ui-v2 .rail-kicker,
body.ui-v2 .rail-note,
body.ui-v2 .rail-domain,
body.ui-v2 .rail-links,
body.ui-v2 .rail-links a,
body.ui-v2 .rail-rule-note,
body.ui-v2 .rail-more,
body.ui-v2 .rail-people-who .handle,
body.ui-v2 .feed-recency { font-size: var(--t-meta); }

body.ui-v2 .rail-card p,
body.ui-v2 .rail-card p a,
body.ui-v2 .rail-list li a,
body.ui-v2 .rail-people-who .name,
body.ui-v2 .trust-fold > summary span,
body.ui-v2 .standards-strip span,
body.ui-v2 .standards-strip a,
body.ui-v2 ul.tenets li,
body.ui-v2 ul.tenets li a { font-size: var(--t-ui); }

body.ui-v2 h2.section-mark { font-size: var(--t-section); }

/* The recency fact reads as a note beside the heading, not as a second
   heading: it is the one live number on this page and it says only what it
   measured. */
body.ui-v2 .v2-section-mark {
  display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
  margin: var(--s5) 0 var(--s2);
}
body.ui-v2 .feed-recency { color: var(--v-ink-soft); font-weight: 400; letter-spacing: 0; }

/* Standalone link ROWS get a 44px target. Links inside a sentence do not, and
   that is deliberate rather than an omission: WCAG 2.5.8 exempts a target in
   a line of text, and growing an inline link inside prose breaks the
   paragraph it belongs to. The rail's headline list, its "more" link and its
   people rows are rows; "Ummah One" inside the gate's paid sentence and
   "UmmahCity" inside the rail's prose are not. */
body.ui-v2 .rail-list li a,
body.ui-v2 .rail-more,
body.ui-v2 .rail-people-list li > a,
body.ui-v2 .rail-links a,
body.ui-v2 .trust-fold > summary {
  display: inline-flex; align-items: center; min-height: 44px;
}
body.ui-v2 .rail-people-list li > a { gap: var(--s3); }

/* ---------- the guest fold at 1440 (hero lines retired, Part 1) --------- */
body.ui-v2.is-guest main.shell { padding-top: 0; }

/* ---------- the guest has no tabs row ------------------------------------
   nav.tabs is inside the @auth branch of the home view, so a guest's sticky
   stack is the top bar and nothing else. --v-stick was over-counting it by
   48px, which would have parked an anchored post 48px too low on every guest
   permalink. Measured, not assumed: the probe lists what is above the first
   post and the tabs row is not in it. */
body.ui-v2.is-guest { --v-tabs-h: 0px; }
html.ui-v2-guest { scroll-padding-top: 60px; }
@media (min-width: 1100px) {
  html.ui-v2-root.ui-v2-guest { scroll-padding-top: 60px; }
}

/* ---------- the guest fold at 1440, second measurement (retired, Part 1) --
   The guest headline no longer lives in this layer; see the masthead block. */

/* Two strays the measurement found on the post page: the page's own H1 sat at
   20px and the reply composer's note at 12.48px, neither of them a step. */
body.ui-v2 h1.section-mark { font-size: var(--t-section); }
body.ui-v2 form#compose.composer .count { font-size: var(--t-meta); }

/* ---------- the guest column lost its side gutter -----------------------
   Caught in the capture, not in a number: at 390 the heading "Everyone here,
   newest first" and the recency line under it ran to x=0, hard against the
   left edge of the screen, and the trust fold-out's rounded card bled off it.

   The cause is that main.shell carries `padding: 0` in v2 -- the timeline rows
   and the hero each supply their own side padding, which is what lets a post
   row's hairline run the full width of the column. The guest page's remaining
   direct children were relying on v1's shell padding and nothing replaced it.

   Given as MARGIN rather than padding, because two of these are cards: padding
   would leave their background running to the edge with the text inset, which
   is the same defect one layer in. */
body.ui-v2 main.shell > h2.section-mark,
body.ui-v2 main.shell > p.feed-frame,
body.ui-v2 main.shell > details.trust-fold,
body.ui-v2 main.shell > ul.tenets,
body.ui-v2 main.shell > .standards-strip,
body.ui-v2 main.shell > .invite-strip,
body.ui-v2 main.shell > .uo-legend-footnote,
body.ui-v2 main.shell > .flash,
body.ui-v2 main.shell > .empty,
body.ui-v2 main.shell > form.composer + .empty,
/* Sep 20 2026: the same defect, found on two more pages the design pass
   never touched. /stories/new (section.story-compose) and /@handle/sands
   (.sands-page) are both direct children of main.shell and both wrote NO
   horizontal padding of their own, on purpose, because v1's `.shell`
   supplied it — `.sands-page`'s own comment says so explicitly. Under v2
   that supply stopped and nothing replaced it, so both pages ran edge to
   edge at 390px. Same fix, same list. */
body.ui-v2 main.shell > section.story-compose,
body.ui-v2 main.shell > .sands-page { margin-left: var(--s4); margin-right: var(--s4); }
/* .feed-frame already had side padding of its own from the v1 rules carried
   above; with a margin now doing the job that would inset it twice. */
body.ui-v2 main.shell > .uo-legend-footnote,
body.ui-v2 main.shell > p.feed-frame { padding-left: 0; padding-right: 0; }

/* ==========================================================================
   ROUND 1.3 — the eight items two blind seats named at 88 and 84
   ==========================================================================
   Plan of record: /root/.claude/plans/ummahsocial-design-pass-sep17.md
   Card: https://trello.com/c/1BIpNLgx
   Every number below was measured on the live ?ui=2 surface before the edit and
   again after it. Where a measurement contradicted the brief it is said so at
   the point it happened, not smoothed over.
   ========================================================================== */

/* ---------- 1. DENSITY ON A PHONE ----------------------------------------
   MEASURED BEFORE, at 390x724 signed in: the post body is 17px in a 302px
   column (34 characters a line), the median plain-text row is 302px against a
   204px target, and exactly ONE row is fully visible above the fold. For a
   guest it is none, because the first row's top is 477px of a 724px screen.

   A 17px body is right on a 600px desktop column and wrong on a phone, so the
   step changes with the width rather than everywhere. 15px is --t-ui, an
   existing step on the seven-step scale: no eighth step is invented, and at
   1.5 line-height the phone column goes from 34 to about 40 characters, which
   is closer to the 45-to-75 band than 34 was.

   ARABIC SCRIPT KEEPS 17px. A 15px Latin body and a 15px Arabic body are not
   the same amount of legibility: Arabic's distinguishing marks are below the
   x-height equivalent and the script carries no capitals to anchor a word
   shape. The hook is a SERVER-SIDE class, and it has to be, because neither
   CSS selector people reach for can see this: [dir="rtl"] matches the
   attribute, which x-member-text writes as "auto", and :lang(ar) needs a lang
   attribute that a member typing an ayah into an English post never has. */
@media (max-width: 639.98px) {
  body.ui-v2 li.prow .prow-body { font-size: var(--t-ui); }
  body.ui-v2 li.prow .prow-body.is-arabic { font-size: var(--t-body); }
  /* FIVE LINES BELOW 640, SIX ABOVE. The server-side predicate deliberately
     does NOT move with this one -- see partials/v2/post.blade.php: 370
     characters is six lines of the 58-character desktop column, where the
     class is decided, and a phone column of 40 characters simply renders a
     200-to-370 character post in full with no control, which is what it does
     today. The invariant that matters is "a clamped body always has a Show
     more under it", and that still holds at both widths. */
  body.ui-v2 li.prow .prow-body.clamped { -webkit-line-clamp: 5; }

  /* ROUND 1.4, ITEM 2 -- THE 40px ACTION ROW AND ITS ::after STRIP ARE GONE.
     THE CONTROLS ARE 44px PAINTED ON A PHONE, THE SAME AS EVERY OTHER WIDTH.

     What stood here was `height: 40px` plus a transparent ::after inset -2px
     top and bottom. Two blind seats measured 44x40 and 50x40 and called the
     row non-compliant; the strip was real, but it made the truth depend on a
     measurement almost nobody makes, and the margin it bought was 2px -- a
     probe 2px inside the top of the nominal 44px box lands exactly on the
     strip's own edge, where a hit test is a coin toss.

     A control whose compliance can only be demonstrated with a second
     instrument is a control that will be re-reported every round. The four
     pixels come back out of the row instead. Both instruments now agree:
     getBoundingClientRect reads 44 and elementFromPoint returns the control at
     2px inside the top and 2px inside the bottom of the same box.

     The ::after also has to go rather than merely stop mattering. Left in
     place it would extend 2px past a now-44px control, i.e. 48px of hit area
     reaching into .prow-body's first line of links -- the exact overhang the
     round 1.1 note refused on the header row. */
}

/* (retired, Part 1) the phone hero padding block that carried the r791 note.
   The masthead block at the end of this file replaces the hero entirely. */

/* ---------- 4. THE PERMALINK SCROLLED SIDEWAYS --------------------------
   MEASURED at 390: scrollWidth 402 against a clientWidth of 390 on the photo
   fixture and 405 on the link fixture, with the overflowing element a 60px
   anchor -- the timestamp link inside .prow-when.

   THE CAUSE. .prow-when had no flex declaration, so it took the initial
   `0 1 auto`: shrinkable. Its contents are `white-space: nowrap`, so it
   shrinks below its own content and the content hangs out of the row. A
   .vis-chip ("Link only") in the same line is what pushes it there, which is
   why a permalink to an unlisted post shows it and the feed does not.

   THE FIX IS THREE THINGS, not one. The time stops shrinking; the name and the
   handle are the parts that may give, so they carry min-width: 0 and an
   ellipsis; and the row may wrap as a last resort, with the chip ordered last
   so it is the thing that goes to a second line rather than the timestamp. */
body.ui-v2 li.prow .prow-head { flex-wrap: wrap; row-gap: 0; }
body.ui-v2 li.prow .prow-name { order: 1; min-width: 0; }
body.ui-v2 li.prow .prow-handle { order: 2; flex: 0 1 auto; min-width: 0; }
body.ui-v2 li.prow .prow-when { order: 3; flex: 0 0 auto; }
body.ui-v2 li.prow .vis-chip { order: 4; flex: 0 0 auto; }

/* ---------- 5. THE POST PAGE ---------------------------------------------
   The shell no longer jumps: post.blade.php declares the same rail, so
   body.has-rail is set and the three-column grid holds. What is left here is
   the column's own inset. main.shell carries `padding: 0` in v2 -- the rows
   supply their own side padding, which is what lets a hairline run the full
   width of the column -- and the post page's direct children were relying on
   v1's shell padding. Measured: "Back to the feed" and the H1 both at x=0.

   Margin rather than padding, for the reason the guest block below already
   gives: two of these are cards, and padding would leave their background
   running to the edge with the text inset. */
body.ui-v2 main.shell > a.back-link,
body.ui-v2 main.shell > h1.section-mark,
body.ui-v2 main.shell > h2.section-mark,
body.ui-v2 main.shell > .rail-card { margin-left: var(--s4); margin-right: var(--s4); }
/* The reply composer is a v1 composer on a v2 page. It gets the v2 composer's
   own padding rather than a margin, because it is a full-width sheet with a
   hairline under it, exactly like the one at the top of the feed. */
body.ui-v2 main.shell > form.composer { padding-left: var(--s4); padding-right: var(--s4); }

/* ---------- 6. "PEOPLE TO FOLLOW", IN THE PHONE FEED --------------------
   MEASURED on the live phone feed: five 40 x 46.5 avatar links inside the
   inline module -- the ONLY sub-44 targets on the signed-in page that are not
   inside a sentence. The module was the shared v1 partial with six inline
   `style` attributes, each beaten by an `!important` in the v2 layer, and an
   inline style cannot be beaten on a property nobody thought to override.

   It is now its own v2 partial (partials/v2/sugg-rows) with the QUERY left
   where it was. The rules below are the ones phase 1 wrote for the rail and
   never used, plus the two the new markup needs. The avatar is no longer a
   second link to the same profile: it is an image inside a 44px slot, and the
   row itself is the target. */
body.ui-v2 .v2-sugg-head {
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-body); font-weight: 700; color: var(--v-ink);
  margin: 0 0 var(--s2); letter-spacing: 0;
}
body.ui-v2 .v2-sugg-list { list-style: none; margin: 0; padding: 0; }
body.ui-v2 .v2-sugg-row { position: relative; }
body.ui-v2 .v2-sugg-face {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; flex: none;
}
body.ui-v2 .v2-sugg-face .avatar {
  width: 40px; height: 40px; border-radius: var(--r-full);
  object-fit: cover; display: block;
}
body.ui-v2 .v2-sugg-face .uo-ring { box-shadow: 0 0 0 2px var(--v-card), 0 0 0 4px var(--v-gold); }
/* The row-sized target. An <a> may not contain a <form>, so the name link's
   ::after is stretched over the row and the Follow form is lifted above it. */
body.ui-v2 .v2-sugg-name::after { content: ''; position: absolute; inset: 0; z-index: 0; }
body.ui-v2 .v2-sugg-row:hover .v2-sugg-display { text-decoration: underline; }
body.ui-v2 .v2-sugg-act { margin: 0; flex: none; position: relative; z-index: 1; }
body.ui-v2 .v2-inline-suggest .v2-sugg-head { font-size: var(--t-body); }

/* ---------- 7. THE COMPOSER'S FOCUS RING, AND ITS WAY OUT ---------------
   MEASURED: the composer textarea computed `outline: 0px none` with the only
   remaining indicator a gold box-shadow at 2.78:1 on white. The cause is one
   line in the phase-1 layer -- `textarea[name="body"]:focus { outline: none }`
   at (0,4,2), written to stop a ring painting on a mouse click, which also
   removed it for a keyboard user. This is the Sep 10 lesson arriving by a
   different door: the functional ring must live in `outline`, and anything
   that removes an outline has to say WHICH focus it means.

   :focus-visible keeps the ring; :focus:not(:focus-visible) is the mouse case
   and keeps the silence. The offset is negative so the ring sits inside the
   sheet's edge rather than colliding with it. */
body.ui-v2 form#compose.composer textarea[name="body"]:focus:not(:focus-visible) { outline: none; }
body.ui-v2 form#compose.composer textarea[name="body"]:focus-visible {
  outline: 2px solid var(--v-green-ink);
  outline-offset: -2px;
  box-shadow: 0 0 0 4px var(--v-gold);
}

/* The Close control. It is inside the sheet, at the top right of the fold, and
   it only exists once the composer is open -- a way out of a state the member
   is not in is noise. `[hidden]` is not used here: this file's own history says
   an author `display` beats the user-agent [hidden] rule, and the estate reset
   that fixed that lives in the artifact skeleton, not in this stylesheet. */
body.ui-v2 form#compose.composer { position: relative; }
body.ui-v2 form#compose.composer .v2-composer-close { display: none; }
body.ui-v2 form#compose.composer.is-open .v2-composer-close {
  display: inline-flex; align-items: center; justify-content: center;
  position: absolute; top: var(--s2); right: var(--s2);
  width: 44px; height: 44px; border: 0; background: none; cursor: pointer;
  border-radius: var(--r-full); color: var(--v-ink-soft);
}
body.ui-v2 form#compose.composer .v2-composer-close .v2-icon { width: 18px; height: 18px; }
body.ui-v2 form#compose.composer .v2-composer-close:hover { background: rgba(19,26,32,.06); }
:root[data-theme="dark"] body.ui-v2 form#compose.composer .v2-composer-close:hover { background: rgba(231,228,221,.08); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body.ui-v2 form#compose.composer .v2-composer-close:hover { background: rgba(231,228,221,.08); }
}
/* The open textarea must not run under the Close control. */
body.ui-v2 form#compose.composer.is-open textarea[name="body"] { padding-right: 52px; }

/* ==========================================================================
   ROUND 1.5 (Sep 19 2026) -- the craft pass, plus the pages the flag reached
   that no round had styled. Tab lifted the pause (r778) and ruled that
   everything behind the flag flips together, so nothing under ?ui=2 may
   render rough. The UpScrolled App Store captures arrived the same morning;
   what is matched from them is said at each item.
   Plan: /root/.claude/plans/ummahsocial-design-pass-sep17.md
   ========================================================================== */

/* ---------- 0. THE COMPOSER GRID REACHED EVERY form.composer ------------
   Every rule that was `body.ui-v2 form.composer` is now `form#compose.composer`
   (rewritten in place above). The home composer is the only form with that id;
   the settings form, the search form and the reply form all carry the class
   `composer` too, and the 44px + 1fr grid put a settings checkbox label in a
   44px column (measured: every word on its own line) and the Search button in
   a 48px disc with the word overflowing it. Found by opening the pages, not by
   reading the stylesheet. */

/* ---------- 1. THE AVATAR MONOGRAM WAS UNDERLINED -----------------------
   `a { text-decoration: underline }` is the resting state for links on this
   property and .prow-avatar is an <a> that is not in the opt-out list, so the
   initial disc's letter carried a link underline in both themes (visible on
   every house post in the feed). The image variant escaped only because an
   <img> has no text to underline. */
body.ui-v2 li.prow .prow-avatar,
body.ui-v2 .v2-sugg-face,
body.ui-v2 .v2-profile-avatar { text-decoration: none; }

/* ---------- 2. THE ROW HEADER CARRIES THE OVERFLOW MENU -----------------
   X, Threads and UpScrolled all put the dots at the top right of the row.
   Ours sat at the end of the action row, which is capped at 420px, so at 1440
   the dots painted at x=840 under nothing while the timestamp ended at 954.
   The element is the same <details class="more">; only its parent changed.
   .prow-when keeps margin-left:auto, so time and More sit together at the
   right edge; the v1 rule `details.more { margin-left: auto }` would have
   split the free space between them, hence margin-left: 0 here. The 8px
   negative margin puts the glyph, not the 44px box, on the column edge. */
body.ui-v2 li.prow .prow-head details.more {
  order: 4; flex: 0 0 auto;
  margin-left: 0; margin-right: calc(-1 * var(--s2));
  position: relative; display: inline-flex;
}
body.ui-v2 li.prow .prow-head .vis-chip { order: 5; }

/* ---------- 3. THE ACTION ROW: THREE ON THE LEFT, SHARE ON THE RIGHT ----
   UpScrolled's rhythm, observed in its own captures: heart, comment and
   repost left-aligned with air between them, the share glyph pushed to the
   right edge. The three social actions keep the packed left rhythm round 1.4
   settled on (one rule for guest and member alike); the gap grows from 0 to
   12px because 44px controls with no gap read as one wide control. */
body.ui-v2 li.prow .actions { max-width: none; gap: var(--s3); }
body.ui-v2 li.prow .act { justify-content: center; padding: 0 10px; }
body.ui-v2 li.prow .actions .v2-share-btn { margin-left: auto; margin-right: calc(-1 * var(--s2)); }

/* ---------- 4. HOVER SAYS WHAT IS INTERACTIVE ---------------------------
   On a pointer device the row tints as X's does, and each action gets a halo
   in its own colour: green for reply and repost, gold for appreciate. Only
   where hover exists; a phone never paints a stuck hover state. */
@media (hover: hover) {
  body.ui-v2.v2-rowclick li.prow:hover { background: rgba(19,26,32,.03); }
  body.ui-v2 li.prow .act:hover { background: rgba(19,26,32,.06); color: var(--v-ink); }
  body.ui-v2 li.prow .act[aria-label^="Reply"]:hover,
  body.ui-v2 li.prow .act.repost-btn:hover { color: var(--v-green-ink); background: rgba(22,101,52,.09); }
  body.ui-v2 li.prow .act.like-btn:hover,
  body.ui-v2 li.prow .act.signin-action:hover { color: var(--v-gold-deep); background: rgba(200,145,46,.14); }
  body.ui-v2 li.prow .prow-head details.more > summary:hover { background: rgba(19,26,32,.06); color: var(--v-ink); }
}
@media (hover: hover) and (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body.ui-v2.v2-rowclick li.prow:hover { background: rgba(231,228,221,.04); }
  :root:not([data-theme="light"]) body.ui-v2 li.prow .act:hover,
  :root:not([data-theme="light"]) body.ui-v2 li.prow .prow-head details.more > summary:hover { background: rgba(231,228,221,.08); }
  :root:not([data-theme="light"]) body.ui-v2 li.prow .act[aria-label^="Reply"]:hover,
  :root:not([data-theme="light"]) body.ui-v2 li.prow .act.repost-btn:hover { background: rgba(92,181,133,.14); }
  :root:not([data-theme="light"]) body.ui-v2 li.prow .act.like-btn:hover,
  :root:not([data-theme="light"]) body.ui-v2 li.prow .act.signin-action:hover { background: rgba(217,169,79,.16); }
}
@media (hover: hover) {
  :root[data-theme="dark"] body.ui-v2.v2-rowclick li.prow:hover { background: rgba(231,228,221,.04); }
  :root[data-theme="dark"] body.ui-v2 li.prow .act:hover,
  :root[data-theme="dark"] body.ui-v2 li.prow .prow-head details.more > summary:hover { background: rgba(231,228,221,.08); }
  :root[data-theme="dark"] body.ui-v2 li.prow .act[aria-label^="Reply"]:hover,
  :root[data-theme="dark"] body.ui-v2 li.prow .act.repost-btn:hover { background: rgba(92,181,133,.14); }
  :root[data-theme="dark"] body.ui-v2 li.prow .act.like-btn:hover,
  :root[data-theme="dark"] body.ui-v2 li.prow .act.signin-action:hover { background: rgba(217,169,79,.16); }
}

/* ---------- 5. FULL-BLEED MEDIA ON A PHONE ------------------------------
   The one structural thing UpScrolled's captures show that we did not have:
   a photo runs edge to edge with no border and no inset, and that is what
   makes their feed read as imagery rather than as forms. Below 640 the grid
   spans both columns and cancels the row's side padding; the photo goes from
   302px to 390px wide on the majority phone, 29% larger, for zero extra row
   height in the 16:10 box. From 640 up it stays inside the content column with
   its radius, which is what X does on a desktop column. App\Support\PostPhoto
   restates this breakpoint in `sizes`, so the browser still fetches the file
   the box needs and no more. */
@media (max-width: 639.98px) {
  body.ui-v2 li.prow .post-images {
    grid-column: 1 / -1;
    margin: var(--s3) calc(-1 * var(--s4)) 0;
    border: 0; border-radius: 0; gap: 2px;
  }
  body.ui-v2 li.prow .post-images.n1 a { max-height: min(400px, 56vh); }
}

/* ---------- 6. THE DAY SPINE, TIGHTER ----------------------------------
   In a room this size almost every post is on its own day, so a marker sits
   between most pairs of rows. 24px above and 8px below made each one a band;
   16 and 4 keep it a label. */
body.ui-v2 li.v2-day { padding: var(--s4) var(--s4) var(--s1); }
body.ui-v2 li.v2-day:first-child { padding-top: var(--s3); }

/* ---------- 7. THE DESKTOP COLUMN HAS EDGES -----------------------------
   X and Truth Social both bound the timeline with a hairline on each side, and
   without it our 600px of rows floated in cream with the day rules ending in
   nothing. The three column tops now share one line: the left rail's mark,
   the composer sheet and the right rail's first card all start at 24px
   (measured before: 32 / 24 / 48). */
@media (min-width: 1100px) {
  body.ui-v2 main.shell {
    box-sizing: border-box;
    border-left: 1px solid var(--v-line); border-right: 1px solid var(--v-line);
    min-height: 100vh;
  }
  body.ui-v2 .right-rail { padding-top: 0; }
  body.ui-v2 .v2-rail-mark { margin-top: 0; }
}

/* ---------- 8. THE MEMBER'S OWN FACE IN THE ME SLOT --------------------
   UpScrolled and Instagram both do this; it is the one glyph a person
   recognises without reading. 24px circle, ringed in green when it is the
   current page. */
body.ui-v2 .v2-me-av {
  width: 24px; height: 24px; border-radius: var(--r-full);
  object-fit: cover; display: block; flex: none;
}
body.ui-v2 span.v2-me-av--initial {
  display: flex; align-items: center; justify-content: center;
  background: var(--v-green-deep); color: #F4F6F1;
  font-family: 'Manrope', system-ui, sans-serif; font-weight: 700;
  font-size: var(--t-meta); line-height: 1;
}
body.ui-v2 .v2-tab.is-here .v2-me-av,
body.ui-v2 .v2-rail-link.is-here .v2-me-av { box-shadow: 0 0 0 2px var(--v-green-ink); }

/* ---------- 9. v1 LIST ROWS ON v2 PAGES ---------------------------------
   Notifications, follow requests, follow lists and the v1 search fallback all
   render <li class="post"> with a .byline, and under the flag they were still
   white cards with a ring, a shadow and a khatam watermark inside a page of
   hairline rows. One mapping for all of them: a row, a hairline, the v2 type
   steps, a 44px avatar link. The markup is untouched, so v1 is untouched. */
body.ui-v2 main.shell > ul.timeline > li.post {
  background: transparent; background-image: none;
  border: 0; border-bottom: 1px solid var(--v-line); border-radius: 0;
  box-shadow: none; margin: 0;
  padding: var(--s3) var(--s4) !important;
}
body.ui-v2 main.shell > ul.timeline > li.post .byline { gap: var(--s3); min-height: 44px; }
body.ui-v2 main.shell > ul.timeline > li.post .byline > a { display: inline-flex; align-items: center; min-height: 44px; }
body.ui-v2 main.shell > ul.timeline > li.post .avatar { width: 40px; height: 40px; border: 0; background: var(--v-paper); }
body.ui-v2 main.shell > ul.timeline > li.post .who .name {
  font-family: 'Manrope', system-ui, sans-serif; font-weight: 700;
  font-size: var(--t-ui) !important; color: var(--v-ink);
}
body.ui-v2 main.shell > ul.timeline > li.post .who .name span { font-weight: 400; }
body.ui-v2 main.shell > ul.timeline > li.post .who .handle { font-size: var(--t-meta); color: var(--v-ink-soft); }
body.ui-v2 main.shell > ul.timeline > li.post .when { font-size: var(--t-meta); color: var(--v-ink-soft); }
body.ui-v2 main.shell > ul.timeline > li.post .body {
  font-size: var(--t-ui) !important; color: var(--v-ink-soft) !important;
  margin: var(--s1) 0 0 52px;
}
body.ui-v2 main.shell > ul.timeline > li.post .actions { margin: 0 0 0 52px; border: 0; padding: 0; gap: var(--s2); }
body.ui-v2 main.shell > ul.timeline > li.post .action-btn {
  display: inline-flex; align-items: center; min-height: 44px; padding: 0;
  color: var(--v-green-ink); font-size: var(--t-ui); font-weight: 600; text-decoration: none;
  background: none; border: 0;
}
body.ui-v2 main.shell > ul.timeline > li.post.notif.fresh { background: rgba(22,101,52,.05); }
:root[data-theme="dark"] body.ui-v2 main.shell > ul.timeline > li.post.notif.fresh { background: rgba(92,181,133,.08); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body.ui-v2 main.shell > ul.timeline > li.post.notif.fresh { background: rgba(92,181,133,.08); }
}
body.ui-v2 .uo-legend {
  display: block; font-size: var(--t-meta); color: var(--v-ink-soft);
  margin: var(--s2) 0 0; line-height: 1.5;
}
body.ui-v2 .uo-legend .ring-dot { display: inline-block; vertical-align: middle; margin: 0 var(--s2) 2px 2px; }
body.ui-v2 main.shell > .uo-legend,
body.ui-v2 main.shell > .uo-legend-footnote { padding: var(--s2) var(--s4); margin: 0; }

/* ---------- 10. SEARCH ---------------------------------------------------
   The field is a pill with the magnifier inside it, the way every phone
   search field reads (UpScrolled's Discover has exactly this). The input's
   own focus ring is handed to the pill, in `outline`, so the ring is one
   shape and not a rectangle inside a capsule. */
body.ui-v2 form.composer.v2-search {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s3) var(--s4); margin: 0;
  background: transparent; border: 0; border-bottom: 1px solid var(--v-line);
  border-radius: 0; box-shadow: none;
}
body.ui-v2 .v2-search-field {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: var(--s2);
  min-height: 44px; padding: 0 var(--s3) 0 var(--s4);
  border-radius: var(--r-full); background: var(--v-card);
  border: 1px solid var(--v-line); cursor: text;
}
body.ui-v2 .v2-search-field .v2-icon { width: 20px; height: 20px; flex: none; color: var(--v-ink-soft); }
body.ui-v2 .v2-search-field input {
  flex: 1 1 auto; min-width: 0; margin: 0; padding: 0; min-height: 42px;
  border: 0; background: transparent; box-shadow: none;
  font: inherit; font-size: var(--t-ui); color: var(--v-ink);
  -webkit-appearance: none; appearance: none;
}
body.ui-v2 .v2-search-field input::placeholder { color: var(--v-ink-soft); }
body.ui-v2 .v2-search-field input:focus,
body.ui-v2 .v2-search-field input:focus-visible { outline: none; box-shadow: none; }
body.ui-v2 .v2-search-field:focus-within { border-color: var(--v-green-ink); outline: 2px solid var(--v-green-ink); outline-offset: 1px; }
body.ui-v2 form.composer.v2-search button.btn { flex: none; }
body.ui-v2 main.shell > h1.section-mark { margin: var(--s4) var(--s4) var(--s2); }
body.ui-v2 .v2-strip-head { margin: var(--s4) var(--s4) var(--s2); }
body.ui-v2 main.shell > .v2-people { padding: 0 var(--s4); }
body.ui-v2 .v2-people .v2-sugg-row { border-bottom: 1px solid var(--v-line); padding: var(--s2) 0; }
body.ui-v2 .v2-people .v2-sugg-row:last-child { border-bottom: 0; }
body.ui-v2 .v2-view-btn { display: inline-flex; align-items: center; text-decoration: none; }
body.ui-v2 .v2-people .vis-chip,
body.ui-v2 .v2-people .follows-you {
  font-size: var(--t-meta); font-weight: 700; color: var(--v-ink-soft);
  border: 1px solid var(--v-line); border-radius: var(--r-full);
  padding: 0 var(--s2); margin-left: var(--s1); background: transparent;
}

/* ---------- 11. THE PROFILE ----------------------------------------------
   Banner, avatar overlapping its lower left, actions on the right below the
   banner's edge, then name, handle, bio and the counts line: the shape X,
   Truth Social and UpScrolled share, observed in UpScrolled's own capture.
   The banner is the deep green with the khatam tessellation at the locked
   opacity band; the gold ring means Ummah One and nothing else; no tick, no
   "followers", no purchasable anything. See partials/v2/profile-head. */
body.ui-v2 .v2-profile { border-bottom: 1px solid var(--v-line); }
body.ui-v2 .v2-profile-banner { height: 120px; background: var(--v-green-deep); }
@media (min-width: 640px) { body.ui-v2 .v2-profile-banner { height: 160px; } }
body.ui-v2 .v2-profile-banner .khatam-tess-rect { opacity: .10; }
:root[data-theme="dark"] body.ui-v2 .v2-profile-banner .khatam-tess-rect { opacity: .08; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body.ui-v2 .v2-profile-banner .khatam-tess-rect { opacity: .08; }
}
body.ui-v2 .v2-profile-top {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s3); padding: 0 var(--s4); margin-top: -40px;
}
body.ui-v2 .v2-profile-avatar {
  width: 80px; height: 80px; border-radius: var(--r-full); object-fit: cover;
  flex: none; border: 0; background: var(--v-paper);
  box-shadow: 0 0 0 4px var(--v-paper);
}
body.ui-v2 .v2-profile-avatar.uo-ring { box-shadow: 0 0 0 4px var(--v-paper), 0 0 0 7px var(--v-gold); }
body.ui-v2 span.v2-profile-avatar {
  display: flex; align-items: center; justify-content: center;
  background: var(--v-green-btn); color: #FFFFFF;
  font-family: 'Manrope', system-ui, sans-serif; font-weight: 700; font-size: var(--t-title);
}
body.ui-v2 .v2-profile-actions {
  display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center;
  gap: var(--s2); margin: var(--s7) 0 0;
}
body.ui-v2 .v2-profile-actions form { margin: 0; }
:root:not([data-theme="light"]) body.ui-v2 .v2-profile-actions .btn.secondary,
:root[data-theme="dark"] body.ui-v2 .v2-profile-actions .btn.secondary,
body.ui-v2 .v2-profile-actions .btn.secondary {
  background: transparent; color: var(--v-ink); border: 1px solid var(--v-ink-soft);
  box-shadow: none; border-radius: var(--r-full);
  min-height: 44px; padding: 0 var(--s4);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: 'Manrope', system-ui, sans-serif; font-size: var(--t-ui); font-weight: 700;
  cursor: pointer;
}
body.ui-v2 .v2-profile-meta { padding: var(--s3) var(--s4) var(--s4); }
body.ui-v2 .v2-profile-name {
  font-family: 'Manrope', system-ui, sans-serif; font-size: var(--t-section); font-weight: 800;
  line-height: 1.2; margin: 0; color: var(--v-ink); overflow-wrap: anywhere;
}
body.ui-v2 .v2-profile-handle {
  font-size: var(--t-ui); color: var(--v-ink-soft); margin-top: 2px;
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
}
body.ui-v2 .v2-profile-handle .vis-chip,
body.ui-v2 .v2-profile-handle .follows-you {
  font-size: var(--t-meta); font-weight: 700; color: var(--v-ink-soft);
  border: 1px solid var(--v-line); border-radius: var(--r-full);
  padding: 1px var(--s2); background: transparent; margin: 0;
}
body.ui-v2 .v2-profile-bio {
  margin: var(--s3) 0 0; font-size: var(--t-ui); line-height: 1.5; color: var(--v-ink);
  max-width: 62ch; white-space: pre-wrap; unicode-bidi: plaintext; overflow-wrap: anywhere;
}
@media (min-width: 640px) { body.ui-v2 .v2-profile-bio { font-size: var(--t-body); } }
/* r792 (Tab, Sep 19 2026): same plain register as .own-count directly below
   it -- no chip, no background, no border. */
body.ui-v2 .v2-profile-meta .profile-stats { margin-top: var(--s3); font-size: var(--t-ui); color: var(--v-ink-soft); }
body.ui-v2 .v2-profile-meta .own-count { margin-top: var(--s3); font-size: var(--t-ui); color: var(--v-ink-soft); }
body.ui-v2 .v2-profile-meta .list-links,
body.ui-v2 .v2-profile-meta .mutual-line { font-size: var(--t-meta); color: var(--v-ink-soft); margin-top: 0; }
body.ui-v2 .v2-profile-meta .list-links a,
body.ui-v2 .v2-profile-meta .mutual-line a { display: inline-flex; align-items: center; min-height: 44px; color: var(--v-ink-soft); font-weight: 600; }
body.ui-v2 .v2-profile-meta .mutual-line a { color: var(--v-ink); }

/* The section label over a page's list ("Posts - in order", "Replies - in
   order", "Follow requests") joins the day spine's grammar: the same 13px
   uppercase green with a rule, so a profile reads as one surface with the
   feed. The guest home's own heading keeps its size; it is a page title. */
body.ui-v2 main.shell > h2.section-mark:not(.v2-section-mark) {
  display: flex; align-items: center; gap: var(--s3);
  margin: 0; padding: var(--s4) var(--s4) var(--s1);
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-meta); font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--v-green-ink);
}
body.ui-v2 main.shell > h2.section-mark:not(.v2-section-mark) .khatam { display: none; }
body.ui-v2 main.shell > h2.section-mark:not(.v2-section-mark)::after { background: var(--v-line); }

/* ---------- 12. THE POST PAGE ------------------------------------------
   "Back to the feed" and a khatam-crowned "Post" title spent 300px of a phone
   before the post. One 52px row now: a 44px back arrow and the word, which is
   the header X, Threads and Bluesky all use. The route and the accessible
   name are unchanged. */
body.ui-v2 .v2-pagehead {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s1) var(--s2) var(--s1) var(--s1); min-height: 52px;
  border-bottom: 1px solid var(--v-line);
}
body.ui-v2 .v2-pagehead h1 {
  font-family: 'Manrope', system-ui, sans-serif; font-size: var(--t-section); font-weight: 800;
  margin: 0; color: var(--v-ink);
}
body.ui-v2 .v2-back {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: var(--r-full);
  color: var(--v-ink); text-decoration: none;
}
body.ui-v2 .v2-back .v2-icon { width: 22px; height: 22px; }
body.ui-v2 .v2-back:hover { background: rgba(19,26,32,.06); }
/* The reply composer is the v1 composer on a v2 page. It gets the home
   composer's sheet: a white surface, a hairline under it, no ring. */
body.ui-v2 main.shell > form.composer {
  background: var(--v-card); border: 0; border-bottom: 1px solid var(--v-line);
  border-radius: 0; box-shadow: none; margin: 0;
  padding: var(--s3) var(--s4);
}
body.ui-v2 main.shell > form.composer textarea {
  width: 100%; box-sizing: border-box; border: 0; background: transparent;
  color: var(--v-ink); font: inherit; font-size: var(--t-body); line-height: 1.5;
  padding: var(--s2) 0; min-height: 72px; resize: vertical; margin: 0;
}
body.ui-v2 main.shell > form.composer textarea:focus:not(:focus-visible) { outline: none; box-shadow: none; }
body.ui-v2 main.shell > form.composer textarea:focus-visible {
  outline: 2px solid var(--v-green-ink); outline-offset: -2px; box-shadow: none;
}
body.ui-v2 main.shell > form.composer .row {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  margin: 0; padding-top: var(--s2); border-top: 1px solid var(--v-line);
}
body.ui-v2 main.shell > form.composer .row .count { font-size: var(--t-meta); color: var(--v-ink-soft); }
body.ui-v2 main.shell > form.composer .row .count:first-child { flex: 1 1 auto; }
body.ui-v2 main.shell > .empty { margin: 0; }

/* ---------- 13. SETTINGS ------------------------------------------------
   Stacked sections in the column, each a sheet with a hairline under it.
   The forms and cards keep their markup; the inline margins on the cards are
   the one thing that needs the flag. */
body.ui-v2 main.shell > .prose { padding: var(--s4) var(--s4) var(--s2); }
body.ui-v2 main.shell > .prose h1 {
  font-family: 'Manrope', system-ui, sans-serif; font-size: var(--t-section); font-weight: 800;
  margin: 0 0 var(--s2); color: var(--v-ink);
}
body.ui-v2 main.shell > .prose p { font-size: var(--t-ui); color: var(--v-ink-soft); margin: 0; }
body.ui-v2 form.composer.settings-form,
body.ui-v2 main.shell > .settings-card {
  background: var(--v-card); border: 0; border-bottom: 1px solid var(--v-line);
  border-radius: 0; box-shadow: none;
  margin: 0 !important; padding: var(--s4);
}
body.ui-v2 form.composer.settings-form label { min-height: 44px; }
body.ui-v2 form.composer.settings-form .row { margin-top: var(--s3); }
body.ui-v2 main.shell > .settings-card h2 { font-family: 'Manrope', system-ui, sans-serif; font-size: var(--t-ui) !important; font-weight: 700; }

/* Round 1.5, measured after the first render: the avatar's top half was
   BEHIND the banner. .khatam-field is position:relative with z-index:0, which
   makes it a positioned box painted after the static avatar beneath it. The
   row holding the avatar is lifted above it. */
body.ui-v2 .v2-profile-top { position: relative; z-index: 1; }
/* The secondary doors: a row of plain links under the bio, 44px tall each,
   where X keeps its joined date and link. One primary control stays beside
   the banner; three pills down the right of a phone was the alternative. */
body.ui-v2 .v2-profile-links {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0 var(--s4);
  margin: var(--s1) 0 0;
}
body.ui-v2 .v2-profile-links a,
body.ui-v2 .v2-profile-links button {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: var(--t-ui); font-weight: 600;
  color: var(--v-green-ink); text-decoration: underline;
  text-decoration-thickness: 1px; text-underline-offset: .16em;
}
body.ui-v2 .v2-profile-links a:hover,
body.ui-v2 .v2-profile-links button:hover { text-decoration-thickness: 2px; }
/* The search field's ring: one shape. outline-offset -1 draws the 2px ring
   over the hairline instead of outside it, so a focused field is not a
   double ring, and the border colour is left alone. */
body.ui-v2 .v2-search-field:focus-within { border-color: var(--v-line); outline-offset: -1px; }
/* The column runs to the top of the window at 1100 and up, as X's does: no
   24px of paper inside the edges above the composer sheet or the profile
   banner. The two rails drop to 12px so the composer avatar's centre (12 + 22)
   and the left rail's mark share a line, and the right rail's first card sits
   on the composer's top edge rather than 24px below it. */
@media (min-width: 1100px) {
  body.ui-v2 main.shell { padding-top: 0; }
  body.ui-v2 .v2-leftrail { padding-top: var(--s3); }
  body.ui-v2 .right-rail { top: var(--s3); }
  body.ui-v2.is-member .right-rail { padding-top: var(--s3); }
}
/* Measured after the first pass (tap sweep): the timestamp link is 44px tall
   and, now that it reads "1d", only 15 to 24px wide. A 44px floor on its
   width, text kept on the right edge. */
body.ui-v2 li.prow .prow-when a { min-width: 44px; justify-content: flex-end; }
/* The search input measured 192x42 inside its 44px field: the input is the
   element a finger lands on, so it is the full height. */
body.ui-v2 .v2-search-field { padding-top: 0; padding-bottom: 0; }
body.ui-v2 .v2-search-field input { min-height: 44px; }
/* A lone link in an empty state's paragraph ("Find people to follow") measured
   161x21. It is a standalone row, not a link inside a sentence. */
body.ui-v2 .empty p > a:only-child { display: inline-flex; align-items: center; min-height: 44px; }
/* Settings carries its type in inline style attributes (nine sizes measured on
   one phone screen), which nothing but an !important can reach. Mapped by the
   inline value: the four small notes to the meta step, the leads to the UI
   step. Its secondary buttons join the 44px quiet pill; the checkboxes grow to
   22px inside their 44px label rows. */
body.ui-v2 main.shell [style*="font-size:.9rem"],
body.ui-v2 main.shell [style*="font-size:.82rem"],
body.ui-v2 main.shell [style*="font-size:.85rem"],
body.ui-v2 main.shell [style*="font-size:.88rem"] { font-size: var(--t-meta) !important; }
body.ui-v2 main.shell [style*="font-size:.95rem"],
body.ui-v2 main.shell [style*="font-size:1rem"] { font-size: var(--t-ui) !important; }
body.ui-v2 .empty-title { font-size: var(--t-section) !important; font-family: 'Manrope', system-ui, sans-serif; }
:root:not([data-theme="light"]) body.ui-v2 .shell-wrap a.btn.secondary:not(.load-more),
:root[data-theme="dark"] body.ui-v2 .shell-wrap a.btn.secondary:not(.load-more),
body.ui-v2 .shell-wrap a.btn.secondary:not(.load-more),
body.ui-v2 .shell-wrap button.btn.secondary {
  background: transparent; color: var(--v-ink); border: 1px solid var(--v-ink-soft);
  box-shadow: none; border-radius: var(--r-full);
  min-height: 44px; padding: 0 var(--s4);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: 'Manrope', system-ui, sans-serif; font-size: var(--t-ui); font-weight: 700;
  text-decoration: none; cursor: pointer;
}
body.ui-v2 .shell-wrap button.btn.secondary:hover,
body.ui-v2 .shell-wrap a.btn.secondary:not(.load-more):hover { background: rgba(19,26,32,.06); }
body.ui-v2 .settings-form input[type="checkbox"] { width: 22px !important; height: 22px !important; }
body.ui-v2 .settings-card label { display: inline-flex; align-items: center; min-height: 44px; gap: var(--s2); }
/* Three strays the second measurement found: the theme radios' labels at
   15.2px (a v1 rule), the native radios and the leave-confirm checkbox at 16
   to 17px, and the visually hidden page name at 30px, which a type-scale
   probe still counts. */
body.ui-v2 .settings-card label { font-size: var(--t-ui) !important; }
body.ui-v2 .settings-card input[type="radio"],
body.ui-v2 .settings-card input[type="checkbox"] { width: 22px !important; height: 22px !important; }
body.ui-v2 .sr-only { font-size: var(--t-ui); }

/* =========================================================================
   DESIGN PASS ROUND 1.6 (Sep 19 2026, card 1BIpNLgx). Two blind seats scored
   round 1.5 at 84 and 85 against 90. The four items below are what they
   agreed on. Everything here is scoped to body.ui-v2; v1 does not load this
   file.
   ========================================================================= */

/* ---------- 1. The ad slot's no-fill state --------------------------------
   Seat A blocked exchange.muslimadnetwork.com at the network layer, which is
   what an ad blocker does for roughly a third of visitors. Measured before
   this rule (member home, both viewports): the wrapper in .is-loading was a
   390x336 block with NO card styling (the card only arrived with .is-live), and
   the iframe, whose navigation had failed, painted its own error document:
   solid #FFFFFF at every sampled pixel on a rgb(17,22,27) page in dark, and an
   unexplained 336px gap in light. On WebKit a blocked frame fires no event at
   all, so the box stands for the full 6s deadline before ads.js collapses it.

   One rule left here (Sep 19 2026): the frame box gets a themed ground of its
   own, which is what a reader sees while a unit is on its way. The card sheet
   moved onto the wrapper for every state (see the ad block above), and the
   frame's opacity gate moved into the shared base rules, because both now
   belong to the contract rather than to v2. */
body.ui-v2 .mrx-frame-box {
  background: var(--v-paper);
  box-shadow: inset 0 0 0 1px var(--v-line);
}

/* ---------- 2, 3, 4. (retired) the guest's trust content on a phone ---------
   Round 1.6 rendered the v1 guest rail IN FLOW under the feed below 1100 and a
   condensed trust strip under the hero. Craft round, Part 1 (r795) replaces
   both: the people card after post three and the trust card after post eight,
   each rendered once per width, see the block at the end of this file. */

/* ---------- 5. One foreground green -----------------------------------------
   Seat A sampled three near-duplicate greens as text. Audited with a treewalker
   over every visible text node (footer excluded), the v2 member home has FIVE
   text colours, all tiers: ink, ink-soft, green-ink, danger, and the pinned
   light label on the green chrome and the green button (a ground pair that
   does not follow the theme, by design, see --v-green-deep). The guest home
   had a sixth: .rail-kicker still took var(--green-deep), the GROUND token,
   as a foreground in light (#14532D beside #166534 everywhere else). Dark was
   already remapped to the ink green. One token now. */
body.ui-v2 .rail-kicker { color: var(--v-green-ink); }

/* ==========================================================================
   ROUND 1.5, SECOND PASS (Sep 19 2026) -- what two blind seats measured
   ========================================================================== */

/* ---------- B1. THE HEADER WRAPPED THE DOTS ONTO THEIR OWN LINE ----------
   MEASURED at 390: name 100 + handle 107 + time 52 + More 44 + three 8px gaps
   = 327 in a 302px row, so More dropped to a second 44px line on 7 of the
   first 12 rows (every @ummahsocial post). flex-wrap wraps BEFORE it shrinks:
   an item's hypothetical size is its content, and the handle's content is
   107px however shrinkable it is. With flex-basis 0 the handle asks for
   nothing at the wrap decision, then takes what is left and ellipsises. The
   audience chip keeps its content basis, so it is still the one thing that
   goes to a second line, which is round 1.3's rule unchanged. */
body.ui-v2 li.prow .prow-handle { flex: 1 1 0; min-width: 0; }
body.ui-v2 li.prow .prow-when { margin-left: 0; }

/* ---------- B2. THE SHELL JUMPED 182px ON PAGES WITH NO RAIL -------------
   Notifications and settings declare no rail, so the grid dropped its third
   track and the column slid right. A member's column now sits in the same
   place on every page; the third track is simply empty where nothing fills
   it, which is what X does on its settings page. */
@media (min-width: 1100px) {
  body.ui-v2.is-member .shell-wrap { grid-template-columns: 72px minmax(0, 600px) 340px; }
}
@media (min-width: 1280px) {
  body.ui-v2.is-member .shell-wrap { grid-template-columns: 240px minmax(0, 600px) 340px; }
}

/* ---------- B3. "REMOVE MY PRESENCE" AT 3.21:1 IN DARK -------------------
   The button carries an inline `background: var(--danger)`, and v1's dark
   block lightens --danger for use as TEXT. A ground is not a foreground
   (Sep 9). The destructive fill is pinned to the dark red in both themes,
   white on it is 6.6:1. */
body.ui-v2 button.btn[style*="danger"] { background: #A63D2F !important; color: #FFFFFF !important; }

/* ---------- B7. SETTINGS: ONE CONTAINER, NOT THREE ----------------------
   div.composer (14px radius, ringed) alternated with the flat forms. Every
   block in the settings column is now the same sheet. Inputs at 5px join the
   control radius. */
body.ui-v2 main.shell > div.composer {
  background: var(--v-card); border: 0 !important; border-bottom: 1px solid var(--v-line) !important;
  border-radius: 0; box-shadow: none; margin: 0; padding: var(--s4);
}
body.ui-v2 main.shell input[type="checkbox"] { width: 22px !important; height: 22px !important; border-radius: var(--r-ctl); }
body.ui-v2 main.shell label { min-height: 44px; cursor: pointer; }
body.ui-v2 main.shell > div.composer .row,
body.ui-v2 main.shell > div.composer form.row { margin-top: var(--s3); display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2) var(--s3); }

/* ---------- B9. THE AVATAR LINK RAN THE HEIGHT OF THE ROW ---------------
   grid-row: span 9 stretched the <a> to the row's full height (399px on one
   post), an invisible profile target down the left of every post. The link
   is its 44px box. */
body.ui-v2 li.prow .prow-avatar { align-self: start; height: 44px; display: block; }

/* ---------- B12. THE TAB'S FOCUS RING WAS CLIPPED UNDER THE HEADER -------
   outline-offset 2px extends the ring above nav.tabs, where the sticky header
   (z 40 over 30) covers it. Drawn inside the tab instead. */
body.ui-v2 nav.tabs a:focus-visible { outline-offset: -3px; }

/* ---------- B15. ONE PAGE-TITLE TREATMENT --------------------------------
   Search and Notifications carried a gold-star h1 with a rule; Settings, the
   post page and profiles a plain one. The star and the rule go; every page
   title is the same 21px Manrope line. */
body.ui-v2 main.shell > h1.section-mark {
  display: block; margin: 0; padding: var(--s3) var(--s4);
  font-family: 'Manrope', system-ui, sans-serif; font-size: var(--t-section); font-weight: 800;
  min-height: 52px; box-sizing: border-box; line-height: 28px;
}
body.ui-v2 main.shell > h1.section-mark .khatam { display: none; }
body.ui-v2 main.shell > h1.section-mark::after { content: none; }
body.ui-v2 main.shell > .prose { padding-bottom: 0 !important; }
body.ui-v2 main.shell > .prose h1 { min-height: 28px; }

/* ---------- B14. THE MODULE SAYS ITS ORDER ------------------------------
   partials/who-to-follow orders by most recent post. The guest module said
   so; the member module did not. */
body.ui-v2 .v2-sugg-note { font-size: var(--t-meta); font-weight: 400; color: var(--v-ink-soft); margin-left: var(--s2); letter-spacing: 0; }

/* ---------- A P2-4. THE GOLD RING AT 2.62:1 ON CREAM ---------------------
   The ring conveys information (an Ummah One member), so it is a graphical
   object with a 3:1 bar, and brand gold #C8912E measures 2.62 on paper. The
   ring takes the same darker gold the appreciated star's edge already uses
   (--v-gold-deep, 5.04:1); in dark the two tokens are one value, 8.4:1. */
body.ui-v2 li.prow .uo-ring,
body.ui-v2 .v2-sugg-face .uo-ring,
body.ui-v2 .v2-profile-avatar.uo-ring,
body.ui-v2 .sands-tray .uo-ring,
body.ui-v2 img.avatar.uo-ring { box-shadow: 0 0 0 2px var(--v-paper), 0 0 0 4px var(--v-gold-deep); }
body.ui-v2 .v2-sugg-face .uo-ring { box-shadow: 0 0 0 2px var(--v-card), 0 0 0 4px var(--v-gold-deep); }
body.ui-v2 .v2-profile-avatar.uo-ring { box-shadow: 0 0 0 4px var(--v-paper), 0 0 0 7px var(--v-gold-deep); }
body.ui-v2 .uo-legend .ring-dot { box-shadow: 0 0 0 2px var(--v-gold-deep), 0 0 0 4px rgba(138,101,32,.25); }

/* ---------- A P2-3. THE RAIL'S FOCUS RING WAS CLIPPED LEFT AND RIGHT -----
   nav.v2-leftrail is overflow-y:auto, which also clips x. The ring is drawn
   inside the link. */
body.ui-v2 .v2-rail-link:focus-visible,
body.ui-v2 .v2-rail-mark:focus-visible { outline-offset: -2px; }

/* ---------- B13. (retired) one ask on screen at a time: the bar is gone. --- */

/* ==========================================================================
   ROUND 1.7 (Sep 19 2026) -- the guest column moved 182px between pages
   ========================================================================== */

/* B2 above reserved the rail's track for a MEMBER, and a member's column now
   sits still. A guest's did not: measured at 1440, main.shell sat at x=238 on
   the home feed (has-rail) and at x=420 on /about, /standards, a profile and
   a post page, which is exactly (340 + 24) / 2. Same treatment, same reason:
   the third track exists on every page and is simply empty where no rail
   fills it. At 1100 the two tracks plus gap and padding total 1044px. */
@media (min-width: 1100px) {
  body.ui-v2.is-guest .shell-wrap { grid-template-columns: minmax(0, 640px) 340px; }
}

/* ---------------------------------------------------------------------------
   Public-ready round, Sep 19 2026: the shared activity strip shifts on a
   warm navigation. Same fix and same measurement as app.css -- see that
   file's comment beside this rule for the full mechanism. Repeated here,
   not inherited, because v2 loads this stylesheet INSTEAD of app.css. */
.umg-activity__list { min-height: 98px; }

/* ==========================================================================
   CRAFT ROUND, PART 1: THE FIRST SCREEN (Sep 19 2026)
   Plan: /root/.claude/plans/ummahsocial-craft-round-sep19.md (r790 to r798).
   What a signed-out stranger meets: the forest masthead (top bar, headline,
   sentence, handle field, the khatim lattice, the 2px gold rule), the feed
   heading row, the gold day marker, white cards on paper, the people card
   after post three and the trust card after post eight on a phone, and the
   three-module rail on desktop. Layout rules are GUEST-scoped; token resets
   are the direction's and apply to the whole v2 layer. Parts 2 and 3 (post
   type scale, action row, link preview, avatar seal, profile) are not here.
   ========================================================================== */

/* ---------- tokens the direction resets ----------------------------------
   Cards go LIGHTER than paper; that is the whole separation. The hairline
   warms to the paper. Gold-as-text exists for the day marker and nothing
   else. --v-forest is a GROUND carrying light text and does not follow the
   theme (Sep 9: 29 broken text nodes the last time a ground was lightened). */
body.ui-v2 {
  --v-ink: #0F1F17;
  --v-ink-2: rgba(15,31,23,.68);
  --v-line: #E3DCCB;
  --v-card: #FFFFFF;
  --v-gold-text: #96631A;
  --v-forest: #166534;
  --v-card-shadow: 0 1px 2px rgba(15,31,23,.06);
  --r-card: 12px;   /* the same value as --r-media: four radii, not five */
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body.ui-v2 {
    --v-ink-2: #A0ABA4; --v-card: #1A2127; --v-line: #2C353C;
    --v-gold-text: #D9A94F; --v-card-shadow: none;
  }
}
:root[data-theme="dark"] body.ui-v2 {
  --v-ink-2: #A0ABA4; --v-card: #1A2127; --v-line: #2C353C;
  --v-gold-text: #D9A94F; --v-card-shadow: none;
}

/* ---------- the top bar joins the masthead ------------------------------
   Same forest as the block under it, 56px, and the wordmark pinned cream in
   every theme state: the dark theme's `:root:not([data-theme="light"]) a`
   at (0,2,1) would otherwise paint it #5CB585 on forest. Three selectors,
   one per theme state, as the button armour above does. */
body.ui-v2.is-guest { --v-head-h: calc(56px + env(safe-area-inset-top)); }
body.ui-v2.is-guest .v2-head { background: var(--v-forest); min-height: 56px; }
:root:not([data-theme="light"]) body.ui-v2 .v2-wordmark,
:root[data-theme="dark"] body.ui-v2 .v2-wordmark,
body.ui-v2 .v2-wordmark { color: #F4F6F1; }
body.ui-v2.is-guest .v2-head :focus-visible { outline-color: #F4F6F1; }

/* ---------- the masthead ------------------------------------------------- */
body.ui-v2 .v2-mast {
  position: relative; overflow: hidden;
  background: var(--v-forest); color: #FAF8F3;
  padding: 16px 16px 12px;
}
body.ui-v2 .v2-mast-lat {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
  color: var(--v-gold); pointer-events: none;
}
body.ui-v2 .v2-mast-in { position: relative; max-width: 952px; margin: 0 auto; }
/* Sora 700 34/1.1 on a phone, tracking -.02em, 48 from 1100 (below). The
   direction wrote 36; measured in Sora 700 the r798 second line "The Ummah's
   square." is 375.3px at 36 on a 358px column and wraps, orphaning the last
   word on a third line. At 34 it is 354.1px and holds (ruled Sep 19 2026,
   the words do not change). The line break is r798's own; the text is not
   balanced or rewrapped by the browser, so a line that does not fit is
   measured, not hidden. */
body.ui-v2 .v2-mast h1 {
  font-family: 'Sora', system-ui, sans-serif;
  font-size: 34px; line-height: 1.1; font-weight: 700; letter-spacing: -.02em;
  color: #FAF8F3; margin: 0 0 10px; text-wrap: wrap;
}
body.ui-v2 .v2-mast-sub {
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: 16px; line-height: 1.45; font-weight: 400;
  color: rgba(250,248,243,.85); margin: 0 0 12px; max-width: 46ch;
}
/* The one control. A paper field and a paper button on forest: the button is
   --paper with forest 700 text (6.7:1). A forest button on the forest masthead
   is invisible, and white on gold measures 2.78:1. */
body.ui-v2 .v2-mast-claim { display: flex; gap: 8px; margin: 0; }
body.ui-v2 .v2-mast-field {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 2px;
  height: 48px; padding: 0 12px;
  background: #FAF8F3; border-radius: 10px; cursor: text;
}
body.ui-v2 .v2-mast-field:focus-within { outline: 2px solid #F4F6F1; outline-offset: 2px; }
body.ui-v2 .v2-mast-at {
  font-family: 'Manrope', system-ui, sans-serif; font-size: 16px; font-weight: 700;
  color: rgba(15,31,23,.68); flex: none;
}
body.ui-v2 .v2-mast-field #handle-try-input {
  flex: 1 1 auto; min-width: 0; margin: 0; padding: 0; min-height: 44px;
  border: 0; background: transparent; box-shadow: none; outline: none;
  font-family: 'Manrope', system-ui, sans-serif; font-size: 16px; color: #0F1F17;
  -webkit-appearance: none; appearance: none;
}
body.ui-v2 .v2-mast-field #handle-try-input::placeholder { color: rgba(15,31,23,.55); opacity: 1; }
/* Part 3b (both seats): a cream button beside a cream field read as two
   fields. The one action is gold with ink text, 6.0:1 by computed colours.
   RECORDED DEVIATION from the direction's "gold only on the lattice, the
   star, the rule and the ring": this button sits above the feed, so it never
   competes with the day markers, and it is pinned in both themes like the
   masthead it sits on. */
body.ui-v2 .v2-mast-btn {
  flex: none; height: 48px; padding: 0 16px; margin: 0;
  border: 0; border-radius: 10px; cursor: pointer;
  background: #C8912E; color: #0F1F17;
  font-family: 'Manrope', system-ui, sans-serif; font-size: 16px; font-weight: 700;
  white-space: nowrap;
}
body.ui-v2 .v2-mast-btn:hover { background: #D19B3A; }
body.ui-v2 .v2-mast-btn:focus-visible { outline: 2px solid #F4F6F1; outline-offset: 2px; }
body.ui-v2 .v2-mast-btn-long { display: none; }
@media (min-width: 640px) {
  body.ui-v2 .v2-mast-btn-long { display: inline; }
  body.ui-v2 .v2-mast-btn-short { display: none; }
}
/* handle-check.js writes into this line. Empty, it takes no room (the v1
   rule reserved 1.2em under every render); with words it opens 8px under the
   field. Both states cream on forest, whatever the word: the sentence carries
   the meaning, not a colour. */
/* Keyed on the masthead ANCESTOR, never on a class of the note itself:
   handle-check.js rewrites the note's className on every answer, which drops
   any hook placed there, and v1's `.handle-try-note.warn` then painted
   #45524D on forest, measured 1.65:1 (caught by a real typed check). */
body.ui-v2 .v2-mast .handle-try-note,
body.ui-v2 .v2-mast .handle-try-note.ok,
body.ui-v2 .v2-mast .handle-try-note.warn {
  margin: 0; min-height: 0; font-size: 13px; line-height: 1.4;
  color: rgba(250,248,243,.92);
}
body.ui-v2 .v2-mast .handle-try-note:not(:empty) { margin-top: 8px; }
/* The masthead's foot: the 2px gold rule, once (r796). */
body.ui-v2 .v2-mast-rule { height: 2px; background: var(--v-gold); }

/* ---------- the feed heading row ----------------------------------------
   Sora 600 20/1.2, ink, one hairline to the right. Nothing beside it. */
body.ui-v2.is-guest .v2-section-mark {
  margin: 12px 16px 0; padding: 0; gap: 12px;
  align-items: center; flex-wrap: nowrap; line-height: 1.2;
}
body.ui-v2.is-guest .v2-section-mark > span {
  font-family: 'Sora', system-ui, sans-serif;
  font-size: 20px; line-height: 1.2; font-weight: 600; letter-spacing: -.005em;
  color: var(--v-ink); white-space: nowrap;
}
body.ui-v2.is-guest .v2-section-mark::after { background: var(--v-line); }

/* ---------- the day marker: gold caps, the only gold text on the page ---- */
body.ui-v2 li.v2-day .d {
  font-size: 13px; line-height: 1; letter-spacing: .12em;
  color: var(--v-gold-text);
}
body.ui-v2 li.v2-day .r { background: var(--v-gold); opacity: .55; }
body.ui-v2 li.v2-day { background: transparent; border-bottom: 0; padding: 10px 16px 6px; }
body.ui-v2 li.v2-day:first-child { padding-top: 8px; padding-bottom: 6px; }

/* ---------- white cards on paper -----------------------------------------
   Every post is an object: --card ground, the hairline, a 12px corner and a
   1px shadow. The ring's inner gap matches the card it sits on. Part 1 scoped
   these to the guest; Part 2 (same day) extends them to every v2 feed, the
   member home included, by widening the selectors in place rather than
   writing a second rule for the same properties. */
body.ui-v2 ul.timeline { margin: 0; padding: 0; }
body.ui-v2 li.prow {
  margin: 0 16px 8px; padding: 10px 16px 2px; /* Part 3: 12/8 -> 10/2, the 44px rows carry their own air */
  background: var(--v-card); border: 1px solid var(--v-line);
  border-radius: var(--r-card); box-shadow: var(--v-card-shadow);
}
body.ui-v2 li.prow .uo-ring { box-shadow: 0 0 0 2px var(--v-card), 0 0 0 4px var(--v-gold-deep); }
body.ui-v2.is-guest li.guest-gate {
  margin: 0 16px 8px; border: 1px solid var(--v-line);
  border-radius: var(--r-card); box-shadow: var(--v-card-shadow);
}
body.ui-v2 .load-more { margin: 4px 16px 0; border-bottom: 0; }
body.ui-v2.is-guest .uo-legend-footnote { margin: 8px 0 0; }

/* ---------- the people card and the trust card --------------------------
   One card shape for the two guest modules, in the rail and inline. */
body.ui-v2 li.v2-inline-card { list-style: none; margin: 4px 16px 12px; padding: 0; }
body.ui-v2 .v2-pcard, body.ui-v2 .v2-tcard {
  background: var(--v-card); border: 1px solid var(--v-line);
  border-radius: var(--r-card); box-shadow: var(--v-card-shadow);
  padding: 14px 16px; margin: 0;
}
body.ui-v2 .v2-guest-rail .v2-pcard, body.ui-v2 .v2-guest-rail .v2-tcard { margin-bottom: 16px; }
body.ui-v2 .v2-pcard-h, body.ui-v2 .v2-tcard-h {
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: 15px; line-height: 1.3; font-weight: 700; color: var(--v-ink);
  margin: 0 0 6px; letter-spacing: 0; text-transform: none;
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
}
body.ui-v2 .v2-pcard-note { font-size: 13px; line-height: 1.3; font-weight: 400; color: var(--v-ink-2); }
body.ui-v2 .v2-pcard-list { list-style: none; margin: 0; padding: 0; }
body.ui-v2 .v2-pcard-list li { margin: 0; padding: 0; }
:root:not([data-theme="light"]) body.ui-v2 .v2-pcard-list a,
:root[data-theme="dark"] body.ui-v2 .v2-pcard-list a,
body.ui-v2 .v2-pcard-list a {
  display: flex; align-items: center; gap: 12px; min-height: 48px;
  color: var(--v-ink); text-decoration: none;
}
body.ui-v2 .v2-pcard-list a:hover .v2-pcard-name { text-decoration: underline; }
body.ui-v2 .v2-pcard-list .avatar {
  width: 40px; height: 40px; border-radius: var(--r-full); object-fit: cover; flex: none;
}
body.ui-v2 .v2-pcard-list .avatar--initial {
  display: flex; align-items: center; justify-content: center;
  background: var(--v-green-deep); color: #F4F6F1;
  font-family: 'Manrope', system-ui, sans-serif; font-size: 15px; font-weight: 700;
}
body.ui-v2 .v2-pcard-who { min-width: 0; display: flex; flex-direction: column; }
body.ui-v2 .v2-pcard-name {
  font-family: 'Manrope', system-ui, sans-serif; font-size: 15px; line-height: 1.3; font-weight: 700;
  color: var(--v-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body.ui-v2 .v2-pcard-handle {
  font-size: 13px; line-height: 1.3; color: var(--v-ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body.ui-v2 .v2-tcard p { font-size: 15px; line-height: 1.5; color: var(--v-ink); margin: 0 0 8px; }
body.ui-v2 .v2-tcard p a { color: var(--v-green-ink); font-weight: 600; }
body.ui-v2 .v2-tcard-links { display: flex; gap: 20px; flex-wrap: wrap; margin: 2px 0 0; }
body.ui-v2 .v2-tcard-links a {
  display: inline-flex; align-items: center; min-height: 44px;
  font-size: 15px; font-weight: 700; color: var(--v-green-ink);
}

/* ---------- the ad card: --card with a post's hairline, both states (r790) */
body.ui-v2 .mrx-ad-wrap { border-radius: var(--r-card); box-shadow: var(--v-card-shadow); }
body.ui-v2 .v2-guest-rail .mrx-ad-wrap { padding: 12px 9px; }
body.ui-v2.is-guest .foot-ad { width: calc(100% - 32px); max-width: 600px; }
/* The house unit on the card ground: forest type, gold kicker, the one
   forest-fill button (it sits on a light ground, where forest fill is the
   rule). Theme-following, so it is never the darkest object on the page.
   Three selectors beat the dark `a` rule and the Part-1 anchor pin. */
:root:not([data-theme="light"]) body.ui-v2 .mrx-ad-wrap .mrx-frame-box a.mrx-house,
:root[data-theme="dark"] body.ui-v2 .mrx-ad-wrap .mrx-frame-box a.mrx-house,
body.ui-v2 .mrx-ad-wrap .mrx-frame-box a.mrx-house { background: var(--v-card); color: var(--v-ink); }
body.ui-v2 .mrx-house-kicker { color: var(--v-gold-text); }
body.ui-v2 .mrx-house-head { color: var(--v-green-ink); font-family: 'Sora', system-ui, sans-serif; font-size: 20px; font-weight: 600; letter-spacing: -.005em; }
body.ui-v2 .mrx-house-cta { background: var(--v-green-btn); color: #FFFFFF; font-size: 15px; }
body.ui-v2 .mrx-house:hover .mrx-house-cta, body.ui-v2 .mrx-house:focus-visible .mrx-house-cta { background: #14532D; }
body.ui-v2 .mrx-house-chip { background: var(--v-paper); border-color: var(--v-line); color: var(--v-ink-2); }

/* ---------- desktop, 1100 and up ------------------------------------------
   Column 600 at x 244, rail 320 at x 876, gap 32; the block is 952 and
   centred. The masthead's inner block and the top bar's padding share the
   same 952 so the wordmark, the headline and the column start on one line. */
@media (min-width: 1100px) {
  body.ui-v2.is-guest { --v-head-h: calc(56px + env(safe-area-inset-top)); }
  body.ui-v2.is-guest .v2-head {
    padding-left: max(16px, calc(50% - 476px));
    padding-right: max(16px, calc(50% - 476px));
  }
  body.ui-v2.is-guest .shell-wrap,
  body.ui-v2.is-guest.has-rail .shell-wrap {
    grid-template-columns: minmax(0, 600px) 320px; gap: 32px; padding: 0 16px;
  }
  body.ui-v2 .v2-mast { padding: 20px 16px 20px; }
  body.ui-v2 .v2-mast h1 { font-size: 48px; line-height: 1.05; margin-bottom: 8px; }
  /* One line at this width: the 46ch phone cap broke the sentence in two on a
     600px column and cost the masthead 23px. */
  body.ui-v2 .v2-mast-sub { margin-bottom: 12px; max-width: none; }
  body.ui-v2 .v2-mast-claim { max-width: 520px; }
  body.ui-v2.is-guest .v2-section-mark { margin: 12px 0 0; }
  body.ui-v2 li.v2-day { padding-left: 0; padding-right: 0; }
  body.ui-v2 li.prow,
  body.ui-v2 li.guest-gate { margin-left: 0; margin-right: 0; }
  body.ui-v2 .load-more { margin-left: 0; margin-right: 0; }
  body.ui-v2.is-guest li.v2-inline-card { display: none; }
  /* The rail's first card sits level with the heading row (12px under the
     rule), and sticks under the 56px bar, not beneath it. */
  body.ui-v2 .v2-guest-rail {
    display: block; padding: 12px 0 24px;
    top: calc(var(--v-head-h) + 12px);
    max-height: calc(100vh - var(--v-head-h) - 24px);
  }
}

/* ==========================================================================
   CRAFT ROUND, PART 2: THE FEED (Sep 19 2026)
   The post's own type, from the direction's table: body Manrope 15/1.4 at
   every width (the 17px step at 640+ goes), paragraph gap 6px, measure 60ch;
   poster name 15/1.3 700 ink; handle and time 13/1.3 at ink .68. White cards
   for every v2 feed (the Part 1 block above, widened). The action row, the
   link preview and the avatar seal are Part 3 and are untouched here.
   ========================================================================== */
body.ui-v2 li.prow .prow-body {
  font-size: 15px; line-height: 1.4; max-width: 60ch;
}
/* Arabic script keeps the larger step at every width now that Latin is 15
   everywhere: the round 1.3 reasoning (marks below the x-height, no capitals
   to anchor a word shape) does not change with the column. */
body.ui-v2 li.prow .prow-body.is-arabic { font-size: 17px; line-height: 1.5; }
@media (max-width: 639.98px) {
  body.ui-v2 li.prow .prow-body { font-size: 15px; }
  body.ui-v2 li.prow .prow-body.is-arabic { font-size: 17px; }
}
/* Paragraphs are blocks (BodyFormat::paragraphs) so the gap is 6px, not a
   pre-wrap blank line worth a whole line-height. */
body.ui-v2 li.prow .prow-body p { margin: 0 0 6px; }
body.ui-v2 li.prow .prow-body p:last-child { margin-bottom: 0; }
/* The name keeps its 44px painted target (round 1.4) with the type at 1.3:
   19.5px of line plus 12.25px above and below. A block, so ellipsis works. */
body.ui-v2 li.prow .prow-name {
  font-size: 15px; line-height: 1.3; padding: 12.25px 0; min-height: 44px; box-sizing: border-box;
}
body.ui-v2 li.prow .prow-handle,
body.ui-v2 li.prow .prow-when { font-size: 13px; line-height: 1.3; color: var(--v-ink-2); }
body.ui-v2 li.prow .prow-when a { color: inherit; }
/* The ring legend inside the first ringed card, under its head row. */
body.ui-v2 li.prow .uo-legend {
  grid-column: 2; margin: 0 0 6px; font-size: 13px; line-height: 1.3; color: var(--v-ink-2);
}
body.ui-v2 li.prow .uo-legend .ring-dot { background: var(--v-card); box-shadow: 0 0 0 2px var(--v-gold-deep), 0 0 0 4px rgba(138,101,32,.25); }
body.ui-v2 li.prow .uo-legend a { color: var(--v-green-ink); }
/* The member's inline "People to follow" module sits between cards below
   1100, so it takes the same card shape as the guest's people card. */
body.ui-v2 li.v2-inline-suggest {
  margin: 4px 16px 12px; padding: 14px 16px;
  border: 1px solid var(--v-line); border-radius: var(--r-card); box-shadow: var(--v-card-shadow);
}

/* ==========================================================================
   CRAFT ROUND, PART 3: THE FINISH (Sep 19 2026)
   The action row, the head row's air, the house avatar, the member fallback
   disc, the link preview, the wordmark lockup. Profile composition and r792
   counts untouched; masthead, people card layout, Ummah One copy untouched.
   ========================================================================== */
body.ui-v2 { --v-sand: #F3E8CF; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) body.ui-v2 { --v-sand: #3A3327; } }
:root[data-theme="dark"] body.ui-v2 { --v-sand: #3A3327; }

/* ---------- the action row: a grid, evenly spaced --------------------------
   Every control is a 44px painted target with its 20px glyph centred; the
   row is a column-flow grid spread across the column up to 332px, the first
   glyph pulled 12px left so it sits on the body's own left edge. The count
   sits inside the same target and only when it is non-zero. */
body.ui-v2 li.prow .actions {
  display: grid; grid-auto-flow: column; grid-auto-columns: max-content;
  justify-content: space-between; align-items: center;
  width: calc(100% + 12px); max-width: 332px; margin: 0 0 0 -12px;
}
body.ui-v2 li.prow .act { padding: 0; gap: 4px; justify-content: center; min-width: 44px; }
body.ui-v2 li.prow .act:has(.n:not([style*="none"])) { padding-right: 8px; }
/* The appreciate star at 2.1 against the other glyphs' 1.75: its strokes
   are short diagonals and read lighter at the same weight. */
body.ui-v2 li.prow .act .khatam { fill: none; stroke: currentColor; stroke-width: 2.1; stroke-linejoin: round; }
body.ui-v2 li.prow .act.like-btn .burst { left: 22px; }

/* ---------- the house avatar and the member fallback ---------------------- */
body.ui-v2 li.prow .avatar--seal { border-radius: var(--r-ctl); object-fit: cover; }
body.ui-v2 li.prow .avatar--initial,
body.ui-v2 .v2-pcard-list .avatar--initial {
  background: var(--v-sand); color: var(--v-green-ink);
  font-family: 'Manrope', system-ui, sans-serif; font-weight: 700;
}

/* ---------- the link preview: a 1.91:1 box, centre crop, no text on it ---- */
body.ui-v2 li.prow .link-card { max-height: none; min-height: 0; align-items: center; }
body.ui-v2 li.prow .link-card-thumb {
  width: 128px; height: 67px; flex: none; object-fit: cover; object-position: center;
  margin: 8px 0 8px 8px; border: 0; border-radius: var(--r-ctl);
  /* v1's carried rule sets min-height:100% and max-height:108px on this
     element; a rule wins only the properties it names (round 1.3), and the
     first cut of this one did not name these, so the box stretched to the
     card's height (measured 128x106). */
  min-height: 0; max-height: none;
}
@media (min-width: 640px) {
  body.ui-v2 li.prow .link-card { max-height: none; }
  body.ui-v2 li.prow .link-card-thumb { width: 160px; height: 84px; }
}
body.ui-v2 li.prow .link-card-text { padding: 8px 12px; }
/* The domain is one line and never breaks inside itself: with a 128px thumb
   the phone's text column is 116px, and the inherited overflow-wrap:anywhere
   split "ummahplaces.com" across two lines (measured, 39px for one word). */
body.ui-v2 li.prow .link-card-meta { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; overflow-wrap: normal; }

/* ---------- the wordmark lockup -----------------------------------------
   One lockup: the filled star on the text baseline, a fixed 8px gap, word
   Manrope 700 at 17px with the star scaled to the cap height so the two read
   at one weight. Used in the top bar and the left rail. */
body.ui-v2 .v2-wordmark,
body.ui-v2 .v2-rail-mark {
  display: inline-flex; align-items: baseline; gap: 8px;
  font-family: 'Manrope', system-ui, sans-serif; font-weight: 700;
  font-size: 17px; line-height: 1; letter-spacing: -.01em;
  min-height: 0; height: 44px; padding: 13.5px 0; box-sizing: border-box;
}
body.ui-v2 .v2-rail-mark { padding: 13.5px 12px; margin-bottom: 12px; }
body.ui-v2 .v2-wordmark .khatam,
body.ui-v2 .v2-rail-mark .khatam { width: 13px; height: 13px; color: var(--v-gold); flex: none; }
@media (min-width: 1100px) and (max-width: 1279.98px) {
  body.ui-v2 .v2-rail-mark { justify-content: center; padding: 11px 0; }
  body.ui-v2 .v2-rail-mark .khatam { width: 22px; height: 22px; }
}

/* ==========================================================================
   CRAFT ROUND, PART 3b (Sep 19 2026): what two blind craft seats required
   ========================================================================== */
/* ---------- one fallback disc everywhere under v2 -------------------------
   Every initial disc, whatever module rendered it: forest on sand, Manrope
   700. Listed by class so each earlier, more specific rule is beaten at its
   own specificity rather than by an !important. */
body.ui-v2 .avatar--initial,
body.ui-v2 li.prow .avatar--initial,
body.ui-v2 .v2-pcard-list .avatar--initial,
body.ui-v2 span.v2-composer-avatar,
body.ui-v2 span.v2-me-av.avatar--initial,
body.ui-v2 .v2-profile-avatar.avatar--initial,
body.ui-v2 .v2-sugg-face .avatar--initial,
body.ui-v2 .starter .faces .avatar--initial,
body.ui-v2 .sands-ring .avatar--initial {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--v-sand); color: var(--v-green-ink);
  font-family: 'Manrope', system-ui, sans-serif; font-weight: 700;
  border-radius: var(--r-full); text-decoration: none;
}
/* The starter pack's faces and the Sands tray's own tile were sized by
   `img` selectors; the component may render a span there. */
body.ui-v2 .v2-suggest .starter .faces .avatar,
body.ui-v2 .starter .faces .avatar { width: 34px; height: 34px; font-size: 15px; border: 2px solid var(--v-card); margin-left: -8px; box-sizing: content-box; flex: none; }
body.ui-v2 .starter .faces .avatar:first-child { margin-left: 0; }
body.ui-v2 .sands-ring .avatar { width: 52px; height: 52px; font-size: 20px; }
body.ui-v2 span.v2-me-av.avatar--initial { width: 24px; height: 24px; font-size: 12px; }
/* ---------- the ring legend under the head row ---------------------------- */
body.ui-v2 li.prow .uo-legend { margin: 0 0 4px; }
/* ---------- the paragraph clamp for desk posts that quote scripture ------- */
body.ui-v2 li.prow .prow-body.clamp-para.clamped {
  display: block; -webkit-line-clamp: unset; -webkit-box-orient: initial; overflow: visible;
}
body.ui-v2 li.prow .prow-body.clamp-para.clamped p.p-after { display: none; }

/* ---------- the ad frame's visible edge is ours, in both states -----------
   Seat B, dark: a bright 1px white frame on the ad card, the only white line
   on the page. Measured: our wrapper computes the hairline token
   (rgb(44,53,60)); the white ring is INSIDE the served creative, which paints
   its own 1px light border around its 300x250. The frame's edge is now drawn
   by us, above the unit, in the same --v-line the post cards use, so the
   visible border follows the theme whatever the creative brings. Pointer
   events pass through; the house unit sits above it (z-index 2) and carries
   no edge of its own. */
body.ui-v2 .mrx-frame-box { position: relative; }
body.ui-v2 .mrx-frame-box::after {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  border-radius: inherit; box-shadow: inset 0 0 0 1px var(--v-line);
}
