[x-cloak] { display: none !important; }

.edition-page {
  --ed-ink: var(--edition-ink-light);
  --ed-line: color-mix(in srgb, var(--border) 90%, transparent);
  --ed-panel: color-mix(in srgb, var(--surface) 92%, var(--on-image) 8%);
  --ed-accent: var(--accent-canonical);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--page-accent) 8%, transparent), transparent 360px),
    radial-gradient(circle at 12% 12%, color-mix(in srgb, var(--accent-recent) 12%, transparent), transparent 34%),
    var(--bg);
}

/* Shared edition-view container. Both the page (wrapper div) and the modal
   (.edition-view--modal root) carry .edition-view, so the view's responsive
   rules can size to the *container* (modal width) via @container queries,
   not the viewport. Placed on an inner wrapper so it never becomes the
   containing block for the page's position:fixed sticky bar / FAB. */
.edition-view {
  --dossier-action-min-height: 44px;
  --dossier-action-hover-transform: translateY(-1px);
  container-type: inline-size;
  container-name: edition;
}

[data-theme="dark"] .edition-page {
  --ed-ink: var(--text);
  --ed-panel: color-mix(in srgb, var(--surface) 88%, var(--shadow-ink) 12%);
}

.edition-masthead {
  position: relative;
  isolation: isolate;
  min-height: 360px;
  padding: 1.25rem 0 4.5rem;
  overflow: hidden;
  border-bottom: 1px solid var(--border);
}

.edition-masthead::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  background:
    linear-gradient(105deg, color-mix(in srgb, var(--surface) 96%, transparent) 0%, color-mix(in srgb, var(--surface) 88%, transparent) 47%, color-mix(in srgb, var(--surface) 62%, transparent) 100%),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--border) 42%, transparent) 0 1px, transparent 1px 72px);
}

[data-theme="dark"] .edition-masthead::before {
  background:
    linear-gradient(105deg, color-mix(in srgb, var(--edition-dark-masthead) 98%, transparent) 0%, color-mix(in srgb, var(--edition-dark-masthead) 90%, transparent) 52%, color-mix(in srgb, var(--edition-dark-masthead) 68%, transparent) 100%),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--on-image) 5.5%, transparent) 0 1px, transparent 1px 72px);
}

.edition-masthead::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 34%;
  z-index: -1;
  background: linear-gradient(180deg, transparent, var(--bg));
}

.edition-masthead-wash {
  position: absolute;
  inset: -12% -4% -22% auto;
  width: min(46vw, 620px);
  z-index: -3;
  opacity: 0.22;
  transform: rotate(-8deg);
  filter: saturate(0.75) contrast(1.08);
}

.edition-masthead-wash img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.edition-backlink {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-height: 40px;
  color: var(--text-muted);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-decoration: none;
  text-transform: uppercase;
}

.edition-backlink:hover { color: var(--text); }

.edition-random-fab {
  position: fixed;
  right: 1.25rem;
  /* Slot-free resting position (no chat FAB on the page). The lifted rule below
     reads --cc-fab-slot-2, which carries the allowance in theme.css. */
  bottom: max(1.25rem, var(--safe-bottom));
  z-index: 1040;
  width: 3.25rem;
  height: 3.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--accent-canonical);
  color: var(--edition-fab-ink);
  font-size: 1.4rem;
  text-decoration: none;
  box-shadow: 0 4px 14px color-mix(in srgb, var(--shadow-ink) 35%, transparent);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}

.edition-random-fab:hover {
  transform: translateY(-2px) rotate(-8deg);
  color: var(--edition-fab-ink);
  box-shadow: 0 6px 20px color-mix(in srgb, var(--shadow-ink) 45%, transparent);
}

.edition-random-fab:active {
  transform: translateY(0) scale(0.95);
}

/* GH-126. Slot 2 of the bottom-right floating stack (theme.css), taken only
   when the reading-log chat FAB is actually on the page -- it renders for
   authed premium users behind `show_log_chat`, so most readers have nothing
   below this FAB and it stays at its own resting offset.

   Both states are listed deliberately. The collision is NOT specific to the
   docked sticky bar: measured with the chat widget rendered, the two FABs
   overlapped 96.2% at 390x844 with the bar docked AND 92.5% at 1280x900 at the
   top of the page, `elementFromPoint` returning the chat FAB in both. A rule
   scoped to the sibling selector alone would fix the mobile half and leave
   desktop broken. The second selector exists because
   `.edition-stickybar.is-shown ~ .edition-random-fab` below carries three
   classes and would otherwise outrank a two-class override. */
/* GH-124 note: the SECOND selector is now inert. The sticky bar exists only
   inside the 640px block below, and the FAB is `display: none` for that whole
   width, so the docked-bar state can no longer put these two on screen
   together. It is kept because it shares this declaration with the first
   selector, which is still load-bearing: the 92.5% overlap GH-126 measured at
   1280x900 is a DESKTOP collision, and that is the half that still renders. */
body:has(.lc-fab) .edition-random-fab,
body:has(.lc-fab) .edition-stickybar.is-shown ~ .edition-random-fab {
  bottom: var(--cc-fab-slot-2);
}

/* This FAB occupies slot 2, so anything stacking above it needs slot 3. The
   Discord CTA is the only such occupant, and it reads `--cc-cta-slot` rather
   than naming this page: discord_cta.css must not carry a breakpoint of its
   own (it would drift against the chat FAB's), so the page that owns the FAB
   is the one that declares how much room the FAB takes. Paired with the
   override in the 640px block below, where the FAB is hidden and the CTA takes
   the slot back. */
body:has(.edition-random-fab) {
  --cc-cta-slot: var(--cc-fab-slot-3);
}

.edition-hero {
  display: grid;
  grid-template-columns: minmax(138px, 230px) minmax(0, 1fr);
  gap: clamp(1.4rem, 4vw, 3.25rem);
  /* The answer ledger is a full-width row above cover+meta, and the column gap
     above is far too large to double as the gap beneath it. */
  row-gap: 1.05rem;
  align-items: end;
  max-width: 1120px;
  margin-top: 1.4rem;
}

.edition-cover-wrap {
  position: relative;
  width: min(100%, 230px);
  transform: rotate(-1.25deg);
}

.edition-cover-wrap::before {
  content: "";
  position: absolute;
  inset: 5% -7% -5% 7%;
  z-index: -1;
  border: 1px solid color-mix(in srgb, var(--text) 18%, transparent);
  background: color-mix(in srgb, var(--surface) 34%, transparent);
  transform: rotate(3deg);
}

.edition-hero-cover {
  width: 100%;
  height: auto; /* neutralize the height="600" attribute so aspect-ratio governs */
  aspect-ratio: 2 / 3;
  display: block;
  object-fit: cover;
  border-radius: 3px;
  background: var(--surface-2);
  box-shadow: 0 28px 58px -34px color-mix(in srgb, var(--shadow-ink) 72%, transparent), 0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent);
}

.edition-hero-meta {
  min-width: 0;
  padding-bottom: 0.5rem;
}

.edition-kicker {
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  align-items: center;
  margin: 0 0 0.8rem;
  color: var(--text-muted);
  font-size: 0.76rem;
  font-weight: 800;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}

.edition-kicker span {
  display: inline-flex;
  align-items: center;
}

/* Publisher link in the kicker — use the hero accent, not Bootstrap blue. */
.edition-kicker a {
  color: var(--ed-accent);
  text-decoration: none;
  transition: color 0.15s ease;
}
.edition-kicker a:hover {
  color: color-mix(in srgb, var(--ed-accent) 78%, var(--text));
  text-decoration: underline;
}

.edition-kicker span + span::before {
  content: "";
  width: 4px;
  height: 4px;
  margin-right: 0.55rem;
  border-radius: 50%;
  background: var(--ed-accent);
}

