/* ══════════════════════════════════════════════════════════════════════
   motion.css — the AMBIENT + MOTION layer of bbaforum.com.
   SEVENTH partial, linked LAST by shell() (forum/functions/_layout.js),
   after pages.css. Paired with /ambient.js, which adds the two behaviours
   CSS cannot do alone (scroll-reveal and the stat count-up).

   WHAT THIS FILE IS. Everything on the forum that MOVES, and nothing that
   merely IS. Layout, type, colour, spacing and contrast are owned by the six
   sheets before it; this one only adds time to them — how a page arrives,
   how a row answers the pointer, how the chrome behaves under scroll, and
   how one page becomes the next. Every colour, radius, easing and duration
   is a token forum.css already declares; the only literals are alpha
   channels on those same hues.

   THE THREE LAWS OF THIS SHEET
   1. NOTHING IS HIDDEN WITHOUT JS. A rule may start an element at opacity 0
      only behind a class that /ambient.js adds (.bf-js, .bf-reveal) or as
      the `from` of a keyframe with fill-mode — never on a bare selector. A
      browser with scripting off, a crawler, a print stylesheet, a reader
      mode: all of them get every pixel of content. test_forum.mjs greps
      this file for exactly that.
   2. REDUCED MOTION IS A FULL STOP, NOT A SPEED-UP. forum.css already sets
      every animation/transition to 0.001ms — but it cannot zero an
      animation-DELAY, so a staggered row would still blink in 300ms late.
      The block at the foot of this file zeroes delays, unhides reveals and
      turns the view transition into a plain swap.
   3. COMPOSITOR ONLY. Nothing here animates layout: transform and opacity
      on content, box-shadow/background on the sticky bar (one element),
      text-decoration-color on titles. The ambient blobs are two fixed
      layers moved by transform — no filter:blur(), which would re-raster
      a viewport-sized surface every frame on a phone.

   CROSS-DOCUMENT VIEW TRANSITIONS. The forum is server-rendered, one full
   document per URL, on purpose (see _layout.js's header). @view-transition
   lets that stay true while navigation FEELS like one app: the browser
   snapshots the old page, loads the new one, and cross-fades. Elements
   that carry a view-transition-name are captured separately and MORPH
   between their two positions — so the sidebar, topbar and banners stay
   perfectly still while the content beneath them changes, the active nav
   row slides from Home to Categories, the active sort tab slides from
   RECENT to NEW, and the page title travels. Browsers without support
   (Firefox, older Safari) ignore every rule in that section and navigate
   exactly as before. Each name may appear on at most ONE element per page
   or the transition is skipped for that navigation; the selectors below
   are scoped so that holds (the nav group and the category group light
   different rows, under different names).
═══════════════════════════════════════════════════════════════════════ */

/* ── 0 · TOKENS ────────────────────────────────────────────────────── */
:root{
  --m-ease:   var(--ease-out);                 /* cubic-bezier(.22,1,.36,1) */
  --m-in:     460ms;                           /* content arrival */
  --m-row:    420ms;                           /* one list row */
  --m-step:   26ms;                            /* stagger between rows */
  --m-rise:   8px;                             /* how far a block rises in */
  --m-vt:     240ms;                           /* page-to-page cross-fade */
}

/* ── 1 · PAGE-TO-PAGE: cross-document view transitions ─────────────── */
@view-transition{ navigation: auto; }

/* The root is a plain cross-fade. Content rises in by its own entrance
   animation (§3) — a translate here as well would move everything twice. */
::view-transition-old(root){ animation: bf-vt-out 160ms var(--m-ease) both; }
::view-transition-new(root){ animation: bf-vt-in var(--m-vt) var(--m-ease) both; }
@keyframes bf-vt-out{ to{ opacity:0; } }
@keyframes bf-vt-in { from{ opacity:0; } }

/* The chrome is captured under its own name, so it is NOT part of the
   root cross-fade: it stays put, pixel for pixel, while the page changes
   under it. That stillness is most of what makes navigation read as one
   app rather than a reload. */
