/* HYPERFIC — component styles
   Точная фиксация текущего дизайна. Desktop-first, mobile-переопределения ниже.
   Breakpoint: --hf-bp = 900px (desktop ≥ 900, mobile < 900).
   Hero-блок: см. секцию «HERO» — там же место под SVG-морф. */

/* ============================================================
   BASE
   ============================================================ */
* { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--hf-coal-canvas);
}

body {
  font-family: var(--hf-font-ui);
  color: var(--hf-ivory);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--hf-steel); text-decoration: none; }
a:hover { color: var(--hf-ivory); }

/* ============================================================
   SCREEN / ФОН
   Порядок слоёв: gradient → peripheral stitches → grain → контент
   ============================================================ */
.hf-screen {
  position: relative;
  overflow: hidden;
  min-height: 100vh;
  min-height: 100dvh; /* tracks the real visible viewport on iOS/Telegram, no jump on toolbar collapse */
  background: var(--hf-coal);
  color: var(--hf-ivory);
}

/* В продакшне — просто фон страницы. В артборд-режиме фиксируется 1440×900. */
.hf-screen--artboard {
  width: 1440px;
  height: 900px;
  min-height: 0;
  border: var(--hf-border-artboard);
}

.hf-bg,
.hf-grain,
.hf-stitch-field {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.hf-bg { background: var(--hf-bg-gradient); }

.hf-grain {
  opacity: 0.55;
  background-image: var(--hf-grain-desktop);
}

/* Разрежённые стежковые × по периферии. Статичные, разной заметности. */
.hf-stitch-field {
  font-family: var(--hf-font-stitch);
  color: var(--hf-steel-stitch);
}
.hf-stitch-field > span {
  position: absolute;
  line-height: 1;
}

/* ============================================================
   HEADER
   ============================================================ */
/* Soft legibility wash behind HYPERFIC/DRAMIONE now that the header is a
   fixed layer a scrolling result can pass under (see the header-unify
   commit) — content fades out before it reaches the wordmark instead of
   reading through it. Not a panel: no solid fill, no blur, no
   border/shadow, and no change to .hf-header's own geometry/padding —
   this is a separate, non-interactive sibling painted immediately before
   .hf-header in the markup, so it always sits behind the header's own
   text (same z-index, DOM order decides paint order) while still staying
   above .hf-layout's scrolling content (z-index 1) and the decorative
   bg/grain/stitch layers (z-index auto). --hf-coal-top is the exact top
   stop of --hf-bg-gradient — using it (not --hf-coal) is what makes the
   fade's own top edge invisible against the screen's real background
   instead of reading as a step. Height is intentionally taller than the
   header's own rendered height (99px desktop / 46px mobile) so the fade
   has room to actually reach transparent before the header's content,
   rather than cutting off right at its edge. Only rendered when
   state.view === "result" (see app.js) — home's hero and the short,
   never-scrolling empty state don't have the under-header overlap
   problem this solves, so they get no fade at all. */
.hf-header-fade {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 2;
  height: 160px;
  pointer-events: none;
  background: linear-gradient(to bottom, var(--hf-coal-top) 0%, transparent 100%);
  /* Desktop-only in practice (see .is-panel-open below), but harmless to
     declare unconditionally — same duration/easing as the header's own
     padding-right and .hf-layout__panel's flex-basis, so the fade's
     right edge tracks the drawer opening/closing in lockstep instead of
     snapping to its final width. */
  transition: right 360ms var(--hf-ease-sheet);
}
/* Keeps the fade clear of the desktop drawer column, geometrically and
   visually — end at the drawer's own left edge (524px, the same width
   as .hf-drawer/.hf-layout__panel's open flex-basis) instead of
   spanning the full screen width and dimming the panel's opaque
   background from above. Desktop only: on mobile the drawer is a
   position:fixed bottom sheet at z-index:4, already above the fade's
   z-index:2 with its own opaque background, so it already fully covers
   the fade beneath it with no extra rule needed. */
@media (min-width: 900px) {
  .hf-header-fade.is-panel-open { right: 524px; }
}

.hf-header {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 36px 44px;
  /* Full-width transparent shell — must not steal clicks from whatever
     sits underneath it (e.g. the drawer's ✕, top-right of the panel,
     was unreachable by real pointer events because this box, despite
     being invisible there, was still on top and intercepting hits). */
  pointer-events: none;
  /* Desktop-only: when the filter panel is open, DRAMIONE (the header's
     own right-aligned flex child, see .hf-stage__fandom below) must stay
     clear of the drawer's 524px column instead of sitting on top of it —
     see .hf-header.is-panel-open in the LAYOUT section. Transition lives
     here (not just on the modifier) so both the open *and* close edges
     animate; scoped to desktop only in that same rule, so this base
     transition is harmless (never triggered) on mobile. */
  transition: padding-right 360ms var(--hf-ease-sheet);
}
.hf-header > * {
  pointer-events: auto;
}

.hf-wordmark {
  display: flex;
  align-items: center;
  /* ×1.6 of the original 17px — was reading as a small service label next
     to DRAMIONE rather than the brand mark. Header padding/position is
     untouched; this only makes the flex item itself taller, and
     align-items:center on .hf-header keeps it vertically centered against
     DRAMIONE, which stays at its own unchanged size. */
  height: 27px;
}
.hf-wordmark:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 4px;
  border-radius: 2px;
}
/* Final MVP wordmark asset (hyperfic-logo.svg) — colors/geometry/the
   stitch-I are baked into the file itself, never overridden here. Only
   height is set; width is intentionally omitted so the browser scales
   the image proportionally from its own viewBox aspect ratio instead of
   stretching either axis. */
.hf-wordmark__logo {
  display: block;
  height: 27px;
  width: auto;
}
/* The asset is a plain <img>, so its baked-in #FFF4E8 can't read the page's
   tokens. In a themed pairing the same file is used as a mask (letters and the
   stitch-I are one colour, so the silhouette is the whole logo) over the
   pairing's own ivory token — the one the hero and typography already use —
   and the img's own pixels are pushed out of its box. Size, position and
   geometry are untouched; the default pairing (no [data-pairing]) keeps the
   file's original colours exactly. */
[data-pairing] .hf-wordmark__logo {
  background-color: var(--hf-ivory);
  -webkit-mask: url("/hyperfic-logo.svg") center / contain no-repeat;
  mask: url("/hyperfic-logo.svg") center / contain no-repeat;
  object-fit: none;
  object-position: -9999px 0;
}

/* Groups the wordmark + burger together as one left-side flex item, so
   .hf-header's own justify-content:space-between still just balances two
   things (this group vs. DRAMIONE) — the burger sits directly next to the
   logo instead of splitting the header into three unevenly-spaced parts. */
.hf-header__left {
  display: flex;
  align-items: center;
  gap: 22px;
}

/* Deliberately smaller than the wordmark it sits beside (see
   .hf-header__left) and never boxed — no background/border, matching
   every other bare icon-glyph trigger in the app (see filterSection.js's
   renderStitchGlyph usages). Height still matches the wordmark's own 27px
   line so the two stay vertically centered against each other without
   nudging .hf-header's own height. */
.hf-burger {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 27px;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
}
.hf-burger:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
}

/* Closed: 3 short horizontal bars (classic hamburger). Open: the same 3
   bars rotate/fade into a thin X, in place — same box (14px), same color,
   same position, so toggling never resizes the button or shifts
   .hf-wordmark next to it. */
.hf-burger__bars {
  position: relative;
  display: block;
  width: 14px;
  height: 10px;
}
.hf-burger__bars span {
  position: absolute;
  left: 0;
  width: 14px;
  height: 1px;
  background: var(--hf-ivory-76);
  transition: transform 200ms var(--hf-ease-out), opacity 150ms ease, top 200ms var(--hf-ease-out);
}
.hf-burger__bars span:nth-child(1) { top: 0; }
.hf-burger__bars span:nth-child(2) { top: 4.5px; }
.hf-burger__bars span:nth-child(3) { top: 9px; }
.hf-burger.is-open .hf-burger__bars span:nth-child(1) { top: 4.5px; transform: rotate(45deg); }
.hf-burger.is-open .hf-burger__bars span:nth-child(2) { opacity: 0; }
.hf-burger.is-open .hf-burger__bars span:nth-child(3) { top: 4.5px; transform: rotate(-45deg); }
@media (prefers-reduced-motion: reduce) {
  .hf-burger__bars span { transition: none; }
}