.edition-hero-title {
  /* Cap driven by a per-tier variable (like --title-scale) so longer titles,
     which are shrunk to a smaller font, are allowed a WIDER box — otherwise a
     ch-based cap narrows exactly as the font shrinks, clipping long titles
     while the column beside them stays empty. Fallback = tier-1 (short) cap. */
  max-width: var(--title-maxw, 15ch);
  margin: 0;
  color: var(--ed-ink);
  font-family: var(--font-display);
  /* Base size per breakpoint (--ed-title-base) * length-tier shrink step
     (--title-scale). Tier classes below set the scale; tier 1 = no class = 1. */
  font-size: calc(var(--ed-title-base, 3.4rem) * var(--title-scale, 1));
  font-weight: 400;
  line-height: 0.92;
  letter-spacing: 0;
  /* Hard cap above the phone breakpoint: after shrinking, a title never
     exceeds 3 lines. Phones (and the wide modal) unclamp it below. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Length-tier shrink steps (see services/title_tier.py). Each longer tier also
   widens the cap so the (smaller-font) title still fits in the 3-line box using
   the horizontal room the byline/synopsis below it already occupy. */
.edition-hero-title.ed-title-t2 { --title-scale: 0.78; --title-maxw: 20ch; }
.edition-hero-title.ed-title-t3 { --title-scale: 0.6; --title-maxw: 26ch; }
.edition-hero-title.ed-title-t4 { --title-scale: 0.48; --title-maxw: 32ch; }

/* Variant qualifier ("Signed Edition", "2nd Printing"). Reads as a chip under
   the title so a separately solicited SKU is visibly distinct from the standard
   printing it sits next to. `:empty` hides it before Alpine boots — the element
   is always rendered so the swap picker can populate it, and x-show would
   otherwise leave an empty box for one frame. */
.edition-qualifier {
  display: inline-block;
  margin: 0.5rem 0 0;
  padding: 0.15rem 0.55rem;
  border: 1px solid var(--ed-line);
  border-radius: 999px;
  color: var(--ed-accent);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.01em;
}
.edition-qualifier:empty { display: none; }

.edition-byline {
  margin: 0.7rem 0 0;
  color: var(--text-muted);
  font-size: 0.95rem;
  font-weight: 600;
}

/* Creator links in the byline — use the hero accent, not Bootstrap blue. */
.edition-byline a {
  color: var(--ed-accent);
  text-decoration: none;
  font-weight: 700;
  transition: color 0.15s ease;
}
.edition-byline a:hover {
  color: color-mix(in srgb, var(--ed-accent) 78%, var(--text));
  text-decoration: underline;
}

/* ---- Hero answer ledger --------------------------------------------------
   Occupies the space the synopsis teaser used to. That teaser reprinted the
   first ~180 characters of a blurb the dossier already ships in full, so it
   was a duplicate rather than a summary; this states what the page COMPUTED
   instead, and leads the hero with it.

   Rendered at EVERY breakpoint. The teaser it replaces was display:none below
   720px, and the phone is the device this line matters most on -- a reader
   with a checkout tab open wants the overlap, not the solicitation copy.

   The slot reserves its own height because all three numbers arrive from
   separate async fetches: without it, each landing number would push the 54px
   title down the page. min-height is measured against the resolved ledger so
   the pending and resolved states occupy the same box. */
.edition-answer {
  /* Spans both hero columns and leads the row: the cover art and the display
     title sit underneath the line, not beside it. Full width is also what
     keeps the mobile wrap to two lines rather than three -- in the meta column
     the ledger had ~220px to work with at 390px. */
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  min-height: 2.1rem;
  margin: 0;
}

.edition-answer-ledger,
.edition-answer-pending {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  /* The separator now sits INSIDE each segment as a trailing dot, so the
     column gap only has to keep the dot off the next figure -- 0.95rem on top
     of it pushed the second segment onto its own line at 390px for no reason
     a reader could see. */
  gap: 0.35rem 0.55rem;
  margin: 0;
}

.ea-seg {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  color: inherit;
  text-decoration: none;
}

.ea-n {
  color: var(--ed-ink);
  font-family: var(--font-display);
  font-size: 1.45rem;
  font-weight: 400;
  line-height: 1;
  /* Tabular figures: the three numbers change independently as their fetches
     land, and proportional digits reflow the labels beside them when they do. */
  font-variant-numeric: tabular-nums;
}

.ea-l {
  color: var(--text-muted);
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}

/* The payoff. "Complete match" is the verdict the whole page exists to
   deliver, so its FIGURE takes the accent while the two counts leading to it
   stay neutral -- one emphasis, not three.

   The label deliberately does not follow the figure. Light-theme
   --accent-canonical is the metallic gold theme.css marks as its own contrast
   EXCEPTION; measured against the cream hero field it is 3.31:1. That clears AA for the 23.2px figure (large text, 3.0) and fails it
   for an 11.5px label (4.5), so the label keeps the neutral every other label
   uses -- 7.75:1 light, 7.93:1 dark. Sampled from rendered pixels, since the
   hero's background is a painted wash that a computed-style ancestor walk
   reads straight through. */
.ea-seg--match .ea-n {
  color: var(--ed-accent);
}

.ea-seg:hover .ea-l,
.ea-seg:focus-visible .ea-l {
  color: var(--ed-ink);
  text-decoration: underline;
}

/* Dot separator, the same one the kicker below draws between its own spans --
   but TRAILING, on every segment that has another after it.

   Leading (::before) is the obvious spelling and it breaks on the wrap: at
   390px the ledger runs to two lines, and a leading dot travels to the start
   of line two, where it reads as a bullet rather than as a separator. A
   trailing dot ends line one instead, which is the ordinary typographic cue
   that a line continues.

   :has(~ .ea-seg) rather than :not(:last-child), because Alpine leaves each
   x-if's own <template> element in the DOM after the segment it renders -- the
   final .ea-seg is never the last child, so :not(:last-child) would draw a
   separator after it with nothing following. */
.ea-seg:has(~ .ea-seg)::after {
  content: "";
  align-self: center;
  width: 4px;
  height: 4px;
  margin-left: 0.55rem;
  border-radius: 50%;
  background: var(--ed-accent);
}

.ea-pending-mark {
  display: inline-block;
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  background: var(--ed-accent);
  opacity: 0.55;
  animation: edition-pulse 900ms var(--ease-standard) infinite alternate;
}

.edition-specline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.85rem;
  margin: 0.9rem 0 0;
  color: var(--text-muted);
  font-size: 0.92rem;
  font-weight: 600;
}
.edition-specline .es-item { color: var(--text); font-weight: 700; white-space: nowrap; }
.edition-specline .es-item:first-child { color: var(--ed-accent); }

.edition-social-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  margin-top: 1rem;
}

.edition-recommend-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 40px;
  padding: 0.5rem 0.8rem;
  border: 1px solid var(--ed-line);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  color: var(--text);
  font-weight: 700;
  box-shadow: var(--shadow-sm);
}

.edition-recommend-btn:hover:not(:disabled) {
  border-color: color-mix(in srgb, var(--accent-canonical) 55%, transparent);
  color: var(--ed-accent);
}

.edition-recommend-btn:disabled {
  opacity: 0.72;
}

.edition-main {
  position: relative;
  margin-top: -3rem;
  padding-bottom: 1.25rem;
  /* Reserve the dossier's vertical space until Alpine hydrates the seeded
     dossier. Its x-if/x-text body can't render server-side, so for the brief
     pre-hydration window the area would otherwise collapse to the loading pill
     and the community sections below would leap down when it paints. A one-
     viewport reservation pushes those sections to the viewport edge, so the
     reflow lands off-screen (CLS ~0 on desktop and mobile). */
  min-height: 100vh;
}

/* Release the reservation the instant Alpine drops [x-cloak] from the now-
   rendered shell, so a rare dossier shorter than the viewport never leaves a
   gap. Degrades safely without :has() — the reservation simply persists, which
   still prevents the jump. */
.edition-main:has(.edition-dossier-shell:not([x-cloak])) {
  min-height: 0;
}

.edition-anchor-target {
  display: block;
  height: 0;
  overflow: hidden;
  scroll-margin-top: 1rem;
}

.edition-loading {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  margin: 0;
  padding: 0.85rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-muted);
  font-weight: 700;
  box-shadow: var(--shadow-md);
}

.edition-loading-mark {
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  background: var(--ed-accent);
  animation: edition-pulse 900ms var(--ease-standard) infinite alternate;
}

.edition-dossier-shell {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--ed-panel);
  box-shadow: 0 26px 70px -48px color-mix(in srgb, var(--shadow-ink) 72%, transparent);
}

/* Dossier-shaped loading skeleton. Reuses the real .edition-dossier-shell frame and
   the shared skeleton primitives so the placeholder occupies the same footprint as
   the loaded dossier and the shimmer matches the rest of the site. A gentle fade keeps
   the swap from feeling like a hard cut. */
.edition-view .dossier-skeleton {
  padding: clamp(1rem, 2.5vw, 1.65rem);
  animation: dossier-skeleton-in 240ms var(--ease-standard) both;
  --sk-base: color-mix(in srgb, var(--ed-line, var(--border)) 60%, transparent);
  --sk-hi: color-mix(in srgb, var(--text-muted) 20%, transparent);
}

.edition-view .dossier-skeleton .skeleton-block,
.edition-view .dossier-skeleton .skeleton-line {
  /* Slightly warmer than the default surface pair so the shimmer reads on the panel. */
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--ed-line, var(--border)) 60%, transparent),
    color-mix(in srgb, var(--text-muted) 20%, transparent),
    color-mix(in srgb, var(--ed-line, var(--border)) 60%, transparent));
  background-size: 220% 100%;
  border-color: transparent;
}

.dsk-cover {
  width: 100%;
  aspect-ratio: 2 / 3;
  height: auto;
  border-radius: 3px;
}

.dsk-chips {
  display: flex;
  gap: 0.4rem;
  margin-top: 0.6rem;
}

.dsk-pill-sm {
  width: 3.4rem;
}

.dsk-facts {
  display: grid;
  gap: 0.7rem;
  margin-top: 0.9rem;
}

.dsk-facts .skeleton-line {
  height: 0.72rem;
}
.dsk-facts .skeleton-line:nth-child(2n) {
  width: 78%;
}
.dsk-facts .skeleton-line:nth-child(3n) {
  width: 88%;
}

.dsk-h {
  height: 1rem;
  width: 42%;
  max-width: 220px;
  margin: 0.35rem 0 1rem;
}
.dossier-content .dsk-h:not(:first-child) {
  margin-top: 1.85rem;
}

.dsk-cover-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 0.85rem;
}

.dsk-card {
  aspect-ratio: 2 / 3;
  border-radius: 4px;
}

.dsk-chip-cloud {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.dsk-chip-cloud .skel-pill:nth-child(3n) {
  width: 6.6rem;
}

.dsk-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@keyframes dossier-skeleton-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .edition-view .dossier-skeleton {
    animation: none;
  }
}

.edition-view--modal .edition-dossier-shell--loading {
  min-height: 1320px;
}

.edition-view .edition-dossier {
  padding: clamp(1rem, 2.5vw, 1.65rem);
}

.edition-view .dossier-panes {
  display: grid;
  grid-template-columns: minmax(190px, 240px) minmax(0, 1fr);
  gap: clamp(1.25rem, 3vw, 2.4rem);
}

.edition-view .dossier-rail {
  width: auto;
  top: 1rem;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--surface-2) 82%, transparent);
}

/* The shared view already presents the cover, binding, pages, publisher and
   MSRP in its hero. Keep only additional facts and actions in the rail; bare
   dossier surfaces still need their complete rail. */
.edition-view .edition-dossier-shell--hero-facts .dossier-rail .rail-cover,
.edition-view .edition-dossier-shell--hero-facts .dossier-rail .dossier-chips,
.edition-view .edition-dossier-shell--hero-facts .dossier-rail .rail-facts .rail-core-fact,
.edition-view .edition-dossier-shell--hero-facts .dossier-rail .rail-buy--msrp {
  display: none;
}

.edition-view .dossier-rail .rail-price,
.edition-view .dossier-buy-band .rail-price {
  font-size: 1.75rem;
  line-height: 1;
}

.edition-view .dossier-rail .rail-facts .rf {
  align-items: baseline;
  padding: 0.45rem 0;
}

.edition-view .dossier-rail .rail-facts .rf span,
.edition-view .dossier-rail .rail-store-title,
.edition-view .dossier-buy-band .rail-store-title,
.edition-view .dossier-content .dsec .role-label {
  letter-spacing: 0.09em;
}

.edition-view .dossier-rail .rail-store-link,
.edition-view .dossier-buy-band .rail-store-link,
.edition-view .dossier-manage > summary {
  min-height: 44px;
}

.edition-view .dossier-rail .rail-store-link,
.edition-view .dossier-buy-band .rail-store-link {
  transition:
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard),
    background-color var(--motion-fast) var(--ease-standard),
    transform var(--motion-instant) var(--ease-standard);
}