.bf-promo  { view-transition-name: bf-promo; }
.bf-order  { view-transition-name: bf-order; }
.bf-sidebar{ view-transition-name: bf-sidebar; }
.bf-topbar { view-transition-name: bf-topbar; }
::view-transition-group(bf-promo),
::view-transition-group(bf-order),
::view-transition-group(bf-sidebar),
::view-transition-group(bf-topbar){ animation-duration: 0s; }

/* The three things that DO move between pages, and should be seen to:
   the lit nav row, the lit category row, the lit sort tab. */
.bf-nav-group:not(.bf-nav-group-cats) .bf-nav-row.is-active{ view-transition-name: bf-nav-active; }
.bf-cat-row.is-active{ view-transition-name: bf-cat-active; }
.bf-tab.is-active    { view-transition-name: bf-tab-active; }
::view-transition-group(bf-nav-active),
::view-transition-group(bf-cat-active),
::view-transition-group(bf-tab-active){
  animation-duration: 300ms;
  animation-timing-function: var(--m-ease);
}

/* The page title travels. Snapshots of text must not be STRETCHED between
   two widths (a scaled word reads as a rendering fault), so the old and
   new captures keep their natural size and are clipped by the moving box
   instead of resized into it. */
.bf-main h1{ view-transition-name: bf-h1; }
::view-transition-group(bf-h1){ animation-duration: 280ms; animation-timing-function: var(--m-ease); }
::view-transition-old(bf-h1),
::view-transition-new(bf-h1){
  height:100%; object-fit:none; object-position:left top;
  animation-duration: 200ms;
}
::view-transition-image-pair(bf-h1){ isolation:auto; }

/* ── 2 · AMBIENT CANVAS ─────────────────────────────────────────────
   Two soft lights, low behind the page, drifting on minute-long loops:
   teal (the brand accent) upper-left, clay-into-blue lower-right. They live
   under the cream canvas and show through the feed, which has no ground of
   its own; the white sidebar, cards and lists sit on top untouched, which is
   what gives the page depth without adding a single box. Alpha is kept low:
   at the centre of the teal blob the canvas composites to roughly #ECEFEC,
   under which forum.css's lightest running text (--text-ghost, .64 ink)
   still computes above 4.5:1. The banner's aurora below is capped the same
   way — its blue lobe at .22 keeps the band's dimmest text (brand line,
   .72 white on navy) above 4.5:1 by computation; at .40 it did not.

   position:fixed + z-index:-1 inside body's own stacking context paints
   above the root background (the canvas colour, which body hands to the
   viewport) and below every in-flow child. transform-only animation, so
   both layers are rasterised once and moved by the compositor. ─────── */
.bf-body{ position:relative; isolation:isolate; }
.bf-body::before,
.bf-body::after{
  content:""; position:fixed; z-index:-1; pointer-events:none;
  width:min(72vw, 880px); aspect-ratio:1; border-radius:50%;
  will-change:transform;
}
.bf-body::before{
  left:-8vw; top:-14vh;
  background:radial-gradient(closest-side,
    rgba(12,110,120,.11) 0%, rgba(12,110,120,.05) 42%, rgba(12,110,120,0) 72%);
  animation: bf-drift-a 52s ease-in-out infinite alternate;
}
.bf-body::after{
  right:-16vw; bottom:-22vh;
  background:radial-gradient(closest-side,
    rgba(201,138,75,.10) 0%, rgba(46,120,194,.05) 46%, rgba(46,120,194,0) 72%);
  animation: bf-drift-b 68s ease-in-out infinite alternate;
}
@keyframes bf-drift-a{
  from{ transform:translate3d(0,0,0) scale(1); }
  to  { transform:translate3d(10vw,8vh,0) scale(1.14); }
}
@keyframes bf-drift-b{
  from{ transform:translate3d(0,0,0) scale(1.06); }
  to  { transform:translate3d(-9vw,-10vh,0) scale(.92); }
}
@media (max-width:860px){
  .bf-body::before, .bf-body::after{ width:min(120vw, 620px); }
  /* The sidebar becomes a sticky strip here (forum.css) with the same
     backdrop blur the topbar had — same per-frame cost over the drifting
     layers, same answer (§5): no blur, slightly more opaque ground. */
  .bf-sidebar{
    -webkit-backdrop-filter:none; backdrop-filter:none;
    background:rgba(252,251,248,.96);
  }
}