/* Static label, not a control — MVP has nothing for it to open. */
.hf-fandom {
  display: flex;
  align-items: center;
  font-size: 12px;
  letter-spacing: 0.18em;
  color: var(--hf-ivory-55);
}
/* Very deliberately the only default-state hover in the app that isn't
   plain ivory — a barely-there hint of the hidden magic/Dramione layer. */
.hf-fandom:hover { color: var(--accent-magic); }

/* DRAMIONE (.hf-fandom.hf-stage__fandom) is now a plain flex child of
   .hf-header — justify-content:space-between puts it at the header's
   right edge, .hf-wordmark at the left. align-self:flex-start overrides
   .hf-header's own align-items:center: without it, the row centers
   DRAMIONE's own shorter line-box against .hf-wordmark's explicit 27px
   height and it lands ~6px lower than before — flex-start instead pins
   it to the row's own top (== .hf-header's padding-top), which is
   exactly where the old hardcoded `top: 36px` used to put it, so the
   pixel position is unchanged even though the mechanism is now flex, not
   absolute. The .hf-stage__fandom class name is kept in the markup
   purely to keep this diff minimal, even though it no longer lives in
   .hf-stage. Living in the header (which never scrolls) rather than
   .hf-stage (which scrolls its own overflow) is what makes it move as
   one unit with HYPERFIC instead of scrolling away independently — see
   .hf-header.is-panel-open below for how it still stays clear of the
   drawer on desktop. */
.hf-stage__fandom {
  align-self: flex-start;
}

/* ============================================================
   LAYOUT — desktop: two-column flex row (stage | filter panel).
   The panel column animates its flex-basis (0 ↔ 524px) instead of
   translateX; the stage is flex:1, so it re-centers on its own as
   the panel grows/shrinks — no JS-computed offsets needed.
   Mobile disables this entirely (see media query): stage goes back
   to absolute inset:0 and the drawer becomes a fixed bottom sheet.
   ============================================================ */
.hf-layout {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
}

/* Mirrors .hf-layout__panel's flex-basis (0 ↔ 524px) as a right inset on
   the header instead — desktop only (the mobile drawer is a fixed-overlay
   bottom sheet, see the @media block, so it never claims horizontal
   space from the header there). Keeps DRAMIONE clear of the open drawer
   the same way it always was when it lived inside .hf-stage (whose own
   width used to shrink for the same reason) — just expressed on the
   header now that DRAMIONE lives there instead. */
@media (min-width: 900px) {
  .hf-header.is-panel-open { padding-right: 568px; }
}

/* ============================================================
   CENTER STAGE — главная функция всегда в центре (своей колонки)
   ============================================================ */
/* flex-direction:column + margin:auto on the actual content (below),
   rather than align-items/justify-content:center on this container —
   centering via align-items/justify-content on an oversized flex item
   overflows the container equally top AND bottom, and the top half of
   that overflow is unreachable/clipped once overflow-y:auto is added
   (the classic flexbox-centering-plus-scroll bug). Auto margins don't
   have that problem: they centre only while there's room, and collapse
   to 0 the moment content is taller than the box, so a long result
   simply starts at the top and everything below it stays reachable by
   scrolling this container. */
.hf-stage {
  flex: 1 1 auto;
  min-width: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow-y: auto;
  overflow-x: hidden;
  /* Top/bottom padding is symmetric on purpose: with the margin:auto
     centering below, equal top/bottom padding shrinks the centering
     canvas equally on both sides, so it does NOT shift where short
     content ends up centered (verified: offset-from-viewport-top stays
     identical regardless of the padding value, only symmetric ones
     preserve that). Its real job is the overflow case — it's the "safe
     top padding" a long result scrolls up against instead of running
     under the fixed header, and matching bottom space so the last
     action isn't flush against the viewport edge either. */
  padding: 72px 24px;
}
.hf-stage > .hf-home,
.hf-stage > .hf-result,
.hf-stage > .hf-contacts {
  margin-top: auto;
  margin-bottom: auto;
}

/* Filter panel column — width is the only thing that animates. */
.hf-layout__panel {
  flex: 0 0 0px;
  overflow: hidden;
  display: flex;
  justify-content: flex-end;
  transition: flex-basis 360ms var(--hf-ease-sheet);
}
.hf-layout.is-panel-open .hf-layout__panel {
  flex-basis: 524px;
}

/* ============================================================
   HERO  ← место cross-stitch объекта и будущей morph-анимации
   ------------------------------------------------------------
   .hf-hero__art — фиксированная по высоте область (190px desktop /
   140px mobile). Высоту НЕ менять: она держит вертикальный ритм,
   иначе контент ниже поедет. Внутрь монтируется inline <svg> с
   отдельными <path class="stitch"> — по одному на каждый крестик.
   Сейчас область пустая (ассет будет поставлен отдельно).
   ============================================================ */
.hf-home {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 760px;
  max-width: 100%;
}

.hf-hero__art {
  height: 190px;                /* ← зарезервированная высота hero-объекта */
  display: flex;
  align-items: center;
  justify-content: center;
}
.hf-hero__art > svg,
.hf-hero__art > img {
  display: block;
  height: 200px;                /* ассет крупнее области и центрируется по ней */
  width: auto;
  max-width: 100%;
}

.hf-h1 {
  margin: 22px 0 0;
  font-family: var(--hf-font-display);
  font-weight: 700;
  font-size: 66px;
  line-height: 1;
  letter-spacing: -0.035em;
  text-align: center;
  color: var(--hf-ivory-strong);
}

.hf-sub {
  margin: 26px 0 0;
  max-width: 350px;
  font-size: 19px;
  line-height: 1.6;
  color: var(--hf-ivory-50);
  text-align: center;
}

/* /contacts/ — same container width/typography as .hf-home (.hf-h1 is
   reused verbatim, see components/contactsView.js). One centered editorial
   block (heading, two short paragraphs, CTA, quiet email line) — never a
   left-aligned list under a centered heading. Not a form, not a catalog —
   deliberately the smallest possible page. */
.hf-contacts {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 460px;
  max-width: 100%;
}
/* Heading and all three text blocks (the owl aside + both paragraphs)
   deliberately reuse .hf-result__title/.hf-result__desc verbatim — the
   exact same responsive typography (font-size/line-height/weight/letter-
   spacing/color) as a fic permalink's title/description, not a hand-copied
   approximation (see components/contactsView.js). The owl line is not a
   separate caption style; it's the same .hf-result__desc, differentiated
   from the two paragraphs below it only by spacing — hierarchy is
   spacing-only, per spec.

   Root cause of the earlier oversized heading→body gap (found via computed
   getBoundingClientRect, not guessed): .hf-contacts is display:flex, so its
   direct children's margins ADD instead of collapsing like normal block
   siblings — and .hf-result__title, designed for resultView.js's own <div>
   wrapper (zero default margin), silently picked up the browser's default
   <h1> UA margin (~0.67em, ~48px at 72px font-size) once reused on a real
   <h1> here. That 48px was adding on top of every spacing rule below it,
   not any single margin-top value being "too large" on its own — fixed at
   the source (margin:0 on .hf-result__title itself, harmless for the fic
   card's <div> usage, which already had no margin).

   Every gap below is deliberately set directly on the element that starts
   it, once, as a flex-item margin-top (added, not collapsed, against the
   previous flex sibling) — no reliance on ambiguous collapse-through
   between .hf-contacts__body and its first child. (.hf-result__title's own
   margin:0 lives on its base rule below, not here — see that rule's
   comment for why it's needed at all.) */
.hf-contacts__body { margin-top: 30px; }            /* heading → owl line (desktop) */
.hf-contacts__body p:first-child { margin-top: 0; } /* owl line: gap above it is .hf-contacts__body's own margin-top, not this */
.hf-contacts__body p:nth-child(2) { margin-top: 26px; } /* owl line → paragraph 1 (desktop) */
.hf-contacts__body p:nth-child(3) { margin-top: 18px; } /* paragraph 1 → paragraph 2 */
.hf-contacts .hf-btn--hero { margin-top: 30px; }     /* paragraph 2 → CTA */
.hf-contacts__email {
  display: block;
  margin-top: 16px;                                  /* CTA → email */
  font-size: 13px;
  color: var(--text-muted);
  text-decoration: none;
}
.hf-contacts__email:hover { color: var(--text-meta); }
.hf-contacts__email:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 4px;
}