.edition-view .dossier-rail .rail-store-link:hover,
.edition-view .dossier-buy-band .rail-store-link:hover {
  transform: translateY(-1px);
}

/* Server-rendered "Other covers / Also available as" rail on the edition
   page (show_edition_siblings). Sits below the dossier shell, outside
   .dossier-rail, so it gets its own copy of the rail-store-* look rather
   than inheriting the dossier-scoped rules above. */
.edition-sibling-rail {
  margin-top: 1.5rem;
  padding: 1rem;
  display: grid;
  gap: 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.edition-sibling-rail .rail-store-title {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.edition-sibling-rail .rail-store-title:not(:first-child) {
  margin-top: 0.75rem;
}

.edition-sibling-rail .rail-store-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  min-height: var(--dossier-action-min-height, 44px);
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
  transition:
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard),
    background-color var(--motion-fast) var(--ease-standard),
    transform var(--motion-instant) var(--ease-standard);
}

.edition-sibling-rail .rail-store-link:hover {
  transform: var(--dossier-action-hover-transform, translateY(-1px));
}

.edition-view .dossier-content {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem;
}

.edition-view .dossier-content .dsec,
.edition-view .dossier-manage {
  margin: 0;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.edition-view .dossier-content .dsec > h6,
.edition-view .dossier-content .dsec-h {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  border-bottom: 0;
  padding-bottom: 0;
  color: var(--text);
  font-size: 0.8rem;
}

/* Anchor targets for the hero ledger's segments. Clears the section's own
   1rem padding so the summary row is not flush against the viewport edge. */
.edition-view .dossier-content #dossier-contents,
.edition-view .dossier-content #dossier-related {
  scroll-margin-top: 1.25rem;
}

.edition-view .dossier-content .dsec > h6::before,
.edition-view .dossier-content .dsec-h::before {
  content: "";
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--ed-accent);
}

.edition-view .dossier-content .dsec p {
  max-width: 78ch;
  color: var(--text);
  font-size: 0.98rem;
}

.edition-view .list-group {
  gap: 0.4rem;
}

.edition-view .list-group-item {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text);
}

.edition-view .list-group-item + .list-group-item {
  border-top-width: 1px;
}

.edition-view .list-group-item-action:hover {
  border-color: var(--border-strong);
  background: color-mix(in srgb, var(--surface-2) 78%, var(--ed-accent) 8%);
  color: var(--text);
}

/* Contents-issue links inherit the list item's --text instead of Bootstrap blue. */
.edition-view .list-group-item > a {
  color: inherit;
}

.edition-view .cc-pill--compact {
  min-height: 32px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--surface-2) 88%, var(--ed-accent) 5%);
}

.edition-view .dossier-manage {
  overflow: hidden;
}

/* When open, let the merge widget's typeahead dropdown (.bts-results,
   position:absolute) escape the panel instead of being clipped. Collapsed
   keeps overflow:hidden so the summary's negative-margin bleed stays clipped
   at the rounded corners. */
.edition-view .dossier-manage[open] {
  overflow: visible;
}

.edition-view .dossier-manage > summary {
  margin: -1rem;
  padding: 0.85rem 1rem;
}

.edition-view .dossier-manage > summary::after {
  content: "+";
  color: var(--ed-accent);
  font-size: 1.15rem;
  line-height: 1;
}

.edition-view .dossier-manage[open] > summary::after {
  content: "-";
}

.edition-view .dossier-manage .dm-body {
  margin: 1rem -1rem -1rem;
  padding: 1rem;
  background: color-mix(in srgb, var(--surface-2) 70%, transparent);
  border-bottom-left-radius: var(--radius-sm);
  border-bottom-right-radius: var(--radius-sm);
}

.edition-view .dossier-debug {
  border-style: dashed;
}

.edition-community {
  max-width: 1120px;
}

/* Reviews + comments share the dossier's panel language so the whole page
   reads as one editorial document rather than a styled card bolted to a
   bare Bootstrap list. */
.edition-community .reviews-section,
.edition-community .comments-section {
  margin-top: 0;
  padding: clamp(1.1rem, 2.5vw, 1.65rem);
  border: 1px solid var(--border);
  border-top: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--ed-panel, var(--surface));
  box-shadow: 0 26px 70px -48px color-mix(in srgb, var(--shadow-ink) 72%, transparent);
}

.edition-community .reviews-heading,
.edition-community .comments-heading {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 1rem;
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--ed-ink, var(--text));
}

.edition-community .reviews-heading::before,
.edition-community .comments-heading::before {
  content: "";
  flex: none;
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  background: var(--ed-accent, var(--accent-canonical));
}

@media (max-width: 640px) {
  .edition-community .reviews-section,
  .edition-community .comments-section {
    padding: 1rem;
  }
  .edition-community .reviews-heading,
  .edition-community .comments-heading {
    font-size: 1.3rem;
  }
}

@keyframes edition-pulse {
  from { opacity: 0.35; transform: scale(0.85); }
  to { opacity: 1; transform: scale(1.08); }
}

@container edition (max-width: 860px) {
  .edition-masthead {
    min-height: 0;
    padding-bottom: 4rem;
  }

  .edition-hero {
    grid-template-columns: 132px minmax(0, 1fr);
    align-items: center;
  }

  /* The title shrinks hard at this width (tier scaling takes it to ~22px),
     so the ledger's figures have to come down with it -- at the desktop
     1.45rem they are LARGER than the title of the book they describe, which
     inverts the hierarchy. Leading is a matter of position and colour here,
     not of out-sizing the name of the thing. */
  .ea-n {
    font-size: 1.15rem;
  }

  /* Two lines, because three honest labels cannot fit one at this width:
     measured 541px of content against 366px of room at 390px. Reserving the
     wrapped height is what keeps the numbers from pushing the cover and the
     title down as each fetch lands -- desktop needs no equivalent, since the
     ledger fits one line there in both states.

     The cost is ~29px of quiet space on an edition that turns out to have no
     related editions at all, whose ledger resolves to the issue count alone.
     That is the deliberate trade: a fixed slot with occasional slack beats a
     hero that moves under the reader's thumb on the device they read it on. */
  .edition-answer {
    min-height: 3.25rem;
  }

  .edition-hero-title {
    /* Set the cap via the variable (not max-width directly) so the per-tier
       widths above still widen long titles at this breakpoint; tier-1 keeps the
       14ch cap. Longer tiers clamp to the narrower column here anyway. */
    --title-maxw: 14ch;
    --ed-title-base: 3.25rem;
  }

  .edition-view .dossier-panes {
    grid-template-columns: 1fr;
  }

  /* Lead with the synopsis: content column first, rail drops below it. */
  .edition-view .dossier-content {
    order: -1;
  }

  .edition-view .dossier-rail {
    position: static;
    display: block;
  }

  /* Cover and Own/Want already live in the masthead on mobile; price stays in
     the rail since the hero spec line no longer carries it. */
  .edition-view .dossier-rail .rail-cover,
  .edition-view .dossier-rail .rail-actions {
    display: none;
  }
}

/* Chrome that lives inside the container but stays viewport-keyed: the backlink
   is a touch-target on the page; its sizing tracks the viewport, not the modal. */
@media (max-width: 640px) {
  .edition-backlink {
    min-height: 44px;
  }
}

@container edition (max-width: 640px) {
  .edition-masthead {
    padding: 0.85rem 0 3.25rem;
  }

  .edition-masthead-wash {
    width: 88vw;
    opacity: 0.12;
  }

  .edition-hero {
    grid-template-columns: 104px minmax(0, 1fr);
    gap: 1rem;
    margin-top: 0.6rem;
  }

  .edition-cover-wrap {
    width: 104px;
  }

  /* Keep cover + title side-by-side at every mobile width (no single-column
     stack). The title wraps in full here (GH-726): the 3-line clamp cut long
     titles off with only the hover-only `title` tooltip as the reveal, which
     touch cannot reach. At 1.35rem a long title costs a few lines of hero
     height, not a blown-up layout; `anywhere` keeps an unbroken token from
     forcing horizontal overflow. */
  .edition-hero-title {
    --ed-title-base: 1.35rem;
    line-height: 1.08;
    max-width: 100%;
    display: block;
    -webkit-line-clamp: unset;
    -webkit-box-orient: initial;
    overflow: visible;
    overflow-wrap: anywhere;
  }
  /* Neutralize the length-tier shrink on mobile: these selectors match the tier
     rules' specificity (0,2,0) and win by source order, so mobile shows the
     full title at the base size rather than shrinking the (already small) text
     further. */
  .edition-hero-title.ed-title-t2,
  .edition-hero-title.ed-title-t3,
  .edition-hero-title.ed-title-t4 { --title-scale: 1; }
  .edition-specline { font-size: 0.82rem; margin-top: 0.6rem; }

  .edition-byline,

  .edition-kicker {
    gap: 0.35rem;
    margin-bottom: 0.55rem;
    font-size: 0.68rem;
  }

  .edition-main {
    margin-top: -2rem;
    padding-left: 0.75rem;
    padding-right: 0.75rem;
  }

  .edition-view .edition-dossier {
    padding: 0.75rem;
  }

  .edition-view .dossier-content .dsec,
  .edition-view .dossier-manage {
    padding: 0.85rem;
  }

  .edition-view .dossier-manage > summary {
    margin: -0.85rem;
    padding: 0.85rem;
  }

  .edition-view .dossier-manage .dm-body {
    margin: 0.85rem -0.85rem -0.85rem;
    padding: 0.85rem;
  }

  .edition-view .d-flex.flex-wrap {
    gap: 0.35rem;
  }

  .edition-view .btn,
  .edition-view .list-group-item-action {
    min-height: 44px;
  }

  .edition-view .list-group-item {
    align-items: flex-start !important;
    gap: 0.65rem;
    padding: 0.7rem 0.75rem;
  }

  /* Dossier rail facts → bordered spec table on mobile. */
  .edition-view .dossier-rail .rail-facts {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--surface-2) 60%, transparent);
    overflow: hidden;
  }
  .edition-view .dossier-rail .rail-facts .rf {
    display: flex;
    justify-content: space-between;
    gap: 0.75rem;
    margin: 0;
    padding: 0.7rem 0.8rem;
    border-bottom: 1px solid var(--border);
  }
  .edition-view .dossier-rail .rail-facts .rf:last-child { border-bottom: 0; }
  .edition-view .dossier-rail .rail-facts .rf > span { color: var(--text-muted); }
  .edition-view .dossier-rail .rail-facts .rf b { color: var(--text); text-align: right; }
}