/* The store banner gets its own weather: a slow aurora behind the copy,
   inside the band's existing isolation (chrome.css sets isolate; its ::after
   is the 2px brand rule, so ::before is free). Brand teal-bright and blue,
   with a whisper of clay — the three hues the band already owns. */
.bf-order{ overflow:hidden; }
.bf-order::before{
  content:""; position:absolute; inset:-60% -12%; z-index:-1; pointer-events:none;
  background:
    radial-gradient(38% 46% at 20% 45%, rgba(43,185,201,.20), rgba(43,185,201,0) 70%),
    radial-gradient(34% 44% at 80% 55%, rgba(46,120,194,.22), rgba(46,120,194,0) 70%),
    radial-gradient(26% 36% at 52% 18%, rgba(201,138,75,.12), rgba(201,138,75,0) 70%);
  will-change:transform;
  animation: bf-aurora 30s ease-in-out infinite alternate;
}
@keyframes bf-aurora{
  from{ transform:translate3d(-4%,-3%,0) rotate(-2deg) scale(1); }
  to  { transform:translate3d( 4%, 3%,0) rotate( 2deg) scale(1.08); }
}

/* ── 3 · ARRIVAL — how a page's content enters ──────────────────────
   The chrome is still (§1); the CONTENT rises. Each direct child of .bf-main
   lifts 8px into place, one after another; inside a list the first ten rows
   follow at 26ms intervals, so a feed reads as being dealt rather than
   dropped. Rows past the tenth arrive with the list — a stagger that runs
   past the fold is motion nobody sees and a delay everybody pays. fill-mode
   `both` holds the first frame during the delay and the last frame after,
   and the last frame is opacity:1 / transform:none, so once an animation
   ends it leaves no stacking context and no containing block behind it (the
   mobile composer's position:fixed and the sticky topbar depend on that).
   Rows the pointer is already over are not exempt — they simply finish. */
@keyframes bf-rise{
  from{ opacity:0; transform:translateY(var(--m-rise)); }
  to  { opacity:1; transform:none; }
}
.bf-main > *:not(.bf-foot){
  animation: bf-rise var(--m-in) var(--m-ease) both;
}
.bf-main > :nth-child(2){ animation-delay:40ms; }
.bf-main > :nth-child(3){ animation-delay:80ms; }
.bf-main > :nth-child(4){ animation-delay:120ms; }
.bf-main > :nth-child(n+5){ animation-delay:160ms; }

.bf-list > li,
.bf-thread-posts > .bf-post{
  animation: bf-rise var(--m-row) var(--m-ease) both;
  animation-delay: calc(90ms + var(--m-i, 9) * var(--m-step));
}
.bf-list > li:nth-child(1),  .bf-thread-posts > .bf-post:nth-child(1) { --m-i:0; }
.bf-list > li:nth-child(2),  .bf-thread-posts > .bf-post:nth-child(2) { --m-i:1; }
.bf-list > li:nth-child(3),  .bf-thread-posts > .bf-post:nth-child(3) { --m-i:2; }
.bf-list > li:nth-child(4),  .bf-thread-posts > .bf-post:nth-child(4) { --m-i:3; }
.bf-list > li:nth-child(5),  .bf-thread-posts > .bf-post:nth-child(5) { --m-i:4; }
.bf-list > li:nth-child(6),  .bf-thread-posts > .bf-post:nth-child(6) { --m-i:5; }
.bf-list > li:nth-child(7),  .bf-thread-posts > .bf-post:nth-child(7) { --m-i:6; }
.bf-list > li:nth-child(8),  .bf-thread-posts > .bf-post:nth-child(8) { --m-i:7; }
.bf-list > li:nth-child(9),  .bf-thread-posts > .bf-post:nth-child(9) { --m-i:8; }
.bf-list > li:nth-child(10), .bf-thread-posts > .bf-post:nth-child(10){ --m-i:9; }
/* Past ten: no per-row animation at all. That is what lets ambient.js
   pick them up for scroll-reveal — it skips any element that already has
   an animation-name, so the two systems never fight over one row. */