/* ============================================================
   BUTTONS
   Primary — спокойный gunmetal, pill. Металл статичен.
   Один короткий highlight на hover будет добавлен позже в коде:
   точка расширения — .hf-btn::after (сейчас выключена).
   ============================================================ */
.hf-btn {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  height: 62px;
  padding: 0 28px;
  border: var(--hf-border-btn);
  border-radius: var(--hf-radius-pill);
  background: var(--hf-btn-surface);
  box-shadow: var(--hf-shadow-cta-desktop);
  font-family: inherit;
  font-size: 17px;
  font-weight: 500;
  color: var(--text-primary);
  cursor: pointer;
  transition: border-color 160ms ease;
}
.hf-btn__arrow { color: var(--text-primary); }
.hf-btn:hover  { border-color: rgba(201,204,209,0.34); }
.hf-btn:active { transform: translateY(1px); }
/* Outline auto-follows the pill's own border-radius — no separate
   radius needed here. Same steel outline token as the rest of the app's
   focus states (.hf-textlink, .hf-result__warnings-toggle, etc). */
.hf-btn:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
}
.hf-btn:disabled,
.hf-btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: default;
  transform: none;
  border-color: rgba(201,204,209,0.12);
}
/* Зарезервировано под one-shot metallic highlight (не включать сейчас) */
.hf-btn::after { content: none; }

.hf-btn--hero   { margin-top: 34px; width: 322px; padding: 0; justify-content: center; }
.hf-btn--hero .hf-btn__arrow {
  position: absolute;
  right: 24px;
  top: 50%;
  transform: translateY(-50%);
}
.hf-btn--read   { width: 212px; height: 58px; font-size: 16px; box-shadow: none; }
.hf-btn--sticky { width: 100%; padding: 0; box-shadow: none; border: var(--hf-border-btn-cta); }

/* "Слушать" — secondary to "Читать": same pill/height/font family, but an
   outline instead of the primary's filled gunmetal surface, and dimmer
   ivory-76 text (same resting color as .hf-action's secondary actions)
   so the reading CTA stays the visually dominant one. Width sizes to
   content rather than the primary's fixed 212px — "Слушать" is a short
   label and forcing it to match would just add empty pill on both sides. */
.hf-btn--listen {
  width: auto;
  height: 58px;
  padding: 0 24px;
  gap: 9px;
  font-size: 15px;
  color: var(--hf-ivory-76);
  background: transparent;
  border: var(--hf-border-btn-cta);
  box-shadow: none;
}
.hf-btn--listen:hover { color: var(--text-primary); border-color: rgba(201,204,209,0.34); }
.hf-btn__play { flex-shrink: 0; }

/* Secondary — текстовое действие, не конкурирует с primary.
   No underline in any state; text is the same brightness as primary CTA
   text, just smaller and unweighted so it stays clearly secondary. */
.hf-textlink {
  margin-top: 28px;
  padding: 0;
  background: none;
  border: 0;
  font-family: inherit;
  font-size: 15px;
  font-weight: 400;
  color: var(--hf-ivory-strong);
  text-decoration: none;
  cursor: pointer;
}
.hf-textlink:hover {
  opacity: 0.8;
}
.hf-textlink:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 4px;
  border-radius: 2px;
}
.hf-textlink--star {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

/* Hierarchy here comes from component form, not color: a large filled pill
   (Читать) reads as primary, lightweight text (Давай другой / Изменить
   подбор) reads as secondary — both stay in the same ivory family as every
   other clickable action in the app. Steel is for metadata/tags/inactive
   controls/decoration, never for actionable text. Hover is a restrained
   brightness step within ivory, not a family change. Padding + matching
   negative margin enlarges the actual clickable box without changing the
   button's visual footprint or nudging its neighbor in the flex row. */
.hf-action {
  display: flex;
  align-items: center;
  gap: 8px;
  background: none;
  border: 0;
  padding: 8px 6px;
  margin: -8px -6px;
  font-family: inherit;
  font-size: 15px;
  color: var(--hf-ivory-76);
  cursor: pointer;
  transition: color 140ms ease;
}
.hf-action:hover { color: var(--text-primary); }
.hf-action .hf-glyph { flex-shrink: 0; }
.hf-action:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ============================================================
   DRAWER (desktop) — persistent sidebar column, not an overlay.
   No scrim: the stage stays fully visible and interactive next to it.
   ============================================================ */
.hf-scrim {
  display: none;
}

.hf-drawer {
  position: relative;
  flex: 0 0 524px;
  width: 524px;
  display: flex;
  flex-direction: column;
  background: var(--hf-graphite-panel);
  border-left: var(--hf-border-panel);
  box-shadow: var(--hf-shadow-drawer);
}
.hf-drawer__top {
  display: flex;
  justify-content: flex-end;
  padding: 26px 34px 0;
}
.hf-drawer__close {
  background: none; border: 0; padding: 0;
  font-size: 19px;
  line-height: 1;
  color: var(--text-meta);
  cursor: pointer;
}
.hf-drawer__close:hover { color: var(--text-primary); }
.hf-drawer__close:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
  border-radius: 4px;
}

.hf-drawer__scroll {
  flex: 1;
  overflow-y: auto;
  padding: 16px 46px 26px;
}
.hf-drawer__foot {
  padding: 18px 46px 30px;
  border-top: var(--hf-border-hairline);
  background: var(--hf-graphite-panel);
}

.hf-h3 {
  margin: 0;
  font-family: var(--hf-font-display);
  font-weight: 700;
  font-size: 36px;
  line-height: 1.12;
  letter-spacing: -0.03em;
  color: var(--text-primary);
}
.hf-drawer__sub {
  margin: 0;
  font-size: 16px;
  line-height: 1.55;
  color: var(--text-muted);
}

.hf-drawer__subrow {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin-top: 14px;
}
.hf-reset-link {
  flex-shrink: 0;
  margin-top: 2px;
  background: none;
  border: 0;
  padding: 0;
  font-family: inherit;
  font-size: 13px;
  color: var(--text-meta);
  text-decoration: underline;
  text-decoration-color: rgba(163, 171, 179, 0.3);
  cursor: pointer;
  animation: hfFade 200ms ease both;
}
.hf-reset-link:hover { color: var(--stitch-bright); }
.hf-reset-link:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ============================================================
   FILTER GROUPS
   ============================================================ */
.hf-group { margin-top: 30px; }
.hf-group__head {
  display: flex;
  align-items: center;
  gap: 11px;
}
.hf-group__title {
  font-size: 17px;
  font-weight: 500;
  color: var(--text-primary);
}

/* Level-1 (micro) easter egg — reserved-height slot so it never shifts
   layout when it appears/disappears. */
/* Hyperfic's one shared "reaction" voice — micro quips (next to a filter
   question), combo quips (above the sticky CTA) and the result screen's
   match-reason are the same character speaking, not three separate content
   types: same family/style/weight/color, close size/line-height. A reader
   should recognize the voice by typography, not just by the green. Layout
   (position, max-width, animation, text-align) stays per-instance below —
   only the voice itself is shared. Any future reactive text should join
   this selector list rather than inventing its own treatment. */
.hf-micro-quip,
.hf-combo-quip,
.hf-result__why {
  font-family: var(--hf-font-ui);
  font-style: normal;
  font-weight: 400;
  letter-spacing: 0.01em;
  color: var(--accent-magic);
}

/* TEMPORARY (local Tomione prototype): the hero stitch art's stroke is the
   pairing's primary foreground. The figure/animation itself is untouched —
   only the stroke color, which CSS overrides over the SVG presentation
   attribute. Dramione has no [data-pairing], so it is unaffected. */
[data-pairing="tomione"] .hf-hero__art svg { stroke: var(--hf-ivory); }

.hf-micro-quip-slot {
  height: 20px;
  margin-top: 8px;
}
.hf-micro-quip {
  display: inline-block;
  font-size: 13px;
  animation: hfMicroQuipIn 260ms ease both;
}

/* Level-2 (combo) easter egg — above the sticky CTA, replaces itself. */
.hf-combo-quip {
  margin: 0 0 10px;
  font-size: 13.5px;
  text-align: center;
  animation: hfFade 260ms ease both;
}
/* The line is always in the DOM so the footer never changes height: when no
   combo applies it holds the same single line, just invisible. */
