/* feed-v2.css
 *
 * Reactive feed UI. All rules scoped under `body.feed-v2` so they only
 * apply to /stream; digest emails, permalinks, and legacy flows
 * untouched.
 *
 * Glass pass: unified --v2-radius token, lensed hairline via box-shadow
 * on every grid-peer surface, translucent material + backdrop-filter,
 * community-hue tint, modal-as-grown-card.
 */

/* ─── Design tokens ──────────────────────────────────────────── */

body.feed-v2 {
  --v2-radius: 10px;
  --v2-radius-mobile: 8px;
  /* Lensed hairline + layered drop. Re-used on every grid-peer surface
   * (cards, tweak pads, notices, modal). The inner 1px highlight is
   * the single gesture that makes the whole thing read as glass. */
  /* Dialled up 2× from the conservative first pass — hairline now
   * reads as a visible edge, not a suggestion. Top specular + outer
   * hairline tripled; bottom shadow edge thickened so the card's
   * lower boundary catches the eye. */
  --v2-glass-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.14),
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    inset 0 -1px 0 rgba(0, 0, 0, 0.40),
    0 2px 8px rgba(0, 0, 0, 0.28),
    0 14px 40px rgba(0, 0, 0, 0.30);
  --v2-glass-shadow-modal:
    inset 0 0 0 1px rgba(255, 255, 255, 0.18),
    inset 0 1px 0 rgba(255, 255, 255, 0.26),
    inset 0 -1px 0 rgba(0, 0, 0, 0.44),
    0 10px 30px rgba(0, 0, 0, 0.45),
    0 40px 96px rgba(0, 0, 0, 0.55);
  /* Translucent card bg — community-hue tinted. Dropped alpha to
   * 0.55 so the page (and neighbor cards / bleeding imagery) reads
   * through. Fallback solid bumped up slightly so no-blur browsers
   * still get a legible card. */
  --v2-card-bg: hsla(var(--community-hue, 210), 22%, 10%, 0.55);
  --v2-card-bg-solid: hsla(var(--community-hue, 210), 22%, 10%, 0.94);
}

/* ─── Stream grid ────────────────────────────────────────────── */

body.feed-v2 .stream {
  max-width: 1680px;
  margin: 0 auto;
  padding: var(--space-lg);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  grid-auto-rows: minmax(0, auto);
  gap: var(--space-md);
  justify-content: center;
}

/* Tablet: tighter gaps, slightly smaller minimum */
@media (max-width: 960px) {
  body.feed-v2 .stream {
    grid-template-columns: repeat(auto-fit, minmax(216px, 1fr));
    gap: var(--space-sm);
  }
}

/* Phone: force exactly 2 columns regardless of viewport width.
 * The auto-fit model would collapse to 1 col on narrow phones;
 * the brief requires 2 cols on mobile. */
@media (max-width: 720px) {
  body.feed-v2 .stream {
    grid-template-columns: repeat(2, 1fr);
    padding: var(--space-sm);
    gap: var(--space-sm);
  }
}

@media (max-width: 360px) {
  body.feed-v2 .stream {
    gap: 6px;
    padding: 6px;
  }
}

/* ─── Tile shape (narrower, a touch wider than square) ─────── */

body.feed-v2 .view-card {
  aspect-ratio: 11 / 10;
  border-radius: var(--v2-radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  position: relative;
  /* Default z-index: 1 so non-bleed cards paint above the z-index: 0
   * bleed cards (see .v2-image-bleed rule). Text in any card is
   * guaranteed above any bleed-image extension from another card. */
  z-index: 1;
  /* Translucent glass material. The inner 1px highlight is the lensed
   * hairline that defines the 2026 look. Falls back to solid on
   * browsers without backdrop-filter support (see @supports below). */
  background: var(--v2-card-bg-solid);
  border: none;
  box-shadow: var(--v2-glass-shadow);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
  /* Perf: let the browser skip layout + paint + composite for cards
   * that are currently off-screen. `contain-intrinsic-size` preserves
   * scroll geometry so the scrollbar doesn't jump. Biggest win for
   * deep-scroll fps — at 90+ cards in the DOM, most are off-screen
   * and should contribute zero compositing cost.
   *
   * NOTE: Chromium's `content-visibility: auto` implementation
   * applies paint containment even to ON-SCREEN elements (not just
   * off-screen as the spec suggests). That clips .v2-image-bleed
   * at the card edge — so bleed cards get content-visibility: visible
   * below, trading their per-card perf skip for a working bleed. */
  content-visibility: auto;
  /* `auto` intrinsic-size lets the browser remember the last-
   * rendered size per element — so shape-tall / shape-big cards
   * claim the right scroll space once they've been seen once. */
  contain-intrinsic-size: auto 260px;
  /* Isolate layout + style so a class change on one card can't force
   * style recalc on its siblings. Deliberately NOT paint-contained
   * explicitly — but Chromium adds paint containment implicitly via
   * content-visibility:auto, which is why bleed cards need the
   * override below. */
  contain: layout style;
}

/* Bleed cards need content-visibility: visible to keep the
 * overflow painting (Chromium's content-visibility: auto applies
 * paint containment even to on-screen elements, which clips the
 * bleed). BUT: when a bleed card is FAR off-screen, nobody sees
 * the clipping — so we can let off-screen bleed cards fall back
 * to content-visibility: auto and reclaim the perf skip.
 *
 * IntersectionObserver in feed-v2.js (Bleed.observe) toggles
 * data-in-view="1" when the card is within ~500px of the viewport.
 * Cards near/in view → rendered normally with bleed. Cards far
 * away → perf-skipped like any other card. */
body.feed-v2 .view-card.v2-image-bleed[data-in-view="1"] {
  content-visibility: visible;
}

body.feed-v2 .view-card:hover {
  background: var(--v2-card-bg-solid);
  transform: translateY(-1px);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.22),
    inset 0 1px 0 rgba(255, 255, 255, 0.30),
    inset 0 -1px 0 rgba(0, 0, 0, 0.40),
    0 6px 18px rgba(0, 0, 0, 0.32),
    0 20px 50px rgba(0, 0, 0, 0.30);
}
/* Immediate tap feedback — the card presses down instantly on
 * click, so the user feels the interaction before the modal
 * finishes opening (~100ms first-paint). Pure transform, no
 * layout — cheap. */