.bf-list > li:nth-child(n+11),
.bf-thread-posts > .bf-post:nth-child(n+11){ animation:none; }

/* The community rail arrives a beat after the feed, so the eye lands on
   the threads first — which is the order the page is built to be read. */
.bfl-side > *{ animation: bf-rise var(--m-in) var(--m-ease) both; animation-delay:140ms; }
.bfl-side > :nth-child(2){ animation-delay:200ms; }
.bfl-side > :nth-child(3){ animation-delay:260ms; }

/* ── 4 · SCROLL-REVEAL (class added by ambient.js, never by markup) ──
   Anything that started below the fold fades up as it is scrolled into
   view. .bf-reveal is applied by the script only to elements that (a) were
   off-screen at load and (b) carry no entrance animation of their own; a
   page with scripting off never has the class and never hides a thing.
   THE RULES LIVE AT THE END OF §6, not here: the rows this class lands on
   also match the hover shorthands in §6 (.bf-list-row, .bfl-feed .bfl-post,
   .bfl-side-card, and thread.css's .bf-thread-posts .bf-post), and a
   `transition` shorthand declared later or at higher specificity resets the
   whole property list — measured in Chrome, every revealed row computed
   `transition-property: background` and popped in with no fade. The reveal
   rule therefore comes last, at (0,2,0), and re-lists what those rules ease. */

/* Imported photos fade in once decoded instead of popping. Gated on the
   html.bf-js stamp ambient.js sets before it touches a single image, and
   the script marks an image loaded on `error` too — a broken photo still
   shows its alt text, never a blank. */
.bf-js .bf-post-img{ opacity:0; transition: opacity 480ms var(--m-ease); }
.bf-js .bf-post-img.is-loaded{ opacity:1; }

/* ── 5 · THE STICKY BAR UNDER SCROLL ────────────────────────────────
   Flat while the page is at rest; a soft lift once anything has scrolled
   under it, so the bar reads as sitting OVER the content it now covers.

   Two decisions here are about the compositor, not the look:
   • forum.css gives the bar backdrop-filter:blur(16px). With §2's two
     fixed layers drifting beneath it, that blur would have to re-sample the
     backdrop every frame, forever, on every page. The blur is dropped and
     the bar's ground raised from .90 to .94 — at that alpha the frosted
     effect was already imperceptible, and now the bar costs nothing while
     idle. The mobile sidebar strip gets the same treatment in §2.
   • the lift itself lives on a ::before pseudo and animates ONLY opacity.
     Animating background/box-shadow off a scroll timeline repaints the bar
     on every scroll frame; fading a pre-painted shadow layer composites.
   A scroll-driven animation where supported (no script, no scroll handler);
   ambient.js toggles .is-scrolled for engines without animation-timeline,
   and under reduced motion, where the timeline animation is switched off. */
.bf-topbar{
  -webkit-backdrop-filter:none; backdrop-filter:none;
  background:rgba(252,251,248,.94);
}
.bf-topbar::before{
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
  box-shadow:0 10px 28px -14px rgba(14,32,56,.22), 0 1px 0 var(--hair-2);
  opacity:0;
  transition: opacity var(--dur-base) var(--ease-out);
}
@keyframes bf-bar-lift{ to{ opacity:1; } }
@supports (animation-timeline: scroll()){
  .bf-topbar::before{
    animation: bf-bar-lift linear both;
    animation-timeline: scroll(root);
    animation-range: 0 72px;
  }
}
.bf-topbar.is-scrolled::before{ opacity:1; }