.hf-combo-quip--empty { visibility: hidden; }
/* Tomione, desktop only: the single reserved combo line sat 18px under the
   footer's hairline but only 10px above the CTA, so it read as pressed against
   the button. Move 4px from the footer's top padding to the line's bottom margin
   (18+16+10 = 14+16+14) — the line is now centred between hairline and CTA and
   the footer's height and the CTA's position are exactly unchanged. Mobile and
   Dramione keep the shared spacing. */
@media (min-width: 900px) {
  [data-pairing="tomione"] .hf-drawer__foot { padding-top: 14px; }
  [data-pairing="tomione"] .hf-combo-quip { margin-bottom: 14px; }
}

/* ---------- STITCH SLIDER ----------
   Track height is the real clickable/drag hit area (≥40px) — the 1px rail
   and small × ticks are purely visual and stay centered inside it. Ticks
   and labels share one repeat(N, 1fr) grid (template set inline per
   instance, see stitchSlider.js) so a tick and its label always land in
   the same column, no manual per-item offsets needed. */
.hf-stitch-slider { margin-top: 22px; }
.hf-stitch-slider__track {
  position: relative;
  height: 40px;
}
.hf-stitch-slider__rail {
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 2px;
  background: var(--stitch-default);
  opacity: 0.3;
  transform: translateY(-50%);
}
/* The "already stitched" portion — soft ivory (text-body), ends exactly at
   the selected tick's own column center (same math the thumb/ticks use).
   Width is 0 in the neutral state, so the whole rail reads as uniformly
   cold/muted with nothing to suggest a value has been picked. Softer than
   full-strength ivory on purpose — this is an ordinary control, not a
   headline — the contrast with the steel remainder still carries the read. */
.hf-stitch-slider__rail-fill {
  position: absolute;
  left: 0; top: 50%;
  height: 2px;
  width: 0%;
  background: var(--text-body);
  transform: translateY(-50%);
  transition: width 180ms ease;
}
.hf-stitch-slider__ticks {
  position: absolute;
  inset: 0;
  display: grid;
  align-items: center;
}
.hf-stitch-slider__tick {
  justify-self: center;
  width: 12px;
  height: 12px;
  transform: scale(1);
  transition: transform 180ms ease;
  font-family: var(--hf-font-stitch);
}
.hf-stitch-slider__tick::before {
  content: "×";
  display: flex;
  width: 100%;
  height: 100%;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  line-height: 1;
  color: var(--stitch-default);
  opacity: 0.4;
  transition: color 180ms ease, opacity 180ms ease;
}
/* Stops already passed on the way to the current value sit on the ivory
   portion of the rail — a real color shift (steel → ivory), matching the
   rail-fill's direction but at full strength, since a small glyph needs
   more punch than a line to read clearly. Never accent-magic green: this
   is an ordinary UI state. */
.hf-stitch-slider__tick.is-filled::before {
  color: var(--text-primary);
  opacity: 1;
}
.hf-stitch-slider__tick:hover,
.hf-stitch-slider__tick.is-near {
  transform: scale(1.25);
}
/* ~19px footprint (3×3 stitch-star grid, cell size set in stitchSlider.js) —
   clearly larger than any ordinary 12px tick, so the current stop never
   gets lost among the others. */
.hf-stitch-slider__thumb {
  position: absolute;
  top: 50%;
  width: 19px;
  height: 19px;
  margin-left: -9.5px;
  transform: translateY(-50%);
  left: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  transition: left 180ms ease;
}
.hf-stitch-slider__thumb .hf-glyph { pointer-events: none; }
.hf-stitch-slider__input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  z-index: 2;
}
.hf-stitch-slider__input:focus-visible ~ .hf-stitch-slider__thumb {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
  border-radius: 999px;
}
.hf-stitch-slider__labels {
  display: grid;
  /* Tighter than before on purpose — rail + labels should read as one
     control, not a divider with a caption floating under it. */
  margin-top: 7px;
}
.hf-stitch-slider__label {
  background: none;
  border: 0;
  padding: 0;
  font-family: inherit;
  font-size: 12px;
  line-height: 1.3;
  text-align: center;
  /* Inactive options are not disabled — text-meta reads at ~5:1 against
     the panel background, well clear of the ~4.5:1 floor. Selected still
     reads unambiguously brighter via .is-active → text-primary (ivory). */
  color: var(--text-meta);
  cursor: pointer;
  transition: color 140ms ease;
}
.hf-stitch-slider__label:hover { color: var(--stitch-bright); }
.hf-stitch-slider__label.is-active { color: var(--text-primary); }
.hf-stitch-slider__label:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ---------- CHIPS (pill) ---------- */
.hf-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 18px;
}
.hf-chip {
  font-family: inherit;
  font-size: 14px;
  padding: 10px 14px;
  border-radius: var(--hf-radius-pill);
  background: var(--hf-chip-surface-off);
  border: var(--hf-border-soft);
  color: var(--text-meta);
  cursor: pointer;
  transition: border-color 140ms ease, color 140ms ease, background 140ms ease;
}
.hf-chip:hover {
  border-color: rgba(163,171,179,0.4);
  color: var(--stitch-bright);
}
.hf-chip:active { transform: translateY(1px); }
/* Outline auto-follows the pill's own border-radius. */
.hf-chip:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 2px;
}
.hf-chip[aria-pressed="true"],
.hf-chip[aria-checked="true"] {
  /* Border + text carry the selected state; the fill stays close to the
     unselected surface so a row of several selected chips (e.g. multi-pick
     Draco traits) never reads as one heavy gray block. */
  background: rgba(241, 231, 216, 0.045);
  border: var(--hf-border-chip-on);
  color: var(--text-primary);
}
.hf-chip__x {
  display: inline-block;
  margin-right: 6px;
  color: var(--hf-steel);
  opacity: 0.8;
}
.hf-chip:disabled,
.hf-chip[aria-disabled="true"] {
  opacity: 0.4;
  cursor: default;
  transform: none;
}

/* ---------- Tropes "Ещё +N" ---------- */
.hf-tropes-more {
  display: block;
  margin-top: 12px;
  background: none;
  border: 0;
  padding: 0;
  font-family: inherit;
  font-size: 13px;
  color: var(--text-meta);
  text-decoration: underline;
  text-decoration-color: rgba(163, 171, 179, 0.3);
  cursor: pointer;
}
.hf-tropes-more:hover { color: var(--stitch-bright); }
.hf-tropes-more:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ---------- «Все параметры +» ---------- */
.hf-more {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  margin-top: 34px;
  padding: 20px 0 0;
  background: none;
  border: 0;
  border-top: var(--hf-border-hairline);
  font-family: inherit;
  cursor: pointer;
}
.hf-more__label { font-size: 16px; color: var(--text-meta); }
.hf-more__sign  { font-size: 20px; line-height: 1; color: var(--text-meta); }
.hf-more:hover .hf-more__label { color: var(--stitch-bright); }
.hf-more:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
  border-radius: 4px;
}

.hf-extra {
  margin-top: 22px;
  display: flex;
  flex-direction: column;
  gap: 22px;
  animation: hfRise 320ms ease both;
}
.hf-extra__title {
  font-size: 11px;
  letter-spacing: 0.2em;
  color: var(--text-muted);
}
.hf-extra .hf-chips { margin-top: 14px; }
/* Tropes/Ending/Status inside "Для фанатов" reuse .hf-group for the
   title+glyph treatment, but .hf-extra's own flex gap already spaces them —
   an extra 30px group margin on top would double it up. */
.hf-extra .hf-group { margin-top: 0; }
/* Now the last item inside .hf-extra's own flex column — its flex `gap`
   already spaces it from ИСКЛЮЧИТЬ above, same as every other advanced
   filter block, so the checkbox's own margin-top would double it up. */
.hf-extra .hf-checkbox { margin-top: 0; }

/* ---------- Audio-only checkbox ----------
   Light, single-row constraint — deliberately not a .hf-group (no glyph,
   no title row, no section treatment) and not another chip (a real
   checkbox reads as "an extra always-on constraint", not "one more
   pickable value" among the trope/ending/status chips above it). No new
   color/radius/spacing token: box and checked-state reuse the exact chip
   tokens (--hf-border-soft/--hf-chip-surface-off off-state,
   --hf-border-chip-on + the same 0.045 ivory fill checked), so the
   checked box reads exactly as calm as a selected chip, no new accent. */