body.feed-v2 .view-card:not([data-tweak]):active {
  transform: scale(0.985) translateY(0);
  transition: transform 0.08s ease;
}

@supports (backdrop-filter: blur(20px)) or (-webkit-backdrop-filter: blur(20px)) {
  body.feed-v2 .view-card {
    background: var(--v2-card-bg);
    backdrop-filter: blur(20px) saturate(1.15);
    -webkit-backdrop-filter: blur(20px) saturate(1.15);
  }
  body.feed-v2 .view-card:hover {
    background: var(--v2-card-bg);
  }
  /* Perf: image-forward cards have an opaque hero image filling the
   * whole tile — the card's own translucent bg is fully covered, so
   * the backdrop-filter does real compositing work for zero visual
   * payoff. Drop it. Saves ~70% of the per-card blur cost in a
   * typical image-heavy feed (77/91 cards in a 25-viewport scroll).
   * The hairline box-shadow still applies so the edge treatment is
   * unchanged. */
  body.feed-v2 .view-card.card-image-forward {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: transparent;
  }
  /* Perf: while the modal is open, the page under the overlay is
   * already being backdrop-blurred by the overlay itself. Having
   * 90+ individual card blurs also running under that is pointless
   * triple-compositing — drop them to solid while modal is up. */
  body.feed-v2.v2-modal-open .view-card,
  body.feed-v2.v2-modal-open .v2-tweak-pad,
  body.feed-v2.v2-modal-open .v2-notice {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
    background: var(--v2-card-bg-solid);
  }
}

@media (max-width: 720px) {
  body.feed-v2 .view-card {
    border-radius: var(--v2-radius-mobile);
  }
}

/* Cards with hero image should fill the tile; text overlays are handled
 * by existing .card-overlay rules which we keep. */
body.feed-v2 .view-card .card-image,
body.feed-v2 .view-card .card-hero {
  flex: 1 1 auto;
  min-height: 0;
}

/* Squeeze the existing card body to fit the squarer aspect. Extra
 * bottom padding so the headline doesn't kiss the card edge. */
body.feed-v2 .view-card .card-body {
  padding: var(--space-sm) var(--space-sm) calc(var(--space-md) + 4px);
  font-size: 0.9em;
  overflow: hidden;
}

