/* ══════════════════════════════════════════════════════════════════════
   BBA Peptides Forum — visual design system (bbaforum.com)
   Sampled verbatim from the shipped store, not approximated: tokens below
   were cross-checked against design/store_final.html's live :root (canvas/
   ink/hairline/radius/shadow/motion/ring scales) and design/site-header.css
   + site-header.js + site-footer.js + chat.js for the header/footer/pill/
   button idiom. This is the SAME design system wearing a forum layout, not
   a lookalike — nothing here invents a new palette, radius, or type rule.

   STRUCTURE is the "Atella" dashboard reference izzy supplied (persistent
   sidebar, stat cluster, split hero, 3-up card grid, two bottom panels) —
   but every surface, radius and type choice below is BBA's own: STIX Two
   Text serif for headlines/identity names, IBM Plex Sans for UI/body, IBM
   Plex Mono UPPERCASE for every label/meta/number, hairline borders over
   shadows, 8px radius ceiling (not the reference's pillowy 14px), navy
   #102A4A for the one dark panel (not near-black, not purple).

   FONT LOADING — reuses the exact mechanism store_final.html uses: a
   self-hosted stylesheet at /vendor/fonts.css (no Google Fonts / CDN, no
   new font service). _layout-reference.html links it at that root-relative
   path, matching the store's own <link rel="stylesheet" href="/vendor/
   fonts.css">. Because bbaforum.com is a separate REGISTRABLE domain and
   not a subdomain of the store (forum/wrangler.toml explains why), whoever
   wires the forum's static hosting must COPY that folder into the forum's
   own deploy root — cross-origin @font-face requests are blocked without a
   CORS header the main site doesn't currently send, and there is no
   same-site relaxation to fall back on between two different registrable
   domains the way there would have been across a subdomain. Until that's
   the font stack's system fallbacks (Georgia for the serif, Segoe UI /
   Helvetica for the sans, Consolas for the mono) keep the page fully
   legible and on-model. See the mockup's <head> comment for the same note.

   Prefixed .bf- (BBA Forum) throughout — deliberately distinct from the
   store's .bba- prefix (site-header.js / site-footer.js / chat.js) so this
   sheet can never collide with those scripts if a future page loads both.
═══════════════════════════════════════════════════════════════════════ */

/* ── THE BRAND SERIF, ACTUALLY LOADED (2026-08-21) ─────────────────────
   MEASURED BUG, not a preference. /vendor/fonts.css ships IBM Plex Sans and
   IBM Plex Mono and NOTHING ELSE — document.fonts on the live preview listed
   exactly seven faces, none of them STIX. So every element set in
   --f-display was falling through to the second name in the stack and
   rendering in TIMES NEW ROMAN. Proof, run against the live page: a canvas
   measure of the same string at 500 15.5px came back 241.9453125px for
   "STIX Two Text" and 241.9453125px for "Times New Roman" — bit-identical,
   i.e. the same face. Georgia measured 263.03px, so it was not reaching the
   third fallback either.

   That single missing @font-face is most of "something just seems off": the
   forum's headlines were set in the one serif that reads as an unstyled
   document. The file below is the SAME latin-subset .woff2 the store itself
   serves for STIX Two Text (design/vendor/fonts.css maps normal 500/600/700
   to it — one variable file, exactly like the Plex Sans entry above it),
   copied into this project's own /vendor/fonts/ for the same-origin reason
   the header of this file documents. No CDN, no external URL, no new
   typeface — this is the brand's own serif finally arriving. */