@container edition (max-width: 430px) {
  .edition-hero {
    grid-template-columns: 84px minmax(0, 1fr);
  }
  .edition-cover-wrap { width: 84px; }
}

@media (max-width: 640px) {
  /* GH-124. The dice FAB does not render on a phone at all.

     It is `position: fixed`, so it does not sit in a margin the way it does on
     a wide screen -- there is no margin at 390px, the text column is the whole
     viewport, and the FAB rides the scroll. Measured at 390x844: a 52x52 circle
     at top=776 with `elementsFromPoint` returning `.dossier-panes` underneath,
     i.e. it permanently covers a strip of the record no matter where the reader
     scrolls. Moving it (bottom-left, or inset from the gutter) only relocates
     the same overlap, because every horizontal position is over the column.

     Hiding it costs nothing reachable: the FAB is a link to /random, and
     `random` is a TOP-LEVEL entry in the nav (navigation.py), so the
     destination is one menu tap away on exactly the viewports this hides it on.
     Its `title="Random (R)"` advertises a keyboard shortcut, which is also the
     desktop-shaped half of the affordance.

     640px is this sheet's own definition of "mobile" -- the same boundary the
     sticky action bar uses below, so the page has one mobile edge and not two. */
  .edition-random-fab {
    display: none;
  }

  /* `:has()` matches PRESENCE IN THE DOM, not visibility, so the slot-3
     reservation above keeps matching a hidden FAB and would float the Discord
     CTA a slot high over a 52px hole. Same rule-set as the `display: none`
     that causes it, so the two can never disagree about the width. */
  body:has(.edition-random-fab) {
    --cc-cta-slot: var(--cc-fab-slot-2);
  }
}

@media (prefers-reduced-motion: reduce) {
  .edition-loading-mark,
  .ea-pending-mark,
  .edition-random-fab,
  .edition-view .dossier-rail .rail-store-link,
  .edition-view .dossier-buy-band .rail-store-link {
    animation: none;
    transition: none;
  }

  .edition-random-fab:hover,
.edition-view .dossier-rail .rail-store-link:hover,
.edition-view .dossier-buy-band .rail-store-link:hover {
    transform: none;
  }
}

/* Sticky action bar — mobile only. Hidden until the hero scrolls off. */
.edition-stickybar { display: none; }

@media (max-width: 640px) {
  .edition-stickybar {
    --lg-safe-bg: color-mix(in srgb, var(--bg) 88%, transparent);
    --lg-safe-blur: blur(10px);
    display: flex;
    gap: 0.5rem;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 24;
    padding: 0.55rem 0.75rem calc(0.55rem + env(safe-area-inset-bottom));
    padding-right: 4.5rem; /* leave room for the round dice FAB on the right edge */
    background: transparent;
    border-top: 1px solid var(--border);
    transform: translateY(120%);
    transition: transform var(--motion-standard, 240ms) var(--ease-standard);
    isolation: isolate;
  }
  .edition-stickybar.is-shown { transform: translateY(0); }
  .edition-stickybar .esb-btn {
    flex: 1;
    min-height: 46px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--surface) 80%, transparent);
    color: var(--text);
    font-weight: 800;
    font-size: 0.9rem;
    text-decoration: none;
  }
  .edition-stickybar .esb-btn.is-own,
  .edition-stickybar .esb-btn.is-want {
    border-color: color-mix(in srgb, var(--accent-canonical) 55%, transparent);
    color: var(--ed-accent);
  }
  .edition-stickybar .esb-log { flex: 0 0 auto; padding: 0 0.8rem; color: var(--text-muted); }

  /* Lift the dice FAB above the bar ONLY while the bar is docked. The bar
     precedes the FAB in the DOM (Step 2), so this sibling selector resolves. */
  /* GH-124 removed `.edition-stickybar.is-shown ~ .edition-random-fab
     { bottom: 4.75rem; }` from here. The sticky bar only exists inside this
     640px block and the FAB is now `display: none` for its whole width, so the
     rule could never apply again -- and it was a hardcoded offset outside the
     shared slot scale, which is the exact shape GH-126 removed everywhere else. */
}

/* ---- Data ribbon (desktop hero) ---- */
.edition-ribbon {
  display: flex;
  flex-wrap: wrap;
  margin-top: 1.6rem;
  max-width: 1120px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
}
.edition-advanced .er-cell,
.edition-ribbon .er-cell {
  flex: 1 1 calc(100% / 6);
  min-width: 0;
  box-sizing: border-box;
  padding: 0.95rem 1.1rem;
  border-right: 1px solid var(--border);
  text-decoration: none;
  color: inherit;
}
.edition-ribbon .er-cell:last-child { border-right: 0; }
.edition-ribbon a.er-cell:hover { background: color-mix(in srgb, var(--surface) 80%, var(--ed-accent) 10%); }
.edition-ribbon .er-cta { background: color-mix(in srgb, var(--accent-canonical) 6%, transparent); }
/* Value + label rows both reserve a fixed height for EVERY cell, so a cell's
   own font size can no longer decide where its label lands. Without this the
   strip rendered three label baselines at once: the 1.4rem values (Cover price,
   Pages), the 1.02rem `er-sm` values (Format, Released) and the taller social
   cells each ended at a different y. Centre rather than bottom-align so a short
   `er-sm` value sits optically centred in the reserved box. */
.edition-advanced .er-v,
.edition-ribbon .er-v {
  font-family: var(--font-display);
  font-size: 1.4rem;
  line-height: 1;
  color: var(--text);
  /* Keeps a long unbreakable value shrinkable: .er-v is now a flex container,
     and `anywhere` (unlike break-word) reduces the item's automatic minimum
     size, so free text such as PaperStock cannot widen the column. */
  overflow-wrap: anywhere;
  min-height: 1.5rem;
  display: flex;
  align-items: center;
}
.edition-advanced .er-v.er-sm,
.edition-ribbon .er-v.er-sm { font-size: 1.02rem; }
.edition-ribbon .er-v.er-gold { color: var(--ed-accent); }
.edition-ribbon .er-cta .er-v { font-family: var(--font-body); font-size: 0.92rem; font-weight: 800; color: var(--ed-accent); }
/* Community cell that has real data: drop the CTA tint, render like a fact. */
.edition-ribbon .er-cell.er-has-data { background: transparent; }
.edition-ribbon .er-cell.er-has-data .er-v { font-family: var(--font-display); font-size: 1.4rem; font-weight: 400; color: var(--text); }
.edition-advanced .er-k,
.edition-ribbon .er-k {
  margin-top: 0.42rem;
  font-size: 0.63rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  /* 1.4rem is the Rating cell's own natural label height -- its .er-rating-k
     row carries the recommend pill -- so levelling the plain labels up to it
     costs the ribbon no extra vertical space. */
  min-height: 1.4rem;
  display: flex;
  align-items: center;
}

@container edition (max-width: 1024px) {
  .edition-ribbon .er-cell { flex-basis: calc(100% / 3); }
  /* keep row borders tidy when wrapping to 2 rows of 3 */
  .edition-ribbon .er-cell:nth-child(3n) { border-right: 0; }
}
@container edition (max-width: 560px) {
  .edition-ribbon .er-cell { flex-basis: 50%; }
  .edition-ribbon .er-cell:nth-child(3n) { border-right: 1px solid var(--border); }
  .edition-ribbon .er-cell:nth-child(2n) { border-right: 0; }
  .edition-ribbon .er-cell:last-child { border-right: 0; }
}

/* ---- Advanced disclosure: the ribbon's footer tray ----
   One box with the ribbon, not a widget under it. The ribbon gives up its
   bottom corners and the disclosure carries the side and bottom edges, so the
   ribbon's bottom border is the seam. Closed, it is one slim row in the
   ribbon's label type; open, its facts are ribbon cells (the cell, value and
   label rules above are shared) on the ribbon's own column basis, so Size
   sits under MSRP and the source outlink closes the row on the right. */
.edition-ribbon:has(+ .edition-advanced) {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.edition-advanced {
  max-width: 1120px;
  border: 1px solid var(--border);
  border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
  overflow: hidden;
  background: var(--surface);
}
.edition-advanced > summary.eadv-toggle {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem 1.1rem;
  cursor: pointer;
  list-style: none;
  font-size: 0.63rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.edition-advanced > summary::-webkit-details-marker { display: none; }
.edition-advanced > summary:hover {
  color: var(--text);
  background: color-mix(in srgb, var(--surface) 80%, var(--ed-accent) 10%);
}
.edition-advanced > summary:focus-visible {
  outline: 2px solid var(--ed-accent);
  outline-offset: -2px;
}
.edition-advanced .eadv-chev {
  font-size: 0.7rem;
  transition: transform 0.15s ease;
}
.edition-advanced[open] .eadv-chev { transform: rotate(180deg); }
/* Every cell draws its right and bottom edge, and the row reaches 1px past
   the box on those sides so the outermost edges are clipped by the box's
   overflow. Whatever the facts wrap into -- one row of six, rows of three,
   rows of two -- the dividers come out as one grid with no doubled edge. The
   <dl>'s `display: contents` means `nth-child` cannot find a row's end. */
.edition-advanced .eadv-row {
  display: flex;
  flex-wrap: wrap;
  margin: 0 -1px -1px 0;
  border-top: 1px solid var(--border);
}
/* The facts' <dl> is a grouping for assistive tech only; its cells sit in the
   row beside the outlink. */
.edition-advanced .eadv-facts { display: contents; }
.edition-advanced .er-cell {
  flex: 0 0 calc(100% / 6);
  border-bottom: 1px solid var(--border);
  /* <dt> must precede <dd>; the ribbon reads value-over-label. */
  display: flex;
  flex-direction: column-reverse;
}
.edition-advanced dd { margin: 0; }
.edition-advanced .eadv-src { flex-direction: column; }
/* Beside facts, the outlink closes the row on the right; alone, it simply
   takes the first column rather than stranding itself across an empty row. */
.edition-advanced .eadv-facts ~ .eadv-src {
  margin-left: auto;
  border-left: 1px solid var(--border);
}
/* Provenance, not a fact: the ribbon's CTA value type, not its display face. */
.edition-advanced .eadv-src .er-v {
  gap: 0.4rem;
  font-family: var(--font-body);
  font-size: 0.92rem;
  font-weight: 800;
  color: var(--ed-accent);
}
.edition-advanced .eadv-src .bi { font-size: 0.8rem; }
.edition-advanced .eadv-src:hover { background: color-mix(in srgb, var(--surface) 80%, var(--ed-accent) 10%); }
/* Wrapped, the outlink takes the next column like any cell: pushed right it
   could leave a gap the grid lines would not close. */
@container edition (max-width: 1024px) {
  .edition-advanced .er-cell { flex-basis: calc(100% / 3); }
  .edition-advanced .eadv-facts ~ .eadv-src { margin-left: 0; border-left: 0; }
}
@container edition (max-width: 560px) {
  .edition-advanced .er-cell { flex-basis: 50%; }
}
@media (max-width: 640px) {
  .edition-advanced .er-cell { padding: 0.5rem 0.9rem; }
  .edition-advanced .er-v { min-height: 1.1rem; }
  .edition-advanced .er-k { margin-top: 0.2rem; min-height: 0; }
  .edition-advanced > summary.eadv-toggle { min-height: 44px; padding-block: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .edition-advanced .eadv-chev { transition: none; }
}

/* ---- Social ribbon cells (shared: edition page + issue page) ---- */
.edition-ribbon .er-social { position: relative; background: color-mix(in srgb, var(--accent-canonical) 5%, transparent); }
.edition-ribbon .er-social::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--accent-canonical) 80%, transparent), transparent);
}
/* Only the type differs here. The reserved value-row height that keeps the dim
   placeholder (— / dot) pixel-identical to the loaded content (16px avatars /
   stars + number) now comes from the shared .er-v rule above, which every cell
   gets -- so the four static cells neither resize when the social cells fill in
   nor sit on a different baseline from them. */