body.feed-v2 .view-card .card-body h2 {
  font-size: 1em;
  line-height: 1.25;
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 5;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ─── Image vignette (safe, mask-image) ────────────────────────
 * Image-forward hero images fade to alpha at their own corners. When
 * two image cards sit side by side, the images appear to "breathe"
 * across the gutter rather than snap at a hard edge. The dark-
 * gradient overlay that carries the headline stays solid underneath,
 * so legibility is untouched.
 *
 * Mask applies only to the <img> element, not .card-hero-wrap, so
 * the ::after gradient stays opaque where the text sits. */
body.feed-v2 .view-card.card-image-forward .card-hero-wrap img {
  -webkit-mask-image: radial-gradient(ellipse 115% 110% at center,
    #000 50%, rgba(0, 0, 0, 0.55) 75%, rgba(0, 0, 0, 0.15) 92%, transparent 100%);
  mask-image: radial-gradient(ellipse 115% 110% at center,
    #000 50%, rgba(0, 0, 0, 0.55) 75%, rgba(0, 0, 0, 0.15) 92%, transparent 100%);
}

/* Shape-big gallery + image cards get a slightly more aggressive
 * vignette — the extra real estate makes edge fade look intentional
 * instead of an accident. */
body.feed-v2 .view-card.v2-shape-big.card-image-forward .card-hero-wrap img,
body.feed-v2 .view-card.v2-shape-big .card-hero img {
  -webkit-mask-image: radial-gradient(ellipse 112% 108% at center,
    #000 42%, rgba(0, 0, 0, 0.45) 70%, rgba(0, 0, 0, 0.12) 90%, transparent 100%);
  mask-image: radial-gradient(ellipse 112% 108% at center,
    #000 42%, rgba(0, 0, 0, 0.45) 70%, rgba(0, 0, 0, 0.12) 90%, transparent 100%);
}

/* Hero card opt-in: one card per viewport (picked client-side, first
 * visible image-forward card). Overlaps its own gutters by a few px
 * so its hero image visually leans into neighbors — the flourish
 * that makes the grid feel alive. Desktop only; mobile keeps tidy
 * grid tracks because 2-col can't afford the overlap. */
@media (min-width: 961px) {
  body.feed-v2 .view-card[data-hero="1"] {
    margin: -10px;
    /* z-index: 0 (not 3). z-index 3 lifted the hero's entire
     * stacking context above all siblings — which caused the
     * hero's bleed-image extension to paint over neighbor cards'
     * text. Neighbors at z-index: 1 (set below) now always stack
     * above the hero's bleed. The -10px margin still achieves
     * the gutter-overlap geometry; losing the explicit z-bump is
     * the tradeoff for making text-always-above-images work. */
    z-index: 0;
  }
  body.feed-v2 .view-card[data-hero="1"] .card-hero-wrap img {
    -webkit-mask-image: radial-gradient(ellipse 120% 115% at center,
      #000 52%, rgba(0, 0, 0, 0.55) 75%, rgba(0, 0, 0, 0.15) 92%, transparent 100%);
    mask-image: radial-gradient(ellipse 120% 115% at center,
      #000 52%, rgba(0, 0, 0, 0.55) 75%, rgba(0, 0, 0, 0.15) 92%, transparent 100%);
  }
}

/* ─── Image bleed (the real cross-border gesture) ──────────────
 *
 * A card tagged .v2-image-bleed lets its hero image physically
 * escape the card's bounds via overflow:visible + transform:scale.
 * A strong radial mask fades the image from solid at the card
 * center to fully transparent well before it reaches the scaled
 * extent — so the visible "halo" extends into the gutter and
 * faintly laps onto neighbor cards.
 *
 * Why this works without a z-index war: by the time the image
 * crosses into neighbor territory, the mask has already reduced
 * it to ≤30% alpha. The bleed reads as a soft glow, not as an
 * occluding rectangle. Headline overlay + card background stay
 * inside the card because only the <img> uses the scale/mask
 * trick — the .card-overlay is unaffected.
 *
 * Desktop only — two-column phones can't afford the overflow. */
@media (min-width: 961px) {
  body.feed-v2 .view-card.v2-image-bleed {
    overflow: visible;
  }
  body.feed-v2 .view-card.v2-image-bleed .card-hero-wrap {
    overflow: visible;
  }
  body.feed-v2 .view-card.v2-image-bleed .card-hero-wrap img {
    /* Scale 1.35 — restored for visible cross-border halo. Earlier
     * plateau-with-outer-fade experiment (scale 1.18 + aggressive
     * 0.10-outer-rim) tightened too far: the image stopped
     * meaningfully bleeding into neighbor territory. Back to
     * something closer to the first-working rectangular pass that
     * read as "photograph crossing its own edge." */
    transform: scale(1.35);
    transform-origin: center;
    /* Perf: promote the bleed image to its own compositing layer.
     * Without this, scrolling forces a repaint per card per frame
     * to reapply the scale+mask. */
    will-change: transform;
    /* Rectangular vignette — two crossed linear gradients composited
     * via mask-composite: intersect. Image is fully opaque across
     * the inner rectangle (12% → 88% on each axis) with a simple
     * linear fade to transparent at the outer 12% of each edge.
     *
     * No plateau, no "hold at 0.55 and then drop to 0.1" — the
     * plateau made the halo feel confined. A straight linear ramp
     * from alpha 1 (just inside card) → alpha 0 (image outer edge)
     * produces a readable halo because the midpoint of the fade
     * (alpha 0.5) lands right around the card edge / into the
     * gutter, which is where the user's eye wants to see the
     * spillover. */
    -webkit-mask-image:
      linear-gradient(to right,  transparent 0%, #000 12%, #000 88%, transparent 100%),
      linear-gradient(to bottom, transparent 0%, #000 12%, #000 88%, transparent 100%);
    mask-image:
      linear-gradient(to right,  transparent 0%, #000 12%, #000 88%, transparent 100%),
      linear-gradient(to bottom, transparent 0%, #000 12%, #000 88%, transparent 100%);
    -webkit-mask-composite: source-in;
    mask-composite: intersect;
  }
  /* Bleed cards sit BELOW non-bleed cards in z-order so the
   * overflowing image never paints over a neighbor's text. Dark
   * halo box-shadow extends the card's material 60px past the
   * border so the bleeding image's partial-alpha pixels fade over
   * a consistent dark backdrop instead of the lighter page bg
   * (which was making bright images like phone screenshots look
   * "brighter outside the card" — user-flagged).
   *
   * isolation: isolate keeps the card a single compositing unit
   * (stable GPU layer, perf from will-change on the image stays
   * effective). Combined with z-index: 0 explicit, it sits just
   * below the z-index: 1 baseline we apply to all non-bleed cards. */
  body.feed-v2 .view-card.v2-image-bleed {
    isolation: isolate;
    z-index: 0;
    box-shadow:
      inset 0 0 0 1px rgba(255, 255, 255, 0.14),
      inset 0 1px 0 rgba(255, 255, 255, 0.22),
      inset 0 -1px 0 rgba(0, 0, 0, 0.40),
      0 0 60px 18px rgba(0, 0, 0, 0.55),
      0 14px 40px rgba(0, 0, 0, 0.30);
  }
  /* Shape-big bleed: the big hero is the natural canvas for this
   * effect — scale up slightly more since the card's wider. */
  body.feed-v2 .view-card.v2-shape-big.v2-image-bleed .card-hero-wrap img {
    transform: scale(1.45);
  }
}

/* Image-forward cards: stronger bottom-up gradient under the overlay
 * so the headline stays readable on any hero image. Extra bottom
 * padding keeps the headline off the tile edge. Overrides the softer
 * ohpan.css gradient (30% → bg-surface) which was tuned for the older
 * wider-card layout. */
body.feed-v2 .view-card.card-image-forward .card-hero-wrap::after {
  background: linear-gradient(
    to bottom,
    transparent 0%,
    rgba(0, 0, 0, 0.15) 35%,
    rgba(0, 0, 0, 0.55) 65%,
    rgba(0, 0, 0, 0.85) 100%
  );
}

/* Bleed cards: extend the headline-backing dark gradient past the
 * card's bottom AND top so the bleeding image is always sitting on
 * top of dark material — not transitioning abruptly from "darkened
 * image under headline inside the card" to "raw image in the bleed
 * zone outside." User feedback: the tie/torso bleeding below the
 * card was clearly visible against the page because the dark wash
 * stopped at the card edge. Now the wash extends with the image.
 *
 * top: -35% / bottom: -35% makes the pseudo-element cover the card's
 * full bleed extent (scale 1.35 = 17.5% each side, with room for the
 * fade). The gradient stops are re-mapped so the DARKEST band lands
 * right where the headline sits (inside-card bottom third), and
 * stays dark through the bleed below. The top extension is a
 * symmetric darkening pass so the upward bleed matches. */
body.feed-v2 .view-card.v2-image-bleed.card-image-forward .card-hero-wrap::after {
  top: -35%;
  bottom: -35%;
  background: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.85) 0%,     /* dark through the top bleed */
    rgba(0, 0, 0, 0.35) 20%,    /* fade toward middle of card */
    transparent 35%,             /* clear at ~card-top interior */
    transparent 48%,
    rgba(0, 0, 0, 0.30) 58%,    /* pick up darkening where headline lives */
    rgba(0, 0, 0, 0.60) 68%,
    rgba(0, 0, 0, 0.85) 78%,    /* peak darkness at card bottom area */
    rgba(0, 0, 0, 0.85) 100%     /* stay dark through the downward bleed */
  );
}

body.feed-v2 .view-card.card-image-forward .card-overlay {
  padding: var(--space-sm) var(--space-md) calc(var(--space-md) + 4px);
  background: linear-gradient(
    to bottom,
    transparent 0%,
    rgba(0, 0, 0, 0.35) 60%,
    rgba(0, 0, 0, 0.7) 100%
  );
}

body.feed-v2 .view-card.card-image-forward .card-overlay h2 {
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}

/* Headline-only: hide every card's summary/body paragraph. Typed cards
 * (quote, book, repo, etc.) carry their content in other elements and
 * are unaffected. */
body.feed-v2 .view-card .card-body .lead,
body.feed-v2 .view-card .card-body p.lead {
  display: none;
}

body.feed-v2 .view-card .card-footer,
body.feed-v2 .view-card .card-actions {
  padding: 4px var(--space-sm);
  font-size: 0.75em;
}

/* .card-recency (text cards) and .card-meta (image cards) used to
 * carry time + avatars; feed-v2.js reparents both onto .view-card
 * directly, so these containers are now empty and hidden. */
body.feed-v2 .view-card .card-recency,
body.feed-v2 .view-card.card-image-forward .card-overlay .card-meta {
  display: none;
}

/* Prominent time pill, top-left. Works for both text and image cards
 * (reparented as a direct child of .view-card, which is position:
 * relative). Monospace + dark pill with backdrop blur so it reads on
 * any hero image. */
body.feed-v2 .view-card > .card-time {
  position: absolute;
  top: 8px;
  left: 10px;
  z-index: 4;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: #fff;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  padding: 3px 9px;
  border-radius: 8px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}

/* Source avatars pinned bottom-left. Higher z-index than the overlay
 * gradient so they stay legible. */
body.feed-v2 .view-card > .source-avatars {
  position: absolute;
  bottom: 8px;
  left: 10px;
  z-index: 4;
  margin-left: 0;
}

/* Stronger red glow for live sources. Overrides the 8-px max halo
 * in ohpan.css — we want this to catch the eye at small tile size. */
body.feed-v2 .src-avatar.avatar-live {
  animation: v2-avatar-pulse 1.8s ease-in-out infinite;
  border-color: rgba(239, 68, 68, 0.9);
}

@keyframes v2-avatar-pulse {
  0%, 100% {
    box-shadow:
      0 0 0 0 rgba(239, 68, 68, 0.7),
      0 0 10px 2px rgba(239, 68, 68, 0.45);
  }
  50% {
    box-shadow:
      0 0 0 6px rgba(239, 68, 68, 0),
      0 0 22px 6px rgba(239, 68, 68, 0.8);
  }
}

/* ─── Picker stays at top for Phase 1; later phases fold its
 *     tiles into the same grid as reaction surfaces. ───────── */

body.feed-v2 .picker-root {
  max-width: 1680px;
  margin: 0 auto;
}

/* ─── Modal overlay (Phase 2) ─────────────────────────────── */

.v2-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* Delay visibility flip until the opacity fade finishes, so the
   * overlay fully unmounts visually — WebKit and some Chromium
   * builds leave backdrop-filter ghosts if the element lingers with
   * opacity 0 but still participating in compositing. */
  transition: opacity 0.2s ease, visibility 0s linear 0.2s;
  z-index: 1000;
}

.v2-modal-overlay.open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity 0.2s ease, visibility 0s linear 0s;
  /* Filter applies only while .open so there's no ghost blur when
   * the user closes the modal. 14px is the sweet spot between
   * "page pushed back" and compositing cost — 22px was visibly
   * nicer but cost a 150ms frame on modal-open (perf harness).
   * The higher-alpha dark underlay compensates. */
  backdrop-filter: blur(14px) saturate(1.1);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  background: rgba(0, 0, 0, 0.55);
}

.v2-modal-dialog {
  position: relative;
  /* Same material as a card, grown: same hairline, same translucent
   * body (less alpha so the page behind reads as pushed-back), same
   * radius token. Card → modal now feels like "the card lifted
   * forward" instead of "the card vanished, a dialog arrived." */
  background: hsla(var(--community-hue, 210), 18%, 12%, 0.88);
  color: var(--text-primary, #eee);
  border: none;
  border-radius: var(--v2-radius);
  width: min(1040px, 94vw);
  max-height: 90vh;
  overflow-y: auto;
  padding: var(--space-lg, 24px);
  box-shadow: var(--v2-glass-shadow-modal);
  transform: translateY(12px) scale(0.98);
  opacity: 0;
  transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.22s ease;
  line-height: 1.7;
}

/* Perf: the overlay already runs blur(22px), so the dialog would
 * be blurring an already-blurred image — double-composite for
 * marginal visual gain. Keep the dialog solid-ish with a high-alpha
 * hsla fill so the page behind stays pushed back via the overlay,
 * without paying for a second blur pass. */

/* Main article body inside the modal — extra line-height for
 * comfortable reading in the wider dialog. */
.v2-modal-content,
.v2-modal-content p,
.v2-modal-content li,
.v2-modal-content .view-expanded,
.v2-modal-content .full-story {
  line-height: 1.75;
}

.v2-modal-content h1,
.v2-modal-content h2,
.v2-modal-content h3 {
  line-height: 1.3;
}

.v2-modal-overlay.open .v2-modal-dialog {
  transform: translateY(0) scale(1);
  opacity: 1;
}

.v2-modal-close {
  position: absolute;
  top: 10px;
  right: 14px;
  width: 36px;
  height: 36px;
  border: none;
  background: transparent;
  color: inherit;
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
  border-radius: 50%;
  opacity: 0.7;
  transition: opacity 0.15s, background 0.15s;
}

.v2-modal-close:hover {
  opacity: 1;
  background: rgba(255, 255, 255, 0.08);
}

.v2-modal-loading {
  padding: 48px;
  text-align: center;
  opacity: 0.6;
}

body.v2-modal-open {
  overflow: hidden;
}

@media (max-width: 720px) {
  .v2-modal-dialog {
    width: 100vw;
    height: 100vh;
    max-height: 100vh;
    border-radius: 0;
    padding: var(--space-md, 16px);
  }
}

/* ─── Reaction pad (post-modal-close) ────────────────────── */

.v2-reaction-pad {
  position: absolute;
  inset: 0;
  /* Frosted glass over the origin card — the image underneath stays
   * faintly visible through the blur, so the pad reads as "same card,
   * now in action mode" instead of "opaque overlay." */
  background: linear-gradient(180deg, rgba(10, 12, 18, 0.55) 0%, rgba(10, 12, 18, 0.82) 100%);
  color: #fff;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 10px;
  gap: 8px;
  opacity: 0;
  transition: opacity 0.25s ease;
  border-radius: inherit;
  z-index: 5;
}

/* Title echo — first two lines of the card headline, kept visible
 * at the top of the pad so the user remembers what they just read.
 * Background stays transparent so the glass material shows through. */
.v2-pad-title-echo {
  font-size: 0.82rem;
  font-weight: 600;
  line-height: 1.25;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  flex: 0 0 auto;
}

@supports (backdrop-filter: blur(16px)) or (-webkit-backdrop-filter: blur(16px)) {
  .v2-reaction-pad {
    /* Perf: the parent .view-card already runs blur(20px), so an
     * additional blur here is double-compositing for marginal
     * visual gain. Keep a light 6px on the pad for the extra frost
     * look; the heavy lift comes from the gradient tint. */
    background: linear-gradient(180deg, rgba(10, 12, 18, 0.45) 0%, rgba(10, 12, 18, 0.78) 100%);
    backdrop-filter: blur(6px) saturate(1.1);
    -webkit-backdrop-filter: blur(6px) saturate(1.1);
  }
}

.v2-reaction-pad.open { opacity: 1; }

.v2-pad-actions {
  flex: 1;
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: 6px;
  padding: 2px 0 8px;
}

/* Reaction-pad action cells mirror the tweak-choice vocabulary: the
 * same hairline + glass background + icon-above-label layout, so
 * "more", "less", and "share" read as one consistent surface model
 * across the reaction pad AND the MORE/LESS tweak pads. Only the
 * accent hue (red / green / neutral) differs per action. */
.v2-pad-action {
  flex: 1 1 0;
  min-width: 0;
  background: rgba(255, 255, 255, 0.08);
  border: none;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.14),
    inset 0 1px 0 rgba(255, 255, 255, 0.12);
  color: inherit;
  padding: 8px 4px;
  border-radius: 10px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 0.68rem;
  transition: background 0.15s, box-shadow 0.15s, transform 0.1s;
}

.v2-pad-action:hover {
  background: rgba(255, 255, 255, 0.18);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.22),
    inset 0 1px 0 rgba(255, 255, 255, 0.18);
}

.v2-pad-action:active {
  transform: scale(0.95);
}

.v2-pad-action:disabled {
  opacity: 0.6;
  cursor: wait;
}

.v2-pad-action-icon {
  width: 26px;
  height: 26px;
  display: block;
  flex: 0 0 auto;
}

.v2-pad-action-label {
  font-size: 0.65rem;
  letter-spacing: 0.01em;
  text-align: center;
  line-height: 1.1;
  opacity: 0.9;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

.v2-pad-less-big {
  background: rgba(210, 70, 85, 0.18);
}
.v2-pad-less-big:hover {
  background: rgba(210, 70, 85, 0.32);
}

.v2-pad-more-big {
  background: rgba(70, 170, 90, 0.22);
}
.v2-pad-more-big:hover {
  background: rgba(70, 170, 90, 0.36);
}

.v2-pad-share:hover {
  background: rgba(180, 180, 220, 0.22);
}

.v2-pad-save {
  background: rgba(240, 200, 110, 0.20);
}
.v2-pad-save:hover {
  background: rgba(240, 200, 110, 0.34);
}
/* Saved confirmation state — amber fill, no-more-action cursor. */
.v2-pad-save.v2-pad-saved {
  background: rgba(240, 200, 110, 0.55);
  cursor: default;
}
.v2-pad-save.v2-pad-saved:hover {
  background: rgba(240, 200, 110, 0.55);
}

/* ─── Reaction pad stage 2 — "less what?" / "more what?" ─────
 *
 * When the user clicks less/more on the post-read reaction pad, we
 * swap the actions row for a topic picker built from the card's
 * data-topics (real cluster topics, not tile slugs). Up to 4
 * choices; fewer if the cluster has fewer topics (no top-up).
 * Clicking a topic fires /react with mode="topic" — same inject /
 * prune effect as a tweak-pad MORE / LESS, but the signal recorded
 * is the real topic string. */
.v2-pad-stage2 {
  /* Subtle tint of the pad body by action — red-ish for less, green-
   * ish for more — so the user knows what they're picking into. */
  transition: opacity 0.25s ease, background 0.25s ease;
}
.v2-pad-stage2.v2-pad-stage2-less {
  background: linear-gradient(180deg, rgba(60, 12, 20, 0.55) 0%, rgba(50, 8, 15, 0.80) 100%);
}
.v2-pad-stage2.v2-pad-stage2-more {
  background: linear-gradient(180deg, rgba(14, 50, 24, 0.55) 0%, rgba(10, 42, 20, 0.80) 100%);
}

.v2-pad-stage-label {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: center;
  opacity: 0.85;
  margin-bottom: 4px;
}

.v2-pad-topic-grid {
  flex: 1;
  display: grid;
  gap: 5px;
  min-height: 0;
  align-content: stretch;
}
/* Layout shrinks with choice count — no top-up when cluster has <4
 * topics. 1 choice = 1 row, 2 = 2 rows, 3 = 1+2, 4 = 2x2. */
.v2-pad-topic-grid-1 { grid-template-rows: 1fr; }
.v2-pad-topic-grid-2 { grid-template-rows: 1fr 1fr; }
.v2-pad-topic-grid-3 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.v2-pad-topic-grid-3 .v2-pad-topic-choice:first-of-type { grid-column: 1 / -1; }
.v2-pad-topic-grid-4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }

.v2-pad-topic-choice {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 6px 8px;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 500;
  color: #fff;
  background: rgba(255, 255, 255, 0.12);
  border: none;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.14),
    inset 0 1px 0 rgba(255, 255, 255, 0.12);
  border-radius: 10px;
  cursor: pointer;
  text-align: center;
  line-height: 1.15;
  overflow: hidden;
  transition: background 0.15s, box-shadow 0.15s, transform 0.1s;
}
.v2-pad-topic-choice:hover {
  background: rgba(255, 255, 255, 0.22);
}
.v2-pad-topic-choice:active {
  transform: scale(0.96);
}
.v2-pad-topic-choice:disabled {
  opacity: 0.6;
  cursor: default;
}
.v2-pad-topic-choice.v2-pad-topic-chosen {
  background: rgba(255, 255, 255, 0.35);
}

.v2-pad-topic-label {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 100%;
}

/* Leftwards-draining decay bar. transform: scaleX animates from 1 to 0
 * (right-edge eats the bar). The fill is anchored to the left, so the
 * right side shrinks first — matches the user's "leftwards moving"
 * description. */
.v2-pad-decay-bar {
  height: 3px;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 3px;
  overflow: hidden;
  flex: 0 0 auto;
}

.v2-pad-decay-fill {
  height: 100%;
  width: 100%;
  transform-origin: left center;
  background: linear-gradient(
    90deg,
    hsla(var(--community-hue, 200), 60%, 60%, 0.95),
    hsla(var(--community-hue, 200), 60%, 50%, 0.55)
  );
}

@media (max-width: 480px) {
  .v2-pad-action-label { font-size: 0.58rem; }
  .v2-pad-action-icon  { width: 18px; height: 18px; }
}

.v2-pad-title {
  font-size: 0.75em;
  text-align: center;
  opacity: 0.75;
  margin-bottom: 4px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Topic icon — shared by tweak-choice icons and any other topic-icon
 * consumer (tile-× popup is CSS-unaware of this class but does not
 * need it). */
.v2-topic-icon {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: brightness(1.2);
}

/* Pill-style buttons used by the quiz pad and the tile-× popup. */
.v2-pad-btn {
  border: 1px solid rgba(255,255,255,0.3);
  background: rgba(255,255,255,0.06);
  color: inherit;
  padding: 4px 10px;
  font-size: 0.8em;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
  font-weight: 500;
}

.v2-pad-btn:hover {
  background: rgba(255,255,255,0.16);
  border-color: rgba(255,255,255,0.5);
}

.v2-pad-less { border-color: rgba(220, 120, 120, 0.6); }
.v2-pad-less:hover { background: rgba(220, 120, 120, 0.18); }
.v2-pad-hard { border-style: dashed; }

.v2-pad-btn:disabled { opacity: 0.5; cursor: wait; }

/* ─── Tweak pad (Phase 4) ─────────────────────────────────── */

/* Tweak pads live "behind" the feed — no hairline, no drop shadow,
 * no backdrop-filter. Just a tinted panel that feels like part of
 * the page surface one layer below the content cards. The 1%
 * parallax (via transform: translateY on --feed-scroll) reinforces
 * the depth: pads scroll a touch slower than the feed, so on a
 * long scroll the eye reads them as a background plane instead of
 * as another card. Recessed inner-shadow rim adds just enough
 * definition to find the edges without a border. */
body.feed-v2 .v2-tweak-pad {
  background: linear-gradient(135deg, rgba(80,110,160,0.18), rgba(150,100,170,0.18));
  border: none;
  /* Behind-plane pads are NOT glass — kill the inherited blur from
   * the base .view-card rule. Makes them read as a flat colored
   * panel of the page, not a translucent card. */
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  padding: 10px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  cursor: default;
  aspect-ratio: 11 / 10;
  border-radius: var(--v2-radius);
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, 0.18),
    inset 0 2px 6px rgba(0, 0, 0, 0.18);
  position: relative;
  opacity: 0;
  transition: opacity 0.35s ease;
  /* Parallax: tweak pads move ~1% slower than the feed so the eye
   * reads them as a plane behind the content. --feed-scroll is set
   * on <html> by a passive scroll listener in feed-v2.js. */
  transform: translateY(calc(var(--feed-scroll, 0px) * 0.01));
  will-change: transform;
  /* Kill the content-visibility default from .view-card — the
   * parallax transform makes the browser's "is this visible?"
   * check on the LAYOUT position, not the transformed position,
   * which is fine, but paint containment would still interfere
   * with scroll-driven transform updates. */
  content-visibility: visible;
  contain: none;
}

@media (max-width: 720px) {
  body.feed-v2 .v2-tweak-pad { border-radius: var(--v2-radius-mobile); }
}

/* Match the specificity of the base body.feed-v2 .v2-tweak-pad rule
 * (0,2,1) — otherwise opacity:0 wins and the pad pops in via the
 * v2-popped animation and then snaps back to invisible when the
 * animation ends, leaving a blank interactive slot. */
body.feed-v2 .v2-tweak-pad.revealed { opacity: 1; }

/* Final pad — styling is shared with v2-tweak-more (green four-choice
 * MORE tile). The v2-final-pad class is a marker for locator logic
 * (ensureFinalPad removes & re-inserts it at the grid tail). */

.v2-tweak-title {
  color: var(--text-secondary, #aaa);
  font-size: 0.72em;
  letter-spacing: 0.05em;
}

/* ─── Tile × button + popup (Phase 5) ────────────────────── */

.v2-tile-x {
  position: absolute;
  top: 4px;
  right: 6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.35);
  color: #fff;
  font-size: 14px;
  line-height: 16px;
  text-align: center;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s, background 0.15s;
  z-index: 2;
  user-select: none;
}

.pk-tile { position: relative; }

.pk-tile:hover .v2-tile-x,
.v2-tile-x:focus,
.v2-tile-x:hover { opacity: 1; }

.v2-tile-x:hover { background: rgba(220, 60, 60, 0.8); }

.v2-tile-popup {
  position: absolute;
  background: hsla(var(--community-hue, 210), 18%, 12%, 0.92);
  border: none;
  border-radius: var(--v2-radius);
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  box-shadow: var(--v2-glass-shadow);
  z-index: 500;
  min-width: 180px;
  animation: v2-pop-in 0.15s ease;
}
@supports (backdrop-filter: blur(20px)) or (-webkit-backdrop-filter: blur(20px)) {
  .v2-tile-popup {
    background: hsla(var(--community-hue, 210), 18%, 12%, 0.72);
    backdrop-filter: blur(20px) saturate(1.2);
    -webkit-backdrop-filter: blur(20px) saturate(1.2);
  }
}

@keyframes v2-pop-in {
  from { opacity: 0; transform: translateY(-4px) scale(0.95); }
  to   { opacity: 1; transform: translateY(0)    scale(1); }
}

/* ─── Choreography helpers ─────────────────────────────── */

/* Cards start hidden and pop in one at a time. Initial grid + every
 * subsequent batch (scroll, MORE) staggers reveals via JS adding the
 * `.v2-popped` class. Without the class, a card stays invisible.
 *
 * Scoped filter override: only neutralize the inherited ohpan.css
 * reveal-blur on *popped* cards. Leaving filter: none on ALL
 * .view-card would cancel any future filter-based effect (glass
 * shimmer, etc.). Scoping to .v2-popped keeps the door open. */
body.feed-v2 .view-card.v2-popped {
  filter: none;
}

body.feed-v2 .view-card:not(.v2-popped) {
  opacity: 0;
  transform: scale(0.96);
  filter: none;
}

body.feed-v2 .view-card.v2-popped {
  animation: v2-pop-in 220ms cubic-bezier(0.2, 1.1, 0.4, 1) forwards;
}

/* Tweak pads use a keyframe that includes the parallax translate,
 * and NO `forwards` fill-mode — so when the animation ends the
 * element releases to its declared transform (our live parallax
 * expression), and further scroll updates the translate in real
 * time. The default v2-pop-in above latches `scale(1)` via forwards
 * which would override the declared translateY. */
@keyframes v2-pad-pop-in {
  0%   { opacity: 0; transform: translateY(calc(var(--feed-scroll, 0px) * 0.01)) scale(0.96); }
  60%  { opacity: 1; transform: translateY(calc(var(--feed-scroll, 0px) * 0.01)) scale(1.015); }
  100% { opacity: 1; transform: translateY(calc(var(--feed-scroll, 0px) * 0.01)) scale(1); }
}
body.feed-v2 .v2-tweak-pad.v2-popped {
  animation: v2-pad-pop-in 220ms cubic-bezier(0.2, 1.1, 0.4, 1);
}

@keyframes v2-pop-in {
  0%   { opacity: 0; transform: scale(0.96); }
  60%  { opacity: 1; transform: scale(1.015); }
  100% { opacity: 1; transform: scale(1); }
}

/* 100ms fade on remove — matches the 100ms settings JS-side. */
body.feed-v2 .view-card.v2-entering {
  animation: v2-enter 100ms ease forwards;
}

@keyframes v2-enter {
  from { opacity: 0; }
  to   { opacity: 1; }
}

body.feed-v2 .view-card.v2-fading-out {
  animation: v2-exit 100ms ease forwards;
}

@keyframes v2-exit {
  from { opacity: 1; }
  to   { opacity: 0; }
}

.view-card.v2-flash {
  animation: v2-flash-in 0.9s ease;
}

/* Newly-added highlight — green halo for ~5 s on cards added by a
 * tweak/reaction MORE click or by the post-decay single-card
 * backfill. Implemented via filter: drop-shadow so the card's
 * own box-shadow (the lensed hairline) survives underneath.
 * Held strong for the first 80% of the duration, faded over
 * the last 20%. */
body.feed-v2 .view-card.v2-newly-added {
  animation: v2-newly-added 5s cubic-bezier(0.4, 0, 0.4, 1) forwards;
}

@keyframes v2-newly-added {
  0%, 80% {
    filter:
      drop-shadow(0 0 0 rgba(110, 220, 130, 0.85))
      drop-shadow(0 0 14px rgba(110, 220, 130, 0.55));
  }
  100% {
    filter:
      drop-shadow(0 0 0 rgba(110, 220, 130, 0))
      drop-shadow(0 0 0 rgba(110, 220, 130, 0));
  }
}

/* Ghost cell — invisible grid slot left behind by a column-up
 * removal. Matches card radius so the grid track stays consistent
 * while the ghost waits for backfill. */
body.feed-v2 .v2-ghost-cell {
  aspect-ratio: 11 / 10;
  visibility: hidden;
  pointer-events: none;
  border-radius: var(--v2-radius);
}

@media (max-width: 720px) {
  body.feed-v2 .v2-ghost-cell {
    border-radius: var(--v2-radius-mobile);
  }
}

/* Origin "stretch" on MORE — a brief scale pulse so new cards feel
 * like they popped out of the card the user reacted to. */
body.feed-v2 .view-card.v2-expanding {
  animation: v2-expand-pulse 700ms cubic-bezier(0.34, 1.4, 0.64, 1);
  z-index: 2;
}

@keyframes v2-expand-pulse {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.08); box-shadow: 0 6px 20px rgba(120, 200, 120, 0.25); }
  100% { transform: scale(1); }
}

/* LESS collapse — scale-to-zero + fade before removal. FLIP reflows
 * neighbors into the vacated grid cell. */
body.feed-v2 .view-card.v2-collapsing {
  animation: v2-collapse 260ms cubic-bezier(0.5, 0, 0.75, 0) forwards;
  pointer-events: none;
}

@keyframes v2-collapse {
  0%   { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.1); }
}

/* Shape primitives — work on tweak pads AND content cards. "wide" =
 * 2 cols, "tall" = 2 rows, "big" = 2×2. aspect-ratio: auto so the
 * cell sizes to its grid track rather than forcing 11/10.
 *
 * Per-content-type usage:
 *   data-content-type="quote"   → v2-shape-tall  (portrait pull-quote)
 *   data-content-type="gallery" → v2-shape-big   (2×2 image grid)
 *   data-content-type="video"   → v2-shape-wide  (landscape thumb)
 * Tweak pads continue to opt into shapes via the same classes. */
body.feed-v2 .v2-shape-wide { grid-column: span 2; aspect-ratio: auto; }
body.feed-v2 .v2-shape-tall { grid-row: span 2; aspect-ratio: auto; }
body.feed-v2 .v2-shape-big  { grid-column: span 2; grid-row: span 2; aspect-ratio: auto; }

/* Mobile: two-column phones can't afford shape variants — collapse
 * every shape class back to a 1×1 card cell. */
@media (max-width: 720px) {
  body.feed-v2 :is(.v2-shape-wide, .v2-shape-tall, .v2-shape-big) {
    grid-column: auto;
    grid-row: auto;
    aspect-ratio: 11 / 10;
  }
}

/* Tweak variants: solid tinted panels (no blur / no hairline).
 * They read as background patches of the page at the relevant
 * accent hue, not as cards. Tint alphas are chosen so they sit
 * about one step darker than the page without looking dimmed. */
body.feed-v2 .v2-tweak-pad.v2-tweak-source {
  background:
    linear-gradient(135deg, rgba(240, 190, 90, 0.22), rgba(220, 130, 90, 0.22)),
    hsla(36, 45%, 14%, 0.82);
}

body.feed-v2 .v2-tweak-pad.v2-tweak-more {
  background:
    linear-gradient(135deg, rgba(70, 170, 90, 0.32), rgba(100, 200, 120, 0.22)),
    hsla(135, 40%, 12%, 0.85);
}

body.feed-v2 .v2-tweak-pad.v2-tweak-less {
  background:
    linear-gradient(135deg, rgba(210, 70, 85, 0.30), rgba(200, 90, 90, 0.22)),
    hsla(355, 45%, 12%, 0.85);
}

body.feed-v2 .v2-tweak-pad.v2-tweak-more .v2-pad-title,
body.feed-v2 .v2-tweak-pad.v2-tweak-less .v2-pad-title {
  font-size: 0.80rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #fff;
  opacity: 0.95;
  margin-bottom: 4px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}

body.feed-v2 .v2-tweak-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 6px;
  flex: 1;
  min-height: 0;
}

body.feed-v2 .v2-tweak-choice {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 4px 3px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.10);
  border-radius: 10px;
  color: #fff;
  cursor: pointer;
  min-height: 0;
  overflow: hidden;
  transition: background 0.15s, transform 0.1s;
  font: inherit;
}