.hf-checkbox {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  margin-top: 20px;
  padding: 0;
  background: none;
  border: 0;
  font-family: inherit;
  font-size: 14px;
  color: var(--text-meta);
  cursor: pointer;
}
.hf-checkbox:hover { color: var(--stitch-bright); }
.hf-checkbox:hover .hf-checkbox__box { border-color: rgba(163,171,179,0.4); }
.hf-checkbox:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
  border-radius: 4px;
}
.hf-checkbox__box {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  border-radius: var(--hf-radius-thumb);
  border: var(--hf-border-soft);
  background: var(--hf-chip-surface-off);
  color: transparent;
  transition: border-color 140ms ease, background 140ms ease, color 140ms ease;
}
.hf-checkbox[aria-checked="true"] .hf-checkbox__box {
  border: var(--hf-border-chip-on);
  background: rgba(241, 231, 216, 0.045);
  color: var(--text-primary);
}
.hf-checkbox[aria-checked="true"] .hf-checkbox__label { color: var(--text-primary); }

/* ---------- Zero-match helper under the (disabled) sticky CTA ---------- */
.hf-drawer__hint {
  margin: 10px 0 0;
  font-size: 13px;
  line-height: 1.4;
  text-align: center;
  color: var(--text-muted);
}

/* ============================================================
   RESULT
   ============================================================ */
.hf-result {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 980px;
  max-width: 100%;
}
/* Стежковая линия-«нить» над результатом */
.hf-result__rule {
  height: 36px;
  display: flex;
  align-items: center;
}
.hf-result__meta {
  margin-top: 20px;
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: 11px;
  letter-spacing: 0.24em;
  color: var(--text-muted);
  animation: hfFade 340ms ease both;
}
.hf-result__titlewrap {
  min-height: 96px;
  margin-top: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Название фанфика — same display family as home's H1 and the drawer's H3
   (var(--hf-font-display)), so the app reads as one typographic voice: the
   thing HYPERFIC says (headings) and the thing it found (the title) share
   a family, just at different sizes/weights for hierarchy. Golos Text
   stays the body/UI voice everywhere else.
   font-size/line-height below are the pre-JS default (== the largest tier)
   — fitResultTitle() in resultView.js measures actual rendered line count
   and overrides both via inline style, stepping down through a fixed set
   of tiers until the title fits within ~2 lines (or hits the smallest
   tier, which is allowed to run longer rather than shrink indefinitely).
   It's never sized off raw character count — a short title in a narrow
   viewport and a long one in a wide container need different answers, and
   only measuring the actual layout gets that right. Tiers are tuned for
   Unbounded's own (wider-than-Golos-Text) glyph metrics. */
.hf-result__title {
  font-family: var(--hf-font-display);
  font-weight: 600;
  font-size: 72px;
  line-height: 1.1;
  letter-spacing: -0.02em;
  text-align: center;
  color: var(--text-primary);
  padding: 0 4px;
  max-width: 780px;
  /* Explicit reset, not a no-op: resultView.js wraps fic titles in a <div>
     (zero UA default margin), but contactsView.js's /contacts/ heading
     reuses this same class on a real <h1> (see components/contactsView.js)
     — without this, the browser's default h1 margin-block (~0.67em, ~48px
     at this font-size) silently reappears there and inflates every spacing
     rule below it in .hf-contacts. Harmless here since a <div> already had
     no margin to reset. */
  margin: 0;
  /* No transition on font-size — deliberately. fitResultTitle() (resultView.js)
     is a layout calculation (which tier actually fits), not a reveal effect,
     and it reruns after *every* render(), not just when a new result
     appears. A transition here previously caused a real regression: its own
     tier-search loop forces a synchronous style/layout recalc at the
     CSS-default 72px on the first iteration before reaching the correct
     tier, which is enough for a transition to pick up 72px as the "old"
     value and visibly animate 72px → fitted size — on every render(),
     including ones triggered by nothing more than opening the burger menu
     (see app.js's setMenuOpen, added specifically to avoid re-rendering the
     result at all for that case). The actual letter-by-letter reveal for a
     genuinely new result is animateResultTitle(), driven by app.js's
     reveal()/quickSwap() timers — untouched by this. */
  /* Never break inside a word — wrap by whole word only, however long the
     title is. */
  word-break: normal;
  overflow-wrap: normal;
  hyphens: none;
}
.hf-result__body {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  animation: hfRise 420ms ease both;
}
/* One calm centered metadata line — "Original title · author" (or just
   author when there's no original title) — rather than a decorative dash
   pairing two differently-styled elements. Same size/tracking/color for
   both halves so the author never reads louder than the title next to it.
   max-width keeps a long original title wrapping onto a second line by
   whole words, same as before. */
.hf-result__byline {
  margin-top: 6px;
  max-width: 420px;
  font-size: 14px;
  line-height: 1.4;
  letter-spacing: 0.04em;
  color: var(--text-meta);
  text-align: center;
  word-break: normal;
  overflow-wrap: normal;
  hyphens: none;
}
.hf-result__desc {
  margin: 22px 0 0;
  max-width: 560px;
  font-size: 17px;
  line-height: 1.7;
  color: var(--text-body);
  text-align: center;
  text-wrap: pretty;
  word-break: normal;
  overflow-wrap: normal;
  hyphens: none;
}
.hf-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  margin-top: 20px;
  max-width: 640px;
}
.hf-tag {
  font-size: 13px;
  padding: 7px 12px;
  border-radius: var(--hf-radius-pill);
  background: var(--hf-chip-surface-off);
  border: var(--border-neutral);
  color: var(--text-meta);
}

/* ---------- Content warnings disclosure ----------
   Service information, not a fact-card — no surface, no pill, no border,
   no glow, and deliberately never accent-magic/red/ivory: warnings stay in
   the cold-steel "interface" register, same as any other metadata. Renders
   nothing at all when a fic has no warnings (see resultView.js). */
.hf-result__warnings {
  margin-top: 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.hf-result__warnings-toggle {
  background: none;
  border: 0;
  /* Padding + matching negative margin keeps the visible row small while
     giving the button a real ~44px tap target — same trick as .hf-action.
     line-height is pinned so the target height is predictable at both the
     13px desktop and 12px mobile sizes. */
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 15px 10px;
  margin: -15px -10px;
  font-family: inherit;
  font-weight: 400;
  font-size: 13px;
  line-height: 1.2;
  letter-spacing: 0.01em;
  color: var(--text-meta);
  cursor: pointer;
  transition: color 140ms ease;
}
.hf-result__warnings-toggle:hover { color: var(--stitch-bright); }
.hf-result__warnings-toggle:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
  border-radius: 4px;
}
.hf-result__warnings-label { display: inline-block; }
/* Drawn, not Unicode, so it stays crisp and centers exactly on the text
   baseline; rotates in place instead of swapping glyphs. */
.hf-result__warnings-chevron {
  display: block;
  flex: none;
  width: 11px;
  height: 11px;
  color: inherit;
  transform: rotate(0deg);
  transition: transform 190ms ease;
}
.hf-result__warnings-chevron.is-open { transform: rotate(180deg); }
/* Height/opacity transition via the CSS grid-rows trick (0fr → 1fr) — no
   JS height measurement needed to animate to the content's intrinsic
   height. */
.hf-result__warnings-panel {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  width: 100%;
  max-width: 680px;
  transition: grid-template-rows 200ms ease, opacity 200ms ease;
}
.hf-result__warnings-panel.is-open {
  grid-template-rows: 1fr;
  opacity: 1;
}
.hf-result__warnings-list {
  margin: 0;
  overflow: hidden;
  min-height: 0;
  padding-top: 6px;
  font-size: 13px;
  font-weight: 400;
  line-height: 1.45;
  text-align: center;
  /* Same muted-steel token as everything else in this tier — the extra-dim
     one-off rgba this used before read as disabled/inactive rather than
     "quiet metadata", which fought against actually wanting the content
     read. */
  color: var(--text-muted);
  word-break: normal;
  overflow-wrap: normal;
  hyphens: none;
}
/* tags/metadata (info about the work) vs. the green Hyperfic reaction
   (Hyperfic's own voice) need to read as two different tiers, not two
   adjacent captions — extra breathing room only kicks in when a warnings
   block actually sits between them, collapsed or expanded alike. */
.hf-result__warnings + .hf-result__why { margin-top: 30px; }