.edition-ribbon .er-social .er-v { font-family: var(--font-display); font-size: 1.25rem; }
.edition-ribbon .er-social-ph {
  font-family: var(--font-display); font-size: 1.25rem; line-height: 1;
  color: color-mix(in srgb, var(--on-image) 18%, transparent);
}
@media (prefers-reduced-motion: no-preference) {
  @keyframes er-social-in { from { opacity: 0; } to { opacity: 1; } }
  .edition-ribbon .er-social-in { animation: er-social-in 0.12s ease-out; }
}
.edition-ribbon .er-readers-btn,
.edition-ribbon .er-rating-btn {
  background: none; border: 0; padding: 0; margin: 0; color: inherit; font: inherit;
  cursor: pointer; display: inline-flex; align-items: center;
}
/* The Rating cell's button IS its value row (`class="er-rating-btn er-v"`),
   so the inline-flex above lands on the .er-v itself at equal specificity and,
   being later in the sheet, wins. An inline-level box adds its line box's
   half-leading on top of the reserved height, which pushed that one label 1.8px
   below the other five. Block-level flex for the overlap only -- the Readers
   button is a CHILD of its .er-v, so it is a flex item, already blockified, and
   must keep inline-flex for the cells that nest it. */
.edition-ribbon .er-v.er-rating-btn { display: flex; }
.edition-ribbon .er-social-empty { font-family: var(--font-body); font-size: 0.9rem; color: var(--text-muted); }
.edition-ribbon .er-avstack { display: inline-flex; vertical-align: middle; margin-right: 7px; }
.edition-ribbon .er-av { width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid var(--surface); margin-left: -6px; display: inline-block; }
.edition-ribbon img.er-av { object-fit: cover; }
.edition-ribbon .er-av-1 { margin-left: 0; background: var(--edition-avatar-one); }
.edition-ribbon .er-av-2 { background: var(--edition-avatar-two); }
.edition-ribbon .er-av-3 { background: var(--edition-avatar-three); }
.edition-ribbon .er-av-friend { box-shadow: 0 0 0 1.5px var(--brand); }
.er-readers-pop .sp-pop-name { display: inline-flex; align-items: center; gap: 6px; }
.er-readers-pop .sp-pop-av { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; }
.er-readers-pop .sp-pop-friend-tag {
  font-size: 0.66rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--brand); border: 1px solid var(--brand); border-radius: 999px; padding: 0 5px;
}
.edition-ribbon .er-stars { display: inline-flex; gap: 1px; font-size: 0.85rem; line-height: 1; margin-right: 6px; }
.edition-ribbon .er-stars .on { color: var(--ed-accent); }
.edition-ribbon .er-stars .off { color: var(--text-dim); }
/* The ribbon's star row is an INPUT for a signed-in reader (the anonymous
   branch keeps the display-only .er-rating-btn above). It borrows .er-stars'
   scale and palette so the two read as the same object: what a visitor sees
   as the community average is where the reader sets their own rating. */