body.feed-v2 .v2-tweak-choice:hover {
  background: rgba(255, 255, 255, 0.20);
}

body.feed-v2 .v2-tweak-choice:active {
  transform: scale(0.95);
}

body.feed-v2 .v2-tweak-choice:disabled {
  opacity: 0.4;
  cursor: wait;
}

body.feed-v2 .v2-tweak-choice.v2-tweak-choice-pending {
  opacity: 1;
  background: rgba(255, 255, 255, 0.30);
  animation: v2-choice-pending 0.7s ease;
}

@keyframes v2-choice-pending {
  0%   { background: rgba(255, 255, 255, 0.30); }
  50%  { background: rgba(255, 255, 255, 0.55); }
  100% { background: rgba(255, 255, 255, 0.18); }
}

body.feed-v2 .v2-tweak-choice-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
}

body.feed-v2 .v2-tweak-choice-icon .v2-topic-icon {
  width: 100%;
  height: 100%;
  filter: brightness(1.15);
}

body.feed-v2 .v2-tweak-choice-label {
  font-size: 0.60rem;
  font-weight: 500;
  text-align: center;
  line-height: 1.1;
  letter-spacing: -0.01em;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  max-width: 100%;
}

/* When a MORE/LESS pad is rendered as v2-shape-wide (2×1), lay the
 * four choices across a single row instead of 2×2. */
