/* 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
   ============================================================ */
.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;
}
.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;
}

/* 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 lives inside .hf-stage (not the global header) specifically so
   it's bounded by the stage's own flex-basis — it must never sit on top of
   the filter panel once that column claims part of the viewport. */
.hf-stage__fandom {
  position: absolute;
  top: 36px;
  right: 44px;
  z-index: 2;
}

/* ============================================================
   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;
}

/* ============================================================
   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 .hf-stage__fandom, 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 {
  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;
}

/* ============================================================
   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);
}

.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;
}

/* ---------- 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; }

/* ---------- 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;
  /* 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;
}

/* Поповер выбора источника (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;
}

/* ============================================================
   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; } }
@keyframes hfSheet { from { transform: translateY(24px); opacity: 0.6; } to { transform: none; opacity: 1; } }
@keyframes hfMicroQuipIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .hf-result__body, .hf-result__meta, .hf-extra, .hf-sheet, .hf-sources { 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 { 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-fandom { font-size: 11px; letter-spacing: 0.16em; }
  .hf-stage__fandom { top: 24px; right: 24px; }

  /* 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-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 */
  .hf-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 3;
    background: var(--hf-scrim-sheet);
  }
  .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: hfSheet 320ms 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; }
  .hf-result__meta { margin-top: 16px; font-size: 10px; letter-spacing: 0.2em; }
  .hf-result__titlewrap { margin-top: 12px; }
  .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; }
  .hf-result__desc { margin-top: 20px; font-size: 15px; }
  .hf-tags { gap: 6px; margin-top: 20px; }
  .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;
  }
}