.edition-ribbon .er-rating-v { display: flex; align-items: center; }
.edition-ribbon .star-rate--ribbon { margin-right: 6px; }
.edition-ribbon .star-rate--ribbon button {
  background: none; border: 0; margin: 0; padding: 0 1px;
  font-size: 0.95rem; line-height: 1;
  /* These controls sit on the theme surface, rather than over cover art. */
  color: var(--text-muted);
}
.edition-ribbon .star-rate--ribbon button.is-on { color: var(--ed-accent); }
.edition-ribbon .star-rate--ribbon:hover button {
  color: var(--text);
}
.edition-ribbon .star-rate--ribbon:hover button.is-on { color: var(--ed-accent); }
.edition-ribbon .er-rating-k { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.edition-ribbon .er-sep { color: var(--text-dim); }
.edition-ribbon .er-rec {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  font-size: 0.72rem; font-weight: 800; line-height: 1; padding: 3px 8px;
  border-radius: 999px; border: 1px solid color-mix(in srgb, var(--accent-canonical) 50%, transparent);
  color: var(--ed-accent); background: color-mix(in srgb, var(--accent-canonical) 7%, transparent);
  text-decoration: none; transition: background .12s, color .12s, border-color .12s;
}
.edition-ribbon .er-rec .er-rec-em { font-size: 0.9rem; }
.edition-ribbon .er-rec.is-on { background: var(--ed-accent); color: var(--edition-rec-on-ink); border-color: var(--ed-accent); }
.edition-ribbon .er-readers-pop { position: absolute; z-index: 30; margin-top: 4px; left: 0; }
.edition-view--modal .edition-ribbon .er-readers-pop--modal {
  position: fixed;
  z-index: 80;
  margin-top: 0;
  min-width: min(250px, calc(100vw - 32px));
  max-width: min(320px, calc(100vw - 32px));
}

.edition-view .rail-variants { margin-top: 0.6rem; }
.edition-view .rail-variants-title {
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
}
/* No empty-state rule by design: the rail renders nothing when an edition has
   no cover variants, rather than asserting "standard edition" (TTA-198). */
.edition-view .rail-variants-gallery {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.35rem;
}
.edition-view .rail-variant {
  display: block;
  width: 54px;
  border-radius: 3px;
  overflow: hidden;
  line-height: 0;
  border: 1px solid var(--ed-line);
}
.edition-view .rail-variant:hover,
.edition-view .rail-variant:focus-visible {
  border-color: var(--ed-accent);
}
.edition-view .rail-variant-cover {
  width: 100%;
  height: auto;
  display: block;
}

/* ---- Edition view inside a modal (detail modal parity) ---- */
/* The shared body normally relies on the page masthead's full-bleed wash and
   the page background. Inside a modal it is a contained band. */
.edition-view--modal .edition-masthead { padding: 0; background: none; }
.edition-view--modal .edition-masthead .container { padding: 0; }
.edition-view--modal .edition-hero { padding-top: .25rem; }
/* The page sticky bar / FAB never render in the fragment, but guard anyway. */
.edition-view--modal .edition-stickybar,
.edition-view--modal .edition-random-fab { display: none !important; }

/* No-layout-shift: reserve vertical space so late content fills downward
   instead of pushing siblings. Values approximate the typical filled height;
   real content sets its own height once it meets or exceeds the floor. */
/* The page pulls the dossier up under the masthead's tall padding-bottom
   (.edition-main { margin-top: -3rem }). The modal masthead has no such
   padding (set to 0 above), so that pull-up would overlap the ribbon — cancel
   it and add a clean gap below the ribbon instead. */
.edition-view--modal .edition-main { min-height: 460px; margin-top: 1.25rem; }
.edition-view--modal .edition-community { min-height: 160px; }
.edition-view--modal .edition-community-pulse-wrap { min-height: 56px; }

@container edition (min-width: 641px) {
  .edition-view--modal .edition-hero-title {
    max-width: 100%;
    display: block;
    -webkit-line-clamp: unset;
    -webkit-box-orient: initial;
    overflow: visible;
    overflow-wrap: anywhere;
  }
}

/* The desktop modal (~750px container) is wide enough for the dossier sidebar,
   so keep the two-column layout — rail left, content right, matching the page —
   instead of collapsing at the generic 860px breakpoint, which exists for
   phones and would otherwise bump the Buy/Find rail inline below the content.
   The phone full-screen modal (≤560px container) still stacks via the 860px
   rules above; this only re-establishes columns at ≥561px. Same specificity as
   the 860px rules, so source order (this is later) makes it win in 561–860px. */
@container edition (min-width: 561px) {
  .edition-view--modal .dossier-panes {
    grid-template-columns: minmax(180px, 230px) minmax(0, 1fr);
  }
  .edition-view--modal .dossier-content { order: 0; }
}


/* Skeleton blocks (used by both the pre-fetch skeleton and the in-fragment
   dossier skeleton). Solid neutral fill + opacity pulse — NOT a gradient, so
   it stays clear of the gradients.css "gradient literals live in one file" rule. */
.ev-skel-line, .ev-skel-block, .ev-skel-section, .ev-skel-cover,
.ev-skel-ribbon {
  background: color-mix(in srgb, var(--on-image) 7%, transparent);
  animation: ev-pulse 1.4s ease-in-out infinite;
  border-radius: 6px;
}
@keyframes ev-pulse { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
.ev-skel-hero { display: flex; gap: 1rem; padding: .5rem 0 1rem; }
.ev-skel-cover { width: 140px; height: 196px; flex: 0 0 auto; }
.ev-skel-herometa { flex: 1; display: flex; flex-direction: column; gap: .6rem; }
.ev-skel-line { height: 14px; width: 100%; }
.ev-skel-line--sm { width: 35%; height: 10px; }
.ev-skel-line--lg { width: 70%; height: 22px; }
.ev-skel-specline { width: 50%; height: 12px; }
.ev-skel-ribbon { height: 44px; width: 100%; margin-top: .4rem; }
.ev-skel-ribbon-bar { height: 56px; margin: .5rem 0; display: flex; gap: 1px; }
.ev-skel-block { height: 120px; margin: .75rem 0; }
.ev-skel-section { height: 140px; margin: .75rem 0; }
.ev-skel-dossier { display: flex; flex-direction: column; gap: .6rem; }
.ev-dossier-skeleton { display: flex; flex-direction: column; gap: .6rem; min-height: 460px; }
@media (prefers-reduced-motion: reduce) {
  .ev-skel-line, .ev-skel-block, .ev-skel-section, .ev-skel-cover, .ev-skel-ribbon { animation: none; }
}

/* Coordinated fade: the mount starts opaque (skeleton visible), goes to 0
   before the innerHTML swap, then transitions back to 1 so the real content
   fades in smoothly — masking the height delta between skeleton and SSR. */
[data-ev-mount] {
  transition: opacity 150ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
  [data-ev-mount] { transition: none; }
}

/* Inline store-link curation (admin only) */
.rail-store-admin { margin-top: .5rem; }
.rail-store-admin-toggle { font-size: .8rem; background: none; border: 1px solid var(--border); border-radius: 6px; padding: .2rem .5rem; cursor: pointer; }
.rail-store-admin-panel { margin-top: .5rem; display: flex; flex-direction: column; gap: .35rem; }
.rail-store-admin-row { display: flex; align-items: center; gap: .4rem; font-size: .8rem; flex-wrap: wrap; }
.rail-store-admin-name { font-weight: 600; }
.rail-store-admin-tag { font-size: .7rem; opacity: .6; }
.rail-store-admin-edit input, .rail-store-admin-add input { width: 100%; max-width: 18rem; }
.rail-store-admin-error { color: var(--edition-admin-error); font-size: .78rem; }
.rail-store-admin-status { color: var(--edition-admin-status); font-size: .78rem; }

/* The canonical issue modal reuses the dossier rail+content but has no masthead/
   hero, so its rail cover is the only cover. Keep it visible on mobile, where the
   shared rule hides the edition rail cover (there the hero owns the cover). Equal
   specificity to the hiding rule, later in source order, so it wins. */
@media (max-width: 900px) {
  .edition-view--issue-modal .dossier-rail .rail-cover { display: block; }
}

.edition-page > .edition-compare-builder {
  margin: 1rem auto 1.25rem;
  max-width: min(1120px, calc(100vw - 2rem));
}

/* Previous/next through this edition's own format sequence. The rail is ~214px
   wide, so one row of prev | position | next gave each neighbour title ~66px and
   a long one ("The Complete Fabulous Furry Freak Brothers Vol. 1") broke one word
   per line. Stack instead: the position heads the block and each neighbour gets
   the full rail width. An absent neighbour's empty span collapses. */
.dossier-rail .edition-format-nav {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "position" "prev" "next";
  margin-top: .75rem;
  padding-top: .75rem;
  border-top: 1px solid var(--border);
  font-size: .82rem;
}
.dossier-rail .edition-format-nav__prev { grid-area: prev; text-align: left; color: var(--text); }
.dossier-rail .edition-format-nav__next { grid-area: next; justify-self: end; text-align: right; color: var(--text); }
.dossier-rail .edition-format-nav > span:empty { display: none; }
.dossier-rail .edition-format-nav__position {
  grid-area: position;
  justify-self: start;
  color: var(--text-muted);
  white-space: nowrap;
}
.dossier-rail .edition-format-nav a {
  display: inline-block;
  min-height: 44px;
  align-content: center;
  text-decoration: none;
}
.dossier-rail .edition-format-nav a:hover { text-decoration: underline; }

/* GH-116. Mobile tap targets for the ribbon's Rating cell.

   Both of these are NAVIGATION/toggle controls, not a rating input: the star
   row is one button with `aria-label="View reviews"` whose only job is to jump
   to the reviews section, and its five glyphs are aria-hidden renderings of the
   average. The control that actually WRITES a rating is `.review-star` in
   _reviews.html, which is already 44x44 at every width. So the failure here is
   a missed tap, not a wrong value recorded.

   min-height rather than a transparent ::after, which is what the dense chip
   walls got: these two sit ~7px apart in the same cell, so expanded areas would
   overlap each other and one would steal the other's taps. Growing the boxes is
   the only way to give both a real 44px without that. Costs the ribbon 41px on
   mobile (253 -> 294), which is the honest price of two tappable controls. */
@media (max-width: 640px) {
  .edition-ribbon .er-rating-btn { min-height: 44px; }
  /* The signed-in star row puts FIVE targets in the cell the two controls
     above already stretched. Full height, ~30px of width: short of the 44x44
     guidance, and the honest trade for keeping the row on one line at 320px.
     A mis-tap here is now a wrong VALUE, not just a missed jump -- which is
     why it is one tap to correct, and why the two full-size writers stay on
     the page (.review-star, 44x44 at every width, and .ms-star). */
  .edition-ribbon .star-rate--ribbon button {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 30px; min-height: 44px;
  }
  .edition-ribbon .er-rec {
    min-height: 44px;
    min-width: 44px;
    justify-content: center;
  }
}

/* ============================================================
   Price anchor + purchase band
   ============================================================
   Two halves of one move. The rail used to carry the whole shop: "Where to
   buy", "Buy / find", the Marketplace strip and the store-link editor, stacked
   in a 300px column. That width is why whereToBuyRows() only ever showed two
   priced rows -- a retailer row is a five-part record (shop, delivered total,
   shipping split, stock, saving) and three of them would not fit. Meanwhile the
   ribbon led with the edition's MSRP in the accent colour at 1.4rem, so the
   loudest number on the page was the one price nobody charges, and the number
   the reader can act on sat below the fold in a 300px column.

   So: the rail keeps ONE number, the payable one, and takes the accent with it
   (.rail-buy-amount). The shop moves to .dossier-buy-band, a SIBLING of
   .dossier-panes sitting beneath it. That is what closes the empty region right
   of the rail at wide viewports -- the rail is now short, and the band is as
   wide as the page.

   It shipped as a third child of .dossier-panes spanning `grid-column: 1 / -1`,
   which looked equivalent and was not: .dossier-rail is position: sticky, and a
   sticky grid item is held by the GRID, not by its row, so the band lay inside
   the rail's stick range and the rail painted over it -- 240x506px of cover and
   spec facts on top of the store prices, on the page and in the modal alike.
   Being outside the grid is what fixes that; being full width never depended on
   being in it, since .edition-dossier is a block. */

.edition-view .dossier-buy-band {
  /* The panes' own row-gap used to supply this interval. Outside the grid the
     band has to bring it, and it matches that gap on purpose: a deliberately
     larger step than the 1rem between .dsec cards, so the band reads as a new
     section rather than one more card. */
  margin-top: clamp(1.25rem, 3vw, 2.4rem);
  padding: 1.15rem 1.25rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  display: grid;
  gap: 1.5rem;
  /* .rail-buy-jump lands here from a sticky-nav page. */
  scroll-margin-top: calc(var(--nav-h, 0px) + 1rem);
}

/* An edition with no retailer rows and no link editor renders the band's
   Alpine templates to nothing; an empty bordered box below the dossier would
   be worse than no band at all. */
.edition-view .dossier-buy-band:not(:has(.rail-store-links)) {
  display: none;
}

/* The moved blocks keep their own .rail-store-title. In the rail those were
   0.68rem muted captions above a stack; at band width they are section
   headings, so they take the .dsec-h treatment (accent dot, same size) and the
   band needs no heading of its own. */
.edition-view .dossier-buy-band .rail-store-links {
  margin-top: 0;
}

.edition-view .dossier-buy-band .rail-store-title {
  /* Both containers below are flex rows, so this takes a whole row of its own
     the flex way -- `flex: 1 0 100%` before `display: flex`, which is what the
     title's own children lay out under. */
  flex: 1 0 100%;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-bottom: 0.4rem;
  color: var(--text);
  font-size: 0.8rem;
}

.edition-view .dossier-buy-band .rail-store-title::before {
  content: "";
  flex: none;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--ed-accent);
}

/* Priced offers. Flex-wrap, NOT the grid this started as, and the reason is
   the one trap in this band: `repeat(auto-fill, minmax(15.5rem, 1fr))` sizes
   its track count from the container, not from how many offers exist, so two
   retailers in a 1199px band got two 294px cards hugging the left edge and
   500px of dead space -- and the price-position block landed in track 3 as a
   bare, uncarded grid item beside them.

   `auto-fit` is the reflex fix here and it does nothing at all. auto-fit
   collapses only tracks that no item occupies, and this container always holds
   items spanning `1 / -1` -- the section title, the market row, the wait block
   -- so every track is occupied by one of them and none is ever collapsible.
   Verified against the served page 2026-08-26: swapping the keyword left the
   layout byte-identical. Flex has no track model, so a row's items divide the
   row they are actually in; `flex: 1 1 15.5rem` keeps the same wrap threshold
   the minmax() named. The top rule is the rail's separator from the block
   above it, which no longer exists here. */
.edition-view .dossier-buy-band .rail-price-compare {
  border-top: 0;
  padding-top: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: stretch;
}

/* "Full market & price history" is a route into everything, not one more
   offer, so it never sits in the offer grid as a peer. `flex: 1 0 100%` is the
   flex spelling of the `grid-column: 1 / -1` this rule used to carry -- and a
   stray `grid-column` in a flex container is silently inert, not an error,
   which is exactly how a full-width row quietly becomes a card. */
.edition-view .dossier-buy-band .rail-market-row {
  flex: 1 0 100%;
}

/* The price-position readout and its alert control. In the 240px rail this is
   a plain block under the offers and needs no frame. Dropped into the band it
   sits BETWEEN two carded offers and the carded market row, so unframed it
   reads as debris that escaped a card rather than as a row of its own. Same
   surface and padding as .rail-price-row, full width because it describes the
   whole offer set rather than any one retailer. */
.edition-view .dossier-buy-band .rail-wait {
  flex: 1 0 100%;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.edition-view .dossier-buy-band .rail-price-row {
  /* .rail-price-row sets align-items: stretch at (0,1,0) and loses to
     .dossier-buy-band .rail-store-link's align-items: center at (0,2,0) -- the
     same cascade gap this file documents for .rail-family-price. Unfixed, every
     card centred its shop name and its price, so the one column a reader scans
     down was ragged. */
  align-items: stretch;
  padding: 0.7rem 0.8rem;
  gap: 0.45rem;
  background: var(--surface-2);
}

.edition-view .dossier-buy-band .rail-price-amount {
  font-size: 1.05rem;
}

/* In the rail these two sat on one line, shop left and price right, because a
   240px column has room for nothing else. In a 15.5rem grid cell that line
   wraps for a long shop name and not for a short one, so the cards came out
   different heights and the prices landed at different depths -- the one
   number being compared, unalignable. Stack them instead: shop, then price,
   then the meta row, identically in every card. */
.edition-view .dossier-buy-band .rail-price-row-main {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
}

.edition-view .dossier-buy-band .rail-price-values {
  align-items: flex-start;
  text-align: left;
}

/* The two rules that size a card in its row, and the ONLY place either
   container's flex basis is set. The obvious spelling is a `> *` child rule,
   and it is a trap: `.edition-view .dossier-buy-band .rail-price-compare > *`
   and `.edition-view .dossier-buy-band .rail-store-title` are both (0,3,0), so
   whichever is written second wins, and the full-width items above silently
   became 15.5rem cards sharing a row with the first offer. Measured that way
   once -- "BUY / FIND" rendered 217px wide beside InStockTrades. Naming the
   wrapper instead means the spanners are not matched at all and the cascade
   never has to be argued about.

   `display: grid` is the equal-height half: the wrapper is what the flex row
   stretches, but the <a> inside it is not a flex item of that row, so without
   this the shorter links float against the top of a taller neighbour -- which
   is how the Buy/Find row shipped, ISBN badges at three different depths. A
   one-item grid stretches its child by default. */
.edition-view .dossier-buy-band .rail-price-compare > .rail-store-link-row {
  flex: 1 1 15.5rem;
  display: grid;
}

.edition-view .dossier-buy-band [data-tour="edition-buy"] > .rail-store-link-row {
  flex: 1 1 11.5rem;
  display: grid;
}

/* Priced rows arrive in run order -- in stock new, then used, then pre-order,
   then not in stock -- and whereToBuyGroupLabel() heads each run once the list
   mixes kinds. The slot is display: contents so the row inside it is still an
   item of the flex row above (the selector below names it through the slot;
   the direct-child rule above stays for any row rendered without one). The
   heading takes a whole line, so each run starts its own row of cards. */
.edition-view .dossier-buy-band .rail-price-compare > .rail-offer-slot {
  display: contents;
}

.edition-view .dossier-buy-band .rail-price-compare > .rail-offer-slot > .rail-store-link-row {
  flex: 1 1 15.5rem;
  display: grid;
}

.edition-view .dossier-buy-band .rail-offer-group {
  flex: 1 0 100%;
}

.edition-view .dossier-buy-band .rail-price-row {
  justify-content: flex-start;
}

/* Unpriced search links are a denser, quieter row than the offers above: they
   are routes to look, not prices to compare. Flex for the same reason as
   .rail-price-compare -- four links against a 6-track auto-fill grid ended the
   row two thirds of the way across the band. */
.edition-view .dossier-buy-band [data-tour="edition-buy"] {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: stretch;
}

.edition-view .dossier-buy-band .mkt-strip,
.edition-view .dossier-buy-band .rail-store-admin {
  flex: 1 0 100%;
}

/* ---- The rail's price anchor ---- */

.edition-view .dossier-rail .rail-buy {
  display: grid;
  gap: 0.25rem;
  margin: 0.85rem 0 0.35rem;
}

.edition-view .dossier-rail .rail-buy-amount {
  font-family: var(--font-display);
  /* storePriceMain() returns the whole claim -- "Delivered $74.99", or
     "$92.00 + shipping" for a row whose shipping we only assumed -- and the
     wording stays in one place, so the type has to fit the sentence rather
     than the sentence being cut to fit the type. At 1.75rem the display face
     broke it across two equally-weighted lines in a 216px rail, which read as
     "Delivered" first and the number second. */
  font-size: 1.35rem;
  line-height: 1.05;
  text-wrap: balance;
  /* --text, not --ed-accent. The obvious move was to hand this number the gold
     the ribbon's MSRP cell used to wear, and measured against palettes.css it
     does not survive: --accent-canonical in the light pressroom palette is
     3.14:1 on --surface and 2.70:1 on the --surface-2 the rail is actually
     tinted with -- under the 3:1 floor for large text, on the one number the
     page exists to deliver. There are nine palettes x two themes here and no
     accent-on-surface token that holds across them, so no single darkened
     value would be checkable either. The hierarchy is carried by what does not
     vary: the display face at 1.35rem against 0.74rem, the struck reference
     directly beneath, and the retailer's name. */
  color: var(--text);
  text-decoration: none;
}

.edition-view .dossier-rail a.rail-buy-amount:hover {
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
}

.rail-buy-ref {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.3rem 0.4rem;
  margin: 0;
  font-size: 0.74rem;
  color: var(--text-muted);
}

/* Hairline, not a slab: this is a reference price being set aside, and a heavy
   strike reads as an error state. */
.rail-buy-list {
  text-decoration-thickness: 1px;
  text-underline-offset: 0.1em;
}

.rail-buy-store {
  color: var(--text);
  font-weight: 650;
}

.rail-buy-jump {
  justify-self: start;
  color: var(--text-muted);
  font-size: 0.72rem;
  font-weight: 650;
  letter-spacing: 0.03em;
  text-decoration: none;
  border-bottom: 1px solid var(--border);
  transition: color var(--motion-fast) var(--ease-standard),
              border-color var(--motion-fast) var(--ease-standard);
}

.rail-buy-jump::after {
  content: " \2193";
}

.rail-buy-jump:hover {
  color: var(--text);
  border-bottom-color: currentColor;
}

/* An edition with no offer we can price falls back to its own MSRP. Nothing
   distinguishes it by colour -- the words do it: this one says "$125.00" over
   the label MSRP, where a real offer says "Delivered $74.99" over a struck
   reference and a shop name. */

@container edition (max-width: 860px) {
  /* hero -> price -> Contents. The rail is one grid item, so ordering it as a
     whole can only put the entire spec column either before or after the
     content; display: contents promotes its blocks to grid items in their own
     right, which is what lets the price -- and only the price -- come first.
     Everything else in the rail keeps its source order after the content. */
  .edition-view .dossier-rail {
    display: contents;
  }

  .edition-view .dossier-rail > * {
    order: 1;
  }

  .edition-view .dossier-content {
    order: 0;
  }

  /* No `order` here any more. The band is a sibling of .dossier-panes rather
     than a grid item inside it, so order would be inert -- and it is not
     needed: the band already follows the whole stack in source order, which is
     exactly where `order: 2` used to put it. */
  .edition-view .dossier-buy-band {
    padding: 1rem;
    gap: 1.25rem;
  }

  /* The rail's own border and background go with display: contents, so the
     anchor carries its own -- it is a distinct claim, not a run of spec rows. */
  .edition-view .dossier-rail > .rail-buy {
    order: -1;
    margin: 0;
    padding: 0.85rem 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
  }

  /* The panes gap is tuned for two columns; as a single stack of many more
     items than before it reads as drift. */
  .edition-view .dossier-panes {
    gap: 1rem;
  }
}

/* ---------------------------------------------------------------------------
   Long contents (the omnibus scroll).

   Measured on /edition/cns_1a0da338f464b3f9 (Fantastic Four by Waid &
   Wieringo Omnibus): 53 rows, every one of them reading "Fantastic Four #NNN"
   plus a date, at the 44px every contents row is held to. That is roughly
   three screens of near-identical text sitting between the hero and where-to-
   buy, related editions, reviews and comments. Three rules below answer it --
   say the whole thing in one line, print the constant once, and stop spending
   one full row of height on each varying number.
   --------------------------------------------------------------------------- */

/* The one-line answer, ahead of the evidence. Reads as prose, not as a field:
   it is a sentence about the book. */
.edition-view .dossier-contents-summary {
  margin: 0 0 0.75rem;
  color: var(--text);
  font-size: 0.95rem;
  line-height: 1.5;
}

/* The same sentence in the collapsed header. The summary is a flex row of
   heading + "+" marker; wrapping and ordering the peek last puts it on its
   own line under both. It inherits .dsec-h's display face, uppercase and
   tracking, all of which must be undone or a sentence reads as a label. Two
   lines at most: the long form ends "+1 more · 22 issues", and the full text
   is one click away. */
.edition-view .dossier-contents > summary {
  flex-wrap: wrap;
  row-gap: 0.35rem;
}

.edition-view .dossier-contents > summary .dsec-peek {
  order: 3;
  flex: 1 0 100%;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text-muted);
}

.edition-view .dossier-contents[open] > summary .dsec-peek {
  display: none;
}

/* The series name the whole list shares, printed once. Set in the label idiom
   rather than the row idiom so it reads as a heading over the numbers below
   and never as another entry in the list. */
.edition-view .dossier-contents-series,
.edition-view .dossier-contents-section-h {
  margin: 0.35rem 0 0.4rem;
  color: var(--text-muted);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.edition-view .dossier-contents-section + .dossier-contents-section {
  margin-top: 0.9rem;
}

/* Columns, not shorter rows: every row keeps its 44px target and the list
   stops being 53 of them tall. CSS multi-column rather than a grid because
   columns fill top-to-bottom, so a numbered run still reads in order down the
   first column before it moves to the second -- a row-major grid would
   scatter a consecutive run of issue numbers across the page.
   (Spelling one out here is not an option: a bare number sign followed by
   three digits is a hex colour to the raw-colour audit.)

   `display: block` is load-bearing: .list-group is a flex column, and columns
   do not apply to a flex container. */
@media (min-width: 768px) {
  .edition-view .dossier-contents-list--cols {
    display: block;
    columns: 2;
    column-gap: 1rem;
  }

  .edition-view .dossier-contents-list--cols > .list-group-item {
    /* The flex `gap` that spaced these is gone with the flex container. */
    margin-bottom: 0.4rem;
    break-inside: avoid;
  }
}

@media (min-width: 900.1px) {
  .edition-view .dossier-contents-list--cols {
    columns: 3;
  }
}

.edition-view .dossier-contents-more {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: 0.6rem;
  padding: 0 0.9rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font-size: 0.88rem;
  transition:
    border-color var(--motion-fast) var(--ease-standard),
    background-color var(--motion-fast) var(--ease-standard);
}

.edition-view .dossier-contents-more:hover {
  border-color: var(--ed-accent);
  background: color-mix(in srgb, var(--ed-accent) 10%, transparent);
}

/* ---------------------------------------------------------------------------
   "On this page" index (rail).

   Desktop only, and hidden rather than moved below the lg boundary: there the
   rail is `position: static` and sits BELOW the content column, so an index of
   what the reader has already scrolled past is worse than no index at all.
   --------------------------------------------------------------------------- */
.dossier-index {
  display: grid;
  gap: 0.1rem;
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
}

.dossier-index-link {
  /* Deliberately not the 44px floor the mobile tap targets carry: this nav
     never renders on a touch viewport, and seven 44px rows would make the
     index taller than the rail it lives in. */
  padding: 0.32rem 0.45rem;
  border-left: 2px solid transparent;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--text-muted);
  font-size: 0.82rem;
  line-height: 1.3;
  text-decoration: none;
  transition:
    color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard),
    background-color var(--motion-fast) var(--ease-standard);
}