/* Reading progress on a thread page: a 2px brand line along the bottom of
   the bar that fills as you read. Only where there is something to read
   through (:has scopes it to pages carrying the post list), and only in
   engines with scroll timelines — there is no script fallback for this one
   because a progress bar driven from a scroll handler is the kind of thing
   that stutters on exactly the phones it is meant to delight. */
@supports (animation-timeline: scroll()) and selector(:has(*)){
  .bf-body:has(.bf-thread-posts) .bf-topbar::after{
    content:""; position:absolute; left:0; right:0; bottom:-1px; height:2px;
    background:var(--grad-brand-h);
    transform-origin:left center; transform:scaleX(0);
    animation: bf-progress linear both;
    animation-timeline: scroll(root);
    pointer-events:none;
  }
  @keyframes bf-progress{ to{ transform:scaleX(1); } }
}

/* ── 6 · MICRO-INTERACTIONS — how surfaces answer the pointer ────────
   One small, honest response per control. Nothing bounces; nothing moves
   more than 2px; every state resolves in under 300ms. */

/* Feed rows. The wash was already there; a teal spine at the left edge and
   the reply count colouring make "this row is live" unmistakable, and the
   title underline DRAWS instead of appearing. text-decoration-color is what
   makes a multi-line title underline correctly — a gradient trick would draw
   a bar the width of the box. */
.bfl-feed .bfl-post{
  transition: background var(--dur-base) var(--ease-out),
              box-shadow 220ms var(--m-ease);
}
.bfl-feed .bfl-post:hover{ box-shadow: inset 3px 0 0 var(--teal); }
.bfl-post__score-n{ transition: color 180ms var(--m-ease), transform 220ms var(--m-ease); }
.bfl-feed .bfl-post:hover .bfl-post__score-n{ color:var(--teal); transform:translateY(-1px); }
.bfl-post__title{
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color 160ms var(--m-ease), text-decoration-color 240ms var(--m-ease);
}
.bfl-post__title:hover,
.bfl-feed .bfl-post:hover .bfl-post__title{ text-decoration-color: currentColor; }

/* Every list row (category page, search, categories index): the same spine. */
.bf-list-row{ transition: background var(--dur-base) var(--ease-out), box-shadow 220ms var(--m-ease); }
.bfl-threads .bf-list-row:hover,
.bfl-cats .bf-list-row:hover{ box-shadow: inset 3px 0 0 var(--teal); }
/* The category marker stripe — the one colour in that list — brightens and
   grows toward the row it belongs to. */
.bfl-marker{
  transform-origin:center;
  transition: transform 220ms var(--m-ease), box-shadow 220ms var(--m-ease), opacity 220ms var(--m-ease);
}
/* scaleY, not height: the stripe is a grid item, and a height change would
   re-run layout for the whole row on every hover down the list. */
.bfl-cats .bf-list-row:hover .bfl-marker{
  transform:scaleY(1.3);
  box-shadow: 0 0 0 3px hsl(var(--av-h, 190) 48% 40% / .16);
}

/* Avatars: a ring, and a breath. */
.bf-avatar{ transition: transform 200ms var(--m-ease), box-shadow 200ms var(--m-ease); }
.bfl-feed .bfl-post:hover .bfl-post__av,
.bf-list-row:hover .bf-avatar{
  transform:scale(1.12);
  box-shadow: 0 0 0 2px var(--surface-1), 0 0 0 3.5px var(--teal-edge);
}
.bf-thread-posts .bf-post:hover .bf-post-avatar{
  box-shadow: 0 0 0 3px var(--surface-1), 0 0 0 4.5px var(--teal-edge);
}
.bf-post-avatar{ transition: box-shadow 200ms var(--m-ease); }

/* Category chips are destinations; they fill on hover like the button they
   effectively are. */
