/* ================================================================= *
 |  MASSIVE DYNAMIC  —  the /episode page skin
 |  Promoted from the theme-preview mock-up on 2026-07-23:
 |  overlaid hero meta + legacy-shadow title + full-width prose.
 |
 |  Everything here is scoped under `body.md-skin`, which episode/show.blade.php
 |  sets only for ?hero=massive (the default). The legacy hero and the A/B/C
 |  exploration variants are untouched — they never get this class.
 |
 |  Loaded AFTER the legacy style/*.css stack, so it overrides rather than
 |  replaces: all ~20 sections, the image modals, the per-episode effects
 |  (firefly, flicker, ticker, barbed wire, RESIST, LSD/observer fonts) and the
 |  D3 connection chart keep working on their original markup.
 * ================================================================= */

/* ---- per-episode accent -------------------------------------------
   Same legacy verse -> hex mapping the mock-up used, tuned for contrast
   on the white Massive Dynamic surfaces. `--md-accent-lit` is the
   light-on-photo tint used anywhere the accent sits on a dark ground. */
body.md-skin                      { --md-accent: #0056b7 }
body.md-skin.md-accent-blueverse  { --md-accent: #0056b7 }
body.md-skin.md-accent-redverse   { --md-accent: #c8102e }
body.md-skin.md-accent-amberverse { --md-accent: #b07d00 }
body.md-skin.md-accent-flashback  { --md-accent: #17788d }
body.md-skin.md-accent-future     { --md-accent: #5f6f7c }
body.md-skin.md-accent-potential-future { --md-accent: #7a828c }

body.md-skin {
    --md-ink:     #2a3540;
    --md-body:    #3c4a57;
    --md-muted:   #6b7a86;
    --md-line:    #d3dfea;
    --md-surface: #ffffff;
    --md-accent2: #42707f;
    --md-accent-lit: #a8d4ff;
    --md-accent-lit: color-mix(in srgb, var(--md-accent) 42%, #ffffff);
    --md-scrim:   8, 17, 28;

    --md-font:    Helvetica, Arial, "Helvetica Neue", sans-serif;
    --md-display: "Futura", "Century Gothic", "Twentieth Century", "Avant Garde", system-ui, sans-serif;

    /* the 2008 viral-site sky, fading to white over the first screenful or two
       (a full-page gradient would stretch across a 20,000px episode page) */
    background-color: #ffffff;
    background-image: linear-gradient(180deg, #cfe2f2 0%, #e7f0f8 38%, #ffffff 100%);
    background-repeat: no-repeat;
    background-size: 100% 1500px;
    color: var(--md-ink);
    font-family: var(--md-font);
    counter-reset: md-sec;
}

/* Anchor jumps must clear BOTH fixed bars: the navbar and the sticky jump bar.
   js/episode-jumpbar.js measures the real heights and overwrites these two
   custom properties on <html>; the values here are the pre-JS fallbacks
   (navbar 57px + a two-row jump bar + breathing room). */
:root { --md-nav-h: 57px; --md-anchor-offset: 145px }
body.md-skin [id] { scroll-margin-top: var(--md-anchor-offset) }


/* ================================================================= *
 |  HERO  —  meta overlaid on the still
 * ================================================================= */
.md-skin .md-hero {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    margin-top: 56px; /* navbar is .fixed-top; body has no global offset */
}
.md-skin .md-hero__media,
.md-skin .md-hero__body { grid-column: 1; grid-row: 1 }

.md-skin .md-hero__media {
    position: relative;
    overflow: hidden;
    height: clamp(380px, 40vw, 560px);
    background: #bcd3e6;
}
.md-skin .md-hero__media img {
    width: 100%; height: 100%;
    object-fit: cover;   /* crop, never stretch */
    display: block;

    /* THE HERO'S FOCAL POINT. The value comes from the admin tool at
       /admin/hero-focus (App\Support\HeroFocus), which the view emits inline as
       --fr-hero-pos. Don't write an object-position rule for a hero in any
       stylesheet: it would lose to nothing (the inline value is a custom
       property now, not a declaration) and then silently disagree with the tool. */
    object-position: var(--fr-hero-pos, 50% 50%);
}

/* WIDE VIEWPORTS GET THEIR OWN CROP (2026-07-28). The band above stops growing
   at 1400px while the viewport keeps widening, so past that the crop window is
   both shallower and shifted — a hero framed on a laptop can clip its subject on
   a 1920px monitor. --fr-hero-pos-wide is the second value from the same tool;
   heroes that don't need one simply don't emit it and fall through to the base
   crop.

   1601, NOT 1401 (which is where the clamp actually caps): the base crops are
   all framed on a 1536px viewport, so that width has to keep getting them —
   see HeroFocus::WIDE_FROM for the full reasoning. Keep the two in step. */
@media (min-width: 1601px) {
    .md-skin .md-hero__media img {
        object-position: var(--fr-hero-pos-wide, var(--fr-hero-pos, 50% 50%));
    }
}

.md-skin .md-hero__body {
    align-self: end;
    position: relative;
    z-index: 2;
    padding: 7rem 0 1.6rem;
    background: linear-gradient(0deg,
        rgba(var(--md-scrim), .94) 0%,
        rgba(var(--md-scrim), .88) 26%,
        rgba(var(--md-scrim), .66) 55%,
        rgba(var(--md-scrim), .26) 80%,
        rgba(var(--md-scrim), 0) 100%);
}
@media (max-width: 700px) {
    /* the overlay stack is tall relative to a narrow frame — shorten the
       scrim's run-up so it doesn't swallow the whole still */
    .md-skin .md-hero__body { padding-top: 4.5rem }
}

.md-skin .md-hero__kicker {
    font-family: var(--md-display);
    font-size: .75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .3em;
    color: var(--md-accent-lit);
    text-shadow: 0 1px 3px rgba(0, 0, 0, .75);
}

.md-skin .md-hero__title {
    font-family: "Montserrat", var(--md-display);
    font-weight: 700;
    font-size: clamp(2.1rem, 4.6vw, 3.5rem);
    line-height: 1.05;
    letter-spacing: .04em;
    margin: .4rem 0 .7rem;
    text-wrap: balance;
}
/* style.css .fringeshadow is tuned for a light ground; brighten the top edge
   and deepen the extrusion so the emboss reads on the dark scrim. */
.md-skin .md-hero__title.fringeshadow {
    color: transparent;
    -webkit-text-fill-color: transparent;
    text-shadow:
        -1px -1px 1px rgba(255, 255, 255, .6),
        0 1px 0 #1c1c1c, 0 2px 0 #1a1a1a, 0 3px 0 #181818, 0 4px 0 #161616,
        2px 7px 7px rgba(0, 0, 0, .8);
}

.md-skin .md-hero__byline {
    font-size: .92rem;
    line-height: 1.55;
    color: rgba(255, 255, 255, .92);
    text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
}
.md-skin .md-hero__byline .sep { opacity: .5; margin: 0 .25rem }
.md-skin .md-hero__byline a { color: #fff; text-decoration: underline; text-underline-offset: 2px }

/* ---- the FEATURE hero (a header carrying `fr-feature-hero`) ----
   Two adjustments every feature page wants, and that character-page.css,
   differences-page.css, alternate-us-page.css and scavenger-hunt-page.css had
   each written out for themselves with identical values:

   1. A title a shade under the episode clamp. A feature title is short ("2036",
      "Glyphs", "The Tapes") and at the episode size it shouts over a byline
      that, unlike an episode's one-line credit, is a paragraph.
   2. A measure on that byline. Un-capped it runs the full 1320px container and
      reads as a banner subtitle rather than prose.

   Promoted 2026-07-27 with the eight remaining feature pages, which would have
   been copies five through twelve. The four earlier pages keep their own local
   copies — same values, so adopting this is cosmetic and can happen whenever one
   of them is next touched. */
.md-skin .fr-feature-hero .md-hero__title { font-size: clamp(2rem, 3.8vw, 3rem) }
.md-skin .fr-feature-hero .md-hero__byline { max-width: 62ch }
.md-skin .fr-feature-hero .md-hero__byline p { margin: 0 }
.md-skin .fr-feature-hero .md-hero__byline p + p { margin-top: .35rem }
/* between the first and last appearance in a stats line */
.md-skin .fr-hero-stats .fr-hero-stats__arrow { margin: 0 .4rem; opacity: .6 }

/* ---- hero stats line ----
   A short run of facts under the byline that the legacy prose never stated —
   "16 versions - 100 episodes - S1E01 to S5E13", "93 differences - 23 episodes",
   "5 ads - 1:45 total".

   PROMOTED HERE 2026-07-26. It was copied verbatim into character-page.css
   (.fc-hero__stats), differences-page.css (.fd-hero__stats),
   alternate-us-page.css (.au-hero__stats) and radio-ads-page.css
   (.ra-hero__stats) — four identical blocks, each kept local so the change that
   introduced it wouldn't reach into the others. /scavenger-hunt would have been
   the fifth, which is well past the point where that reasoning holds. All five
   pages now use `.fr-hero-stats`.

   Lives with .fr-hero-links rather than in a page sheet because it sits inside
   .md-hero and depends on the hero's scrim for its contrast: the white text and
   the dark text-shadow below are tuned for a photograph behind them. */
.md-skin .fr-hero-stats {
    display: flex;
    flex-wrap: wrap;
    /* BASELINE, not center. An item that opens with a <strong> count ("7
       SIGHTINGS") has a 25px box against 19px for a plain one, because the
       strong is 1.05rem inside a .8rem line. Centering those boxes puts the
       taller item's baseline ~2px below its neighbours', so "SIGHTINGS" sat
       lower than "SPORTS & TELEVISION" and "2009" on every page with a count.
       Baseline lines the text up and leaves the boxes to hang where they may. */
    align-items: baseline;
    gap: .3rem 1rem;
    list-style: none;
    margin: .85rem 0 0;
    padding: 0;
    font-family: var(--md-display);
    font-size: .8rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, .86);
    text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
}
.md-skin .fr-hero-stats strong {
    font-weight: 700;
    font-size: 1.05rem;
    color: #fff;
    margin-right: .1rem;
}
/* the same accent dot the hero link groups use, as a separator */
.md-skin .fr-hero-stats li:not(:first-child)::before {
    content: "";
    display: inline-block;
    vertical-align: middle;
    width: 3px; height: 3px;
    margin-right: .9rem;
    border-radius: 50%;
    background: var(--md-accent-lit);
    box-shadow: 0 1px 3px rgba(0, 0, 0, .8);
}

@media (max-width: 575.98px) {
    .md-skin .fr-hero-stats { font-size: .72rem; gap: .25rem .8rem }
    .md-skin .fr-hero-stats strong { font-size: .95rem }
}

/* ---- hero link pills (episode/partials/hero-links.blade.php) ---- */
.md-skin .fr-hero-links {
    display: flex; flex-wrap: wrap; gap: .4rem; align-items: center;
    margin-top: 1.15rem;
}
.md-skin .fr-hero-links__grp {
    font-size: .66rem; text-transform: uppercase; letter-spacing: .1em;
    color: rgba(255, 255, 255, .75);
    text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
}
/* Separate one section's pills from the next label with extra space + a small
   accent dot (echoing the .fringe-divider marker). Skipped on the first label
   so the row doesn't open with a stray dot. */
.md-skin .fr-hero-links__grp:not(:first-child) {
    margin-left: .55rem;
}
.md-skin .fr-hero-links__grp:not(:first-child)::before {
    content: "";
    display: inline-block; vertical-align: middle;
    width: 3px; height: 3px; margin-right: .7rem; border-radius: 50%;
    background: var(--md-accent-lit);
    box-shadow: 0 1px 3px rgba(0, 0, 0, .8);
}
.md-skin .md-hero .fr-pill {
    display: inline-flex; align-items: center; gap: .35rem;
    font-size: .75rem; line-height: 1; white-space: nowrap;
    padding: .4rem .8rem; border-radius: 999px; text-decoration: none;
    border: 1px solid rgba(255, 255, 255, .45);
    background: rgba(255, 255, 255, .14);
    color: #fff;
    backdrop-filter: blur(3px);
    transition: background-color .15s ease, color .15s ease, border-color .15s ease;
}
.md-skin .md-hero .fr-pill:hover { border-color: #fff; background: #fff; color: #12202f }


/* ================================================================= *
 |  SECTION NAV  —  the dark "jump to" bar under the hero
 |
 |  It starts in normal flow at the foot of the hero and is `position:
 |  sticky`, so once it reaches the fixed navbar it parks just below it and
 |  stays there for the whole (very long) page; scrolling back to the top
 |  releases it into the hero again. No JS is needed for the stick itself —
 |  js/episode-jumpbar.js only adds the `.is-stuck` shadow and keeps the
 |  anchor offset in sync with the bar's measured height.
 |
 |  The bar deliberately does NOT change size when it sticks: it keeps its
 |  space in the flow, so a size change would jolt the page at the moment of
 |  sticking. Only the shadow changes.
 * ================================================================= */
.md-skin .md-secnav {
    display: block;
    background: #223040;
    border-top: 3px solid var(--md-accent);
    padding: .6rem 0;
    position: sticky;
    top: var(--md-nav-h);
    /* under Bootstrap's .fixed-top navbar (1030), over page content */
    z-index: 1020;
    transition: box-shadow .18s ease;
}
.md-skin .md-secnav.is-stuck {
    box-shadow: 0 12px 24px -10px rgba(8, 17, 28, .6);
}
.md-skin .md-secnav .fr-secnav__inner {
    display: flex; flex-wrap: wrap; gap: .35rem; align-items: center;
}
.md-skin .md-secnav .fr-secnav__label {
    font-size: .64rem; text-transform: uppercase; letter-spacing: .14em;
    color: #8fb7dd; margin-right: .2rem;
}

/* Below lg the wrapped bar is 200px+ tall — far too much to park under the
   navbar — so it becomes a single swipeable row instead, at every scroll
   position (switching only on stick would jolt the page). */
@media (max-width: 991.98px) {
    .md-skin .md-secnav .fr-secnav__inner {
        flex-wrap: nowrap;
        overflow-x: auto;
        scrollbar-width: none;
        -ms-overflow-style: none;
        -webkit-overflow-scrolling: touch;
    }
    .md-skin .md-secnav .fr-secnav__inner::-webkit-scrollbar { display: none }
    .md-skin .md-secnav .fr-secnav__label,
    .md-skin .md-secnav .fr-pill { flex: 0 0 auto; white-space: nowrap }
    /* fade hint at the right edge so the overflow reads as scrollable */
    .md-skin .md-secnav::after {
        content: "";
        position: absolute; top: 0; right: 0; bottom: 0; width: 2.5rem;
        background: linear-gradient(90deg, rgba(34, 48, 64, 0), #223040 70%);
        pointer-events: none;
    }
}
.md-skin .md-secnav .fr-pill {
    display: inline-flex; align-items: center; line-height: 1;
    font-size: .68rem; text-transform: uppercase; letter-spacing: .1em;
    padding: .3rem .6rem; border-radius: 2px; text-decoration: none;
    color: #dbe7f3; border: 1px solid transparent; background: transparent;
}
.md-skin .md-secnav .fr-pill::before {
    content: "\25B8"; color: var(--md-accent-lit); margin-right: .35rem;
}
.md-skin .md-secnav .fr-pill:hover { color: #fff; background: rgba(255, 255, 255, .08) }

/* active section (js/episode-jumpbar.js): the accent underline is the signal,
   the wash just anchors it. No size/weight change — the pills are laid out in
   a wrapping row, so a bolder label would re-flow the whole bar as you scroll. */
.md-skin .md-secnav .fr-pill.is-current {
    color: #fff;
    background: rgba(255, 255, 255, .07);
    box-shadow: inset 0 -2px 0 var(--md-accent-lit);
}
.md-skin .md-secnav .fr-pill.is-current::before { color: #fff }


/* ================================================================= *
 |  SECTION HEADINGS  —  numbered rule, Futura caps
 |  Markup: <div class="row subsection-header"><h2 class="col pb-2">…</h2>
 |          <div class="col-2 fr-section-heading">? popover</div></div>
 * ================================================================= */
.md-skin .subsection-header {
    counter-increment: md-sec;
    align-items: center;
    border-bottom: 1px solid var(--md-line);
    padding-bottom: .55rem;
}
.md-skin .subsection-header h2 {
    display: flex; align-items: center; gap: .85rem;
    margin: 0; padding-bottom: 0;
    font-size: 1.4rem; font-weight: 400;
    text-transform: uppercase; letter-spacing: .1em;
    color: var(--md-ink);
}
/* the LSD comic (319) and observer (419/S5) fonts are per-episode effects —
   keep their typeface, just adopt the numbering + rule */
.md-skin .subsection-header h2:not(.lsd-title):not(.observer-font) {
    font-family: var(--md-display);
}
/* The Observer alphabet reads thin next to Futura, so the legacy treatment gave
   it 500. That used to be an inline style on the heading, which meant a 419/S5
   title carried the heavier weight even while it was still in English. It rides
   on the alphabet now, so it arrives with it — under the flicker arc that means
   at the moment the letter converts. */
.md-skin .subsection-header h2.observer-font { font-weight: 500 }
.md-skin .subsection-header h2::before {
    content: "";
    flex: none;
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--md-accent); /* the same dot as the .fringe-divider marker */
    -webkit-text-fill-color: var(--md-accent); /* beat .lsd-title's white fill */
    -webkit-text-stroke-width: 0;
    text-shadow: none;
}

/* ---- sub-headings inside a section (below the numbered h2) --------
   Several sections carry their own labels/headings BELOW the section h2:
   the clues section's "Previous/Next Episode Clue" (h3), the glyph word
   the glyphs spell (h4), and each connected episode's title (h4). None of
   them are touched by the skin otherwise, so they fall through to the
   legacy Freelancer heading rules — Montserrat 700 at 24–28px, i.e. BIGGER
   and heavier than the 22.4px/400 Futura section header above them, which
   inverts the hierarchy. Bring them onto the skin's type system, sized and
   weighted to read as clearly subordinate to the section header. */

/* Clue labels — a compact accent eyebrow, well under the section header. */
.md-skin .section-text h3 {
    font-family: var(--md-display);
    font-size: .95rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .12em;
    color: var(--md-accent);
    margin: 0 0 .35rem;
    padding-bottom: 0;
}

/* The word the episode's glyphs spell — a focal display plate, so it stays
   a touch larger than a label, but on the display font at a calm weight and
   never out-weighting the section header. Its letters are hyphen-separated,
   so a little tracking suits it. */
.md-skin section#glyphs h4.text-center {
    font-family: var(--md-display);
    font-size: 1.35rem;
    font-weight: 500;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--md-ink);
    margin: 0 0 .35rem;
}

/* Per-connection episode titles — a tidy sub-heading. Left in normal case:
   these are real episode titles with proper nouns, so uppercasing reads as
   shouty and hurts scanning. */
.md-skin .md-conn-title {
    font-family: var(--md-display);
    font-size: 1.15rem;
    font-weight: 600;
    letter-spacing: .01em;
    color: var(--md-ink);
    margin: 0;
}

/* The whole connected-episode title is the link now (it used to be plain text
   next to a `d-none d-lg-block` icon, so below 992px there was no link at all).
   The arrow is an affordance, not a separate target — it rides inside the <a>. */
/* Note the extra class in these selectors: the generic `.md-skin .col-lg a`
   in-content link rule (accent + underline) is 0,2,1 and would otherwise win. */
.md-skin .md-conn-title .md-conn-link {
    color: inherit;
    text-decoration: none;
    display: inline-flex;
    align-items: baseline;
    gap: .45rem;
}
.md-skin .md-conn-title .md-conn-link:hover,
.md-skin .md-conn-title .md-conn-link:focus-visible {
    color: var(--md-accent);
    text-decoration: underline;
    text-underline-offset: 3px;
}
.md-skin .md-conn-link__icon {
    font-size: .72em;
    opacity: 0;
    transform: translateX(-.25rem);
    transition: opacity .16s ease, transform .16s ease;
}
.md-skin .md-conn-title .md-conn-link:hover .md-conn-link__icon,
.md-skin .md-conn-title .md-conn-link:focus-visible .md-conn-link__icon {
    opacity: .75;
    transform: translateX(0);
}
/* Touch devices never hover, so the arrow would never show — pin it there. */
@media (hover: none) {
    .md-skin .md-conn-link__icon { opacity: .55; transform: none }
}
@media (prefers-reduced-motion: reduce) {
    .md-skin .md-conn-link__icon { transition: none }
}

/* ---- connections: chip index (?conn=index, the default) ----------------
   The section is the largest thing on the page — ep 215 carries 35 connected
   episodes, about half the page's HTML — so it gets its own jump nav. --------- */
.md-skin .fc-conn-index__lbl {
    font-family: var(--md-display);
    font-size: .82rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .12em;
    color: var(--md-muted);
    margin: 0 0 .7rem;
}
.md-skin .fc-conn-index__lbl-spoiler { color: var(--md-accent) }

.md-skin .fc-conn-index__list {
    list-style: none;
    margin: 0; padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: .4rem;
}
/* Extra class again — see the .md-conn-link note; these chips are <a>s inside
   the same `.col-lg` wrapper the generic in-content link rule targets. */
.md-skin .fc-conn-index .fc-conn-index__chip {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .3rem .6rem;
    border: 1px solid var(--md-line);
    border-radius: 999px;
    background: var(--md-surface);
    font-family: var(--md-display);
    font-size: .82rem;
    font-weight: 600;
    letter-spacing: .04em;
    color: var(--md-ink);
    text-decoration: none;
    white-space: nowrap;
    transition: border-color .14s ease, color .14s ease, background-color .14s ease;
}
.md-skin .fc-conn-index .fc-conn-index__chip:hover,
.md-skin .fc-conn-index .fc-conn-index__chip:focus-visible {
    border-color: var(--md-accent);
    color: var(--md-accent);
    text-decoration: none;
}
/* Chips for still-blurred episodes read as muted + eye-slash. They stay
   clickable: the connected episode's TITLE is never a spoiler, only its facts. */
.md-skin .fc-conn-index .fc-conn-index__chip.is-spoiler {
    color: var(--md-muted);
    background: transparent;
    border-style: dashed;
}
.md-skin .fc-conn-index__icon { font-size: .8em; opacity: .8 }
.md-skin .fc-conn-index__n {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.15rem; height: 1.15rem;
    padding: 0 .22rem;
    border-radius: 999px;
    background: var(--md-accent);
    color: #fff;
    font-size: .68rem;
    line-height: 1;
}
.md-skin .fc-conn-index__chip.is-spoiler .fc-conn-index__n {
    background: var(--md-muted);
}
@media (prefers-reduced-motion: reduce) {
    .md-skin .fc-conn-index__chip { transition: none }
}

/* ---- connections: the facts sit UNDER their episode heading -------------
   Every other rail on the site is the body of a section, directly under a
   `.subsection-header` h2. This one is nested one level deeper: each connected
   episode has its own `.md-conn-title` h4, and the facts belong to THAT. Without
   an indent the heading, the bullet and the pictures all landed on the same left
   edge (98 at 1280px) and a group read as a flat run of lines rather than as
   "these facts belong to that episode" (owner, 2026-07-28).

   The inset is the site's own 1.5rem — the §6 body inset, the same step the
   sections themselves take under their headings — applied as PADDING so the
   right edge stays on the section measure instead of pushing 24px past it.
   Scoped to connections: /differences and the episode difference sections put
   their rails directly under an h2 and are correct flush. ------------------- */
.md-skin .fc-conn-facts { padding-left: 1.5rem }

/* ---- connections: the spoiler gate ------------------------------------
   Replaces the red `alert-danger alert-dismissible` whose X dismissed the
   warning but left everything blurred. --------------------------------------- */
.md-skin .fc-spoiler {
    /* Full content width — both edges line up with the facts lists, the
       connection titles and the chip index (its wrapper is the same `.container
       > .px-4 > .col-lg` the rest of the section uses). To make it a narrower
       inset callout instead, add `max-width: 46rem; margin-inline: auto` — but
       then its left edge no longer meets the section text. */
    border: 1px solid var(--md-line);
    border-left: 3px solid var(--md-accent);
    border-radius: 4px;
    background: rgba(255, 255, 255, .72);
    padding: 1.1rem 1.3rem;
}
.md-skin .fc-spoiler__head {
    display: flex;
    align-items: center;
    gap: .55rem;
    font-family: var(--md-display);
    font-size: 1rem;
    letter-spacing: .02em;
    color: var(--md-ink);
    margin: 0 0 .5rem;
}
.md-skin .fc-spoiler__head i { color: var(--md-accent) }
.md-skin .fc-spoiler__text { color: var(--md-body); margin: 0 0 .9rem }
.md-skin .fc-spoiler__note {
    font-size: .85rem;
    color: var(--md-muted);
    margin: .8rem 0 0;
}

.md-skin .fc-spoiler__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem 1.25rem;
}
.md-skin .fc-spoiler__btn {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    padding: .45rem .95rem;
    border: 1px solid var(--md-accent);
    border-radius: 3px;
    background: var(--md-accent);
    color: #fff;
    font-family: var(--md-display);
    font-size: .88rem;
    font-weight: 600;
    letter-spacing: .04em;
    cursor: pointer;
}
.md-skin .fc-spoiler__btn:hover:not(:disabled) { filter: brightness(1.12) }
.md-skin .fc-spoiler__btn:disabled {
    background: transparent;
    border-color: var(--md-line);
    color: var(--md-muted);
    cursor: default;
}

.md-skin .fc-spoiler__pref {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    margin: 0;
    font-size: .88rem;
    color: var(--md-body);
}
.md-skin .fc-spoiler__select {
    max-width: 20rem;
    padding: .4rem .5rem;
    border: 1px solid var(--md-line);
    border-radius: 3px;
    background: var(--md-surface);
    color: var(--md-ink);
    font-size: .88rem;
}
.md-skin .fc-spoiler__select:focus-visible { outline: 2px solid var(--md-accent); outline-offset: 1px }

@media (max-width: 575.98px) {
    .md-skin .fc-spoiler__actions { align-items: stretch; flex-direction: column }
    .md-skin .fc-spoiler__pref { flex-direction: column; align-items: flex-start }
    .md-skin .fc-spoiler__select { max-width: 100% ; width: 100% }
}

/* ---- the ahead-of-progress banner -------------------------------------
   Shown (by episode-connections.js) when the reader is viewing an episode past
   their saved watch position. Deliberately a FIXED amber warning rather than the
   per-episode --md-accent: it is a caution, and it needs to read as one on every
   verse (a blue-accent page would otherwise not signal "spoiler risk"). Kept
   subtle — a light amber wash with a stronger top rule and icon. Hidden until JS
   decides it applies. */
.md-skin .fc-ahead {
    --fc-warn:      #c8860a;   /* amber rule + icon */
    --fc-warn-deep: #8a6300;   /* darker amber for the link (contrast on the wash) */
    border: 1px solid #e6d59a;
    border-top: 3px solid var(--fc-warn);
    border-radius: 4px;
    background: #fff8e6;
    padding: 1rem 1.3rem;
}
.md-skin .fc-ahead[hidden] { display: none }
.md-skin .fc-ahead__head {
    display: flex;
    align-items: center;
    gap: .55rem;
    font-family: var(--md-display);
    font-size: 1rem;
    letter-spacing: .02em;
    color: var(--md-ink);
    margin: 0 0 .4rem;
}
.md-skin .fc-ahead__head i { color: var(--fc-warn) }
.md-skin .fc-ahead__text { color: var(--md-body); font-size: .92rem; margin: 0 }
.md-skin .fc-ahead__link {
    display: inline;
    padding: 0;
    border: 0;
    background: none;
    color: var(--fc-warn-deep);
    font: inherit;
    font-weight: 600;
    text-decoration: underline;
    cursor: pointer;
    white-space: nowrap;
}
.md-skin .fc-ahead__link:hover { filter: brightness(1.12) }

/* Blurred text is still selectable and copy-pastable, which defeats the point.
   (It IS still in the page source — a real gate would fetch the facts on demand;
   `inert` + `aria-hidden` on the markup close the focus/screen-reader holes.) */
.md-skin .spoiler-blur { user-select: none; -webkit-user-select: none }

/* the "what is this?" popover trigger.
   THE RING IS DRAWN HERE, so the markup carries a bare `?` glyph — see the
   comment in components/subsection-header.blade.php. Putting a circled icon
   in here gives you two concentric circles. */
.md-skin .fr-section-heading h4 { margin: 0 }
.md-skin .fringe-popover {
    display: inline-grid; place-items: center;

    /* 26px, AN INTEGER, NOT 1.6rem — AND THAT IS LOAD-BEARING, not tidiness.
       1.6rem laid out at 25.59375px against an offsetWidth of 26, and Popper
       reads a box whose fractional rect width disagrees with its integer
       offsetWidth as a CSS `transform: scale()` (scaleX = rect.width /
       offsetWidth = 0.984) and divides the element's page coordinates by it.
       On a coordinate of ~1165px that 1.6% "correction" is an 18px error, so
       Popper placed the panel 18px right of where it belongs — i.e. on top of
       the trigger, which is what made it strobe. Popper's own state said so:
       reference x = 1183 against a DOM rect of 1164.9.
       Keep this an integer. The glyph inside stays in rem. */
    width: 26px; height: 26px; border-radius: 50%;
    border: 1px solid var(--md-line); background: var(--md-surface);
    color: var(--md-muted); font-size: .8rem; text-decoration: none;
    cursor: help;
    transition: border-color .15s ease, color .15s ease, background-color .15s ease;
}
.md-skin .fringe-popover:hover,
.md-skin .fringe-popover:focus-visible {
    border-color: var(--md-accent);
    color: var(--md-accent);
    background: color-mix(in srgb, var(--md-accent) 7%, #fff);
}
.md-skin .fringe-popover:focus-visible { outline: 2px solid var(--md-accent); outline-offset: 2px }
@media (prefers-reduced-motion: reduce) { .md-skin .fringe-popover { transition: none } }

/* ---- and the panel it opens ---------------------------------------
   It was the last thing on the page still wearing stock Bootstrap: a 2px
   rgba(0,0,0,.2) border, a 12px radius, a grey #f0f0f0 header bar and Lato
   body copy — nothing to do with this skin.

   BOOTSTRAP APPENDS THE TIP TO <body> (that is the default container, not
   just the one call site that asks for it), so it is nowhere near the section
   it describes — `body.md-skin` is what reaches it, and a `.md-skin .container`
   -style selector would not. */
body.md-skin .popover {
    /* THE PANEL MUST NOT TAKE THE POINTER — this is what fixed the flicker.
       Bootstrap parks the tip at z-index 1070 with effectively no gap on the
       placement axis, and then pushes .popover-arrow a further 9px OUTWARD,
       i.e. straight back over the trigger. Measured on ep 101: the panel's
       right edge landed 10px inside the 26px-wide trigger and the arrow 19px
       inside it, so 16 of 25 sample points across the control hit the popover
       rather than the control. Every one of those points is a loop — hover
       shows the panel, the panel steals the pointer, mouseleave hides it, the
       pointer is back on the trigger, hover shows it again — which reads as a
       tooltip strobing on and off. `data-bs-offset` on the trigger opens a real
       gap (see subsection-header.blade.php); this makes the overlap harmless
       whatever Popper decides to do at some other viewport width or placement.

       Safe because the panel is non-interactive: all 20 descriptions (16 in the
       section views, 4 in EpisodeController) are plain text with at most <i>
       and <br/>. IF ONE EVER GAINS A LINK, this line has to go and the gap has
       to carry the fix alone. */
    pointer-events: none;

    /* wider than Bootstrap's 276px — these descriptions are a sentence or two —
       but never wider than a phone, since Popper can only slide the panel, not
       shrink it */
    max-width: min(21rem, calc(100vw - 2rem));
    border: 1px solid var(--md-line);
    border-radius: 4px;
    background: var(--md-surface);
    box-shadow: 0 2px 4px rgba(20, 30, 40, .10), 0 10px 28px rgba(20, 30, 40, .16);
    font-family: var(--md-font);
}
/* the header is the section's own name: the skin's compact accent eyebrow,
   the same treatment as the in-section sub-headings */
body.md-skin .popover .popover-header {
    padding: .7rem .95rem .3rem;
    border-bottom: 0;
    background: transparent;
    color: var(--md-accent);
    font-family: var(--md-display);
    font-size: .8rem;
    font-weight: 600;
    line-height: 1.3;
    text-transform: uppercase;
    letter-spacing: .12em;
}
/* Bootstrap draws a header underline as a ::before on bottom-placed popovers;
   this header has no bar to underline. */
body.md-skin .popover .popover-header::before { display: none }
body.md-skin .popover .popover-body {
    padding: 0 .95rem .8rem;
    color: var(--md-body);
    font-size: .92rem;
    line-height: 1.65;
}
body.md-skin .popover .popover-body a { color: var(--md-accent) }

/* The arrow is two stacked triangles — ::before is the border, ::after the
   fill — one pair per placement. Popper flips the placement when there is no
   room, so all four are covered even though the trigger asks for "left". */
body.md-skin .bs-popover-top > .popover-arrow::before,
body.md-skin .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before { border-top-color: var(--md-line) }
body.md-skin .bs-popover-top > .popover-arrow::after,
body.md-skin .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { border-top-color: var(--md-surface) }
body.md-skin .bs-popover-end > .popover-arrow::before,
body.md-skin .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before { border-right-color: var(--md-line) }
body.md-skin .bs-popover-end > .popover-arrow::after,
body.md-skin .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { border-right-color: var(--md-surface) }
body.md-skin .bs-popover-bottom > .popover-arrow::before,
body.md-skin .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before { border-bottom-color: var(--md-line) }
body.md-skin .bs-popover-bottom > .popover-arrow::after,
body.md-skin .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { border-bottom-color: var(--md-surface) }
body.md-skin .bs-popover-start > .popover-arrow::before,
body.md-skin .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before { border-left-color: var(--md-line) }
body.md-skin .bs-popover-start > .popover-arrow::after,
body.md-skin .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { border-left-color: var(--md-surface) }


/* ================================================================= *
 |  BODY COPY  —  full-width prose (the legacy measure)
 * ================================================================= */
.md-skin .container p {
    font-size: 1.02rem;
    line-height: 1.8;
    color: var(--md-body);
}
/* …but not inside the hero, cards, quotes or the chart legend. (`.card` here
   is now only reachable through /episode's `?ioi=`/`?diff=`/`?conn=current`
   section shims — see the IMAGE CARDS block.) */
.md-skin .md-hero p,
.md-skin .card p,
.md-skin .fr-quote p,
.md-skin #connection-chart p {
    font-size: inherit; line-height: inherit; color: inherit;
}
/* The footer is chrome, not prose — but it lives inside `.md-skin .container`
   on every non-home page, so the prose rule above was resizing/recolouring the
   blurb, copyright and disclaimer (dark #3c4a57 on the dark footer). It stays
   on its own type via footer-redesign.css. NB `.frfoot`, not `.fr-footer` —
   the class was mistyped here originally, so this exemption never fired and the
   footer copy matched the prose on every page but the homepage. Colour is set
   explicitly (not `inherit`) because the blurb's own muted colour is lower
   specificity than the prose rule; all three footer paragraphs are muted. */
.md-skin .frfoot p {
    font-size: inherit;
    line-height: inherit;
    color: var(--frfoot-muted);
}
.md-skin .col-lg a,
.md-skin .section-text a,
.md-skin .ioi-list a {
    color: var(--md-accent);
    text-decoration: underline;
    text-underline-offset: 2px;
}
.md-skin .col-lg a:hover,
.md-skin .section-text a:hover,
.md-skin .ioi-list a:hover { color: var(--md-ink) }

/* ---- bulleted lists (items of interest, JJ eggs, lab notes, …) ---- */
.md-skin .ioi-list ul {
    list-style: none;
    margin: 0; padding: 0;
    display: grid; gap: .7rem;
}
.md-skin .ioi-list li {
    position: relative;
    padding-left: 1.3rem;
    font-size: 1.02rem;
    line-height: 1.8;
    color: var(--md-body);
}
.md-skin .ioi-list li::before {
    content: "";
    position: absolute; left: 0; top: .62em;
    width: .42rem; height: .42rem; border-radius: 50%;
    background: var(--md-accent);
}

/* (The `?ioi=reading` .ioi-read__* block lived here until 2026-07-26. Items of
   interest and the four difference sections now use the shared IMAGE RAIL at the
   foot of this file, which supersedes it — see git history.) */

/* ---- dev-only per-section layout switcher (renders in local env only) ---- */
.sec-dev-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    /* wraps because the differences pages carry FOUR options (rail / gallery /
       thumb / legacy), which do not fit 375px on one line — `width: max-content`
       alone let it push the document wider than the viewport */
    flex-wrap: wrap;
    gap: .2rem;
    width: max-content;
    max-width: 100%;
    margin: .6rem auto 0;
    padding: .2rem .28rem;
    background: rgba(34, 48, 64, .06);
    border: 1px solid rgba(34, 48, 64, .18);
    border-radius: 999px;
    font-family: system-ui, -apple-system, sans-serif;
    font-size: .72rem;
}
.sec-dev-toggle__lbl {
    text-transform: uppercase;
    letter-spacing: .08em;
    color: #5a6b7b;
    font-weight: 600;
    padding: 0 .45rem 0 .55rem;
}
.sec-dev-toggle__opt {
    text-decoration: none;
    color: #33475b;
    padding: .22rem .8rem;
    border-radius: 999px;
    line-height: 1.4;
    white-space: nowrap;
}
.sec-dev-toggle__opt:hover { background: rgba(34, 48, 64, .1); color: #223040; }
.sec-dev-toggle__opt.is-active {
    background: var(--md-accent, #0056b7);
    color: #fff;
    font-weight: 600;
}

/* ---- clues + observer: text + paired image ----
   Both rows carry BOTH `row-cols-md-3` and an explicit `col-md-6` on the text
   column. Bootstrap's row-cols wins, so the paragraph is squeezed to a third of
   the container (434px of 1320) — invisible on the narrow legacy page, but it
   reads as a bug next to the now full-width synopsis. Restore the mock-up's
   pairing (clue prose runs nearly full-width, image beside it) without touching
   the sections' layout markup — `.clue-row` is a scoping hook added to the two
   clue partials, inert without `.md-skin`. */
@media (min-width: 992px) {
    .md-skin section#observer .row,
    .md-skin .clue-row {
        display: grid;
        grid-template-columns: 1.7fr 1fr;
        gap: 1.6rem;
        align-items: start;
    }
    .md-skin section#observer .row > *,
    .md-skin .clue-row > * {
        flex: none; width: auto; max-width: none; margin-left: 0; padding: 0;
    }
}

/* ---- quotes ---- */
.md-skin .fr-quote {
    display: block;
    padding: 1rem 1.3rem;
    margin: 0 0 1.1rem;
    background: var(--md-surface);
    border: 1px solid var(--md-line);
    border-left: 3px solid var(--md-accent);
    border-radius: 0 6px 6px 0;
    color: var(--md-body);
    line-height: 1.6;
}
.md-skin .quote-speaker { color: var(--md-accent2) }
.md-skin .redacted { border-radius: 2px }


/* ================================================================= *
 |  IMAGE CARDS  —  LEGACY-SHIM ONLY SINCE 2026-07-31
 |  (components/image-card.blade.php)
 |
 |  NOTHING ON A LIVE PAGE RENDERS A CARD ANY MORE. /episode used to
 |  present the same object two ways — the image rail's shot (a bare
 |  picture with a small [label · timestamp] line under it) and this
 |  component's card (a box round the picture, a second round the caption,
 |  a third round the timestamp) — both on screen at once, Walter's food
 |  and glyph sightings against any connection carrying 2+ images. The
 |  owner asked for one treatment, and for it to be the shot: "I don't
 |  think these images need to be 'cards' anymore… make them
 |  fr-rail__shot elements like the images we see in episode connections."
 |
 |  TWO PASSES OF RESTYLING THE CARD INTO A LOOKALIKE FAILED ON THE SAME
 |  THING — a card draws a box round its body, so the caption kept its
 |  border however the chrome was stripped. So the eight call sites
 |  (Walter's food, glyph sightings, J.J. easter eggs, the observer, both
 |  clues, the lab notes, the mobile case file) now render THE SAME MARKUP
 |  the rail renders, via the shared partials/image-shot.blade.php — which
 |  the rail includes too, so the two cannot drift. Their styles are the
 |  `.fr-rail__shot*` block further down this file.
 |
 |  SO WHY IS THIS BLOCK STILL HERE? Because deleting it was tried and was
 |  WRONG, and the reason is easy to miss: `?design=current` /
 |  `?hero=current` never load this stylesheet, but /episode's PER-SECTION
 |  layout shims do. `?ioi=current`, `?diff=current` and `?conn=current`
 |  each swap one section back to the Phase-1 list + numbered card grid
 |  while the page keeps the massive hero — so `md-skin` is still on the
 |  body and the cards are still on the page (measured on ep 221: 4, 9 and
 |  11 cards respectively, 24 with all three). Delete this and those three
 |  fall through to the legacy Freelancer card on the sky-gradient skin.
 |
 |  These values are therefore FROZEN at what they were before 2026-07-31,
 |  deliberately: a comparison shim's job is to look like what it is
 |  comparing against, so it should not have moved at all. Don't restyle
 |  them — if the live look needs changing, change the shot.
 * ================================================================= */
.md-skin .card {
    background: var(--md-surface) !important; /* beats Bootstrap .bg-transparent */
    border: 1px solid var(--md-line);
    border-radius: 4px;
    box-shadow: 0 1px 3px rgba(42, 53, 64, .1) !important;
    overflow: hidden;
}
.md-skin .card-img-top { cursor: pointer; border-radius: 0 }
.md-skin .card-body {
    padding: .6rem .75rem;
    font-size: .82rem;
    line-height: 1.45;
    color: var(--md-ink);
}
.md-skin .card-footer {
    background: transparent;
    border-top: 1px solid var(--md-line);
    padding: .4rem .75rem;
}
.md-skin .card-footer small,
.md-skin .card-footer .text-muted {
    color: var(--md-accent) !important;
    font-size: .7rem;
    font-variant-numeric: tabular-nums;
}


/* ================================================================= *
 |  DIVIDERS
 |  The between-section .fringe-divider becomes a hairline + accent dot.
 |  The per-episode barbed-wire dividers (419 / season 5) use a different
 |  class and are deliberately left alone — they're a show effect.
 * ================================================================= */
.md-skin .fringe-divider {
    height: auto;
    /* Padding stays symmetric so the background dot (centered over the padding
       box) lines up with the ::before/::after rules; the extra breathing room
       below the divider rides on margin instead, which doesn't move the dot. */
    padding: 1rem 0;
    margin: 0 0 .75rem;
    border: 0 !important;
    box-shadow: none !important;
    background-color: transparent !important; /* beats .blueverse/.redverse/… */
    background-image: radial-gradient(circle at center,
        var(--md-accent) 0 3px, rgba(0, 0, 0, 0) 3.5px) !important;
    background-repeat: no-repeat !important;
    background-position: center !important;
    background-size: 100% 100% !important;
    display: flex; align-items: center; justify-content: center; gap: 1.2rem;
}
.md-skin .fringe-divider::before,
.md-skin .fringe-divider::after {
    content: "";
    flex: 1;
    max-width: 180px;
    border-top: 1px solid var(--md-line);
}


/* ================================================================= *
 |  THE FENCE  —  barbed wire as a page boundary (419 / season 5)
 |
 |  Legacy ran the barbed-wire strip full-width BETWEEN sections, where it read
 |  as a fence line across the page. When Phase 2 dropped the between-section
 |  dividers the wire followed the replacement INSIDE sections — and a boundary
 |  graphic used as punctuation stops meaning anything: six per page is
 |  wallpaper, its 88px mass is ~90x the 1px hairline it sits among, and the
 |  art is one CONTINUOUS strand drawn to span a viewport, so cutting it to a
 |  padded column leaves wire attached to nothing at both ends.
 |
 |  So it is promoted out of the divider system and back to a boundary:
 |    ?wire=frame    (default, owner's pick) two rails, below the hero and below
 |                   the last section — the article is fenced in, page chrome
 |                   (jump bar, prev/next) sits outside
 |    ?wire=current  the in-section dividers, for comparison
 |
 |  A third variant laid one strand across the hero still ("shot through a
 |  fence"); rejected 2026-07-28. Worth knowing if it's ever revisited: it has to
 |  go at the TOP of the hero, because the art is a black silhouette and
 |  .md-hero__body's scrim runs to 94% at the bottom edge, and it must sit fully
 |  INSIDE the hero rather than overhanging its top — the .fixed-top navbar is
 |  z-index 1030 and shears an overhang's barbs into a straight edge.
 |
 |  In-section dividers on these episodes go back to the plain .fringe-divider,
 |  so season 5 differs from every other episode by its FRAME, not by a
 |  different comma.
 |
 |  Two constraints worth keeping in mind before moving any of this:
 |  - The strips are emitted at BODY level, where sections carry their own
 |    .container, so they are full-bleed for free. Don't reach for a 100vw
 |    breakout: on Windows that counts the scrollbar and reintroduces the
 |    horizontal overflow this page has been kept clear of.
 |  - The art is a black silhouette on transparency, so it needs a light ground.
 |    That is what rules out the hero's BOTTOM edge, where .md-hero__body's
 |    scrim runs to 94% and would swallow it whole.
 * ================================================================= */
.md-skin .fr-fence {
    height: 5.5rem;
    /* the source is 1180px wide; tile it (as legacy did) rather than stretching
       one copy to the viewport, which would smear the barbs horizontally */
    background-repeat: repeat-x;
    background-position: center;
    background-size: auto 100%;
    opacity: .92;
    filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .28));
    pointer-events: none;
}

/* ---- the two frame rails ---- */
.md-skin .fr-fence--top { margin: 0 }
.md-skin .fr-fence--bottom { margin: 1rem 0 0 }


/* ================================================================= *
 |  DEV-ONLY hero switcher (episode/partials/hero-dev-toggle.blade.php)
 |  Duplicated from episode-hero.css, which the massive skin doesn't load.
 * ================================================================= */
.md-skin .fr-hero-devbar {
    position: fixed; left: 50%; bottom: 1rem; transform: translateX(-50%);
    z-index: 2000;
    display: flex; align-items: center; gap: .35rem;
    padding: .4rem .5rem;
    background: rgba(23, 24, 28, .95);
    border: 1px solid rgba(255, 255, 255, .15);
    border-radius: 999px;
    box-shadow: 0 6px 24px rgba(0, 0, 0, .4);
    font-size: .8rem;
}
.md-skin .fr-hero-devbar__label {
    color: #9aa0ab; font-size: .7rem; text-transform: uppercase;
    letter-spacing: .05em; padding: 0 .35rem 0 .5rem;
}
.md-skin .fr-hero-devbar a {
    display: inline-block; padding: .3rem .7rem; border-radius: 999px;
    color: #cfd2d8; text-decoration: none;
}
.md-skin .fr-hero-devbar a:hover { background: rgba(255, 255, 255, .1); color: #fff }
.md-skin .fr-hero-devbar a.is-active { background: #fff; color: #17181c; font-weight: 700 }

@media (prefers-reduced-motion: reduce) {
    .md-skin *, .md-skin *::before, .md-skin *::after {
        transition-duration: .01ms !important;
    }
}


/* ================================================================= *
 |  THE JUSTIFIED GALLERY  (partials/justified-gallery.blade.php)
 |
 |  For a section holding MANY pictures whose sizes and aspect ratios disagree —
 |  where a uniform card grid fails in both directions at once, cropping the
 |  portraits to ribbons with `object-fit: cover` while stretching a 250px source
 |  to a 356px card.
 |
 |  So: constant ROW HEIGHT, native aspect ratio, nothing cropped — the layout
 |  Flickr and Google Photos use. A row of a 3.5:1 card next to a 0.75 portrait
 |  is a feature, not something to normalise away.
 |
 |  Built for /scavenger-hunt (2026-07-26, where the images run 140px to 1507px
 |  wide and 0.75 to 3.5 in ratio) and promoted here 2026-07-27 when
 |  /inside-walters-lab — 25 FOX set photos, 21 landscape and 4 portrait — became
 |  the second caller. `.sh-gallery*` was the original name.
 |
 |  HOW IT WORKS, with no JS and no layout shift. Each item carries its ratio as
 |  --fr-gal-ar (measured server-side and committed) and gets
 |
 |      flex-basis: ar * row-height        flex-grow: ar
 |
 |  In a row with free space F and ratio sum S, each item grows by F*ar/S, so its
 |  final width is ar*(H + F/S) — every item in the row resolves to the SAME
 |  implied height H + F/S, and the row justifies exactly. `aspect-ratio` then
 |  makes each box's height follow its own width, so the row bottoms line up
 |  without anything being measured.
 |
 |  THE GUARD RAIL: max-width caps the STRETCH, and it is the only one needed.
 |  Rows justify by growing ABOVE the base height, and a sparse row can grow a
 |  long way — one 400x291 image alone in a 1068px measure would resolve to 777px
 |  tall, a 2.7x blow-up. The cap is expressed as a maximum row HEIGHT
 |  (--fr-gal-row-max) so it applies evenly across a row: max-width: ar * row-max.
 |  A row that hits it stops justifying and leaves a ragged right edge, which is
 |  the lesser evil and is what a photo gallery's last row looks like anyway.
 |  A CALLER WHOSE SOURCES ARE ALL THE SAME HEIGHT should set --fr-gal-row-max to
 |  that height: then no image can ever be drawn above its native size at all
 |  (/inside-walters-lab does exactly this, at 352px).
 |
 |  A ZERO-BASIS, HUGE-FLEX-GROW SPACER was in the original, the standard trick
 |  for stopping the LAST row justifying. It was removed: with a height cap
 |  already in place it protects nothing the cap doesn't, and it over-fires badly
 |  — a gallery that fits in ONE row is also its own last row, so the spacer
 |  stopped those galleries justifying at all. Measured at 1280px it left two of
 |  /scavenger-hunt's clues ending ~80-150px short, both of which now fill the
 |  measure exactly. Don't reintroduce it without re-measuring.
 |
 |  THE NUMBERS ARE MEASURED, not picked. At the site's 1068px body measure
 |  (1280px viewport) a base of 190px puts four 4:3 images in a row — 4*253+24 =
 |  1036, just inside — which then justify to ~196px, so a 250x188 source renders
 |  at essentially 1:1. Raising the base to 200 pushes that row to three-up and
 |  every image with it; these numbers are tighter than they look.
 |
 |  THE BASE SCALES WITH THE VIEWPORT, and it has to. Held flat, a wider measure
 |  doesn't make the rows taller, it makes them SPARSER — the same items pack
 |  differently and a row is left with two images and a lot of free space, which
 |  the cap then refuses to fill. Measured at 1600px with a flat 190px, one
 |  /scavenger-hunt gallery split 3+2 and its second row ended 359px short.
 |  `10vw + 62px` is the line through 190px at 1280 and 222px at 1600 (= 190
 |  scaled by the ratio of the two body measures, 1248/1068), which keeps the
 |  packing proportional. Clamped so it can't run away on an ultrawide.
 * ================================================================= */
.md-skin .fr-gallery {
    --fr-gal-row-h: clamp(190px, calc(10vw + 62px), 250px);
    --fr-gal-row-max: 320px;

    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: flex-start;
    list-style: none;
    margin: 1.5rem 0 0;
    padding: 0;
}

.md-skin .fr-gallery__item {
    flex-grow: var(--fr-gal-ar);
    flex-basis: calc(var(--fr-gal-ar) * var(--fr-gal-row-h));
    /* the guard rail — see above */
    max-width: calc(var(--fr-gal-ar) * var(--fr-gal-row-max));
    aspect-ratio: var(--fr-gal-ar);
    margin: 0;
    min-width: 0;
}

.md-skin .fr-gallery__img {
    display: block;
    width: 100%;
    height: 100%;
    /* the box already carries the image's own aspect-ratio, so `cover` never
       actually crops — it is here to absorb sub-pixel rounding rather than let a
       hairline letterbox open up on one edge */
    object-fit: cover;
    border-radius: 4px;
    background: #e8eef4;
    cursor: zoom-in;
    /* the same resting lift the image rail's thumbnails carry, and for the same
       reason: these open the lightbox, and at rest nothing else says so —
       `cursor: zoom-in` is only discoverable once the pointer is already on it. */
    box-shadow: 0 1px 2px rgba(42, 53, 64, .12), 0 3px 8px rgba(42, 53, 64, .16);
    transition: box-shadow .16s ease, transform .16s ease;
}
.md-skin .fr-gallery__img:hover,
.md-skin .fr-gallery__img:focus-visible {
    box-shadow: 0 2px 4px rgba(42, 53, 64, .16), 0 8px 20px rgba(42, 53, 64, .28);
    transform: translateY(-1px);
    outline: none;
}
.md-skin .fr-gallery__img:focus-visible {
    outline: 2px solid var(--md-accent);
    outline-offset: 3px;
}

/* NARROW: the base row height GOES UP below lg, which looks backwards and isn't.
   The measure shrinks much faster than the images do, so keeping 190px would put
   a single 4:3 image in a row with 45px of dead space beside it. A taller base
   lets a landscape take the full width and pairs portraits two-up, which is what
   actually reads at 375px. */
@media (max-width: 991.98px) {
    .md-skin .fr-gallery { --fr-gal-row-h: 175px; --fr-gal-row-max: 250px }
}
@media (max-width: 575.98px) {
    .md-skin .fr-gallery { --fr-gal-row-h: 180px; --fr-gal-row-max: 280px; gap: 6px }
}

@media (prefers-reduced-motion: reduce) {
    .md-skin .fr-gallery__img { transition: none }
    .md-skin .fr-gallery__img:hover,
    .md-skin .fr-gallery__img:focus-visible { transform: none }
}


/* ================================================================= *
 |  THE APPEARANCE STRIP  (partials/appearances.blade.php)
 |
 |  Season rows of episode tiles, always open — no modal, no click. The strip's
 |  SHAPE is the answer to "when in the show does this thing exist", which is
 |  what the legacy "List of appearances" button never told you however many
 |  times you opened it.
 |
 |  Lived in character-page.css as `.fc-appear*` until 2026-07-27, when
 |  /universe-hopping-methods and /intro-sciences became the second and third
 |  callers and it moved here — the same promotion the image rail made.
 * ================================================================= */
.md-skin .fr-appear { margin-top: 1.4rem }

.md-skin .fr-appear__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .3rem .7rem;
    margin: 0 0 .6rem;
    font-family: var(--md-display);
    font-size: .8rem !important;   /* beats `.md-skin .container p` */
    line-height: 1.4 !important;
    text-transform: uppercase;
    letter-spacing: .12em;
    color: var(--md-muted) !important;
}
.md-skin .fr-appear__n { color: var(--md-accent) !important; font-weight: 600 }
.md-skin .fr-appear__arrow { margin: 0 .35rem; opacity: .65 }

.md-skin .fr-appear__none {
    margin: 0;
    font-size: .92rem !important;
    color: var(--md-muted) !important;
    font-style: italic;
}
.md-skin .fr-appear__none i { margin-right: .45rem; font-style: normal }

.md-skin .fr-appear__grid { display: grid; gap: .45rem }

/* Label column is fixed so every season row's tiles start on the same line —
   the vertical edge is what makes the strips comparable between entries. */
.md-skin .fr-appear__season {
    display: grid;
    grid-template-columns: 5.4rem minmax(0, 1fr);
    align-items: center;
    gap: .75rem;
}
.md-skin .fr-appear__label {
    font-family: var(--md-display);
    font-size: .68rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .16em;
    color: var(--md-muted);
    text-align: right;
}
.md-skin .fr-appear__tiles { display: flex; flex-wrap: wrap; gap: .3rem }

/* The homepage grid's tile, one size down — a strip carries up to 22 of them
   per season and sits inside a section, not across a whole band. */
.md-skin .fr-appear__tile {
    --tile-accent: #4d9de0;
    display: grid;
    place-items: center;
    width: 2rem;
    height: 1.75rem;
    border-radius: 3px;
    border: 1px solid color-mix(in srgb, var(--tile-accent) 40%, #ffffff);
    background: color-mix(in srgb, var(--tile-accent) 12%, #ffffff);
    color: var(--md-body);
    font-family: var(--md-display);
    font-size: .78rem;
    font-weight: 600;
    text-decoration: none !important;   /* beats the generic .col-lg a rule */
    transition: background .13s ease, color .13s ease, transform .13s ease;
}
/* The episodes of the same season this subject is NOT in. Same footprint as a
   tile so the row reads as a real timeline; a hairline instead of a box so the
   appearances are what the eye lands on. */
.md-skin .fr-appear__slot {
    width: 2rem;
    height: 1.75rem;
    border-radius: 3px;
    background: linear-gradient(var(--md-line), var(--md-line)) center / 100% 1px no-repeat;
    opacity: .65;
}

.md-skin .fr-appear__tile:hover,
.md-skin .fr-appear__tile:focus-visible {
    background: var(--tile-accent);
    border-color: var(--tile-accent);
    color: #fff;
    transform: translateY(-2px);
}

@media (max-width: 575.98px) {
    .md-skin .fr-appear__season { grid-template-columns: minmax(0, 1fr); gap: .25rem }
    .md-skin .fr-appear__label { text-align: left }
    .md-skin .fr-appear__grid { gap: .75rem }
}

@media (prefers-reduced-motion: reduce) {
    .md-skin .fr-appear__tile { transition: none }
    .md-skin .fr-appear__tile:hover { transform: none }
}


/* ================================================================= *
 |  THE IMAGE RAIL — the shared list-with-pictures component
 |
 |  One list. Every item is a row of THREE columns:
 |      [ figure | bullet | text ]
 |  and only the first is optional. EVERY item carries the accent dot, whether
 |  or not it has a picture (owner, 2026-07-25) — the dot is what says "this is
 |  one entry in a list", and an item doesn't stop being a bullet because it also
 |  has a screenshot. The bullet column is what makes the two kinds of row read
 |  as one list; the figure column is an extra the illustrated ones fill.
 |
 |  Columns are the whole idea: the text keeps one hard left edge all the way
 |  down (the thing a bulleted list gives you, which floating the thumbnail
 |  destroyed), and the pictures keep another (the thing a gallery gives you,
 |  which the split list threw away).
 * ================================================================= */
.md-skin .fr-rail {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* The middle column is `auto` (the dot's own 6.7px) rather than a fixed gutter,
   so the bullet sits exactly one gap from the text on every row. Columns are
   assigned EXPLICITLY below, so a text-only row can omit its figure entirely
   instead of carrying an empty box — auto-placement would otherwise slide its
   dot into the figure column. */
.md-skin .fr-rail__row {
    display: grid;
    grid-template-columns: var(--fr-rail-thumb-w, 152px) auto minmax(0, 1fr);
    align-items: start;
    column-gap: 1rem;
    padding: .5rem 0;
}
/* A row with a picture needs a little more air than a one-line text row, or the
   thumbnails touch down the column. Text rows stay tight so a sparse page reads
   as a list, not as a gallery with the pictures missing. */
.md-skin .fr-rail__row:has(.fr-rail__thumb) { padding: .65rem 0 }

.md-skin .fr-rail__figure { grid-column: 1 }

/* The dot is aligned to the FIRST LINE of its text, not to the middle of the
   row: a two-line item would otherwise put its marker halfway down the second
   line, and an illustrated item would put it halfway down a 108px thumbnail.
   .62em matches the .ioi-list bullet's own offset. */
.md-skin .fr-rail__dot {
    grid-column: 2;
    display: block;
    width: .42rem;
    height: .42rem;
    margin-top: .62em;
    border-radius: 50%;
    background: var(--md-accent);
}

.md-skin .fr-rail__thumb {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    border-radius: 4px;
    border: 1px solid var(--md-line);
    background: var(--md-surface);
    /* Two-layer lift, at rest (owner, 2026-07-26): the thumbnails open the
       lightbox, and on a flat white page a 1px hairline was the only thing
       saying so — an affordance you had to hover to discover. Contact shadow +
       diffused drop, the pattern the rest of the site's raised surfaces use.
       THE PLACEHOLDER DELIBERATELY STAYS FLAT: it is the same size in the same
       column but is NOT clickable, so the shadow is now what separates the two. */
    box-shadow: 0 1px 2px rgba(42, 53, 64, .12), 0 3px 8px rgba(42, 53, 64, .16);
    cursor: pointer;
    transition: border-color .14s ease, box-shadow .14s ease;
}
/* Hover/focus has to escalate clearly past the resting lift, or raising the
   default would have flattened the interaction instead of advertising it. */
.md-skin .fr-rail__thumb:hover,
.md-skin .fr-rail__thumb:focus-visible {
    border-color: var(--md-accent);
    box-shadow: 0 2px 4px rgba(42, 53, 64, .16), 0 8px 20px rgba(42, 53, 64, .28);
    outline: none;
}

/* Drop the 16/9 crop for a picture that is a DOCUMENT rather than a screen
   grab: the Fringe Division case file is 1038x703 and Walter's lab notes 4:3,
   and covering either to 16/9 cuts off the thing you are meant to read.
   (2026-07-31, with those two moving onto the shared shot.) */
.md-skin .fr-rail__thumb--natural { aspect-ratio: auto }

/* Timestamps were buried in a card footer before. Under the thumb they cost no
   layout — the figure column is already wider than the number.
   `text-align: start` because the shot is now also used outside a rail, and
   two of those call sites (the observer, both clues) sit in a legacy
   `.text-end` column that would otherwise right-align this line away from
   every other one on the page. */
.md-skin .fr-rail__time {
    display: block;
    margin-top: .3rem;
    font-family: var(--md-display);
    font-size: .68rem;
    letter-spacing: .08em;
    color: var(--md-muted);
    font-variant-numeric: tabular-nums;
    text-align: start;
}

.md-skin .fr-rail__text {
    grid-column: 3;
    line-height: 1.62;
}
.md-skin .fr-rail__text > :last-child { margin-bottom: 0 }

/* Connections' quoted dialogue is built out of Bootstrap `.row`/`.col` (134 of
   the 375 connection rows are the `Speaker|note|episode|line` encoding — see
   Legacy::connectionItemText). `.row`'s negative gutter pulled the quote LINES
   12px left of the prose above them, because the speaker line sits in a `.col`
   that pays the padding back and the quote line does not. Inside a rail the text
   column has a hard left edge, so that 12px read as two different left margins
   in the same list. Zeroing the gutters is safe: nothing here is a grid. */
.md-skin .fr-rail__text > .row { margin-left: 0; margin-right: 0 }
.md-skin .fr-rail__text > .row > * { padding-left: 0; padding-right: 0 }

@media (min-width: 1200px) {
    .md-skin .fr-rail__row { --fr-rail-thumb-w: 176px }
}

/* ----------------------------------------------------------------- *
 |  MORE THAN ONE PICTURE FOR ONE FACT — THE BREAK-OUT PAIR (2026-07-28)
 |
 |  Added for /episode's connections. 64 of the site's 177 illustrated connection
 |  rows carry two filenames, and 63 of those two are one frame from EACH of the
 |  two episodes — the correspondence the fact is about, photographed twice.
 |
 |  SUCH A ROW LEAVES THE FIGURE COLUMN. The frames run side by side UNDERNEATH
 |  the fact at 340px each, aligned to the picture column's own left edge; the dot
 |  and the text keep their places on the line above, so it is still one bullet in
 |  one list.
 |
 |  It was built in the gutter first — two frames sharing the 176px column at 84px
 |  each — and that was wrong, which the owner spotted immediately. The reason it
 |  is wrong is in the data: a pair is not an illustration BESIDE a fact the way a
 |  Redverse thumbnail is, it IS the fact ("this thing there, this thing here"),
 |  and of the 63 groups that contain a pair, 54 hold exactly one fact. So the
 |  typical pair group is a heading, one sentence and two frames — there is no
 |  column of pictures to keep unbroken, which is the rail's whole reason for
 |  existing, and a comparison is the last thing that should be halved to fit a
 |  gutter. Same call /alternate-united-states made: rank the elements by what the
 |  content is instead of reaching for the component that happens to exist.
 |
 |  340px is the character page's version-still width, i.e. a size the site
 |  already uses for "a picture you are meant to look at". Half the body column
 |  (526px) was measured too: it adds ~2,200px to ep 513, which carries ten pairs,
 |  against ~1,200px at 340.
 |
 |  SIDE BY SIDE, NOT ONE ABOVE THE OTHER: a comparison is horizontal, and pair
 |  rows carry the SHORTEST text of any kind here (160 chars on average, against
 |  196 for one-image rows and 257 for text-only).
 |
 |  The single-image case goes through the same `.fr-rail__shot` wrapper, so the
 |  three original callers (items of interest, the episode difference sections,
 |  /differences) render byte-identically.
 * ================================================================= */
.md-skin .fr-rail__shot { min-width: 0 }

.md-skin .fr-rail__figure--multi {
    display: flex;
    gap: 1rem;
}
.md-skin .fr-rail__figure--multi .fr-rail__shot {
    flex: 0 1 var(--fr-rail-pair-w, 340px);
    max-width: var(--fr-rail-pair-w, 340px);
}

/* The break-out itself. The dot and text are pinned to row 1 explicitly — every
   child carries an explicit grid-column, so without this the figure would be
   auto-placed into row 1 as well and overlap the text.

   THE FRAMES START AT THE TEXT COLUMN, NOT AT THE ROW'S LEFT EDGE (owner,
   2026-07-28): they belong to one bullet, so they are indented under its
   sentence exactly as the sentence is indented under its dot.

   The text is track 2 in a collapsed list and in every list below sm (both are
   `[bullet | text]`), and track 3 only in a list that still carries a picture
   column — so track 2 is the base and the 3-track case is the override. Doing it
   the other way round breaks the mobile layout silently: `grid-column: 3` on a
   2-track row conjures an implicit third column and the figure lands outside the
   defined tracks. Only 3 groups on the whole site contain both a single-image row
   and a pair, so the override is nearly theoretical, but it is cheap to be right. */
.md-skin .fr-rail__row--stack .fr-rail__dot,
.md-skin .fr-rail__row--stack .fr-rail__text { grid-row: 1 }
.md-skin .fr-rail__row--stack .fr-rail__figure--multi {
    grid-column: 2 / -1;
    grid-row: 2;
    margin-top: .8rem;
}
@media (min-width: 576px) {
    .md-skin .fr-rail:not(.fr-rail--nofig) .fr-rail__row--stack .fr-rail__figure--multi {
        grid-column: 3 / -1;
    }
}

/* Which episode a frame comes from is real information here, not decoration: of
   the 113 single-image connection rows, 38 sit on episode 1 and 75 on episode 2,
   so on any given page a third to two thirds of the shots are the OTHER
   episode's. The card grid this replaces said it in a caption line; the rail says
   it under the thumb, next to the timestamp that was already there. */
.md-skin .fr-rail__shot-ep {
    font-weight: 600;
    color: var(--md-ink);
    margin-right: .45rem;
}
/* (The code used to be forced onto its own line inside a pair, because the two
   frames were sharing the 176px gutter at 84px each and could not fit a code and
   a timestamp on one line. The break-out below gives them 340px, so the label is
   an ordinary one-liner again — including the one row in the data whose
   timestamp is 7 characters.) */

/* The counterpart on the OTHER side of the timestamp, added 2026-07-31 when
   /episode's Walter's food, glyph sightings and J.J. easter eggs moved onto the
   shared shot: those pictures carry a caption where a connection's carries only
   an episode code. Same treatment as the label — the owner asked for
   [timestamp caption] in that order, so it is a left margin, not a right one. */
.md-skin .fr-rail__shot-cap {
    font-weight: 600;
    color: var(--md-ink);
    margin-left: .45rem;
}

/* The column stays the site-standard 152/176px on connections too, so the
   picture column lands in the same place in every section of the page. A pair
   therefore renders at ~84px per frame. That was weighed against widening the
   column to 240 (which would have made a pair 116px) and the widening lost: at
   neither size can you read detail off the thumbnail — an object filling a fifth
   of the frame is 17px across at 84 and 23px at 116 — so the thumbnail is an
   index either way and the lightbox is where you look. Consistency and a shorter
   page win when the extra size buys nothing. `--fr-rail-thumb-w` is the one
   number to change if that judgement is ever revisited. */

/* NO PICTURE COLUMN AT ALL — for a list where not one item has a picture.
 |
 |  The placeholder exists to keep a COLUMN unbroken, which presupposes a column:
 |  on /differences 62% of rows are illustrated, so the few gaps read as gaps in a
 |  run of pictures. Connections is not shaped like that. Of its 322 groups, 154
 |  have no picture anywhere and 152 have one on every single fact — only 16 are
 |  mixed. And 283 of the 322 hold ONE fact, so an ungated placeholder is usually
 |  a lone speech bubble beside a lone sentence, with no column to hold open.
 |  Gating on "this list has at least one picture" takes the site's placeholder
 |  count from 198 to 21 — i.e. it was doing nothing in 89% of its appearances.
 |
 |  Collapsed, the row is exactly the ordinary bulleted list it should have been:
 |  same two columns the rail already falls back to below lg.
 |
 |  SCOPED TO >=576px ON PURPOSE: below that the shared mobile rule already lays
 |  every row out as [bullet | text] with its own .95rem gutter and a zero
 |  column-gap, so an `auto` track here would collapse that gutter and leave a
 |  collapsed list's text 8px left of every other list's.
 * ----------------------------------------------------------------- */
@media (min-width: 576px) {
    .md-skin .fr-rail--nofig .fr-rail__row {
        grid-template-columns: auto minmax(0, 1fr);
    }
    .md-skin .fr-rail--nofig .fr-rail__dot  { grid-column: 1 }
    .md-skin .fr-rail--nofig .fr-rail__text { grid-column: 2 }
}


/* ----------------------------------------------------------------- *
 |  ?cols=2 — TWO COLUMNS (comparison option, 2026-07-26, owner ask)
 |
 |  With ?ph= on, every row is the height of a thumbnail (~120px) but the median
 |  description is only ~50 characters, so a section is tall and half-empty and
 |  the page grew. Two columns halve a section's height without shortening a
 |  single sentence.
 |
 |  It must fill DOWN the left column and then down the right, or the authored
 |  order reads left-right-left-right across the pair.
 |
 |  GRID with an explicit row count, not `columns: 2`. Multicol does flow the
 |  right way and balances itself, but it balances by HEIGHT: five items came out
 |  2 left / 3 right (measured on S4E01), because the rows aren't equal height.
 |  `--fr-rail-rows` is ceil(n/2), set inline per section by
 |  feature/differences/episode.blade.php, so the split is deterministic — 5 goes
 |  3 and 2 — and the two columns' rows land on the same lines instead of
 |  drifting apart down the section.
 |
 |  The cost of the explicit grid: a row's height is the taller of its two items,
 |  so one long description pads the row opposite it. Row heights here are near
 |  uniform (the thumbnail sets them), so it shows only where an item runs to
 |  several lines.
 |
 |  Applied from **1200px**, not from `lg`: in the 992-1199 band the container is
 |  960 and a column leaves the sentence 233px (~33 characters), so a median
 |  description wraps to two lines and the p90 to six — measured, and the reason
 |  the breakpoint isn't lg. At 1200 the text gets 323px, at 1400+ 389px.
 |
 |  And only for a section with 4+ items (the count gate is in the Blade — a
 |  shorter section splits 2/1 and reads ragged, and a lone item would be squeezed
 |  to half width for no gain).
 * ----------------------------------------------------------------- */
@media (min-width: 1200px) {
    .md-skin .fr-rail--2col {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        grid-template-rows: repeat(var(--fr-rail-rows, 1), auto);
        grid-auto-flow: column;
        column-gap: 2.5rem;
    }

    /* A column is roughly half as wide, so the full-size thumbnail would leave
       the sentence about 320px. Dropping to the small-screen figure width buys
       the text back ~25px and costs the picture little. */
    .md-skin .fr-rail--2col .fr-rail__row { --fr-rail-thumb-w: 152px }
}

/* At the widest container (1320px) each column is back up to ~600px, so the
   full-size figure fits without squeezing the sentence. */
@media (min-width: 1400px) {
    .md-skin .fr-rail--2col .fr-rail__row { --fr-rail-thumb-w: 176px }
}


/* ----------------------------------------------------------------- *
 |  THE EMPTY SLOT (owner, 2026-07-26)
 |
 |  A text-only item fills its figure column with a placeholder at exactly the
 |  thumbnail's footprint (same width var, same 16/9, same radius), so the
 |  picture column has no gaps in it.
 |
 |  It is a speech bubble on a wash of the page's accent, because "nothing here"
 |  and "this one is words, not a picture" are different statements and only the
 |  second is true. Two rejected alternatives, for anyone tempted to revisit
 |  (both built and compared on real content, 2026-07-26):
 |    a dashed empty frame       — reads as a missing image, not a text item.
 |    a photo icon with a slash  — reads as an image that FAILED to load.
 |    the bubble on a SOLID accent plate — correct but loud: 35 of the Redverse's
 |      93 rows are text-only, so it put 35 saturated plates against 58 real
 |      screenshots and the placeholders led the page. Hence the 10% wash.
 * ----------------------------------------------------------------- */
.md-skin .fr-rail__ph {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    border-radius: 4px;
    background: color-mix(in srgb, var(--md-accent) 10%, #fff);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--md-accent) 22%, #fff);
}

/* The bubble is drawn as a MASK, not as a coloured data-URI, so it takes its
   colour from CSS — a data URI can't read var(--md-accent), and the accent
   differs per page (red / amber / grey / grey). Outline + two text lines rather
   than a solid blob: the holes read as writing, and it stays light at 34px. */
.md-skin .fr-rail__ph::after {
    content: "";
    background: var(--md-accent);
    position: absolute;
    inset: 0;
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 3.5h14a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-8.5L6 19.5v-4H5a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3z'/%3E%3Cpath d='M5.6 7.6h12.8'/%3E%3Cpath d='M5.6 11.1h7.6'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 3.5h14a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-8.5L6 19.5v-4H5a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3z'/%3E%3Cpath d='M5.6 7.6h12.8'/%3E%3Cpath d='M5.6 11.1h7.6'/%3E%3C/svg%3E");
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    -webkit-mask-position: 50% 50%;
            mask-position: 50% 50%;
    -webkit-mask-size: 34px 34px;
            mask-size: 34px 34px;
}

/* Every row now carries a figure, so every row gets the illustrated row's air.
   (The .fr-rail__thumb rule above covers the picture rows.) */
.md-skin .fr-rail__row:has(.fr-rail__ph) { padding: .65rem 0 }

/* Below sm there isn't room for a 152px figure column beside a sentence, so the
   figure stops being a column and spans the row: the thumbnail sits above its
   own item, and the bullet + text stay paired underneath it. Every row keeps
   the same two-column [bullet | text] base, illustrated or not, so the dots
   still line up down the page. */
@media (max-width: 575.98px) {
    .md-skin .fr-rail__row {
        grid-template-columns: .95rem minmax(0, 1fr);
        column-gap: 0;
        row-gap: .5rem;
        padding: .45rem 0;
    }
    .md-skin .fr-rail__figure { grid-column: 1 / -1 }
    .md-skin .fr-rail__dot    { grid-column: 1 }
    .md-skin .fr-rail__text   { grid-column: 2 }
    .md-skin .fr-rail__thumb  { max-width: 320px }

    /* A break-out pair keeps its two frames side by side here too — the whole
       point is the comparison, and at 375px each still gets ~143px, which is
       wider than the 84px they had in the gutter on a DESKTOP. They share the
       row rather than taking 340px each. */
    .md-skin .fr-rail__figure--multi { gap: .5rem }
    .md-skin .fr-rail__figure--multi .fr-rail__shot {
        flex: 1 1 0;
        max-width: none;
    }

    /* The empty slot exists to keep a COLUMN unbroken; below sm there is no
       figure column (the picture spans the row), so an empty box above every
       text-only item would be noise with nothing to align to. Hide the whole
       wrapper, or its grid row keeps the gap. */
    .md-skin .fr-rail__figure:has(.fr-rail__ph) { display: none }
}

@media (prefers-reduced-motion: reduce) {
    .md-skin .fr-rail__thumb { transition: none }
}


/* ================================================================= *
 |  PREV / NEXT PAGE PAIR  (.fr-pagenav)
 |
 |  The pair of cards at the foot of a page that belongs to an ordered
 |  SET, so the set doesn't read as a row of dead ends. Two callers:
 |
 |    character/partials/character-nav.blade.php  — prev/next character,
 |        in navbar order, wrapping at both ends; media is the round
 |        <x-character-avatar> portrait.
 |    episode/partials/episode-nav.blade.php      — prev/next episode by
 |        SEE-code id, NOT wrapping (101 has no previous, 513 no next);
 |        media is a 16:9 crop of the episode's own hero still.
 |
 |  Lived in character-page.css as `.fc-charnav*` until 2026-07-28, when
 |  /episode became the second caller (owner: "the /character pages have
 |  previous/next buttons at the bottom … this would work well for
 |  /episode pages too").
 |
 |  THE MEDIA IS THE CALLER'S, not the component's — only its position in
 |  the row is fixed here. A round 44px portrait and a 78x44 still are
 |  different shapes doing the same job, and each caller already owns the
 |  rules for its own (the avatar's ring lives with the avatar component).
 |
 |  The row is `justify-content: space-between` AND the next link carries
 |  `margin-left: auto`: with only one link present (the ends of the
 |  episode run) space-between alone would leave a lone "next" hard left,
 |  reading as a previous.
 * ================================================================= */
.md-skin .fr-pagenav { margin: 2.5rem 0 3.5rem }
.md-skin .fr-pagenav__row {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 1rem;
}
.md-skin .fr-pagenav__link {
    display: flex;
    align-items: center;
    gap: .85rem;
    padding: .6rem .9rem;
    border: 1px solid var(--md-line);
    border-radius: 4px;
    background: var(--md-surface);
    text-decoration: none !important;
    color: var(--md-ink) !important;
    transition: border-color .14s ease, box-shadow .14s ease;
}
.md-skin .fr-pagenav__link:hover,
.md-skin .fr-pagenav__link:focus-visible {
    border-color: var(--md-accent);
    box-shadow: 0 3px 12px rgba(42, 53, 64, .12);
}
.md-skin .fr-pagenav__link--next { margin-left: auto; text-align: right }

.md-skin .fr-pagenav__dir,
.md-skin .fr-pagenav__name { display: block }
.md-skin .fr-pagenav__dir {
    font-family: var(--md-display);
    font-size: .66rem;
    text-transform: uppercase;
    letter-spacing: .14em;
    color: var(--md-muted);
}
.md-skin .fr-pagenav__name {
    font-family: var(--md-display);
    font-size: 1rem;
    font-weight: 600;
}

/* The episode still: a 16:9 crop of the same overview.jpg the hero uses.
   Cropped by object-fit rather than letterboxed — every one of the 100 is
   already 16:9, so nothing is actually cut; the cover is insurance. */
.md-skin .fr-pagenav__still {
    flex: 0 0 auto;
    width: 78px;
    height: 44px;
    border-radius: 3px;
    object-fit: cover;
    background: var(--md-line);
}

/* Below sm each card takes a full row: side by side, a two-line label beside
   a still leaves the titles ~4 characters wide. */
@media (max-width: 575.98px) {
    .md-skin .fr-pagenav__link { flex: 1 1 100% }
    .md-skin .fr-pagenav__link--next { margin-left: 0 }
}

@media (prefers-reduced-motion: reduce) {
    .md-skin .fr-pagenav__link { transition: none }
}