@font-face{
  font-family:'STIX Two Text';
  font-style:normal;
  font-weight:500;
  font-display:swap;
  src:url(/vendor/fonts/YA9Vr02F12Xkf5whdwKf11l0p76Miw.woff2) format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:'STIX Two Text';
  font-style:normal;
  font-weight:600;
  font-display:swap;
  src:url(/vendor/fonts/YA9Vr02F12Xkf5whdwKf11l0p76Miw.woff2) format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:'STIX Two Text';
  font-style:normal;
  font-weight:700;
  font-display:swap;
  src:url(/vendor/fonts/YA9Vr02F12Xkf5whdwKf11l0p76Miw.woff2) format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ── TOKENS — sampled verbatim from store_final.html's :root ─────────── */
:root{
  /* Canvas & surfaces */
  --canvas:      #F2F1EE;
  --canvas-deep: #EAE7E0;
  --surface-1:   #FFFFFF;
  --surface-2:   #FAF8F4;
  --surface-3:   #F0EDE6;
  --paper:       #F4F2EE;
  --paper-warm:  #EDE9E2;

  /* Dark panel — the hero's one dark surface. Navy, not near-black. */
  --navy:        #102A4A;
  --navy-deep:   #0A1E36;
  --navy-mid:    #1A3A5C;

  /* Brand accents */
  --teal:        #0C6E78;
  --teal-bright: #2BB9C9;
  --accent:      #0C6E78;
  --accent-press:#0A5A63;
  --blue:        #2E78C2;
  --brand-red:   #CD4642;   /* accents / alerts ONLY — never a UI default */
  --clay:        #C98A4B;

  /* Ink / text — on light surfaces.
     RE-WEIGHTED 2026-08-21 against measured contrast, not taste. The old
     ramp put --text-ghost at .38, which computes to 2.41:1 on white and
     2.36:1 on the canvas — a hard WCAG AA failure (needs 4.5:1), and it was
     carrying thread meta, category descriptions, column heads, counts,
     timestamps and the sidebar. Seven distinct surfaces measured under 2.5:1
     on the live page. The alpha at which ink 20,24,28 first clears 4.5:1 was
     solved per ground: .590 on white, .605 on canvas, .610 on paper-warm.
     The values below sit clear of the worst of those with margin to spare,
     so no surface in this system depends on which ground it lands on.
     Names are untouched — every property that existed still exists. */
  --ink:         #14181C;
  --ink-soft:    rgba(20,24,28,.84);
  --body:        rgba(20,24,28,.74);
  --text-ghost:  rgba(20,24,28,.64);
  /* The old .38 kept, renamed to nothing and removed from nothing — it is a
     NEW name for the one job the old value was actually good at: a mark that
     is decoration, not text (an inert separator glyph, a disabled tick). It
     must never carry a word a member has to read. */
  --text-mute:   rgba(20,24,28,.38);
  /* Ink / text — on the navy hero panel */
  --ink-invert:  #FFFFFF;
  --body-invert: rgba(255,255,255,.72);
  --ghost-invert:rgba(255,255,255,.48);

  /* Hairlines — the system leads with these, not shadows */
  --hairline:      #DDD9D1;
  --hair-1:        rgba(20,26,32,.10);
  --hair-2:        rgba(20,26,32,.16);
  --hair-3:        rgba(20,26,32,.26);
  --cool-panel:    #EEF2F6;
  --hairline-cool: #D0D8E4;

  /* Gradients — brand teal→blue, used sparingly (hero media panel only) */
  --grad-brand:   linear-gradient(180deg,#0C6E78 0%,#2E78C2 100%);
  --grad-brand-h: linear-gradient(90deg,#0C6E78 0%,#2E78C2 100%);

  /* Status — green/amber lifted from chat.js's own presence dots, so a
     forum "open" thread and a "Mia is online" dot read as the same idea */
  --status-open:   #22B26A;
  --status-locked: rgba(20,24,28,.42);
  --status-amber:  #C8912B;

  /* ── WARMTH (added 2026-08-21) ────────────────────────────────────────
     izzy: "it feels cold". It was — the page was ink-on-white plus one teal,
     and every tint in it was a grey. Nothing below is a new HUE: each is
     derived from a colour :root already declares. --clay-ink is the existing
     --clay (#C98A4B) walked down in lightness until it clears AA as TEXT
     (the raw clay measures 2.91:1 on white and can never carry a word);
     --teal-* are the existing --teal at low alpha; --warm-* are the canvas
     and paper the store already ships, used as a ground rather than a card.
     These give the page somewhere to be warm that is not a grey. */
  --clay-ink:      #8C5C2A;              /* clay, darkened to 6.29:1 on white */
  --teal-wash:     rgba(12,110,120,.055);
  --teal-wash-2:   rgba(12,110,120,.10);
  --teal-edge:     rgba(12,110,120,.22);
  --warm-band:     #F6F3ED;              /* page-head ground, warmer than canvas */
  --warm-rule:     #E4DED2;              /* hairline with warmth in it */
  --warm-tint:     rgba(201,138,75,.09); /* clay at a whisper — invitation ground */

  /* Type families */
  --f-display: 'STIX Two Text', 'Times New Roman', Georgia, serif;
  --f-text:    'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
  --f-mono:    'IBM Plex Mono', Consolas, 'Courier New', monospace;

  /* Type scale */
  --t-h1: clamp(32px, 4.2vw, 44px);
  --t-h2: clamp(29px, 3vw, 36px);
  --t-h3: clamp(20px, 2.2vw, 26px);

  /* Radii — tight scale, 8px ceiling on cards. No pillowy 14px anywhere. */
  --radius-xs:   3px;
  --radius-sm:   5px;
  --radius-md:   8px;
  --radius-pill: 999px;

  /* Shadows — used sparingly; hairlines carry most edges */
  --shadow-xs:   0 1px 2px rgba(14,32,56,.06);
  --shadow-sm:   0 2px 8px rgba(14,32,56,.08), 0 1px 2px rgba(14,32,56,.04);
  --shadow-md:   0 6px 24px rgba(14,32,56,.10), 0 2px 6px rgba(14,32,56,.05);
  --shadow-navy: 0 4px 20px rgba(16,42,74,.16);

  /* Motion */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-base: 160ms;
  --dur-slow: 320ms;

  /* Focus ring — teal, 15–18:1 on paper per the store's own a11y pass */
  --ring-width:  2px;
  --ring-offset: 2px;
  --ring-color:  #0C6E78;

  /* Layout */
  --wrap:      1200px;
  --gutter:    clamp(20px, 5vw, 32px);
  --s1: 8px; --s2: 16px; --s3: 32px; --s4: 64px;
  --sidebar-w: 250px;
  --topbar-h:  64px;
}

/* ── RESET & BASE ──────────────────────────────────────────────────── */
*, *::before, *::after{ box-sizing:border-box; margin:0; padding:0; }
/* 16px, up from 15px. Every professional forum measured for this pass runs a
   16px root: Discourse 16, Flarum 16, old.reddit 16; XenForo is the one
   outlier at 15 and it compensates with a 17px thread title. Nothing in these
   four sheets is sized in rem (checked — zero matches), so this changes the
   default text of unstyled elements and form controls and nothing else; it
   does not silently rescale the component layer under it. */
html{ font-size:16px; scroll-behavior:smooth; }
@media (prefers-reduced-motion:reduce){ html{ scroll-behavior:auto; } }

body.bf-body{
  font-family:var(--f-text);
  background:var(--canvas);
  color:var(--ink);
  -webkit-font-smoothing:antialiased;
  line-height:1.55;
  min-height:100vh;
}

img, svg{ display:block; max-width:100%; }
ul, ol{ list-style:none; }
button{ font:inherit; }
a{ color:inherit; }

/* Prevent ANY element in this system from forcing body-level horizontal
   scroll — the one hard failure mode at 375px. Long unbroken strings
   (thread titles, emails) wrap instead of pushing width. */
.bf-shell, .bf-main, .bf-card-grid, .bf-bottom-row{ min-width:0; }
.bf-headline, .bf-hero-title, .bf-card-title, .bf-thread-title{ overflow-wrap:anywhere; }

/* ── ACCESSIBILITY ─────────────────────────────────────────────────── */
.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;
}
.bf-skip{
  position:absolute; left:-9999px; top:0; z-index:1001;
  background:var(--navy); color:#fff; padding:12px 16px;
  border-radius:0 0 var(--radius-sm) 0; font-family:var(--f-text);
  font-size:14px; text-decoration:none; display:inline-flex; align-items:center;
  min-height:44px;
}
.bf-skip:focus{ left:0; }

:where(a, button, input, select, textarea, summary, [tabindex]:not([tabindex="-1"])):focus{
  outline:none;
}
:where(a, button, input, select, textarea, summary, [tabindex]:not([tabindex="-1"])):focus-visible{
  outline:var(--ring-width) solid var(--ring-color);
  outline-offset:var(--ring-offset);
  border-radius:var(--radius-xs);
}

@media (prefers-reduced-motion:reduce){
  *, *::before, *::after{
    animation-duration:0.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:0.001ms !important;
    scroll-behavior:auto !important;
  }
}

/* Touch: every interactive row/button/link grows to the 44px WCAG 2.5.5/
   2.5.8 floor. Desktop keeps its denser, keyboard/mouse rhythm — same
   split the store's own footer + header already ship (site-footer.js
   ".bba-foot-list a", site-header.css ".bba-cart"). */
@media (pointer:coarse){
  .bf-nav-row, .bf-user-chip, .bf-thread-row, .bf-schedule-row,
  .bf-browse-all, .btn, .btn-link, .bf-post-reply{
    min-height:44px;
  }
}

/* ── TYPE PRIMITIVES ───────────────────────────────────────────────── */
.bf-eyebrow{
  font-family:var(--f-mono);
  font-size:11px;
  font-weight:500;
  letter-spacing:.16em;
  text-transform:uppercase;
  color:var(--teal);
  margin:0 0 8px;
}
.bf-eyebrow.on-dark{ color:var(--teal-bright); }
.bf-eyebrow.neutral{ color:var(--body); }

.bf-headline{
  font-family:var(--f-display);
  font-weight:500;
  font-size:var(--t-h1);
  letter-spacing:-.015em;
  line-height:1.08;
  color:var(--ink);
  margin:0 0 12px;
}
.bf-supporting{
  font-family:var(--f-text);
  font-size:15px;
  line-height:1.55;
  color:var(--body);
  max-width:56ch;
  margin:0;
}
.bf-section-title{
  font-family:var(--f-display);
  font-weight:500;
  font-size:var(--t-h3);
  letter-spacing:-.01em;
  line-height:1.2;
  color:var(--ink);
  margin:0;
}

/* ── BUTTONS ───────────────────────────────────────────────────────── */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  min-height:42px; padding:0 20px;
  font-family:var(--f-text); font-size:14px; font-weight:600;
  border-radius:var(--radius-pill); border:0; cursor:pointer;
  text-decoration:none; white-space:nowrap;
  transition:background var(--dur-base) var(--ease-out),
             border-color var(--dur-base) var(--ease-out),
             box-shadow var(--dur-base) var(--ease-out);
}
.btn-primary{ background:var(--teal); color:#fff; }
.btn-primary:hover{ background:var(--accent-press); }

/* Full-width dark variant — navy, matching the hero panel + .btn-primary's
   own :hover target (--navy-mid) from store_final.html, so "dark button"
   reads as one family across the store and the forum. */
.btn-dark{
  display:flex; width:100%; align-items:center; justify-content:center;
  gap:8px; min-height:44px; padding:0 18px;
  background:var(--navy); color:#fff;
  font-family:var(--f-text); font-size:14px; font-weight:600;
  border-radius:var(--radius-md); border:0; cursor:pointer; text-decoration:none;
  transition:background var(--dur-base) var(--ease-out);
}
.btn-dark:hover{ background:var(--navy-mid); }

.btn-link{
  display:inline-flex; align-items:center; gap:6px;
  font-family:var(--f-text); font-size:13.5px; font-weight:600;
  color:var(--teal); text-decoration:underline; text-underline-offset:3px;
  background:none; border:0; cursor:pointer; padding:2px 0;
  transition:color var(--dur-base) var(--ease-out);
}
.btn-link:hover{ color:var(--accent-press); }
.btn-link.on-dark{ color:var(--body-invert); text-decoration-color:var(--ghost-invert); }
.btn-link.on-dark:hover{ color:#fff; }

/* ── PILLS / BADGES ────────────────────────────────────────────────── */
.bf-pill{
  display:inline-flex; align-items:center; gap:6px;
  height:24px; padding:0 10px;
  font-family:var(--f-mono); font-size:10.5px; font-weight:500;
  letter-spacing:.08em; text-transform:uppercase;
  border-radius:var(--radius-pill); white-space:nowrap;
}
/* Category tag — soft teal tint, matches the store's own hover fill
   (rgba(12,110,120,.08)) used on .bba-cart / .bba-drawer a:focus-visible */
.bf-pill-category{
  background:rgba(12,110,120,.08);
  color:var(--teal);
  border:1px solid rgba(12,110,120,.18);
}
/* Status — a leading dot carries the state; text stays neutral ink */
.bf-pill-status{
  background:var(--surface-1);
  color:var(--ink-soft);
  border:1px solid var(--hair-1);
}
.bf-pill-status::before{
  content:''; width:6px; height:6px; border-radius:50%; flex:none;
}
.bf-pill-status.is-open::before{ background:var(--status-open); }
.bf-pill-status.is-locked::before{ background:var(--status-locked); }
.bf-pill-status.is-locked{ color:var(--body); }

/* ── LAYOUT SHELL ──────────────────────────────────────────────────── */
.bf-shell{
  display:flex;
  align-items:stretch;
  min-height:100vh;
}

/* ── ANNOUNCEMENT BAR — same slot/idiom as the store's .bba-promo ────── */
.bf-promo{
  background:var(--teal);
  color:#F7F5F1;
  font-family:var(--f-mono);
  font-size:11px; font-weight:500; letter-spacing:.14em; text-transform:uppercase;
  text-align:center; padding:9px 16px;
  border-bottom:1px solid rgba(4,20,26,.35);
  line-height:1.4;
}
.bf-promo .bf-dot{ color:var(--brand-red); }
.bf-promo a{ color:#fff; font-weight:600; text-decoration:underline; text-underline-offset:2px; }
@media (max-width:520px){ .bf-promo{ font-size:10.5px; padding:8px 12px; letter-spacing:.08em; } }

/* ── SIDEBAR ───────────────────────────────────────────────────────── */
.bf-sidebar{
  flex:none;
  width:var(--sidebar-w);
  display:flex; flex-direction:column;
  background:var(--surface-1);
  border-right:1px solid var(--hair-1);
  position:sticky; top:0; align-self:flex-start;
  height:100vh; overflow-y:auto;
  padding:20px 16px;
}

.bf-brand{ padding:4px 8px 16px; }
.bf-brand-mark{ display:inline-flex; align-items:center; min-height:44px; text-decoration:none; }
.bf-brand-mark img{ width:120px; height:auto; }
/* Pure-HTML degrade: if the cross-origin logo can't load (see file header
   note on /vendor/fonts.css + forum's separate subdomain), the alt text
   below renders as the wordmark instead of a broken-image glyph. */
.bf-affiliation{
  font-family:var(--f-text); font-size:12px; line-height:1.4;
  color:var(--body); margin:8px 2px 0; max-width:22ch;
}

.bf-nav-groups{ display:flex; flex-direction:column; gap:20px; flex:1; }
.bf-nav-label{
  font-family:var(--f-mono); font-size:10.5px; font-weight:600;
  letter-spacing:.14em; text-transform:uppercase; color:var(--text-ghost);
  padding:0 10px; margin-bottom:6px;
}
.bf-nav-group{ display:flex; flex-direction:column; gap:2px; }

.bf-nav-row{
  display:flex; align-items:center; gap:11px;
  min-height:40px; padding:0 10px;
  border-radius:var(--radius-md);
  font-family:var(--f-text); font-size:14px; font-weight:500;
  color:var(--ink-soft); text-decoration:none;
  position:relative;
  transition:background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.bf-nav-row:hover{ background:rgba(20,26,32,.045); color:var(--ink); }
.bf-nav-icon{ flex:none; width:18px; height:18px; color:var(--body); }
.bf-nav-row:hover .bf-nav-icon{ color:var(--ink); }

.bf-nav-row.is-active{
  background:rgba(12,110,120,.08);
  color:var(--teal);
  font-weight:600;
}
.bf-nav-row.is-active .bf-nav-icon{ color:var(--teal); }
.bf-nav-row.is-active::before{
  content:''; position:absolute; left:0; top:8px; bottom:8px; width:3px;
  border-radius:var(--radius-pill); background:var(--teal);
}

.bf-nav-utility{
  display:flex; flex-direction:column; gap:2px;
  margin-top:20px; padding-top:16px; border-top:1px solid var(--hair-1);
}

.bf-sidebar-ruo{
  font-family:var(--f-mono); font-size:9.5px; line-height:1.6;
  letter-spacing:.02em; color:var(--text-ghost);
  margin-top:16px; padding:12px 10px 4px; border-top:1px solid var(--hair-1);
}

/* ── TOPBAR ────────────────────────────────────────────────────────── */
.bf-main-col{ flex:1; min-width:0; display:flex; flex-direction:column; }

.bf-topbar{
  position:sticky; top:0; z-index:20;
  display:flex; justify-content:flex-end; align-items:center;
  min-height:var(--topbar-h); padding:0 var(--gutter);
  background:rgba(252,251,248,.90);
  -webkit-backdrop-filter:blur(16px) saturate(140%);
  backdrop-filter:blur(16px) saturate(140%);
  border-bottom:1px solid var(--hair-1);
}

.bf-user-chip{
  display:flex; align-items:center; gap:9px;
  min-height:44px; padding:5px 14px 5px 6px;
  background:var(--surface-1);
  border:1px solid var(--hair-1); border-radius:var(--radius-pill);
  box-shadow:var(--shadow-xs);
  cursor:pointer;
  transition:border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.bf-user-chip:hover{ border-color:var(--hair-2); box-shadow:var(--shadow-sm); }
.bf-avatar{
  width:30px; height:30px; border-radius:50%; flex:none;
  background:var(--teal); color:#fff;
  font-family:var(--f-text); font-size:12.5px; font-weight:600;
  display:flex; align-items:center; justify-content:center;
}
.bf-user-name{ font-family:var(--f-text); font-size:13.5px; font-weight:600; color:var(--ink); }
.bf-user-chip svg{ width:14px; height:14px; color:var(--body); flex:none; }

/* ── MAIN CONTENT ──────────────────────────────────────────────────── */
.bf-main{
  flex:1;
  max-width:1080px;
  width:100%;
  margin:0 auto;
  padding:var(--s3) var(--gutter) var(--s4);
  display:flex; flex-direction:column; gap:var(--s3);
}

/* ── PAGE HEAD + STAT CLUSTER ──────────────────────────────────────── */
.bf-page-head{
  display:flex; flex-wrap:wrap; align-items:flex-end; justify-content:space-between;
  gap:24px;
}
.bf-page-head-text{ flex:1; min-width:260px; }

.bf-stat-cluster{ display:flex; gap:28px; }
.bf-stat{ display:flex; flex-direction:column; gap:4px; }
.bf-stat:not(:first-child){ border-left:1px solid var(--hairline); padding-left:28px; }
.bf-stat-number{
  font-family:var(--f-text); font-weight:700; font-size:27px; line-height:1;
  color:var(--ink); display:flex; align-items:baseline; gap:4px;
}
.bf-stat-unit{ font-family:var(--f-mono); font-size:11px; color:var(--body); }
.bf-stat-caption{
  font-family:var(--f-mono); font-size:10px; font-weight:500;
  letter-spacing:.1em; text-transform:uppercase; color:var(--body);
  line-height:1.5;
}
@media (max-width:480px){
  .bf-stat-cluster{ flex-direction:column; gap:14px; }
  .bf-stat:not(:first-child){ border-left:0; border-top:1px solid var(--hairline); padding-left:0; padding-top:14px; }
}

/* ── HERO CARD ─────────────────────────────────────────────────────── */
.bf-hero-card{
  display:flex;
  border-radius:var(--radius-md);
  border:1px solid var(--hair-1);
  overflow:hidden;
  box-shadow:var(--shadow-sm);
}

.bf-hero-media{
  flex:0 0 34%;
  min-width:200px;
  background:var(--grad-brand);
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:14px; padding:28px 20px; position:relative;
}
.bf-hero-glyph{
  width:56px; height:56px; border-radius:50%;
  background:rgba(255,255,255,.16);
  border:1px solid rgba(255,255,255,.3);
  display:flex; align-items:center; justify-content:center;
}
.bf-hero-glyph svg{ width:24px; height:24px; color:#fff; }
.bf-hero-media-meta{
  position:absolute; left:16px; bottom:14px;
  font-family:var(--f-mono); font-size:10px; letter-spacing:.1em; text-transform:uppercase;
  color:rgba(255,255,255,.82);
}

.bf-hero-body{
  flex:1; min-width:0;
  background:var(--navy);
  box-shadow:var(--shadow-navy);
  color:#fff;
  padding:32px clamp(20px,3vw,40px);
  display:flex; flex-direction:column; justify-content:center; gap:10px;
}
.bf-hero-title{
  font-family:var(--f-display); font-weight:500;
  font-size:clamp(22px,2.6vw,28px); line-height:1.18; letter-spacing:-.01em;
  color:#fff; margin:0;
}
.bf-hero-meta{
  font-family:var(--f-mono); font-size:11px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--body-invert);
}
.bf-hero-actions{ display:flex; align-items:center; gap:20px; margin-top:14px; flex-wrap:wrap; }

@media (max-width:760px){
  .bf-hero-card{ flex-direction:column; }
  .bf-hero-media{ flex:none; padding:24px; }
  .bf-hero-media-meta{ position:static; margin-top:4px; }
}

/* ── SECTION HEADER ROW ────────────────────────────────────────────── */
.bf-section-head{
  display:flex; align-items:flex-end; justify-content:space-between;
  gap:16px; flex-wrap:wrap;
}
.bf-browse-all{
  font-family:var(--f-text); font-size:13.5px; font-weight:600;
  color:var(--teal); text-decoration:underline; text-underline-offset:3px;
  display:inline-flex; align-items:center; gap:5px; white-space:nowrap;
  padding:4px 0;
}
.bf-browse-all:hover{ color:var(--accent-press); }

/* ── CARD GRID / CATEGORY CARD ─────────────────────────────────────── */
.bf-card-grid{
  display:grid;
  grid-template-columns:repeat(3, 1fr);
  gap:20px;
}
@media (max-width:1100px){ .bf-card-grid{ grid-template-columns:repeat(2, 1fr); } }
@media (max-width:700px){ .bf-card-grid{ grid-template-columns:1fr; } }

.bf-category-card{
  display:flex; flex-direction:column;
  background:var(--surface-1);
  border:1px solid var(--hair-1);
  border-radius:var(--radius-md);
  padding:22px;
  transition:box-shadow var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
.bf-category-card:hover{ box-shadow:var(--shadow-xs); border-color:var(--hair-2); }

.bf-card-badges{ display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:14px; }
.bf-card-title{
  font-family:var(--f-display); font-weight:500; font-size:19px; line-height:1.25;
  color:var(--ink); margin:0 0 8px;
}
.bf-card-desc{
  font-family:var(--f-text); font-size:13.5px; line-height:1.55; color:var(--body);
  margin:0 0 18px;
}
.bf-progress-row{
  display:flex; align-items:baseline; justify-content:space-between;
  font-family:var(--f-mono); font-size:10.5px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--body); margin-bottom:8px;
}
.bf-progress-row strong{ color:var(--ink-soft); font-weight:600; }
.bf-progress-rule{ height:1px; background:var(--hairline); margin-bottom:18px; }
.bf-card-footer{
  font-family:var(--f-mono); font-size:10.5px; letter-spacing:.04em; text-transform:uppercase;
  color:var(--text-ghost); margin-top:14px; line-height:1.5;
}
.bf-card-footer strong{ color:var(--body); font-weight:500; text-transform:none; letter-spacing:0; }

/* ── BOTTOM ROW — two equal panels ─────────────────────────────────── */
.bf-bottom-row{ display:grid; grid-template-columns:1fr 1fr; gap:20px; }
@media (max-width:700px){ .bf-bottom-row{ grid-template-columns:1fr; } }

.bf-panel{
  background:var(--surface-1);
  border:1px solid var(--hair-1);
  border-radius:var(--radius-md);
  padding:24px;
}
.bf-panel-title{ margin-bottom:16px; }

.bf-schedule-list{ display:flex; flex-direction:column; }
.bf-schedule-row{
  display:flex; align-items:center; gap:14px;
  padding:12px 4px; min-height:44px;
  border-top:1px solid var(--hair-1);
  text-decoration:none; color:inherit;
  border-radius:var(--radius-sm);
  transition:background var(--dur-base) var(--ease-out);
}
.bf-schedule-list li:first-child .bf-schedule-row{ border-top:0; }
.bf-schedule-row:hover{ background:rgba(20,26,32,.03); }
.bf-date-block{
  flex:none; width:46px; text-align:center;
  display:flex; flex-direction:column; align-items:center; gap:1px;
}
.bf-date-weekday{
  font-family:var(--f-mono); font-size:9.5px; font-weight:600;
  letter-spacing:.1em; text-transform:uppercase; color:var(--teal);
}
.bf-date-day{ font-family:var(--f-text); font-size:19px; font-weight:700; color:var(--ink); line-height:1.1; }
.bf-schedule-copy{ flex:1; min-width:0; }
.bf-schedule-title{ font-family:var(--f-text); font-size:14px; font-weight:600; color:var(--ink); margin-bottom:2px; }
.bf-schedule-sub{ font-family:var(--f-text); font-size:12.5px; color:var(--body); }
.bf-schedule-row svg{ flex:none; width:16px; height:16px; color:var(--text-ghost); }

/* Deliberate empty state — a new forum starts with nothing, and this
   should read as designed-for, not broken or unfinished. */
.bf-empty-state{
  display:flex; flex-direction:column; align-items:flex-start; gap:12px;
  padding:20px 4px 4px;
}
.bf-empty-icon{
  width:40px; height:40px; border-radius:50%;
  background:var(--cool-panel); display:flex; align-items:center; justify-content:center;
}
.bf-empty-icon svg{ width:18px; height:18px; color:var(--body); }
.bf-empty-copy{ font-family:var(--f-text); font-size:13.5px; line-height:1.55; color:var(--body); max-width:34ch; }

/* ── THREAD LIST ROW ───────────────────────────────────────────────── */
.bf-thread-list{ display:flex; flex-direction:column; background:var(--surface-1); border:1px solid var(--hair-1); border-radius:var(--radius-md); overflow:hidden; }
.bf-thread-row{
  display:flex; align-items:center; gap:14px;
  padding:14px 18px; min-height:44px;
  border-top:1px solid var(--hair-1);
  text-decoration:none; color:inherit;
  transition:background var(--dur-base) var(--ease-out);
}
.bf-thread-list li:first-child .bf-thread-row{ border-top:0; }
.bf-thread-row:hover{ background:rgba(20,26,32,.025); }
.bf-thread-avatar{
  flex:none; width:32px; height:32px; border-radius:50%;
  background:var(--cool-panel); color:var(--navy);
  font-family:var(--f-text); font-size:12px; font-weight:700;
  display:flex; align-items:center; justify-content:center;
}
.bf-thread-main{ flex:1; min-width:0; }
.bf-thread-title{
  font-family:var(--f-display); font-weight:500; font-size:15.5px;
  color:var(--ink); line-height:1.35; margin-bottom:3px;
  overflow:hidden; text-overflow:ellipsis; display:-webkit-box; -webkit-line-clamp:1; -webkit-box-orient:vertical;
}
.bf-thread-meta{ font-family:var(--f-mono); font-size:10.5px; letter-spacing:.04em; text-transform:uppercase; color:var(--body); }
.bf-thread-side{ flex:none; display:flex; align-items:center; gap:16px; }
.bf-thread-replies{ font-family:var(--f-mono); font-size:11px; color:var(--body); text-align:right; white-space:nowrap; }
.bf-thread-replies strong{ color:var(--ink-soft); font-weight:600; }
.bf-thread-time{ font-family:var(--f-mono); font-size:10.5px; color:var(--text-ghost); white-space:nowrap; min-width:52px; text-align:right; }

@media (max-width:640px){
  .bf-thread-row{ flex-wrap:wrap; }
  .bf-thread-side{ width:100%; justify-content:space-between; padding-left:46px; }
}

/* ── POST BLOCK ────────────────────────────────────────────────────── */
.bf-post{
  display:flex; gap:14px;
  background:var(--surface-1);
  border:1px solid var(--hair-1);
  border-radius:var(--radius-md);
  padding:20px;
}
.bf-post-avatar{
  flex:none; width:38px; height:38px; border-radius:50%;
  background:var(--teal); color:#fff;
  font-family:var(--f-text); font-size:14px; font-weight:600;
  display:flex; align-items:center; justify-content:center;
}
.bf-post-body{ flex:1; min-width:0; }
.bf-post-head{ display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; margin-bottom:8px; }
.bf-post-author{ font-family:var(--f-text); font-size:14px; font-weight:700; color:var(--ink); }
.bf-post-time{ font-family:var(--f-mono); font-size:10.5px; letter-spacing:.04em; text-transform:uppercase; color:var(--text-ghost); }
.bf-post-text{ font-family:var(--f-text); font-size:14.5px; line-height:1.6; color:var(--ink-soft); margin-bottom:14px; }
.bf-post-text p + p{ margin-top:10px; }
.bf-post-reply{
  display:inline-flex; align-items:center; gap:6px;
  font-family:var(--f-text); font-size:13px; font-weight:600; color:var(--teal);
  background:none; border:0; cursor:pointer; padding:4px 0;
}
.bf-post-reply:hover{ color:var(--accent-press); }
.bf-post-reply svg{ width:14px; height:14px; }

/* Held-for-moderation state — muted, explained, never looks like an error */
.bf-post-review{
  background:var(--canvas);
  border-style:dashed;
  border-color:var(--hair-2);
}
.bf-post-review .bf-post-avatar{ background:var(--hairline); color:var(--body); }
.bf-post-review .bf-post-author,
.bf-post-review .bf-post-text{ color:var(--body); }
.bf-review-note{
  display:inline-flex; align-items:center; gap:6px;
  font-family:var(--f-mono); font-size:10px; font-weight:600;
  letter-spacing:.08em; text-transform:uppercase; color:var(--status-amber);
  margin-bottom:10px;
}
.bf-review-note svg{ width:12px; height:12px; }
.bf-review-explainer{
  font-family:var(--f-mono); font-size:11px; line-height:1.6; color:var(--text-ghost);
  margin-top:10px; padding-top:10px; border-top:1px solid var(--hair-1);
}

/* ── DEMO SECTION DIVIDER (mockup only — groups the forum-specific
   components the reference layout didn't have: thread list, post, review
   state, pill-state swatch) ───────────────────────────────────────────── */
.bf-demo-section{ display:flex; flex-direction:column; gap:16px; }
.bf-demo-divider{
  display:flex; align-items:center; gap:14px;
  font-family:var(--f-mono); font-size:10px; font-weight:600;
  letter-spacing:.14em; text-transform:uppercase; color:var(--text-ghost);
  margin:8px 0 -4px;
}
.bf-demo-divider::after{ content:''; flex:1; height:1px; background:var(--hairline); }

/* ── FOOTER — recognisably the store's footer (site-footer.js idiom) ─── */
.bf-foot{
  background:var(--surface-3);
  border-top:1px solid var(--hair-1);
  margin-top:8px;
  color:rgba(20,24,28,.72);
  font-family:var(--f-text);
}
.bf-foot-inner{ max-width:1080px; margin:0 auto; padding:36px var(--gutter) 30px; }
.bf-foot-cols{ display:flex; flex-wrap:wrap; gap:32px 56px; }
.bf-foot-col-label{
  font-family:var(--f-mono); font-size:11px; letter-spacing:.14em; text-transform:uppercase;
  color:rgba(20,24,28,.72); margin:0 0 12px;
}
.bf-foot-list{ display:flex; flex-direction:column; gap:2px; }
.bf-foot-list a{
  color:rgba(20,24,28,.80); text-decoration:none; font-size:14px;
  display:flex; align-items:center; min-height:26px; width:fit-content; padding-right:8px;
}
.bf-foot-list a:hover{ color:var(--teal); }
@media (pointer:coarse), (max-width:760px){
  .bf-foot-list{ gap:0; }
  .bf-foot-list a{ min-height:44px; }
}
.bf-foot-ruo{
  margin:28px 0 0; padding:16px 0 0; border-top:1px solid var(--hair-1);
  font-family:var(--f-mono); font-size:11px; line-height:1.6; letter-spacing:.01em;
  color:rgba(20,24,28,.72);
}
.bf-foot-copy{
  margin:10px 0 0; font-family:var(--f-mono); font-size:11px; letter-spacing:.06em;
  text-transform:uppercase; color:rgba(20,24,28,.68);
}
.bf-foot-credit{
  color:rgba(20,24,28,.68); text-decoration:underline; text-underline-offset:2px;
  display:inline-flex; align-items:center; gap:5px; min-height:26px; vertical-align:middle;
}
.bf-foot-credit:hover{ color:var(--teal); }
@media (pointer:coarse), (max-width:760px){ .bf-foot-credit{ min-height:44px; } }

/* ── SIDEBAR → TOP BAR under ~860px ───────────────────────────────────
   Pure CSS, no reorder needed: the sidebar is already first in DOM order,
   so switching .bf-shell to a column keeps it visually on top. Internally
   it flips to a horizontally-scrollable row so nothing ever forces the
   BODY to scroll sideways — only this one strip does, deliberately. ─── */
@media (max-width:860px){
  .bf-shell{ flex-direction:column; min-height:0; }
  .bf-sidebar{
    position:sticky; top:0; z-index:30; align-self:auto;
    width:100%; height:auto; max-height:none;
    flex-direction:row; align-items:center; gap:14px;
    padding:8px 12px;
    border-right:0; border-bottom:1px solid var(--hair-1);
    background:rgba(252,251,248,.92);
    -webkit-backdrop-filter:blur(16px) saturate(140%);
    backdrop-filter:blur(16px) saturate(140%);
    overflow-x:auto; overflow-y:visible;
  }
  .bf-brand{ flex:none; padding:2px 4px; }
  .bf-brand-mark img{ width:96px; }
  .bf-affiliation, .bf-sidebar-ruo{ display:none; } /* RUO persists via the footer */
  .bf-nav-groups{ flex-direction:row; align-items:center; gap:18px; flex:none; }
  .bf-nav-group{ flex-direction:row; align-items:center; gap:2px; }
  .bf-nav-label{ display:none; }
  .bf-nav-row{ white-space:nowrap; padding:0 10px; }
  .bf-nav-row.is-active::before{ left:8px; right:8px; top:auto; bottom:0; width:auto; height:2px; }
  .bf-nav-utility{ flex-direction:row; margin:0; padding:0 0 0 14px; border-top:0; border-left:1px solid var(--hair-1); flex:none; }
}


/* ═══════════════════════════════════════════════════════════════════════
   THE CLASSIC FORUM SURFACES (izzy reversed the Telegram-rooms pivot,
   2026-08-21). Everything below is the chrome + compose + post-detail work
   the SSR routes need on top of the dashboard components already declared
   above — and every value reads ONLY the :root custom properties this file
   already declares. No new palette, no new radius or shadow scale, no
   second type stack. The chat-bubble block that briefly lived here is
   deleted rather than left orphaned: dead CSS is how two design systems
   quietly diverge.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── SHELL ADJUSTMENTS ────────────────────────────────────────────────── */
/* The wordmark is set in the display serif rather than fetched as an image:
   the forum is a separate origin from the store (see this file's header on
   /vendor/*), and a cross-origin logo that fails to load would leave a
   broken-image glyph where the brand should be. */
.bf-brand-word{
  font-family:var(--f-display); font-weight:500; font-size:19px;
  letter-spacing:.01em; color:var(--ink);
}
.bf-brand-mark:hover .bf-brand-word{ color:var(--teal); }

.bf-topbar{ justify-content:space-between; gap:16px; }
.bf-topbar-home{
  font-family:var(--f-mono); font-size:11px; font-weight:600;
  letter-spacing:.14em; text-transform:uppercase; color:var(--body);
  text-decoration:none; display:inline-flex; align-items:center; min-height:44px;
}
.bf-topbar-home:hover{ color:var(--teal); }
.bf-account{ display:flex; align-items:center; gap:14px; }
/* The chip is a STATIC label here, not a menu button — no pointer affordance
   for something that doesn't open. */
span.bf-user-chip{ cursor:default; }

/* The footer sits inside .bf-main (one column, one gap rhythm), so it bleeds
   back out to the full width of the main column instead of floating in the
   content gutter. */
.bf-main > .bf-foot{
  margin:var(--s3) calc(var(--gutter) * -1) calc(var(--s4) * -1);
}
.bf-foot-affil{
  font-family:var(--f-text); font-size:13px; font-weight:600;
  color:var(--ink); margin-top:18px;
}

/* ── PAGE HEAD EXTRAS ─────────────────────────────────────────────────── */
.bf-page-head-actions{ display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
/* A thread title is prose, not a display line — it wraps, it can be long, and
   it must not run at the 44px hero size. */
.bf-thread-headline{
  font-size:clamp(23px, 2.6vw, 31px); line-height:1.22; max-width:26ch;
}
.bf-marker-row{ display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }

/* ── PILLS ────────────────────────────────────────────────────────────── */
/* "In review" reuses the system's amber (the same one chat.js uses for a
   pending state) — a queued post is waiting, not broken, so it never gets the
   red reserved for alerts. */
.bf-pill-status.is-review{ color:#7A5A16; }
.bf-pill-status.is-review::before{ background:var(--status-amber); }

/* ── THREAD LIST ──────────────────────────────────────────────────────── */
/* A muted row is the VIEWER'S OWN held or removed thread — nobody else's copy
   of the page contains the row at all (the API filters it in SQL), so this
   styling is never what keeps a stranger from reading it. */
.bf-thread-row.is-muted{ background:var(--surface-2); }
.bf-thread-row.is-muted .bf-thread-title{ color:var(--body); }
.bf-thread-list .bf-pill{ flex:none; }

.bf-card-title a{ text-decoration:none; }
.bf-card-title a:hover{ color:var(--teal); }
.bf-card-footer a{ color:inherit; text-decoration:none; }
.bf-card-footer a:hover strong{ color:var(--teal); }

/* ── RULE LIST (posting rules, "before you post") ─────────────────────── */
.bf-rule-list{ display:flex; flex-direction:column; gap:9px; }
.bf-rule-list li{
  position:relative; padding-left:18px;
  font-family:var(--f-text); font-size:13.5px; line-height:1.55; color:var(--body);
}
.bf-rule-list li::before{
  content:''; position:absolute; left:2px; top:8px;
  width:5px; height:5px; border-radius:50%; background:var(--teal);
}
.bf-rule-list strong{ color:var(--ink-soft); font-weight:600; }
.bf-panel-foot{ margin-top:16px; }

/* ── POST LIST (thread view) ──────────────────────────────────────────── */
.bf-post-list{
  display:flex; flex-direction:column;
  background:var(--surface-1);
  border:1px solid var(--hair-1); border-radius:var(--radius-md);
  overflow:hidden;
}
.bf-post-list .bf-post{ border-top:1px solid var(--hair-1); }
.bf-post-list .bf-post:first-child{ border-top:0; }
/* The opening post carries the thread — a teal spine marks it without a
   second background colour. */
.bf-post-op{ box-shadow:inset 3px 0 0 var(--teal); }
.bf-post-time{ text-decoration:none; }
.bf-post-time:hover{ color:var(--teal); }
.bf-post-head .bf-pill{ flex:none; }

/* ── REPORT DISCLOSURE ────────────────────────────────────────────────── */
/* Native <details> — the report path works with JS disabled right up to the
   submit, which is the point: moderation surfaces don't get to be the ones
   that need a working script. */
.bf-report{ margin-top:14px; }
.bf-report summary{
  cursor:pointer; list-style:none; display:inline-flex; align-items:center;
  min-height:28px; font-family:var(--f-mono); font-size:10.5px;
  letter-spacing:.06em; text-transform:uppercase; color:var(--text-ghost);
}
.bf-report summary::-webkit-details-marker{ display:none; }
.bf-report summary:hover{ color:var(--brand-red); }
.bf-report-form{
  margin-top:10px; padding:14px; max-width:380px;
  display:flex; flex-direction:column; gap:8px;
  background:var(--surface-2);
  border:1px solid var(--hair-1); border-radius:var(--radius-sm);
}
.bf-report-form label{
  font-family:var(--f-mono); font-size:10.5px; letter-spacing:.06em;
  text-transform:uppercase; color:var(--body);
}
.bf-report-form select, .bf-report-form input[type=text]{
  width:100%; min-height:42px; padding:9px 10px;
  font:400 14px var(--f-text); color:var(--ink);
  background:var(--surface-1);
  border:1px solid var(--hair-2); border-radius:var(--radius-sm);
}
.bf-report-form button{ align-self:flex-start; }

/* ── PAGINATION ───────────────────────────────────────────────────────── */
/* Keyset links ("older" / "more replies"), never numbered pages — there is no
   page N to link to in a keyset walk. */
.bf-pager{
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  padding-top:4px;
}
.bf-pager a{ min-height:44px; display:inline-flex; align-items:center; }

/* ── COMPOSE (new thread + reply + the handle gate) ───────────────────── */
.bf-compose-card{
  background:var(--surface-1);
  border:1px solid var(--hair-1); border-radius:var(--radius-md);
  padding:22px;
  display:flex; flex-direction:column; gap:16px;
}
.bf-compose-gate{ align-items:flex-start; }
.bf-compose-gate .btn{ align-self:flex-start; }
/* The rules sit directly above the box, not behind a link: this is the one
   moment a first-time poster is deciding what to type. */
.bf-compose-rules{
  padding:14px 16px;
  background:var(--surface-2);
  border:1px solid var(--hair-1); border-radius:var(--radius-sm);
}
.bf-compose-form{ display:flex; flex-direction:column; gap:16px; }
.bf-field{ display:flex; flex-direction:column; gap:6px; }
.bf-field label{
  font-family:var(--f-mono); font-size:11px; font-weight:600;
  letter-spacing:.06em; text-transform:uppercase; color:var(--body);
}
.bf-compose-form input[type=text], .bf-compose-form select, .bf-compose-form textarea{
  width:100%; padding:12px;
  font:400 15px/1.6 var(--f-text); color:var(--ink);
  background:var(--surface-1);
  border:1px solid var(--hair-2); border-radius:var(--radius-sm);
}
/* 16px on touch: anything smaller makes iOS Safari zoom the page on focus,
   which is its own horizontal-scroll bug. */
@media (pointer:coarse){
  .bf-compose-form input[type=text], .bf-compose-form select,
  .bf-compose-form textarea, .bf-handle-form input{ font-size:16px; }
}
.bf-compose-form input[type=text]{ min-height:46px; }
.bf-compose-form select{ min-height:46px; }
.bf-compose-form textarea{ resize:vertical; min-height:150px; }
.bf-compose-actions{ display:flex; align-items:center; gap:16px; flex-wrap:wrap; }

.bf-handle-form{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
.bf-handle-form input{
  flex:1; min-width:200px; min-height:46px; padding:12px;
  font:400 15px var(--f-text); color:var(--ink);
  background:var(--surface-1);
  border:1px solid var(--hair-2); border-radius:var(--radius-sm);
}

/* ── MOBILE ───────────────────────────────────────────────────────────── */
@media (max-width:860px){
  .bf-page-head{ align-items:flex-start; }
  .bf-page-head-actions{ width:100%; }
  .bf-page-head-actions .btn, .bf-page-head-actions .btn-dark{ flex:1; justify-content:center; }
  .bf-thread-headline{ max-width:none; }
  .bf-main > .bf-foot{ margin-bottom:0; }

  /* THE MOBILE COMPOSER.
     A composer in normal flow does NOT move when the on-screen keyboard
     opens — the LAYOUT viewport is unchanged while the VISUAL viewport
     shrinks, so the box ends up underneath the keyboard and the person
     cannot see what they are typing. forms.js measures the real inset from
     window.visualViewport (ignoring anything under ~60px, which is browser
     chrome collapsing, not a keyboard), writes it here as --kb and adds
     .is-lifted; only then does the card leave the flow and pin itself
     exactly --kb above the layout viewport's bottom edge — i.e. sitting on
     top of the keyboard. Same mechanism, same token name, as design/chat.js
     in the store.

     position:FIXED, not sticky: a sticky bottom offset measurably does not
     move this card (its containing block is the .bf-main flex column, and
     Chrome leaves the box where it is) — verified on a 375x812 touch
     viewport before writing this rule, not assumed. With no keyboard open
     the card is an ordinary block at the end of the thread; nothing is
     pinned to the screen while you are only reading. */
  .bf-compose-card[data-composer].is-lifted{
    position:fixed; left:0; right:0; bottom:var(--kb, 0px); z-index:60;
    max-height:calc(100dvh - var(--kb, 0px) - 8px); overflow-y:auto;
    padding:14px; gap:12px;
    border-left:0; border-right:0; border-bottom:0;
    border-radius:var(--radius-md) var(--radius-md) 0 0;
    box-shadow:0 -6px 24px rgba(14,32,56,.10);
  }
  /* Lifted = a keyboard is genuinely open, so the card sheds everything that
     is not the box you are typing in — on a 300px-tall visual viewport the
     rules block would push the textarea back off screen. */
  .bf-compose-card[data-composer].is-lifted .bf-compose-rules{ display:none; }
  .bf-compose-card[data-composer].is-lifted .bf-section-title{ display:none; }
  .bf-compose-card[data-composer].is-lifted textarea{ min-height:96px; }
}

@media (max-width:520px){
  /* At 375px nothing may force the BODY to scroll sideways. The thread row's
     side column drops under the title instead of fighting it for width, and
     the report form stops being a fixed 380px. */
  .bf-thread-row{ flex-wrap:wrap; }
  .bf-thread-side{ width:100%; justify-content:space-between; gap:10px; }
  .bf-report-form{ max-width:none; }
  .bf-pager{ flex-wrap:wrap; gap:8px; }
  .bf-post-list{ border-radius:var(--radius-sm); }
  .bf-compose-card{ padding:16px; }
}

/* ── TOUCH TARGETS, RE-ASSERTED LAST ──────────────────────────────────────
   The @media (pointer:coarse) block near the top of this file raises these to
   the 44px WCAG 2.5.5/2.5.8 floor, but it is declared BEFORE the components
   themselves (.btn min-height:42px, .bf-nav-row min-height:40px) and carries
   the same specificity — so the later component rule won and every nav row
   and button measured 40–42px on a phone. Measured on a 375×812 touch
   viewport, not assumed. Re-asserting at the end of the cascade is the fix
   that does not require reordering rules other pages already depend on. */
@media (pointer:coarse){
  .bf-nav-row, .btn, .btn-dark, .btn-link,
  .bf-thread-row, .bf-schedule-row, .bf-browse-all, .bf-post-reply,
  .bf-user-chip, .bf-topbar-home{ min-height:44px; }
  /* A category card's title is its main target, not decoration — it gets a
     real hit box. The "Latest:" line under it is prose with a link in it, so
     it only gets breathing room. */
  .bf-card-title a{ display:inline-flex; align-items:center; min-height:44px; }
  .bf-card-footer a{ display:inline-block; padding:6px 0; }
  .bf-report summary{ min-height:44px; }
}
@media (pointer:coarse){
  /* The report form is a moderation surface — its controls get the same floor
     as every other tappable thing, not the desktop 42px. */
  .bf-report-form select, .bf-report-form input[type=text], .bf-report-form button{ min-height:44px; }
  /* Links inside the posting-rules list are prose, but they are the only way
     into the policy from the composer, so they get a real hit box. */
  .bf-rule-list a{ display:inline-block; padding:5px 0; }
}


/* ═══════════════════════════════════════════════════════════════════════
   FORUM DENSITY — the component layer, re-voiced (2026-08-21)
   ───────────────────────────────────────────────────────────────────────
   Everything above this line speaks DASHBOARD: cards in a grid, a gradient
   hero, generous padding, a drop shadow to lift each panel off the canvas.
   That vocabulary is exactly why the site does not read as a forum. A
   member scans forty threads at a glance; the chrome has to disappear.

   So this block trades the card for the ROW. The unit of the page is now a
   56px grid row separated from its neighbour by a single hairline — no
   radius, no shadow, no per-item padding box. Density comes from three
   moves and no others:
     1. RULES, NOT BOXES. A hairline between rows is one pixel of chrome;
        a bordered rounded card is four edges plus a shadow plus 22px of
        padding, forty times over.
     2. A COLUMN GRID. Title left, counts right, latest activity far right,
        all on fixed tracks so the eye reads DOWN a column instead of
        re-finding each number inside each card.
     3. LABELS DO THE TALKING. IBM Plex Mono uppercase at 10–11px for every
        column head, count and timestamp — already the store's own idiom for
        meta, and it buys a whole tier of hierarchy without a single extra
        weight, colour or box.

   The brand does not move: same canvas, same navy, same teal, same STIX Two
   Text for anything that is a title, same 8px radius ceiling (and mostly
   well under it), same hairline-over-shadow law. Only the DENSITY changes.
   Nothing here renames or drops a custom property, and nothing here removes
   a class — the dashboard rules above are retuned in place at the bottom of
   this block so that any page still rendering them gets the forum voice
   too, rather than being left on the old vocabulary.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── ADDED TOKENS — new names only, nothing above is renamed ──────────── */
:root{
  /* The row rhythm. 56px holds a 15.5px title over a 12.5px meta line with
     the 10px of breathing room that keeps two lines from touching, and it
     clears the 44px WCAG 2.5.5 target floor on its own — a forum row never
     needs a coarse-pointer exception the way a 40px nav row does. */
  --bf-row-min:    56px;
  --bf-row-pad-x:  16px;
  --bf-row-pad-y:  9px;
  --bf-row-gap:    20px;

  /* Hover/press washes. Ink at 3%, not a surface swap: a second background
     colour per state would need its own hairline to stay legible against
     the row above it, and that is how a list starts growing chrome again. */
  --bf-wash:        rgba(20,26,32,.032);
  --bf-wash-strong: rgba(20,26,32,.055);

  /* Fixed column tracks. Fixed, not max-content, so the numbers line up
     between rows — an unaligned count column is unreadable at forty rows
     no matter how correct each individual row is. */
  --bf-col-stat:   64px;
  --bf-col-latest: 190px;

  /* One name for "the line between two rows", so the whole list can be
     retuned from a single declaration. */
  --bf-rule: var(--hair-1);
}

/* ── LIST ─────────────────────────────────────────────────────────────
   No border-radius, no shadow, no outer padding: this is a table of
   content, not a card containing a table. The only edges are the top
   hairline and the bottom hairline of the last row. White ground rather
   than the warm canvas so the rules read crisply and a hover wash has
   somewhere to go. ─────────────────────────────────────────────────── */
.bf-list{
  display:flex; flex-direction:column;
  list-style:none; margin:0; padding:0;
  background:var(--surface-1);
  border-top:1px solid var(--bf-rule);
  min-width:0;
}
/* Separators live on the BOTTOM of every row and head, never on the top:
   adjacent borders collapse to one visual line without any :first-child
   special-casing, which matters because a teammate may or may not wrap
   each row in an <li>. Both shapes produce the same hairlines. */
/* min-width only — NOT display. `.bf-list > li` outspecifies `.bf-list-head`
   (0,1,1 vs 0,1,0), so declaring display:block here silently forced a head
   marked up as an <li> back out of grid and collapsed its column labels
   into one run of text. Verified in a browser, not reasoned about. */
.bf-list > li{ min-width:0; }

.bf-list-head{
  display:grid;
  grid-template-columns:minmax(0,1fr);
  grid-auto-flow:column;
  grid-auto-columns:max-content;
  align-items:center;
  column-gap:var(--bf-row-gap);
  padding:8px var(--bf-row-pad-x);
  border-bottom:1px solid var(--bf-rule);
  background:var(--surface-2);
  font-family:var(--f-mono);
  font-size:11px; font-weight:600;
  letter-spacing:.12em; text-transform:uppercase;
  color:var(--text-ghost);
  line-height:1.3;
}
/* Head cells sit on the same tracks as the row's stat/latest columns. The
   widths are restated here (rather than inherited) because head cells are
   usually plain <span>s carrying a label, not the row's own BEM classes. */
.bf-list-head > *:not(:first-child){ width:var(--bf-col-stat); text-align:right; }
.bf-list-head > *:last-child:not(:first-child){ width:var(--bf-col-latest); text-align:left; }
.bf-list-head .bf-list-row__stat{ width:var(--bf-col-stat); }
.bf-list-head .bf-list-row__latest{ width:var(--bf-col-latest); }

/* ── ROW ──────────────────────────────────────────────────────────────
   position:relative is load-bearing: the markup wraps the title in an <a>
   and stretches it with ::after to make the WHOLE row the click target.
   That anchor's inset:0 resolves against this box. ─────────────────── */
.bf-list-row{
  position:relative;
  display:grid;
  grid-template-columns:minmax(0,1fr);
  grid-auto-flow:column;
  grid-auto-columns:max-content;
  align-items:center;
  column-gap:var(--bf-row-gap);
  min-height:var(--bf-row-min);
  padding:var(--bf-row-pad-y) var(--bf-row-pad-x);
  border-bottom:1px solid var(--bf-rule);
  background:var(--surface-1);
  color:var(--ink);
  min-width:0;
  transition:background var(--dur-base) var(--ease-out);
}
/* The global prefers-reduced-motion block near the top of this file already
   flattens every transition declared here to 0.001ms !important — this one
   included. No second media query needed. */
.bf-list-row:hover{ background:var(--bf-wash); }

/* Full-row click target. Declared here so a row is clickable even before a
   page ships its own stretch rule; a page that DOES ship one simply lands
   the same box on top of this one. z-index:0 puts the overlay in the row's
   stacking context BELOW the explicitly-raised children just under this. */
.bf-list-row__title::after{
  content:''; position:absolute; inset:0; z-index:0;
}
/* Anything genuinely clickable in the right-hand columns has to sit ABOVE
   that overlay or it silently becomes dead pixels that navigate to the
   thread instead. This is the failure mode of every stretched-link list.
   The title link is EXCLUDED by name and pinned static: position:relative
   on it would make it the containing block for its own ::after, and the
   overlay would shrink to the width of the title text — breaking the
   stretch here AND any stretch rule a page ships for itself. */
.bf-list-row a:not(.bf-list-row__title),
.bf-list-row button,
.bf-list-row .bf-pill,
.bf-list-row .bf-badge,
.bf-list-row .bf-pin,
.bf-list-row .bf-avatar{ position:relative; z-index:1; }
.bf-list-row__title{ position:static; }

.bf-list-row__main{
  /* NOT positioned: the stretched ::after has to resolve against the row,
     not against this. min-width:0 is what lets a long title ellipsis
     instead of pushing the count columns off the right edge. */
  min-width:0;
  display:flex; flex-direction:column; gap:2px;
}

.bf-list-row__title{
  display:block;
  font-family:var(--f-display); font-weight:500;
  font-size:15.5px; line-height:1.3;
  color:var(--ink);
  text-decoration:none;
  overflow-wrap:anywhere;
  transition:color var(--dur-base) var(--ease-out);
}
.bf-list-row__title:hover{ color:var(--teal); text-decoration:underline; text-underline-offset:3px; }
/* The hover wash follows the title even when the pointer is over the meta
   line — the whole row is one target, so it has to look like one. */
.bf-list-row:hover .bf-list-row__title{ color:var(--teal); }

.bf-list-row__meta{
  display:flex; flex-wrap:wrap; align-items:center;
  column-gap:8px; row-gap:3px;
  font-family:var(--f-text); font-size:12.5px; line-height:1.4;
  color:var(--text-ghost);
  min-width:0;
}
.bf-list-row__meta a{ color:var(--body); text-decoration:none; }
.bf-list-row__meta a:hover{ color:var(--teal); text-decoration:underline; text-underline-offset:2px; }
.bf-list-row__meta strong{ color:var(--body); font-weight:600; }
/* Dot separators, generated — with two deliberate exclusions.
   (a) An item that ships its own <span class="bf-sep"> is skipped on both
       sides, so markup carrying explicit separators never doubles up.
   (b) A PILL, BADGE or PIN is skipped on both sides. Two reasons, and the
       first is not cosmetic: those chips are inline-FLEX, so a ::before on
       one becomes a flex item INSIDE it and the dot renders on the pill's
       own tinted background — verified on screen, it reads as a typo in
       the category name. The second is that a chip is already visually
       self-separating; Discourse and XenForo both butt the category tag
       straight against the meta text for exactly this reason. Spacing for
       every item, dotted or not, comes from the flex column-gap above. */
.bf-list-row__meta > *:not(.bf-sep):not(.bf-pill):not(.bf-badge):not(.bf-pin)
                   + *:not(.bf-sep):not(.bf-pill):not(.bf-badge):not(.bf-pin)::before{
  content:'·'; margin-right:8px; color:var(--hair-3);
}
/* The gap already spaces it; its own margins would double that. */
.bf-list-row__meta > .bf-sep{ margin:0; }

.bf-list-row__stat{
  width:var(--bf-col-stat);
  text-align:right;
  font-family:var(--f-mono); font-size:12.5px; line-height:1.25;
  color:var(--body);
  font-variant-numeric:tabular-nums;
  white-space:nowrap;
}
/* Two shapes are supported because two are in use: <b>12</b><small>replies
   </small>, and a bare pair of spans. The FIRST child is the number, any
   following one is the label — deliberately not "every span is a label",
   which would shrink a lone <span class="bf-num">12</span> into 9.5px
   uppercase ghost text and lose the count entirely. */
.bf-list-row__stat b,
.bf-list-row__stat strong,
.bf-list-row__stat .bf-num,
.bf-list-row__stat > span:first-child{
  display:block;
  font-family:var(--f-text); font-size:14px; font-weight:600;
  font-variant-numeric:tabular-nums;
  color:var(--ink-soft);
  letter-spacing:0; text-transform:none;
}
.bf-list-row__stat small,
.bf-list-row__stat > span + span{
  display:block;
  font-family:var(--f-mono);
  font-size:9.5px; font-weight:500; letter-spacing:.1em; text-transform:uppercase;
  color:var(--text-ghost);
}

.bf-list-row__latest{
  width:var(--bf-col-latest);
  min-width:0;
  display:flex; align-items:center; gap:8px;
  font-family:var(--f-mono); font-size:11px; letter-spacing:.02em;
  color:var(--text-ghost);
  white-space:nowrap;
}
.bf-list-row__latest a{
  font-family:var(--f-text); font-size:13px; font-weight:600;
  color:var(--ink-soft); text-decoration:none;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  min-width:0;
}
.bf-list-row__latest a:hover{ color:var(--teal); text-decoration:underline; text-underline-offset:2px; }
.bf-list-row__latest time,
.bf-list-row__latest small{
  flex:none; margin-left:auto;
  font-family:var(--f-mono); font-size:10.5px; letter-spacing:.04em;
  color:var(--text-ghost);
  font-variant-numeric:tabular-nums;
}

/* ── ROW STATES ───────────────────────────────────────────────────────
   Each state is ONE signal, never a whole new surface: a spine, a weight,
   or a drop in ink. Three overlapping states on one row still has to read
   as a row. ───────────────────────────────────────────────────────────── */
.bf-list-row.is-pinned{
  background:rgba(12,110,120,.035);
  box-shadow:inset 2px 0 0 var(--teal);
}
.bf-list-row.is-pinned:hover{ background:rgba(12,110,120,.06); }
.bf-list-row.is-pinned .bf-list-row__title{ font-weight:600; }

.bf-list-row.is-locked .bf-list-row__title{ color:var(--body); }
.bf-list-row.is-locked .bf-list-row__stat,
.bf-list-row.is-locked .bf-list-row__latest{ color:var(--text-ghost); }
.bf-list-row.is-locked .bf-list-row__stat b,
.bf-list-row.is-locked .bf-list-row__stat strong{ color:var(--body); }

/* Unread is the only state that ADDS weight — everything else in the list
   is 500, so 700 on the title is unmissable without a colour or a badge. */
.bf-list-row.is-unread .bf-list-row__title{ font-weight:700; color:var(--ink); }
.bf-list-row.is-unread .bf-list-row__stat b,
.bf-list-row.is-unread .bf-list-row__stat strong{ color:var(--ink); }

/* ── ROW COLLAPSE ─────────────────────────────────────────────────────
   Narrow does not mean "wrap the columns under the title" — that turns a
   56px row into a 130px one and destroys the scan. The right-hand tracks
   simply leave, newest-least-important first: "latest activity" goes at
   tablet width (its author and time are usually already in the meta line),
   the counts hold on as long as they fit, and the title column — the only
   thing you actually navigate by — is never dropped. ─────────────────── */
@media (max-width:820px){
  .bf-list-row__latest,
  .bf-list-head > *:last-child:not(:first-child){ display:none; }
  .bf-list-head .bf-list-row__latest{ display:none; }
}
@media (max-width:560px){
  :root{ --bf-col-stat:48px; --bf-row-gap:12px; --bf-row-pad-x:12px; }
  .bf-list-row__stat small,
  .bf-list-row__stat span{ font-size:9px; letter-spacing:.06em; }
}
@media (max-width:430px){
  /* Not "drop the counts" — drop all but the FIRST count. On a phone the
     reply count is the one number a member actually scans (it is the proxy
     for "is anything happening in here"), and a row that has surrendered
     both its latest-activity column and every number is just a title in a
     box. The first stat is replies by convention in every listing on this
     forum; the second, views, is the one that goes. */
  .bf-list-row__stat ~ .bf-list-row__stat{ display:none; }
  .bf-list-head > *:nth-child(n+3){ display:none; }
}

/* ── TABS ─────────────────────────────────────────────────────────────
   Plain links on a hairline, not pill buttons: a pill reads as an action
   ("do this") when a tab is a view ("show me that"), and three dark pills
   in a row is precisely the marketing-dashboard tell we are removing. The
   active tab is marked by a 2px teal rule sitting ON the container's
   hairline, which is the cheapest possible affordance. ──────────────── */
.bf-tabs{
  display:flex; align-items:stretch; gap:24px;
  border-bottom:1px solid var(--hair-1);
  overflow-x:auto; overflow-y:hidden;
  scrollbar-width:none;
  min-width:0;
}
.bf-tabs::-webkit-scrollbar{ display:none; }
.bf-tab{
  position:relative;
  display:inline-flex; align-items:center;
  min-height:40px; padding:8px 0 9px;
  font-family:var(--f-mono); font-size:11.5px; font-weight:500;
  letter-spacing:.12em; text-transform:uppercase;
  color:var(--body); text-decoration:none; white-space:nowrap;
  background:none; border:0; cursor:pointer;
  transition:color var(--dur-base) var(--ease-out);
}
.bf-tab:hover{ color:var(--ink); }
.bf-tab.is-active{ color:var(--ink); font-weight:600; }
.bf-tab.is-active::after{
  content:''; position:absolute; left:0; right:0; bottom:-1px;
  height:2px; background:var(--teal);
}

/* ── PILLS + BADGES ───────────────────────────────────────────────────
   A pill is a CATEGORY (a place you can go); a badge is an ATTRIBUTE of
   the person or post (OP, admin). Different shapes so they never read as
   the same control: the pill keeps its 999px radius, the badge is a 3px
   rectangle. ──────────────────────────────────────────────────────────── */
/* In a dense list a 24px pill is the tallest thing in the row and starts
   dictating the row height. 19px keeps the row at 56px. */
.bf-list-row .bf-pill,
.bf-list-row__meta .bf-pill{
  height:19px; padding:0 8px; font-size:9.5px; letter-spacing:.07em;
}
.bf-badge{
  display:inline-flex; align-items:center; gap:4px;
  height:17px; padding:0 5px;
  font-family:var(--f-mono); font-size:9.5px; font-weight:600;
  letter-spacing:.08em; text-transform:uppercase;
  border-radius:var(--radius-xs);
  background:var(--surface-3); color:var(--body);
  border:1px solid var(--hair-1);
  white-space:nowrap; vertical-align:middle;
}
.bf-badge-op{
  background:rgba(12,110,120,.10); color:var(--teal);
  border-color:rgba(12,110,120,.22);
}
.bf-badge-admin{
  background:rgba(16,42,74,.10); color:var(--navy);
  border-color:rgba(16,42,74,.20);
}
/* The pin marker. Text if the markup supplies it, the word PIN if it does
   not — an empty span must never render as an invisible 0px element that
   silently drops the one signal telling you this thread is stuck to the
   top of the list. */
.bf-pin{
  display:inline-flex; align-items:center; gap:4px;
  height:17px; padding:0 6px;
  font-family:var(--f-mono); font-size:9.5px; font-weight:600;
  letter-spacing:.1em; text-transform:uppercase;
  color:var(--teal); background:rgba(12,110,120,.10);
  border-radius:var(--radius-xs);
  white-space:nowrap; vertical-align:middle; flex:none;
}
.bf-pin:empty::before{ content:'Pin'; }

/* ── AVATAR ───────────────────────────────────────────────────────────
   Deterministic colour: the markup renders style="--av-h:<0-360>" derived
   from the handle, so the same person is the same colour on every page and
   no server round-trip or image request is involved. The teal declaration
   below it is a genuine fallback for a UA that cannot parse the modern
   space-separated hsl() — it is not dead code.

   The 10% ink overlay is a CONTRAST fix, not a style choice: at a fixed
   32%/42% the yellow-green hues land at ~4.0:1 against white initials,
   just under AA. Darkening uniformly pulls the worst hue to ~4.7:1 while
   leaving the hue itself untouched. It is declared as background-IMAGE so
   the pinned `background:` shorthand above stays exactly as specified. */
.bf-avatar{
  background:var(--teal);
  background: hsl(var(--av-h, 205) 32% 42%);
  background-image:linear-gradient(rgba(0,0,0,.10), rgba(0,0,0,.10));
  color:#fff;
  font-variant-numeric:tabular-nums;
  text-transform:uppercase;
  overflow:hidden;
}
.bf-avatar img{ width:100%; height:100%; object-fit:cover; border-radius:50%; }
.bf-avatar-sm{
  width:20px; height:20px; font-size:9px; font-weight:700; letter-spacing:0;
}
.bf-avatar-lg{
  width:40px; height:40px; font-size:15px; font-weight:600;
}
/* The list's own avatars are the small ones by default — a 30px circle in
   a 56px row is a third of the row's height spent on decoration. */
.bf-list-row .bf-avatar{ width:20px; height:20px; font-size:9px; font-weight:700; flex:none; }
.bf-list-row .bf-avatar-lg{ width:32px; height:32px; font-size:12.5px; }

/* ── PAGER ────────────────────────────────────────────────────────────
   Hairline buttons, not the dark CTA: "older posts" is navigation, and a
   navy filled button at the bottom of every listing is the single loudest
   piece of the dashboard vocabulary. ────────────────────────────────── */
.bf-pager{ border-top:1px solid var(--hair-1); margin-top:2px; padding-top:12px; }
.bf-pager-btn{
  display:inline-flex; align-items:center; justify-content:center; gap:6px;
  min-height:36px; padding:0 14px;
  font-family:var(--f-mono); font-size:11px; font-weight:600;
  letter-spacing:.1em; text-transform:uppercase;
  color:var(--ink-soft); text-decoration:none;
  background:var(--surface-1);
  border:1px solid var(--hair-2); border-radius:var(--radius-sm);
  cursor:pointer; white-space:nowrap;
  transition:background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.bf-pager-btn:hover{ background:var(--bf-wash); border-color:var(--hair-3); color:var(--ink); }
.bf-pager-btn[aria-disabled="true"],
.bf-pager-btn:disabled{
  color:var(--text-ghost); border-color:var(--hair-1);
  background:var(--surface-2); cursor:default; pointer-events:none;
}
.bf-pager-info{
  font-family:var(--f-mono); font-size:10.5px; letter-spacing:.08em;
  text-transform:uppercase; color:var(--text-ghost);
  font-variant-numeric:tabular-nums; white-space:nowrap;
}

/* ── SEARCH ───────────────────────────────────────────────────────────
   Lives in the topbar, so its height is the whole design constraint: at
   34px (38px on touch) it sits inside the 64px bar with room to spare and
   cannot grow it. The input carries 16px text on coarse pointers because
   anything smaller makes iOS Safari zoom the page on focus — the box stays
   the same height, only the glyphs get bigger. ──────────────────────── */
.bf-search{
  display:flex; align-items:center;
  flex:1 1 200px; max-width:360px; min-width:0;
  height:34px;
  background:var(--surface-1);
  border:1px solid var(--hair-2);
  border-radius:var(--radius-pill);
  overflow:hidden;
  transition:border-color var(--dur-base) var(--ease-out);
}
.bf-search:hover{ border-color:var(--hair-3); }
/* The ring goes on the CONTAINER, not the borderless input inside it —
   an outline drawn around a transparent field inside a pill reads as a
   rendering bug. Focus stays fully visible either way. */
.bf-search:focus-within{
  outline:var(--ring-width) solid var(--ring-color);
  outline-offset:var(--ring-offset);
  border-color:var(--teal);
}
.bf-search-input{
  flex:1; min-width:0; height:100%;
  padding:0 4px 0 14px;
  font:400 13.5px var(--f-text); color:var(--ink);
  background:none; border:0;
}
.bf-search-input::placeholder{ color:var(--text-ghost); }
.bf-search-input:focus-visible{ outline:none; }
.bf-search-btn{
  flex:none; display:inline-flex; align-items:center; justify-content:center;
  height:100%; min-width:38px; padding:0 12px;
  font-family:var(--f-mono); font-size:10.5px; font-weight:600;
  letter-spacing:.08em; text-transform:uppercase;
  color:var(--body); background:none; border:0; cursor:pointer;
  border-left:1px solid var(--hair-1);
  transition:color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
}
.bf-search-btn:hover{ color:var(--teal); background:var(--bf-wash); }
.bf-search-btn svg{ width:15px; height:15px; }

/* ── CRUMBS ───────────────────────────────────────────────────────────
   Separator on the LINKS, so the last item — which is a plain span, the
   page you are already on — never trails a slash pointing nowhere. The
   ::after is inline-BLOCK deliberately: an inline pseudo would inherit the
   link's underline on hover and the slash would join the word. ───────── */
.bf-crumbs{
  display:flex; flex-wrap:wrap; align-items:center;
  font-family:var(--f-mono); font-size:11px; font-weight:500;
  letter-spacing:.08em; text-transform:uppercase;
  color:var(--body); line-height:1.5; min-width:0;
}
.bf-crumbs a{
  color:var(--text-ghost); text-decoration:none;
  transition:color var(--dur-base) var(--ease-out);
}
.bf-crumbs a:hover{ color:var(--teal); text-decoration:underline; text-underline-offset:3px; }
.bf-crumbs a::after{
  content:'/'; display:inline-block; margin:0 8px;
  color:var(--hair-3); text-decoration:none;
}
.bf-crumbs > :last-child{ color:var(--ink-soft); overflow-wrap:anywhere; }

/* ── UTILITY ──────────────────────────────────────────────────────────
   The quiet empty state. One line of ghost text and, at most, a link out.
   An icon in a circle above a centred paragraph is a MARKETING empty state
   — it makes "there is nothing here yet" the loudest element on the page,
   which is exactly backwards on a forum where empty is a normal, temporary
   and unremarkable condition of a young category. ───────────────────── */
.bf-empty{
  font-family:var(--f-text); font-size:13.5px; line-height:1.55;
  color:var(--text-ghost);
  padding:18px 2px;
  margin:0;
}
.bf-empty a{
  color:var(--teal); text-decoration:underline; text-underline-offset:3px;
}
.bf-empty a:hover{ color:var(--accent-press); }
/* Inside a list it is a row, so it takes the row's gutter and rule. */
.bf-list > .bf-empty,
.bf-list > li > .bf-empty{
  padding:18px var(--bf-row-pad-x);
  border-bottom:1px solid var(--bf-rule);
  background:var(--surface-1);
}

.bf-num{ font-variant-numeric:tabular-nums; }

.bf-sep{
  color:var(--hair-3);
  margin:0 7px;
  -webkit-user-select:none; user-select:none;
}
.bf-sep:empty::before{ content:'·'; }

/* ── POST — same components, forum density ────────────────────────────
   .bf-post keeps every class it had; what changes is that it stops being a
   card in its own right. Inside .bf-post-list it is a row in a stack, so
   the padding drops from 20px to 16/18 and the avatar from 38px to 32px:
   on a thread with thirty replies that is roughly a screen and a half of
   scrolling given back to the actual text. ──────────────────────────── */
.bf-post{
  gap:12px;
  padding:16px 18px;
  border-radius:var(--radius-sm);
}
/* Inside the stack a post is a row, not a card — the list owns the outer
   corners and the hairlines between posts already exist. Written as a
   descendant override rather than :not(.bf-post-list .bf-post), because a
   complex selector inside :not() invalidates the whole rule on any engine
   that only implements Selectors 3. */
.bf-post-list{ border-radius:var(--radius-sm); }
.bf-post-list .bf-post{ border-radius:0; }
.bf-post-avatar{
  width:32px; height:32px; font-size:12.5px;
  background:var(--teal);
  background: hsl(var(--av-h, 205) 32% 42%);
  background-image:linear-gradient(rgba(0,0,0,.10), rgba(0,0,0,.10));
  color:#fff; text-transform:uppercase;
}
.bf-post-head{ gap:8px; margin-bottom:5px; }
.bf-post-author{
  font-family:var(--f-text); font-size:13.5px; font-weight:700;
  color:var(--ink); text-decoration:none;
}
a.bf-post-author:hover{ color:var(--teal); }
.bf-post-time{ font-size:10px; letter-spacing:.06em; }
/* white-space:pre-wrap is NOT cosmetic. Post bodies are escaped plain text
   rendered into a single element — every paragraph break in every post on
   this forum is a literal \n and nothing else. Drop this and the whole
   archive collapses into one run-on block, silently and everywhere. */
.bf-post-text{
  white-space:pre-wrap;
  overflow-wrap:anywhere;
  font-size:14.5px; line-height:1.62;
  color:var(--ink-soft);
  margin-bottom:10px;
}
/* Telegram-import photos — allowlisted markdown → <img class="bf-post-img">
   in renderPostBody. Block-level so they don't sit mid-line under pre-wrap. */
.bf-post-text .bf-post-img{
  display:block;
  max-width:min(100%, 520px);
  width:auto;
  height:auto;
  margin:10px 0;
  border-radius:var(--radius-sm);
  border:1px solid var(--hair);
  background:var(--surface-2);
}
/* The opening post keeps its teal spine (it is the thread), but the spine
   is the ONLY thing marking it — no second background, no heavier border. */
.bf-post-op{ box-shadow:inset 3px 0 0 var(--teal); }

/* ── THE DASHBOARD RULES, RETUNED ─────────────────────────────────────
   Nothing below is deleted — every class above still exists and still has
   a rule, because pages are being rewritten in parallel and a class that
   vanishes from this sheet unstyles a page none of us can see right now.
   Instead the surviving dashboard components are pulled toward the same
   density as the list, so a page mid-migration never shows both
   vocabularies at once. ─────────────────────────────────────────────── */

/* The page's vertical rhythm: 32px between sections was set for cards that
   needed air around them. Rows do not. */
.bf-main{ gap:22px; padding-top:24px; }

/* Shadows off. The system's own stated law is hairlines in preference to
   shadows; the dashboard layer was the one place that broke it. */
.bf-hero-card{ box-shadow:none; border-radius:var(--radius-sm); }
.bf-hero-body{ box-shadow:none; padding:24px clamp(18px,2.4vw,32px); }
.bf-category-card{
  padding:16px; border-radius:var(--radius-sm);
  transition:background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
.bf-category-card:hover{ box-shadow:none; background:var(--surface-2); border-color:var(--hair-2); }
.bf-card-grid{ gap:12px; }
.bf-card-title{ font-size:17px; margin:0 0 6px; }
.bf-card-desc{ font-size:13px; margin:0 0 12px; }
.bf-panel{ padding:18px; border-radius:var(--radius-sm); }
.bf-bottom-row{ gap:12px; }
.bf-user-chip{ box-shadow:none; }
.bf-user-chip:hover{ box-shadow:none; background:var(--bf-wash); }

/* The legacy thread row is the list row's predecessor and now shares its
   metrics exactly, so a page that still renders .bf-thread-row and a page
   that renders .bf-list-row are indistinguishable in rhythm. */
.bf-thread-list{ border-radius:var(--radius-sm); }
.bf-thread-row{ padding:var(--bf-row-pad-y) var(--bf-row-pad-x); min-height:var(--bf-row-min); gap:12px; }
.bf-thread-row:hover{ background:var(--bf-wash); }
.bf-thread-avatar{ width:20px; height:20px; font-size:9px; }
.bf-thread-title{ font-size:15.5px; }
.bf-thread-meta{ font-family:var(--f-text); font-size:12.5px; letter-spacing:0; text-transform:none; color:var(--text-ghost); }

/* The old empty state is still referenced by pages that have not been
   rewritten yet, so it is retuned rather than removed: the 40px icon
   circle collapses to an 18px mark and the copy joins it on one line. New
   markup should use .bf-empty. */
.bf-empty-state{ flex-direction:row; align-items:center; gap:10px; padding:16px 2px; }
.bf-empty-icon{ width:18px; height:18px; background:none; }
.bf-empty-icon svg{ width:16px; height:16px; color:var(--text-ghost); }
.bf-empty-copy{ font-size:13.5px; color:var(--text-ghost); max-width:none; }

/* ── TOUCH FLOORS FOR THE NEW PRIMITIVES ──────────────────────────────
   Declared last for the same cascade reason the block above them documents:
   these components are defined earlier in this file, and a coarse-pointer
   rule that loses a specificity tie to a later component rule is a rule
   that measurably does nothing. Rows are exempt — they are already 56px. */
@media (pointer:coarse){
  .bf-tab, .bf-pager-btn, .bf-search-btn{ min-height:44px; }
  .bf-search{ height:40px; }
  .bf-search-input{ font-size:16px; }
  .bf-crumbs a{ display:inline-flex; align-items:center; min-height:32px; }
  .bf-list-row__latest a{ display:inline-flex; align-items:center; min-height:32px; }
  .bf-empty a{ display:inline-block; padding:6px 0; }
}

/* Topbar on a phone: the search field is the only thing in there that can
   grow, so it is explicitly told not to. The bar keeps its 64px. */
@media (max-width:640px){
  .bf-topbar{ gap:10px; padding:0 14px; }
  .bf-search{ flex:1 1 auto; max-width:none; }
  .bf-search-btn{ min-width:34px; padding:0 10px; }
  .bf-tabs{ gap:18px; }
}


/* ═══════════════════════════════════════════════════════════════════════
   READABILITY + WELCOME (2026-08-21) — the type and colour layer, rebuilt
   against measurements taken from four live professional forums.
   ───────────────────────────────────────────────────────────────────────
   WHAT WAS MEASURED (getComputedStyle on the live sites, this pass):

     Discourse (meta.discourse.org)  title 18.38px / 400 / Inter — a SANS
                                     meta 13.93px, lightest meta 9.46:1
     XenForo  (xenforo.com/community) title 17px / 400 / Segoe UI — a SANS
                                     meta 12px at 5.13:1, row 60.6px, av 36px
     Flarum   (discuss.flarum.org)   title 16px / 400 / system-ui — a SANS
     old.reddit (r/programming)      title 16px / 700 / Verdana — a SANS

   Not one of them sets a thread title in a serif, and not one sets it under
   16px. A list of forty titles is SCANNED, and a scan wants an even
   colour, a large x-height and weight — which is the sans's job and the
   display serif's opposite. So the ROLE of each face moves (the faces
   themselves do not, and neither does the palette):

     --f-display  STIX Two Text  → page identity: h1, section heads, wordmark
     --f-text     IBM Plex Sans  → every title in a list, all body, all meta
     --f-mono     IBM Plex Mono  → column heads, eyebrows, chips, figures

   The second half of this block is warmth. Every value it uses is derived
   from a hue :root already declared — teal at low alpha, the existing clay
   walked down until it is legible as text, the store's own paper as a band.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── 1. TITLES SCAN, SO THEY GO TO THE SANS ──────────────────────────── */
.bf-list-row__title{
  font-family:var(--f-text);
  font-size:16.5px;
  font-weight:600;
  line-height:1.35;
  letter-spacing:-.003em;
  color:var(--ink);
}
/* Hover keeps the colour change but drops the underline: at 600 weight the
   colour shift alone is unmistakable, and forty underlines appearing under
   the pointer is the noisiest possible hover state. */
.bf-list-row__title:hover,
.bf-list-row:hover .bf-list-row__title{ color:var(--accent-press); text-decoration:none; }

/* The legacy row keeps step with its successor. */
.bf-thread-title{
  font-family:var(--f-text);
  font-size:16.5px; font-weight:600; line-height:1.35; color:var(--ink);
}

/* ── 2. META BECOMES READABLE PROSE, NOT MICRO-CAPS ──────────────────── */
/* 12.5px at 2.41:1 was the single least legible thing on the page. 13.5px of
   sans at --text-ghost's new value measures 4.9:1+ on every ground in this
   system — inside XenForo's 12px/5.13:1 idiom rather than below it. */
.bf-list-row__meta{
  font-family:var(--f-text);
  font-size:13.5px;
  line-height:1.45;
  color:var(--text-ghost);
  letter-spacing:0;
  text-transform:none;
}
/* A handle is a PERSON. It gets ink, so the list reads as people talking to
   each other rather than as rows in a database. */
.bf-list-row__meta a,
.bf-list-row__meta strong{ color:var(--body); font-weight:600; }
.bf-list-row__meta a:hover{ color:var(--accent-press); }
.bf-list-row__meta > *:not(.bf-sep):not(.bf-pill):not(.bf-badge):not(.bf-pin)
                   + *:not(.bf-sep):not(.bf-pill):not(.bf-badge):not(.bf-pin)::before{
  color:var(--text-mute);
}

/* ── 3. THE COUNT COLUMN — MEASURED BUG, INVERTED HIERARCHY ──────────────
   On the live page the UNIT was louder than the FIGURE: "threads" measured
   14px / 600 / .80 ink (9.47:1) while the number beside it measured
   12.5px / 400 / .62 ink (4.99:1). Every count on the forum was shouting the
   word and whispering the number.

   Cause: `.bf-list-row__stat > span:first-child` up in the density block
   was written to catch a NUMBER shipped as the first span — but the markup
   actually ships `0<span class="bfl-stat-label">threads</span>`, so the
   figure is a bare text node and the first span is the LABEL. That selector
   is (0,2,1) and out-specifies listing.css's own `.bfl-stat-label` (0,1,0),
   so the label won and kept the number's styling. Restated here, later in
   the same file and at the same specificity, the right way round. */
.bf-list-row__stat{
  font-family:var(--f-text);
  font-size:16px;
  font-weight:700;
  line-height:1.15;
  color:var(--ink);
  font-variant-numeric:tabular-nums;
}
.bf-list-row__stat > span:first-child,
.bf-list-row__stat small,
.bf-list-row__stat > span + span{
  display:block;
  margin-top:1px;
  font-family:var(--f-mono);
  font-size:9.5px;
  font-weight:500;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--text-ghost);
}
.bf-list-row.is-locked .bf-list-row__stat{ color:var(--body); }

/* ── 3b. THE SAME CLASSES, USED AS INLINE TOKENS ─────────────────────────
   The SEARCH page ships `__stat` and `__latest` INSIDE the meta line rather
   than as grid columns of the row:

     <div class="bf-list-row__meta"> … <span class="bf-list-row__stat">172
     replies</span> <span class="bf-list-row__latest">Last activity Mar 13,
     2026</span> </div>

   As COLUMN primitives those two carry width:64px / width:190px, nowrap and
   right-alignment, so in a flowing line they became fixed-width boxes with
   overflowing content — "172 replies" printed straight through "Last
   activity". That collision predates this pass (chrome.css's own comment
   describes the symptom and defines .bf-search-stat / .bf-search-when as
   the cure, but the shipped markup never adopted those names); raising the
   count to 16px/700 is simply what made it impossible to miss.

   Rather than leave two vocabularies fighting, the column geometry is
   switched OFF whenever either class is nested in a meta line — matched at
   (0,2,0) so it beats the primitive, and declared without a media query so
   it also overrides the ≤820px rule that was hiding the search timestamp on
   every tablet and phone. */
.bf-list-row__meta .bf-list-row__stat,
.bf-list-row__meta .bf-list-row__latest{
  display:inline;
  width:auto;
  min-width:0;
  text-align:left;
  white-space:normal;
  font-family:var(--f-text);
  font-size:13.5px;
  font-weight:600;
  line-height:1.45;
  letter-spacing:0;
  text-transform:none;
  color:var(--body);
}
.bf-list-row__meta .bf-list-row__latest time,
.bf-list-row__meta .bf-list-row__latest small{
  display:inline; margin-left:0; font-family:inherit; font-size:inherit; color:inherit;
}

/* ── 4. COLUMN HEADS — the one place micro-caps still earn their keep ─── */
/* Kept mono + uppercase (a column head is a label, and it is the idiom the
   store already speaks) but lifted out of the ghost tier: at .38 it measured
   2.39:1, which is a decorative smudge rather than a heading. */
.bf-list-head{
  font-size:10.5px;
  font-weight:600;
  letter-spacing:.13em;
  color:var(--body);
  background:var(--warm-band);
  border-bottom:1px solid var(--warm-rule);
}

/* ── 5. ROWS — a touch more air, a warmer rule, a warmer hover ───────── */
:root{
  --bf-row-min:  62px;   /* XenForo measured 60.6px; Discourse runs taller */
  --bf-wash:        rgba(12,110,120,.045);
  --bf-wash-strong: rgba(12,110,120,.075);
  --bf-rule: var(--warm-rule);
}
.bf-list{ border-top:1px solid var(--warm-rule); }
.bf-list-row{ border-bottom:1px solid var(--warm-rule); }
/* The hover wash is teal now rather than ink: a grey wash reads as "this row
   is disabled", a faint brand wash reads as "this row is live". */
.bf-list-row:hover{ background:var(--bf-wash); }

/* ── 6. AVATARS — people, at a size you can actually see ─────────────── */
/* 20px was a third the height of the glyph Discourse (24px) and XenForo
   (36px) use. A forum's avatars are the fastest signal that a human is on
   the other end of the row; at 20px they were confetti. */
.bf-list-row .bf-avatar{ width:26px; height:26px; font-size:10.5px; font-weight:700; }
.bf-list-row .bf-avatar-lg{ width:34px; height:34px; font-size:13px; }
/* A little more chroma. The 32%/42% pair was mixed to survive a contrast
   floor that the 10% ink overlay already guarantees; 38%/40% keeps the same
   overlay and the same floor while letting the hues actually read as colour. */
.bf-avatar{ background:hsl(var(--av-h, 205) 38% 40%); }

/* ── 7. TABS + PILLS + CHIPS ─────────────────────────────────────────── */
.bf-tab{ font-size:12px; letter-spacing:.1em; color:var(--body); }
.bf-tab:hover{ color:var(--ink); }
.bf-tab.is-active{ color:var(--teal); font-weight:600; }
.bf-tab.is-active::after{ height:2px; background:var(--teal); }

/* A category chip is a destination. It gets the teal wash it always had,
   one step more ink so the word inside it is readable at 10px. */
.bf-list-row .bf-pill,
.bf-list-row__meta .bf-pill{
  height:20px; padding:0 9px; font-size:10px; font-weight:600; letter-spacing:.06em;
}
.bf-pill-category{
  background:var(--teal-wash-2);
  color:var(--accent-press);
  border:1px solid var(--teal-edge);
}
.bf-badge{ font-size:10px; color:var(--body); }

/* ── 8. POST BODIES — 16px, because a post is READ, not scanned ──────── */
/* Discourse and Flarum both serve post text at their 16px root. 14.5px is a
   caption size; this is the one block of prose on the whole site. */
.bf-post-text{
  font-size:16px;
  line-height:1.65;
  color:var(--ink-soft);
}
.bf-post-author{ font-size:14.5px; font-weight:700; color:var(--ink); }
.bf-post-time{ font-size:11px; letter-spacing:.05em; color:var(--text-ghost); }

/* ── 9. TYPE PRIMITIVES — the serif keeps identity, at a real size ───── */
.bf-supporting{ font-size:16px; color:var(--body); }
.bf-eyebrow{ font-size:11px; font-weight:600; letter-spacing:.14em; }
.bf-eyebrow.neutral{ color:var(--body); }

/* ── 10. THE WELCOME LAYER ────────────────────────────────────────────
   izzy: "five categories reading 0 threads / No threads yet in identical
   grey is a morgue, not an invitation." Correct, and it was the same grey
   that failed contrast (2.41:1). An empty room on a young forum is an
   OPPORTUNITY, and the markup already ships the right words and the right
   link — "No threads yet — start the first one." — it was just painted the
   colour of disabled text. So it gets the warm ground and the live link.

   :has() is progressive: where it is unsupported the row simply keeps the
   default white ground and every word stays exactly as legible. Nothing
   here is load-bearing. */
.bf-empty{
  font-size:14px;
  color:var(--body);
}
.bf-empty a{
  color:var(--teal); font-weight:600;
  text-decoration:underline; text-underline-offset:3px;
  text-decoration-thickness:1.5px;
}
.bf-empty a:hover{ color:var(--accent-press); }

/* An empty room, lit. */
.bf-list-row:has(.bfl-latest-empty){ background:var(--warm-tint); }
.bf-list-row:has(.bfl-latest-empty):hover{ background:rgba(201,138,75,.14); }

/* ── 11. WARM BANDS — where the page gets to breathe in colour ───────── */
.bf-empty-copy{ font-size:14px; color:var(--body); }
.bf-empty-icon svg{ color:var(--teal); }

/* The RUO strip is a legal notice, not a whisper — it was 9.5px at 2.41:1,
   which is a compliance line nobody can read. */
.bf-sidebar-ruo{ font-size:10.5px; line-height:1.65; color:var(--body); }
.bf-foot-ruo, .bf-foot-copy, .bf-foot-col-label{ color:var(--body); }

/* Sundries that were sitting in the old ghost tier and are text. */
.bf-pager-info,
.bf-report summary,
.bf-crumbs a,
.bf-thread-time,
.bf-card-footer,
.bf-review-explainer{ color:var(--body); }
.bf-sep{ color:var(--text-mute); }
.bf-crumbs a::after{ color:var(--text-mute); }