/* ---------- "Почему тебе" ----------
   A single small editorial remark, not a section — no label, no card, no
   line, no border. Centered like a caption, wrapping as whole words same
   as everything else. Voice (family/style/weight/color) comes from the
   shared .hf-micro-quip/.hf-combo-quip/.hf-result__why rule above — this
   is the same character speaking as the filter-drawer quips, just laid
   out centered instead of inline. Only size/line-height/layout live here. */
.hf-result__why {
  margin: 14px 0 0;
  max-width: 480px;
  text-align: center;
  font-size: 14px;
  line-height: 1.45;
  word-break: normal;
  overflow-wrap: normal;
  hyphens: none;
  opacity: 0;
  transform: translateY(3px);
  animation: hfWhyIn 260ms ease-out both;
  animation-delay: 80ms;
}
@keyframes hfWhyIn { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
/* Reaction → CTA needs more air than any other gap on the result screen —
   it's the last beat before the primary action, not another content line.
   Margin-top on the CTA (not a fixed-height wrapper around the reaction)
   means this is measured from wherever the text actually ends, one or two
   lines alike. Base .hf-result__cta margin-top below covers the no-reaction
   case unchanged. */
.hf-result__why + .hf-result__cta { margin-top: 52px; }

.hf-result__cta {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 36px;
}
/* Wraps the "Читать" button + its source popover. inline-flex shrinks the
   wrapper to the button's own width (previously it was width:100%, which
   made the button hug the wrapper's left edge instead of sitting centered
   under the rest of the result content) — the popover's left:0 then lines
   up with the button itself, not the wrapper. */
.hf-result__read {
  position: relative;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  /* Only ever visible when a second (audio) button is actually present —
     a lone "Читать" button has nothing to gap against. */
  gap: 14px;
}
.hf-result__actions {
  display: flex;
  /* +12px over the intended 26px visual gap — each .hf-action's own -6px
     horizontal margin (enlarged hit area) eats into it from both sides. */
  gap: 38px;
  margin-top: 26px;
}

/* "Если хочется ещё" — only ever rendered on a /fic/:slug/ permalink (see
   app.js's render(), components/relatedFics.js), a sibling right after
   .hf-result and right before .hf-result__back below. Deliberately
   editorial, not a card grid: no container border/background/radius on
   .hf-related itself or on each .hf-related__item — a magazine "read next"
   list, divided by thin hairline rules (vertical between desktop's 3
   columns, horizontal between mobile's stacked rows), not boxed UI
   components. Height is whatever the content needs — no forced
   stretch/equal-height, top-aligned. */
.hf-related {
  width: 100%;
  max-width: 1040px;
  margin-top: 56px;
}
.hf-related__heading {
  font-family: var(--hf-font-display);
  font-weight: 600;
  font-size: 24px;
  color: var(--text-primary);
  text-align: center;
  margin: 0 0 24px;
}
.hf-related__row-group {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  align-items: start;
}
.hf-related__item {
  display: block;
  padding: 0 28px;
  text-decoration: none;
}
.hf-related__item:first-child { padding-left: 0; }
.hf-related__item:last-child { padding-right: 0; }
/* The vertical divider between desktop columns — a border on the item
   itself (not a separate element), so it only ever appears between two
   real items, never floating around a lone/short row. One step up from
   the hairline token (0.09 -> 0.12 alpha) so it actually reads as a
   divider at a glance rather than disappearing next to the surrounding
   copy. */
.hf-related__item:not(:first-child) {
  border-left: var(--hf-border-line);
}
.hf-related__row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}
.hf-related__title {
  font-family: var(--hf-font-ui);
  font-weight: 600;
  font-size: 19px;
  line-height: 1.35;
  color: var(--hf-ivory-88);
  transition: color 160ms ease;
}
.hf-related__arrow {
  flex-shrink: 0;
  font-size: 14px;
  line-height: 1.35;
  color: var(--text-muted);
  transition: transform 160ms ease;
}
/* Whole-item hover — deliberately quiet: no background, no border change,
   no scale. Just the title brightening a notch and the arrow nudging
   right, same restrained language as .hf-result__back's own hover. */
.hf-related__item:hover .hf-related__title { color: var(--text-primary); }
.hf-related__item:hover .hf-related__arrow { transform: translateX(3px); }
.hf-related__item:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 3px;
}
.hf-related__byline {
  display: block;
  margin-top: 7px;
  font-size: 14px;
  line-height: 1.4;
  color: var(--text-meta);
}
/* Plain editorial metadata line — same uppercase/letter-spacing recipe as
   .hf-result__meta above (116K · ЗАВЕРШЁН · HEA), not pills. tags itself
   stays Title Case (buildResultTags() is shared with the main result
   card's chips) — text-transform does the uppercasing here only. */
.hf-related__meta {
  display: block;
  margin-top: 9px;
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-meta);
}
/* A related section is always followed by "Подобрать другой фанфик" (see
   app.js's render()) — enough extra room that it reads as its own,
   separate secondary action rather than a fourth related row. */
.hf-related + .hf-result__back { margin-top: 44px; }

/* Only ever rendered on a /fic/:slug/ permalink (see app.js's render(),
   resultView.js's renderBackToHomeLink) — a sibling right after .hf-result,
   never inside .hf-result__cta. Still a plain text action (no pill/border/
   background, smaller than .hf-action), just legible enough not to read as
   disabled — stays clearly secondary to "Читать" above it. */
.hf-result__back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 30px;
  font-size: 13px;
  color: var(--hf-ivory-55);
  text-decoration: none;
}
.hf-result__back-arrow { transition: transform 200ms var(--hf-ease-out); }
.hf-result__back:hover { color: var(--hf-ivory-70); }
.hf-result__back:hover .hf-result__back-arrow { transform: translateX(2px); }
.hf-result__back:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 4px;
}

/* Поповер выбора источника (Ficbook / AO3) */
.hf-sources {
  position: absolute;
  top: 66px; left: 0;
  width: 212px;
  overflow: hidden;
  background: var(--hf-graphite-menu);
  border: var(--hf-border-line);
  border-radius: var(--hf-radius-menu);
  box-shadow: var(--hf-shadow-menu);
  animation: hfFade 200ms ease both;
  z-index: 5;
}
.hf-sources__item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 14px 16px;
  background: none;
  border: 0;
  font-family: inherit;
  font-size: 14px;
  color: var(--hf-ivory-76);
  cursor: pointer;
}
.hf-sources__item:hover {
  background: var(--hf-graphite-hover);
  color: var(--hf-ivory);
}
.hf-sources__item:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: -2px;
}

/* Burger nav menu (Подобрать фанфик / Контакты) — available on every route,
   desktop and mobile alike (see components/header.js, components/navMenu.js),
   so unlike .hf-scrim below it is never display:none by default. Stays
   permanently mounted and toggles via .is-open + a plain transition, same
   surface/radius/shadow recipe as .hf-sources above. Fixed-positioned near
   the header's own left padding rather than nested inside .hf-header (see
   app.js's render()), so it isn't subject to .hf-header's pointer-events:none
   shell trick — it's a sibling, not a descendant. */
.hf-nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 6;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: default;
  pointer-events: none;
}
.hf-nav-scrim.is-open { pointer-events: auto; }

.hf-nav-menu,
.hf-pairing-menu {
  position: fixed;
  top: 77px;
  left: 44px;
  min-width: 220px;
  overflow: hidden;
  background: var(--hf-graphite-menu);
  border: var(--hf-border-line);
  border-radius: var(--hf-radius-menu);
  box-shadow: var(--hf-shadow-menu);
  z-index: 7;
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  transition: opacity 200ms ease, transform 200ms ease;
}
.hf-nav-menu.is-open,
.hf-pairing.is-open .hf-pairing-menu {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.hf-nav-menu__item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 14px 18px;
  background: none;
  border: 0;
  font-family: inherit;
  font-size: 14px;
  color: var(--hf-ivory-76);
  text-decoration: none;
  cursor: pointer;
}
.hf-nav-menu__item + .hf-nav-menu__item { border-top: var(--hf-border-hairline); }
/* ------------------------------------------------------------
   Pairing selector (header, right) — rendered only when more than one
   pairing is available; with one, the header keeps the plain .hf-fandom
   label above and none of this exists. Same visual language as the burger:
   a bare text trigger (no pill/background/border) opening a popover built
   from the very same surface recipe and item markup as .hf-nav-menu.
   ------------------------------------------------------------ */