.dossier-index-link:hover {
  color: var(--text);
  background: color-mix(in srgb, var(--ed-accent) 8%, transparent);
}

.dossier-index-link.is-current {
  border-left-color: var(--ed-accent);
  color: var(--text);
  font-weight: 600;
}

@media (max-width: 767.98px) {
  .dossier-index {
    display: none;
  }
}

/* Anchor targets the index jumps to. Without this the section lands flush
   against the top of the viewport, and a sticky header would cover it. */
.edition-view .dossier-content #dossier-creators,
.edition-view .dossier-content #dossier-characters,
.edition-view .dossier-buy-band,
#community {
  scroll-margin-top: 1.25rem;
}

/* Phone facts wrap within the viewport. Values and labels stay stacked so
   the empty Readers state reads as a value, and the rating controls get a
   full row rather than disappearing beyond a hidden scrollbar. */
@media (max-width: 640px) {
  .edition-ribbon {
    flex-wrap: wrap;
    margin-top: 1rem;
  }

  .edition-ribbon > .er-cell:nth-child(n) {
    flex: 1 1 50%;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 0.5rem 0.9rem;
    border-right: 1px solid var(--border);
    white-space: normal;
  }
  .edition-ribbon > .er-cell:nth-child(n):last-child { border-right: 0; }
  .edition-ribbon > .er-cell[data-cell="pages"]:nth-child(n),
  .edition-ribbon > .er-cell[data-cell="format"]:nth-child(n),
  .edition-ribbon > .er-cell[data-cell="released"]:nth-child(n) { display: none; }
  .edition-ribbon > .er-cell .er-v { min-height: 1.1rem; }
  .edition-ribbon > .er-cell .er-v.er-rating-btn { min-height: 44px; }
  .edition-ribbon > .er-cell .star-rate--ribbon button { min-width: 44px; }
  .edition-ribbon > .er-cell .er-k { margin-top: 0.2rem; min-height: 0; }

  .edition-ribbon > .er-social[data-cell="rating"]:nth-child(n) {
    flex-basis: 100%;
    border-right: 0;
    border-top: 1px solid var(--border);
  }
  .edition-ribbon > .er-social[data-cell="readers"]:nth-child(n) { border-right: 0; }
  .edition-ribbon .er-rating-k { flex-wrap: wrap; }

  /* One 44px line: amount, shop + condition, then the jump link at the end. */
  .edition-view .dossier-rail .rail-buy,
  .edition-view .dossier-rail > .rail-buy {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.2rem 0.6rem;
    min-height: 44px;
    padding: 0.4rem 0.8rem;
    box-sizing: border-box;
  }
  .edition-view .dossier-rail .rail-buy-amount {
    font-size: 1.1rem;
    text-wrap: nowrap;
  }
  .edition-view .dossier-rail .rail-buy-ref { flex: 1 1 auto; min-width: 0; }
  .edition-view .dossier-rail .rail-buy-jump {
    margin-left: auto;
    align-self: center;
    border-bottom: 0;
    padding: 0.6rem 0;
  }
  .edition-view .dossier-rail .rail-buy-jump::after { content: " \203A"; }
}