a.bf-pill-category{
  transition: background var(--dur-base) var(--ease-out),
              color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
a.bf-pill-category:hover{ background:var(--teal); color:#fff; border-color:var(--teal); }

/* Sidebar rows: the icon leans into the row; the category dot lights. */
.bf-nav-icon{ transition: transform 200ms var(--m-ease), color var(--dur-base) var(--ease-out); }
.bf-nav-row:hover .bf-nav-icon{ transform:translateX(2px); }
.bf-cat-dot{ transition: background var(--dur-base) var(--ease-out), transform 200ms var(--m-ease), box-shadow 200ms var(--m-ease); }
.bf-cat-row:hover .bf-cat-dot{ transform:scale(1.35); box-shadow: 0 0 0 3px var(--teal-wash-2); }
.bf-cat-row.is-active .bf-cat-dot{ box-shadow: 0 0 0 3px var(--teal-wash-2); }

/* Buttons lift one pixel and cast one shadow. Active presses back down. */
.btn-primary, .btn-dark, .bfl-side-cta, .bf-pager-btn{
  transition: background var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out),
              color var(--dur-base) var(--ease-out),
              transform 180ms var(--m-ease),
              box-shadow 180ms var(--m-ease);
}
.btn-primary:hover, .bfl-side-cta:hover{
  transform:translateY(-1px);
  box-shadow: 0 8px 18px -10px rgba(12,110,120,.75);
}
.btn-dark:hover{
  transform:translateY(-1px);
  box-shadow: 0 8px 18px -10px rgba(16,42,74,.7);
}
.bf-pager-btn:hover{ transform:translateY(-1px); box-shadow: var(--shadow-xs); }
.btn-primary:active, .btn-dark:active, .bfl-side-cta:active, .bf-pager-btn:active{
  transform:translateY(0) scale(.985);
  box-shadow:none;
  transition-duration: 60ms;
}

/* Sort tabs: a ghost of the indicator previews under the tab you are about
   to choose, growing from the left; the real indicator is the active tab's
   and travels between pages by view transition (§1). */
.bf-tab:not(.is-active)::after{
  /* Same extent as the real indicator (forum.css .bf-tab.is-active::after is
     left:0/right:0), so the bar does not jump outward on selection. */
  content:""; position:absolute; left:0; right:0; bottom:-1px; height:2px;
  background:var(--teal); opacity:.45;
  transform:scaleX(0); transform-origin:left center;
  transition: transform 220ms var(--m-ease), opacity 220ms var(--m-ease);
}
.bf-tab:not(.is-active):hover::after,
.bf-tab:not(.is-active):focus-visible::after{ transform:scaleX(1); }

/* Search: border and ground ease on hover/focus; the a11y ring on
   focus-within is forum.css's and stays exactly as it is. A grow-on-focus
   (max-width 420→520px) was tried here and REMOVED in review: max-width is
   a layout property (it reflowed the crumbs and account chip every frame),
   and its touch escape hatch made tablets worse, not better. Width stays
   whatever chrome.css gives it at each breakpoint. */
.bf-topbar .bf-search{
  transition: border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out);
}

/* Rail cards acknowledge the pointer with an edge, not a lift — they are
   panels, not links. */
.bfl-side-card{ transition: border-color 200ms var(--m-ease), box-shadow 200ms var(--m-ease); }
.bfl-side-card:hover{ border-color:var(--hair-2); box-shadow:var(--shadow-xs); }

/* Post controls and the permalink number: colour only, but eased. */
.bf-post__act, .bf-post__num, .bf-post-time, .bf-crumbs a, .bf-crumb, .bf-foot-list a{
  transition: color var(--dur-base) var(--ease-out);
}
.bf-thread-posts .bf-post:hover .bf-post__num{ color:var(--body); }

/* The OP's teal spine pulses ONCE when the thread arrives — a single
   glance-marker for "start reading here", then it is still. */