.hf-pairing {
  position: relative;
  align-self: flex-start;
}
.hf-pairing__btn {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 27px; /* same line as .hf-wordmark, so the label centers against the logo */
  padding: 0;
  background: none;
  border: 0;
  font-family: inherit;
  font-size: 16px;
  letter-spacing: 0.16em;
  color: var(--hf-ivory-76);
  cursor: pointer;
  transition: color 160ms ease;
}
.hf-pairing__btn:hover,
.hf-pairing.is-open .hf-pairing__btn { color: var(--text-primary); }
.hf-pairing__btn:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: 4px;
  border-radius: 2px;
}
.hf-pairing__chevron { transition: transform 200ms var(--hf-ease-out); }
.hf-pairing.is-open .hf-pairing__chevron { transform: rotate(180deg); }
.hf-pairing-menu {
  position: absolute;
  top: calc(100% + 12px);
  left: auto;
  right: 0;
  min-width: 190px;
}
.hf-nav-menu__item--pairing {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 12px;
  letter-spacing: 0.16em;
}
/* The current pairing: slightly brighter text and one small steel dot — nothing louder. */
.hf-nav-menu__item--pairing.is-current::after {
  content: "";
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--hf-steel-85);
}

.hf-nav-menu__item:hover { background: var(--hf-graphite-hover); color: var(--hf-ivory); }
.hf-nav-menu__item.is-current { color: var(--hf-ivory-strong); }
.hf-nav-menu__item:focus-visible {
  outline: 1px solid var(--hf-steel-70);
  outline-offset: -2px;
}

/* ============================================================
   STITCH GLYPHS (иконки фильтров, × в вордмарке, линия-нить)
   Рендерятся сеткой из символов × — см. StitchGlyph в Hyperfic.jsx
   ============================================================ */
.hf-glyph {
  display: grid;
  font-family: var(--hf-font-stitch);
  line-height: 1;
}
.hf-glyph > i {
  font-style: normal;
  text-align: center;
}

/* ============================================================
   ANIMATIONS — только входы состояний. Никаких loop-анимаций.
   ============================================================ */
@keyframes hfRise { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; } }
@keyframes hfFade { from { opacity: 0; } to { opacity: 1; } }
/* Mobile bottom sheet: a real off-screen slide (native-sheet feel), not
   just a small settle-in nudge — see .hf-drawer/.hf-scrim under the
   max-width:899px block below for where these are actually used. */