/* Trade breakdown: the trade paperbacks that make up this edition
   (docs/features/trade-breakdown.md). */
.trade-breakdown { display: grid; gap: .55rem; }
.trade-breakdown__summary { margin: 0; font-weight: 600; color: var(--text); }
.trade-breakdown__rows { list-style: none; margin: 0; padding: 0; display: grid; gap: .35rem; }
.trade-breakdown__row { display: grid; grid-template-columns: 2.6rem 1fr; gap: .65rem; align-items: center; padding: .45rem .55rem; border-radius: var(--radius); background: var(--surface-2); }
.trade-breakdown__cover { display: block; width: 2.6rem; height: 3.9rem; overflow: hidden; border-radius: 3px; background: var(--surface); box-shadow: var(--shadow-sm); }
.trade-breakdown__cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
.trade-breakdown__body { min-width: 0; }
.trade-breakdown__title { display: block; font-weight: 600; color: var(--text); line-height: 1.3; }
.trade-breakdown__meta { margin: .1rem 0 0; display: flex; flex-wrap: wrap; gap: 0 .4rem; color: var(--text-muted); font-size: .82rem; }
.trade-breakdown__gap { margin: 0; color: var(--text-muted); font-size: .85rem; }
.trade-breakdown__note { margin: 0; color: var(--text-dim); font-size: .75rem; }
@media (max-width: 640px) {
  .trade-breakdown__row { grid-template-columns: 2.2rem 1fr; }
  .trade-breakdown__cover { width: 2.2rem; height: 3.3rem; }
}

/* Format paths: "Other ways to read these issues" (docs/features/format-paths.md).
   The group header is offset by the row's padding + covers column + gap so it
   sits over the ribbon, whose cells are equal-width so gaps line up across rows. */
.format-paths { display: grid; gap: .5rem; }
/* Same grid as the ribbon (--fp-n = the anchor's issue count, each group
   spans its count), so a group label sits exactly over its segments. */
.format-paths__groups { display: grid; grid-template-columns: repeat(var(--fp-n, 1), minmax(0, 1fr)); gap: 1px; margin: 0 .6rem 0 6.1rem; font-size: .7rem; letter-spacing: .04em; text-transform: uppercase; color: var(--text-dim); }
.format-paths__group { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.format-paths__rows { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.format-paths__row { display: grid; grid-template-columns: 5rem 1fr; gap: .5rem; padding: .6rem; border-radius: var(--radius); background: var(--surface-2); }
.format-paths__row--anchor { box-shadow: inset 3px 0 0 var(--brand); }
/* The fan is sized to its column: 4 covers = 2.6rem + 3 x .8rem = 5rem, and
   "+N" is a badge over the last cover, so nothing paints into the body. */
.format-paths__covers { position: relative; display: flex; align-items: flex-start; align-self: start; width: 5rem; }
/* Fixed 2:3 box that clips: a broken image paints nothing outside it. */
.format-paths__cover { display: block; width: 2.6rem; height: 3.9rem; flex: none; overflow: hidden; border-radius: 3px; background: var(--surface); box-shadow: var(--shadow-sm); }
.format-paths__cover + .format-paths__cover { margin-left: -1.8rem; }
.format-paths__cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
.format-paths__more { position: absolute; right: 0; bottom: 0; padding: 0 .3rem; border-radius: var(--radius-pill); background: var(--surface); color: var(--text); font-size: .7rem; font-weight: 700; line-height: 1.4; box-shadow: var(--shadow-sm); }
.format-paths__body { min-width: 0; }
.format-paths__head { display: flex; justify-content: space-between; align-items: baseline; gap: .5rem; }
.format-paths__label { font-weight: 700; color: var(--text); }
.format-paths__price { text-align: right; font-variant-numeric: tabular-nums; }
.format-paths__price strong { font-size: 1.15rem; color: var(--text); }
.format-paths__price small { display: block; color: var(--text-muted); }
.format-paths__ribbon { display: grid; grid-template-columns: repeat(var(--fp-n, 1), minmax(0, 1fr)); gap: 1px; height: .7rem; margin: .35rem 0; }
.format-paths__cell { border-radius: 1px; outline: 1px solid var(--border-strong); outline-offset: -1px; }
.format-paths__cell--filled { background: var(--text-muted); outline-color: transparent; }
.format-paths__row--anchor .format-paths__cell--filled { background: var(--brand); }
/* A group boundary: a cut painted inside the cell, so it costs no width and
   the header grid stays aligned however many issues there are. */
.format-paths__cell--group-start { box-shadow: inset 2px 0 0 var(--surface-2); }
/* Past 120 issues 1px gaps alone would overflow the strip: ribbon and header
   drop them together; cells separate by an inset shadow and the strip itself
   carries the empty-cell frame. */
.format-paths__ribbon--dense { gap: 0; }
.format-paths__ribbon.format-paths__ribbon--dense { box-shadow: inset 0 0 0 1px var(--border-strong); }
.format-paths__ribbon--dense .format-paths__cell { outline-color: transparent; border-radius: 0; box-shadow: inset -1px 0 0 var(--surface-2); }
.format-paths__ribbon--dense .format-paths__cell--group-start { box-shadow: inset 2px 0 0 var(--surface-2), inset -1px 0 0 var(--surface-2); }
.format-paths__more-missing { list-style: none; margin-left: -1rem; color: var(--text-muted); }
.format-paths__caption { display: flex; flex-wrap: wrap; gap: .25rem .75rem; font-size: .85rem; color: var(--text-muted); }
.format-paths__missing summary { cursor: pointer; color: var(--text); }
.format-paths__missing ul { margin: .25rem 0 0; padding-left: 1rem; }
.format-paths__volumes { display: flex; flex-wrap: wrap; gap: .1rem .6rem; font-size: .8rem; }
.format-paths__volumes a { color: var(--text-muted); }
/* The row's own compare link reads as an action, not one more volume title. */
.format-paths__volumes a.format-paths__row-compare { color: var(--link); font-weight: 600; }
.format-paths__foot { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem 1rem; }
.format-paths__note { color: var(--text-dim); }
/* Mobile: covers + title + price on one line, ribbon full width beneath,
   caption under it. The body dissolves into the row grid so its children can
   take their own rows. */
@media (max-width: 767.98px) {
    .format-paths__groups { margin: 0 .6rem; }
    .format-paths__row { grid-template-columns: auto 1fr; align-items: center; }
    .format-paths__covers { align-self: center; }
    .format-paths__body { display: contents; }
    .format-paths__ribbon,
    .format-paths__caption,
    .format-paths__volumes { grid-column: 1 / -1; }
}