@keyframes bf-spine{
  0%  { box-shadow: inset 3px 0 0 var(--teal), 0 0 0 0 rgba(12,110,120,0); }
  40% { box-shadow: inset 3px 0 0 var(--teal), inset 0 0 0 1px var(--teal-edge); }
  100%{ box-shadow: inset 3px 0 0 var(--teal), 0 0 0 0 rgba(12,110,120,0); }
}
.bf-thread-posts .bf-post.bf-post-op{ animation: bf-rise var(--m-row) var(--m-ease) both, bf-spine 1.4s var(--m-ease) 500ms 1; }

/* ── 6b · THE REVEAL RULES — last in the cascade, on purpose (see §4) ── */
.bf-body .bf-reveal{
  opacity:0;
  transform:translateY(10px);
  transition: opacity 560ms var(--m-ease), transform 560ms var(--m-ease),
              background var(--dur-base) var(--ease-out),
              box-shadow 220ms var(--m-ease),
              border-color 200ms var(--m-ease);
}
.bf-body .bf-reveal.is-in{ opacity:1; transform:none; }

/* ── 7 · TYPE POLISH that only a last sheet can safely make ─────────
   Balanced headings: no more one-word orphan on the second line of a
   category name or a long thread title. Engines without text-wrap ignore it. */
.bf-headline, .bfl-title, .bfl-block-title, .bf-section-title, .bf-order-lede{ text-wrap:balance; }

/* Long-form prose (a post, the policy) gets `pretty` — fewer widows, no
   layout cost worth measuring at these lengths. */
.bf-post-text, .bfp-prose p{ text-wrap:pretty; }

/* ── 8 · REDUCED MOTION — a full stop ───────────────────────────────
   forum.css already collapses every duration to 0.001ms. This block does
   the three things that rule cannot: zero the DELAYS (or staggered rows
   still blink in late), un-hide every reveal and image, and make the page
   change a plain swap. The ambient layers hold their first frame — a soft
   static light is not motion. */
@media (prefers-reduced-motion: reduce){
  .bf-main > *, .bf-list > li, .bf-thread-posts > .bf-post, .bfl-side > *,
  .bf-body::before, .bf-body::after, .bf-order::before, .bf-topbar::before,
  .bf-thread-posts .bf-post.bf-post-op{
    animation:none !important;
    animation-delay:0s !important;
  }
  /* Three gradients that will never move do not need three composited
     layers held for them. */
  .bf-body::before, .bf-body::after, .bf-order::before{ will-change:auto !important; }
  .bf-reveal, .bf-reveal.is-in{ opacity:1 !important; transform:none !important; transition:none !important; }
  .bf-js .bf-post-img{ opacity:1 !important; transition:none !important; }
  .bf-topbar::after{ display:none; }
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*){ animation:none !important; }
  .bfl-feed .bfl-post:hover .bfl-post__score-n,
  .bfl-feed .bfl-post:hover .bfl-post__av,
  .bf-list-row:hover .bf-avatar,
  .bf-cat-row:hover .bf-cat-dot,
  .bfl-cats .bf-list-row:hover .bfl-marker,
  .btn-primary:hover, .btn-dark:hover, .bfl-side-cta:hover, .bf-pager-btn:hover,
  .bf-nav-row:hover .bf-nav-icon{ transform:none; }
}

/* Touch devices have no hover; the ghost tab indicator is a pointer
   affordance and is switched off where it would only ever fire on tap.
   Reveals and arrivals stay — they are about scrolling, which a thumb does
   more than a mouse. */
@media (hover:none){
  .bf-tab:not(.is-active)::after{ display:none; }
}

/* Print: no motion, no ambient light, no progress bar. */
@media print{
  .bf-body::before, .bf-body::after, .bf-order::before, .bf-topbar::before, .bf-topbar::after{ display:none !important; will-change:auto !important; }
  .bf-main > *, .bf-list > li, .bf-thread-posts > .bf-post, .bfl-side > *{ animation:none !important; }
  .bf-reveal{ opacity:1 !important; transform:none !important; }
  .bf-js .bf-post-img{ opacity:1 !important; }
}