body.feed-v2 .v2-tweak-pad.v2-shape-wide .v2-tweak-grid {
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: 1fr;
}

/* Quiz tweak — spans 2×2 so the question reads like a small poster.
 * Cool blue/violet gel — intentionally different hue family from
 * MORE/LESS so the user reads it as a different *kind* of interaction
 * (one-shot A/B question vs. durable preference surface). */
body.feed-v2 .v2-tweak-pad.v2-tweak-quiz {
  background:
    linear-gradient(135deg, rgba(120, 160, 220, 0.24), rgba(180, 130, 220, 0.24)),
    hsla(230, 30%, 14%, 0.85);
  padding: 16px;
  justify-content: space-between;
}

body.feed-v2 .v2-tweak-pad.v2-tweak-quiz .v2-quiz-q {
  font-size: 1.1em;
  font-weight: 500;
  line-height: 1.3;
  color: var(--text-primary);
}

body.feed-v2 .v2-tweak-pad.v2-tweak-quiz .v2-quiz-choices {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

/* ─── Inline notice (v2-notice) ──────────────────────────────── */

body.feed-v2 .v2-notice {
  position: relative;
  grid-column: span 2;
  aspect-ratio: auto;
  padding: 14px 16px 16px;
  border-radius: var(--v2-radius);
  /* Notices read as chrome: same glass material + hairline as a card,
   * tinted cool to differentiate from content, no dashed border. The
   * end-of-feed variant keeps a dotted border as the one exception —
   * it's the only surface that's explicitly *system chrome*. */
  border: none;
  background: linear-gradient(135deg, rgba(120, 170, 210, 0.16), rgba(170, 120, 200, 0.16));
  box-shadow: var(--v2-glass-shadow);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  color: var(--text-primary, #eee);
  overflow: visible;
  min-height: 120px;
}
@supports (backdrop-filter: blur(20px)) or (-webkit-backdrop-filter: blur(20px)) {
  body.feed-v2 .v2-notice {
    background: linear-gradient(135deg, rgba(120, 170, 210, 0.14), rgba(170, 120, 200, 0.14)),
                hsla(210, 25%, 14%, 0.45);
    backdrop-filter: blur(20px) saturate(1.2);
    -webkit-backdrop-filter: blur(20px) saturate(1.2);
  }
}

body.feed-v2 .v2-notice-title {
  font-family: var(--font-display, inherit);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}

body.feed-v2 .v2-notice-body {
  font-size: 0.85rem;
  opacity: 0.85;
  line-height: 1.35;
}

body.feed-v2 .v2-notice-cta {
  align-self: flex-start;
  background: var(--text-primary, #fff);
  color: var(--bg-card, #111);
  border: none;
  padding: 6px 14px;
  border-radius: 999px;
  font-weight: 600;
  font-size: 0.85rem;
  cursor: pointer;
  margin-top: 4px;
  transition: opacity 0.15s;
}

body.feed-v2 .v2-notice-cta:hover { opacity: 0.85; }

body.feed-v2 .v2-notice-close {
  position: absolute;
  top: 6px;
  right: 10px;
  background: transparent;
  color: inherit;
  border: none;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.5;
  padding: 4px 6px;
  border-radius: 50%;
}

body.feed-v2 .v2-notice-close:hover { opacity: 1; }

/* End-of-feed variant — calmer, no CTA, slightly smaller. Keeps the
 * dotted hairline as a one-off chrome signal ("this is not content"). */
body.feed-v2 .v2-notice.v2-notice-end {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
  border: 1px dotted rgba(255, 255, 255, 0.18);
  opacity: 0.7;
  text-align: center;
  align-items: center;
  padding: 20px;
}

@keyframes v2-flash-in {
  0%   { box-shadow: 0 0 0 0 rgba(255, 220, 120, 0.65); }
  50%  { box-shadow: 0 0 24px 4px rgba(255, 220, 120, 0.35); }
  100% { box-shadow: 0 0 0 0 rgba(255, 220, 120, 0); }
}