@keyframes hfSheetIn { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes hfSheetOut { from { transform: translateY(0); } to { transform: translateY(100%); } }
@keyframes hfScrimIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes hfScrimOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes hfMicroQuipIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .hf-nav-menu { transition: none !important; }
  .hf-result__body, .hf-result__meta, .hf-extra, .hf-sheet, .hf-sources { animation: none !important; }
  /* Mobile bottom sheet open/close (see hfSheetIn/Out, hfScrimIn/Out
     above) — neither has a base transform/opacity outside the keyframes,
     so turning the animation off alone already leaves both the sheet and
     scrim at their correct resting (open) appearance, no override needed.
     app.js's own close timer is skipped in this case too — see
     prefersReducedMotion() — so the unmount isn't left waiting on an
     animation that never plays. */
  .hf-drawer, .hf-scrim { animation: none !important; }
  /* .hf-result__why's base rule sets opacity:0/translateY(3px) as the
     animation's starting point, not just the keyframe's "from" — disabling
     the animation alone would leave it stuck invisible, so the end state is
     restored explicitly. Content is unchanged, it just appears immediately
     with everything else instead of fading in afterward. The stars' own
     resting opacity (set per-cell in resultView.js, quieter than the full-
     opacity text) is untouched — it was never part of the animation. */
  .hf-result__why {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .hf-result__warnings-panel,
  .hf-result__warnings-panel.is-open,
  .hf-result__warnings-chevron {
    transition: none !important;
  }
  .hf-result__warnings-panel.is-open {
    grid-template-rows: 1fr;
    opacity: 1;
  }
}

/* Переключатели desktop/mobile-разметки (верхняя строка шторки) */
.hf-only-mobile { display: none; }

/* ============================================================
   MOBILE  (< 900px) — home остаётся центрированным,
   фильтры открываются снизу как full-height sheet
   ============================================================ */
@media (max-width: 899px) {
  .hf-only-desktop { display: none; }
  .hf-only-mobile { display: block; }

  .hf-grain { opacity: 0.5; background-image: var(--hf-grain-mobile); }

  .hf-header-fade { height: 100px; }
  .hf-header { padding: 24px 24px 0; }
  /* Close to the same ×1.6 factor as desktop (14px → 22px) — verified at
     320px width there's still a comfortable gap before DRAMIONE, so no
     need to undershoot for safety. */
  .hf-wordmark { height: 22px; }
  .hf-wordmark__logo { height: 22px; }
  .hf-burger { width: 16px; height: 22px; }
  .hf-header__left { gap: 18px; }
  .hf-fandom { font-size: 11px; letter-spacing: 0.16em; }

  /* Mirrors .hf-header's own smaller mobile padding-top/left above. */
  .hf-nav-menu { top: 60px; left: 24px; }
  .hf-pairing__btn { height: 22px; font-size: 13px; letter-spacing: 0.14em; gap: 6px; }
  .hf-pairing-menu { top: calc(100% + 10px); left: auto; right: 0; min-width: 170px; }

  /* Two-column desktop layout is disabled entirely: stage fills the
     screen again and the panel reverts to a fixed bottom-sheet overlay. */
  .hf-layout { position: static; display: block; }
  .hf-stage { position: absolute; inset: 0; flex: none; padding: 56px 28px; }
  .hf-layout__panel { display: contents; }
  .hf-home { width: 100%; }
  .hf-related { margin-top: 40px; }
  .hf-related__heading { font-size: 18px; margin-bottom: 30px; }
  /* Stacked single column — the vertical divider becomes a horizontal one
     between rows instead, and the desktop side-padding (which made room
     for that vertical rule) is no longer needed. */
  .hf-related__row-group { grid-template-columns: 1fr; row-gap: 18px; }
  .hf-related__item { padding: 0; }
  .hf-related__item:not(:first-child) {
    border-left: 0;
    border-top: var(--hf-border-hairline);
    padding-top: 18px;
  }
  /* Mobile keeps its existing recommendation sizes — the desktop-only
     bump above (title 15->16px, byline 12->13px, meta 10->11px/muted-
     >meta contrast) is undone here rather than inherited. */
  .hf-related__title { font-size: 15px; }
  .hf-related__byline { font-size: 12px; }
  .hf-related__meta { font-size: 10px; color: var(--text-muted); }
  .hf-related + .hf-result__back { margin-top: 36px; }
  .hf-contacts { width: 100%; }
  /* Narrower text column than the CTA/container below it — font-size/
     line-height still come from the reused .hf-result__desc mobile rule
     below, just a shorter line length so three short blocks don't read as
     one dense grey sheet edge-to-edge. Gaps below only restate the two
     that actually differ from desktop (heading→owl, owl→paragraph 1);
     paragraph 1→2, paragraph 2→CTA and CTA→email reuse the base rules
     above unchanged, same value at both breakpoints. */
  .hf-contacts__body { max-width: 326px; margin-top: 26px; margin-left: auto; margin-right: auto; }
  .hf-contacts__body p:nth-child(2) { margin-top: 22px; }

  .hf-hero__art { height: 140px; }             /* ← mobile-высота hero-области */
  .hf-hero__art > svg,
  .hf-hero__art > img { height: 164px; object-fit: contain; }

  .hf-h1 { margin-top: 26px; font-size: 38px; line-height: 1.08; letter-spacing: -0.03em; text-wrap: balance; }
  .hf-sub { margin-top: 18px; max-width: 250px; font-size: 15px; color: var(--hf-ivory-48); }

  .hf-btn { height: 56px; font-size: 16px; box-shadow: var(--hf-shadow-cta-mobile); }
  .hf-btn--hero { margin-top: 36px; width: 100%; max-width: 302px; }
  .hf-btn--read { width: 100%; height: 54px; box-shadow: none; }
  .hf-result__read { display: flex; width: 100%; gap: 10px; }
  /* Two-up: "Читать" and "Слушать" split the row evenly instead of the
     lone-button 100% width above. Tighter padding/font than desktop's
     .hf-btn--listen so both labels + the play icon stay comfortably clear
     of each other down to real small-phone widths (this card is already
     capped at max-width:302px below). */
  .hf-result__read.has-audio .hf-btn--read,
  .hf-result__read.has-audio .hf-btn--listen {
    width: auto;
    flex: 1 1 0;
    min-width: 0;
    padding: 0 14px;
    font-size: 14px;
  }
  .hf-btn--listen { height: 54px; }
  .hf-textlink { margin-top: 24px; font-size: 14px; }

  /* Bottom sheet вместо drawer. Open/close both play a real @keyframes
     animation (not a transition) so they run correctly on insertion —
     JS (app.js setDrawerOpen) mounts the sheet with .is-closing already
     present in its very first frame when closing, exactly like the
     is-panel-open trick used for desktop, just via a class instead of a
     transition. Durations are echoed in app.js's DRAWER_CLOSE_MOBILE_ANIM_MS
     (keep the two in sync if either changes) — reveal()'s CTA choreography
     times its own pause off that same close duration. Scrim uses the exact
     same durations/easing as the sheet (not just similar ones) so the fade
     and the slide always read as one motion, never drifting apart. */
  .hf-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 3;
    background: var(--hf-scrim-sheet);
    animation: hfScrimIn 360ms var(--hf-ease-sheet) both;
  }
  .hf-scrim.is-closing {
    animation: hfScrimOut 400ms var(--hf-ease-sheet) both;
  }
  .hf-drawer {
    position: fixed;
    top: auto; left: 0; right: 0; bottom: 0;
    z-index: 4;
    width: auto;
    flex: none;
    /* Capped, not stretched: sizes to content up to this ceiling, so a
       short filter set doesn't force a tall sheet with empty scroll
       space. Two declarations, same progressive-enhancement pattern as
       .hf-screen's min-height — the vh/no-safe-area line is the fallback
       for engines without dvh/env() support, the second overrides it
       where available. Anchored to the real visual viewport (fixed, not
       .hf-screen's dvh-based box) and shrunk by both safe-area insets so
       the sheet's own top/handle/close/title can never be pushed past
       the top of the visible area regardless of a browser's own chrome
       height (see hyperfic.css .hf-scrim comment for the underlying bug
       this and the scrim's position:fixed both address). */
    max-height: min(788px, calc(100vh - 24px));
    max-height: min(788px, calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 24px));
    border-left: 0;
    border-top: var(--hf-border-panel);
    border-radius: var(--hf-radius-sheet) var(--hf-radius-sheet) 0 0;
    box-shadow: var(--hf-shadow-sheet);
    animation: hfSheetIn 360ms var(--hf-ease-sheet) both;
  }
  .hf-drawer.is-closing {
    animation: hfSheetOut 400ms var(--hf-ease-sheet) both;
  }
  /* На мобильном верхняя строка шторки: Закрыть слева, ручка по центру.
     "Сбросить" переехал в hf-drawer__subrow — общий с desktop. */
  .hf-drawer__top {
    position: relative;
    justify-content: flex-start;
    align-items: center;
    padding: 16px 22px 0;
  }
  .hf-sheet__handle {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 38px; height: 3px;
    border-radius: var(--hf-radius-thumb);
    background: rgba(241,231,216,0.16);
  }
  .hf-sheet__text {
    background: none; border: 0; padding: 0;
    font-family: inherit;
    font-size: 14px;
    color: var(--text-meta);
    cursor: pointer;
  }
  .hf-sheet__text--muted { color: var(--text-muted); }
  .hf-sheet__text:hover { color: var(--text-primary); }
  .hf-sheet__text:focus-visible {
    outline: 1px solid var(--hf-steel-70);
    outline-offset: 3px;
    border-radius: 2px;
  }

  .hf-drawer__scroll { padding: 24px 22px 20px; }
  .hf-drawer__foot { padding: 14px 22px max(26px, calc(env(safe-area-inset-bottom) + 16px)); }

  .hf-h3 { font-size: 26px; line-height: 1.15; letter-spacing: -0.025em; }
  .hf-drawer__sub { margin-top: 0; font-size: 14px; }
  .hf-drawer__subrow { margin-top: 12px; }
  .hf-reset-link { font-size: 12px; }

  .hf-group { margin-top: 24px; }
  .hf-group__head { gap: 10px; }
  .hf-group__title { font-size: 15px; }
  .hf-micro-quip-slot { height: 18px; margin-top: 6px; }
  .hf-micro-quip { font-size: 12px; }

  .hf-stitch-slider { margin-top: 18px; }
  /* Track height stays ≥40px on mobile too — it's the real touch target;
     only the visual thumb/label/tick presentation changes below. */
  .hf-stitch-slider__labels { margin-top: 9px; }
  /* Slightly larger + higher-contrast than the desktop scale so it doesn't
     read as a shrunk-down desktop control; selected still reads clearly
     brighter via .is-active → var(--text-primary), untouched here. */
  /* Color now inherits the base rule's text-meta — already at the fixed
     contrast target, no separate mobile boost needed anymore. */
  .hf-stitch-slider__label { font-size: 13px; line-height: 1.3; }
  /* No extra mobile scale-up needed anymore — the base ~19px star already
     reads clearly at mobile viewing distance on its own. */

  .hf-chips { gap: 7px; margin-top: 14px; }
  .hf-chip { font-size: 13px; padding: 9px 13px; }

  .hf-tropes-more { font-size: 12px; }

  .hf-more { margin-top: 26px; padding-top: 18px; }
  .hf-more__label { font-size: 15px; }
  .hf-more__sign { font-size: 18px; }
  .hf-extra { margin-top: 18px; gap: 18px; }
  .hf-extra .hf-chips { margin-top: 12px; }

  .hf-drawer__hint { font-size: 12px; }
  .hf-combo-quip { font-size: 13px; }

  .hf-result { width: 100%; }
  .hf-result__rule { height: 26px; }
  /* Top-of-result vertical rhythm, tightened so the title becomes the
     dominant element sooner — same editorial elements/sizes, just less
     air between them. Title's own font-size/line-height untouched. */
  .hf-result__meta { margin-top: 12px; font-size: 10px; letter-spacing: 0.2em; }
  .hf-result__titlewrap { margin-top: 8px; }
  .hf-result__title { font-size: 38px; line-height: 1.1; }
  .hf-result__byline { margin-top: 4px; max-width: 280px; font-size: 12px; letter-spacing: 0.03em; }
  /* Description: same font-size as before (still a large, editorial read)
     but a mobile-only line-height, ~12% tighter than the 1.7 it would
     otherwise inherit from the desktop rule above — long descriptions no
     longer eat as much vertical space, without reading as dense body
     copy. */
  .hf-result__desc { margin-top: 20px; font-size: 15px; line-height: 1.5; }
  .hf-tags { gap: 6px; margin-top: 16px; }
  .hf-tag { font-size: 12px; padding: 6px 11px; }
  .hf-result__warnings { margin-top: 16px; }
  .hf-result__warnings-toggle { font-size: 12px; gap: 6px; }
  .hf-result__warnings-chevron { width: 10px; height: 10px; }
  .hf-result__warnings-panel { max-width: 300px; }
  .hf-result__warnings-list { font-size: 12px; line-height: 1.45; }
  .hf-result__warnings + .hf-result__why { margin-top: 26px; }
  .hf-result__why { margin-top: 12px; max-width: 300px; font-size: 13px; line-height: 1.4; }
  .hf-result__why + .hf-result__cta { margin-top: 42px; }
  .hf-result__cta { width: 100%; max-width: 302px; margin-top: 28px; }
  .hf-result__actions { gap: 34px; margin-top: 24px; }
  .hf-action { font-size: 14px; gap: 7px; }

  /* Источники под кнопкой, а не поповером */
  .hf-sources {
    position: static;
    width: auto;
    margin-top: 8px;
    box-shadow: none;
  }
  .hf-sources__item { padding: 13px 15px; }
}

/* Real small-phone fallback: if "Читать" + "Слушать" side by side would get
   too cramped, stack them instead of shrinking text further. 359px picked
   so the two-up row above still applies at the common 375-390px phone
   widths (where it has plenty of room) and only the genuinely narrow tier
   (≤iPhone SE-class, 320px) drops to one column. */
@media (max-width: 359px) {
  .hf-result__read.has-audio {
    flex-direction: column;
    width: 100%;
  }
  .hf-result__read.has-audio .hf-btn--read,
  .hf-result__read.has-audio .hf-btn--listen {
    width: 100%;
    flex: none;
  }
}
