:root {
  --bg: #f4f1ea;
  --bg-panel: #ffffff;
  --ink: #15130f;
  --ink-dim: #625c52;
  --line: #15130f;
  --line-weight: 4px;
  --card-bg: #ffffff;
  --card-border: #e2dbcb;
  --shadow: rgba(21, 19, 15, 0.14);
  --hairline: rgba(21, 19, 15, 0.12);
  /* fixed accent for technical milestones — independent of era colors and
     theme, so a milestone always reads as its own distinct category */
  --milestone-color: #f0a202;
  /* page side margins: one fluid formula for all screen sizes, no
     breakpoint jump — ~2rem on a 390px phone, ~6.6rem on a 768px iPad,
     ~10rem on a laptop, capped at 16rem on wide desktops */
  --gutter: clamp(2rem, 20vw - 3rem, 16rem);
  --transition-fast: 0.2s;
  --transition-med: 0.45s;
  --transition-slow: 0.6s;
  --font-ui: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #09090b;
    --bg-panel: #161418;
    --ink: #f5f2ea;
    --ink-dim: #9c968c;
    --line: #ffffff;
    --card-bg: #19171b;
    --card-border: #322e33;
    --shadow: rgba(0, 0, 0, 0.55);
    --hairline: rgba(255, 255, 255, 0.12);
    color-scheme: dark;
  }
}

:root[data-theme="light"] {
  --bg: #f4f1ea;
  --bg-panel: #ffffff;
  --ink: #15130f;
  --ink-dim: #625c52;
  --line: #15130f;
  --card-bg: #ffffff;
  --card-border: #e2dbcb;
  --shadow: rgba(21, 19, 15, 0.14);
  --hairline: rgba(21, 19, 15, 0.12);
  color-scheme: light;
}

:root[data-theme="dark"] {
  --bg: #09090b;
  --bg-panel: #161418;
  --ink: #f5f2ea;
  --ink-dim: #9c968c;
  --line: #ffffff;
  --card-bg: #19171b;
  --card-border: #322e33;
  --shadow: rgba(0, 0, 0, 0.55);
  --hairline: rgba(255, 255, 255, 0.12);
  color-scheme: dark;
}

* {
  box-sizing: border-box;
}

html {
  /* reserve the scrollbar's width whenever the page can scroll, so
     switching between the (shorter) overview and (taller) era view never
     shifts the centered layout sideways — but unlike overflow-y:scroll,
     this doesn't force a scrollbar to show when the page fits */
  scrollbar-gutter: stable;
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
  overflow-x: hidden;
  transition: background var(--transition-med) ease, color var(--transition-med) ease;
}

body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  background-image:
    radial-gradient(circle at 15% 8%, rgba(224, 167, 46, 0.08), transparent 45%),
    radial-gradient(circle at 85% 92%, rgba(63, 174, 114, 0.07), transparent 45%);
}

.site-header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  text-align: center;
  padding: 2.2rem 1.5rem 1rem;
}

/* The header is out of flow, so it doesn't push #app down on its own —
   these two spacers are what centers #app in the full viewport (equal
   space above and below). Their heights are computed and set directly by
   script.js (centerAppVertically), not left to flexbox's own free-space
   distribution: a plain min-height per spacer doesn't actually produce
   true centering here, since resolving a min-height violation on just one
   of two equally-weighted flex-grow items dumps all the remaining space
   on the *other* one instead of splitting it evenly. Computing exact
   pixel heights up front sidesteps that. */
.v-spacer {
  height: 0;
}

.site-title {
  font-family: var(--font-ui);
  font-size: clamp(2.1rem, 5vw, 3.6rem);
  font-weight: 800;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  margin: 0;
  color: var(--ink);
}

.site-subtitle {
  margin: 0.7rem 0 0;
  color: var(--ink-dim);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* ---------- Milestone Toggle ---------- */

.milestone-toggle {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.55rem;
  margin-top: 1.4rem;
  cursor: pointer;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ink-dim);
  user-select: none;
}

.milestone-toggle input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

.milestone-toggle-track {
  position: relative;
  width: 34px;
  height: 20px;
  border-radius: 999px;
  background: var(--hairline);
  border: 1px solid var(--card-border);
  transition: background var(--transition-fast) ease, border-color var(--transition-fast) ease;
  flex-shrink: 0;
}

.milestone-toggle-thumb {
  position: absolute;
  top: 1px;
  left: 1px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--ink-dim);
  transition: transform var(--transition-fast) ease, background var(--transition-fast) ease;
}

.milestone-toggle input:checked + .milestone-toggle-track {
  background: var(--milestone-color);
  border-color: var(--milestone-color);
}

.milestone-toggle input:checked + .milestone-toggle-track .milestone-toggle-thumb {
  transform: translateX(14px);
  background: #fff;
}

.milestone-toggle input:focus-visible + .milestone-toggle-track {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

#app {
  width: 100%;
  /* light vertical padding — the composition (header, timeline, toggle,
     hint, About footer) has to fit the 1920×1080 reference frame as a
     whole, so vertical slack is kept lean and true centering comes from
     the spacers, not padding */
  padding: 1.5rem var(--gutter) 2rem;
  position: relative;
  /* hidden until script.js's revealApp() adds .data-ready to <body> —
     centering #app before eras.json has loaded would measure the empty
     skeleton and visibly jump once the real (taller) content and a second
     centering pass land a moment later; staying invisible through that
     first pass turns the jump into a quick, ordinary fade-in instead */
  opacity: 0;
  transition: opacity 0.25s ease;
}

body.data-ready #app {
  opacity: 1;
}

.view {
  transition: opacity var(--transition-slow) ease, transform var(--transition-slow) ease;
  transform-origin: top center;
}

.view.hidden {
  display: none;
}

.view.leaving {
  opacity: 0;
  transform: scale(0.94);
}

.view.entering {
  opacity: 0;
  transform: scale(1.05);
}

.view.entered {
  opacity: 1;
  transform: scale(1);
}

/* ---------- Overview Timeline ---------- */

.timeline-wrap {
  position: relative;
  height: 480px;
  margin-top: 2.5rem;
}

.timeline-axis {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
  height: var(--line-weight);
}

.timeline-line {
  position: absolute;
  left: 0;
  right: 18px; /* leave room for the arrowhead */
  top: 0;
  height: var(--line-weight);
  background: var(--line);
  border-radius: 2px;
}

.timeline-line::after {
  content: "";
  position: absolute;
  left: 100%;
  top: 50%;
  width: 0;
  height: 0;
  transform: translateY(-50%);
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
  border-left: 18px solid var(--line);
}

.year-ticks {
  position: absolute;
  left: 0;
  right: 32px; /* stop a little short of the line's end so the last tick never touches the arrow */
  top: 0;
}

.year-tick {
  position: absolute;
  top: -5px;
  width: var(--line-weight);
  height: 14px;
  background: var(--line);
  transform: translateX(-50%);
  border-radius: 1px;
}

.year-tick-label {
  position: absolute;
  top: 22px;
  transform: translateX(-50%);
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--line);
  letter-spacing: 0.03em;
  white-space: nowrap;
}

/* an axis element (tick mark or label) hidden by the collision-driven
   layout in script.js — e.g. the off-grid end year when space runs out */
.tick-hidden {
  display: none;
}

/* ---------- Technical Milestones ---------- */

/* same track as .year-ticks so a given year lines up identically */
.milestones {
  position: absolute;
  left: 0;
  right: 32px;
  top: 0;
}

/* toggled off by script.js via a class on <html>, mirroring the theme
   attribute pattern — kept in the DOM (not re-rendered) so the toggle is
   instant either direction */
html.milestones-hidden .milestones {
  display: none;
}

.milestone {
  position: absolute;
  top: calc(var(--line-weight) / 2);
  width: 18px;
  height: 18px;
  transform: translate(-50%, -50%);
  cursor: pointer;
  z-index: 6; /* above ticks and era brackets/film cards that meet the axis */
}

/* Disc, ring and glyph are all painted by one nested SVG built in
   script.js (see dotSvg there for why a CSS border can't draw the
   ring, and why the hover scale is on the inner <g> rather than any
   HTML box); this span only provides the box and the focus outline.
   Child combinator on the svg rule: the glyph is a nested <svg> sized
   by its own attributes, which a width:100% descendant rule would
   override. */
.milestone-dot {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  display: block;
}

.milestone-dot > svg {
  width: 100%;
  height: 100%;
  display: block;
  /* the scaled-up hover state outgrows the 18px viewport */
  overflow: visible;
  fill: var(--milestone-color); /* inherited by the nested glyph */
}

.milestone-dot-content {
  /* shadow drawn in-canvas (not box-shadow on the span) so it grows
     with the dot on hover */
  filter: drop-shadow(0 2px 8px var(--shadow));
  transform-box: view-box;
  transform-origin: center;
  transition: transform var(--transition-fast) ease;
}

.milestone-dot .milestone-dot-disc {
  fill: var(--card-bg);
  stroke: var(--milestone-color);
  stroke-width: 2;
}

.milestone:hover .milestone-dot-content,
.milestone.active .milestone-dot-content {
  transform: scale(1.3);
}

.milestone:focus-visible {
  outline: none;
}

.milestone:focus-visible .milestone-dot {
  outline: 2px solid var(--milestone-color);
  outline-offset: 2px;
}

.era-lanes {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 32px; /* match .year-ticks' track so a given year lines up in both */
}

.era-bar {
  position: absolute;
  top: 50%;
  height: 0;
  cursor: pointer;
}

.era-hit {
  position: absolute;
  left: -8px;
  right: -8px;
  border-radius: 6px;
  transition: background var(--transition-fast) ease;
}

.era-bar:focus-visible {
  outline: none;
}

.era-bar:focus-visible .era-hit {
  outline: 2px solid var(--era-color);
  outline-offset: 2px;
}

/* one element per bracket, drawn with borders — assembling it from
   separate tie/crossbar pieces left visible notches at the corners where
   their rounded ends met. Bar weight matches the timeline's own line and
   tick thickness (--line-weight) for a cohesive look. */
.era-bracket {
  position: absolute;
  /* each edge pulled out by half the bar width so the ties' centerlines
     sit exactly on their year positions, matching the axis tick marks
     (which are centered on theirs) */
  left: -1.5px;
  right: -1.5px;
  border-left: 3px solid var(--era-color);
  border-right: 3px solid var(--era-color);
  transition: filter var(--transition-fast) ease;
}

.era-bar.lane-above .era-bracket {
  border-top: 3px solid var(--era-color);
  border-radius: 3px 3px 0 0;
}

.era-bar.lane-below .era-bracket {
  border-bottom: 3px solid var(--era-color);
  border-radius: 0 0 3px 3px;
}

/* rounded tips on the ties' free ends: borders can't round their own
   ends, so these caps sit exactly over each tie (offset by the border
   width, since pseudo offsets are relative to the padding box), overlap
   its end and extend 2px further with a rounded edge */
.era-bracket::before,
.era-bracket::after {
  content: "";
  position: absolute;
  width: 3px;
  height: 5px;
  background: var(--era-color);
}

.era-bracket::before {
  left: -3px;
}

.era-bracket::after {
  right: -3px;
}

.era-bar.lane-above .era-bracket::before,
.era-bar.lane-above .era-bracket::after {
  bottom: -2px;
  border-radius: 0 0 2px 2px;
}

.era-bar.lane-below .era-bracket::before,
.era-bar.lane-below .era-bracket::after {
  top: -2px;
  border-radius: 2px 2px 0 0;
}

.era-bar:hover .era-bracket {
  filter: brightness(1.25);
}

.era-label {
  position: absolute;
  left: 0;
  text-align: left;
  white-space: nowrap;
  transition: transform var(--transition-fast) ease;
}

/* labels that would run past the timeline's right edge flip to hang
   leftward off their era's END instead (set by fixOverflowingEraLabels),
   mirroring the film cards' right-edge flip */
.era-label.flip {
  left: auto;
  right: 0;
  text-align: right;
}

.era-bar:hover .era-label {
  transform: translateY(-2px);
}

.era-bar.lane-below:hover .era-label {
  transform: translateY(2px);
}

.era-label-title {
  display: block;
  font-size: 1.05rem;
  font-weight: 800;
  color: var(--era-color);
  letter-spacing: -0.01em;
}

.era-label-years {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--ink-dim);
  letter-spacing: 0.04em;
}

/* Compaction stages, applied by layoutAxis in script.js when the eras
   don't fit the viewport at full size — tried in this order before
   falling back to the horizontally scrolling view. Stage 1: titles may
   wrap instead of overhanging their bracket (100% = the era's own span;
   the px floor keeps very short eras from wrapping every word). Stage 2:
   slightly smaller label type. */
.timeline-wrap.era-labels-wrap .era-label {
  white-space: normal;
  max-width: max(100%, 150px);
}

.timeline-wrap.era-labels-tight .era-label-title {
  font-size: 0.92rem;
}

.timeline-wrap.era-labels-tight .era-label-years {
  font-size: 0.72rem;
}

.hint {
  text-align: center;
  color: var(--ink-dim);
  font-size: 0.9rem;
  font-weight: 600;
  margin-top: 1.4rem;
  letter-spacing: 0.03em;
}

/* ---------- Era Detail View ---------- */

.back-button {
  background: none;
  border: 2px solid var(--hairline);
  color: var(--ink);
  padding: 0.55rem 1.1rem;
  border-radius: 6px;
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  transition: border-color var(--transition-fast), background var(--transition-fast), transform var(--transition-fast);
  margin-bottom: 1.5rem;
}

.back-button:hover {
  border-color: var(--ink);
  background: var(--hairline);
  transform: translateX(-2px);
}

.era-panel {
  border-radius: 12px;
  padding: 2.2rem 2.4rem;
  margin-bottom: 3.5rem;
  background: var(--era-color, var(--bg-panel));
  color: var(--panel-ink, #17140d);
  box-shadow: 0 20px 45px var(--shadow);
  transition: background var(--transition-med) ease;
}

.era-panel-year {
  font-size: 0.95rem;
  letter-spacing: 0.08em;
  font-weight: 800;
  opacity: 0.75;
}

.era-panel-title {
  font-family: var(--font-ui);
  font-size: clamp(1.8rem, 3.6vw, 2.8rem);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: -0.01em;
  margin: 0.3rem 0 0.8rem;
}

.era-panel-description {
  margin: 0;
  max-width: 62ch;
  line-height: 1.55;
  font-size: 1.05rem;
  font-weight: 500;
  opacity: 0.92;
}

.era-timeline-wrap {
  position: relative;
  height: 520px;
  margin-top: 2rem;
}

.film-lanes {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 32px; /* match .year-ticks' track so a given year lines up in both */
}

.film-anchor {
  position: absolute;
  top: 50%;
  width: 0;
  height: 0;
}

.film-card {
  position: absolute;
  /* the card takes its width from the still (fixed height, natural
     width), so each box reflects its film's aspect ratio; min-content
     keeps the text from widening the card beyond the image */
  width: min-content;
  cursor: pointer;
  background: var(--card-bg);
  /* same thickness as the connector bar so the two read as one line */
  border: 3px solid var(--era-color);
  border-radius: 10px;
  transition: background var(--transition-fast) ease;
}

.film-card:hover,
.film-card:focus-visible {
  background: color-mix(in srgb, var(--era-color) 12%, var(--card-bg));
  z-index: 5;
}

.film-card:focus-visible {
  outline: 2px solid var(--era-color);
  outline-offset: 2px;
}

.film-card.lane-above {
  bottom: calc(50% + 80px);
}

.film-card.lane-below {
  top: calc(50% + 80px);
}

/* the card edge (and the border inside it) shifts out by half the border
   width, so the border's centerline lands exactly on the year — same
   centering as the axis tick marks */
.film-anchor.align-left .film-card {
  left: -1.5px;
}

.film-anchor.align-right .film-card {
  right: -1.5px;
}

/* square outer corner where the connector joins, curved inner corner:
   the card keeps its normal rounding (so the inside curve is identical
   to the other corners), and this solid square rides on the connector
   BEHIND the opaque card — visible only through the rounded-corner
   notch, which it fills out to a flush square outer corner */
.film-connector::before {
  content: "";
  position: absolute;
  width: 14px;
  height: 14px;
  background: var(--era-color);
}

.film-anchor.align-left .film-connector::before {
  left: 0;
}

.film-anchor.align-right .film-connector::before {
  right: 0;
}

.film-connector.lane-above::before {
  top: 0;
}

.film-connector.lane-below::before {
  bottom: 0;
}


.film-connector {
  position: absolute;
  width: 3px;
  /* 34px visible + 14px tucked behind the card: the card's corner is
     rounded (10px radius), so a connector stopping exactly at the card's
     box edge shows a notch where the border curves away. The card paints
     over the overlap, so the extension is only visible inside that corner
     notch — which is exactly what fills it. */
  height: 48px;
  background: var(--era-color);
  /* rounded tip on the free (axis-side) end; the other end is hidden
     behind the opaque card so its rounding can't disturb the seam */
  border-radius: 2px;
}

/* sit exactly on top of the card's own border line (which runs inward
   from whichever card edge is at the year), so bar and border merge into
   one continuous stroke of identical width */
.film-anchor.align-left .film-connector {
  left: -1.5px;
}

.film-anchor.align-right .film-connector {
  right: -1.5px;
}

.film-connector.lane-above {
  bottom: 46px;
}

.film-connector.lane-below {
  top: 46px;
}

/* elbow variant for cards slid sideways off their year (same-year
   collisions): an SVG S-bend spanning the same vertical strip as the
   straight connector, positioned horizontally by the layout pass */
.film-connector-elbow {
  position: absolute;
  pointer-events: none;
}

.film-connector-elbow.lane-above {
  bottom: 46px;
}

.film-connector-elbow.lane-below {
  top: 46px;
}

.film-still {
  /* fixed height for visual consistency across cards; width follows the
     image's own aspect ratio and dictates the card width */
  height: 120px;
  width: auto;
  display: block;
  background: #000;
  border-radius: 8px 8px 0 0;
}

.film-info {
  padding: 0.8rem 0.9rem 1rem;
}

.film-title {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 1rem;
  font-weight: 800;
  color: var(--ink);
  line-height: 1.25;
}

.film-year {
  display: inline-block;
  margin-top: 0.35rem;
  font-size: 0.82rem;
  color: var(--era-color);
  font-weight: 800;
  letter-spacing: 0.03em;
}

.film-director {
  margin: 0.15rem 0 0;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ink-dim);
}

/* ---------- Milestone Tooltip ---------- */

/* One shared element that script.js moves into the same container as the
   markers when shown, positioned in the timeline's own coordinates. That
   way every kind of scrolling (page and the horizontal timeline wrappers)
   carries it along natively with its star — JS repositioning on scroll
   always runs behind the rendering and jitters, especially on mobile. */
.milestone-tooltip {
  position: absolute;
  z-index: 50;
  max-width: 260px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 10px;
  padding: 0.75rem 0.9rem;
  box-shadow: 0 12px 30px var(--shadow);
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transform: translateY(4px);
  transition: opacity var(--transition-fast) ease, transform var(--transition-fast) ease,
    visibility var(--transition-fast) ease;
}

.milestone-tooltip.visible {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  /* only interactive while shown — tapping the tooltip itself (e.g. to
     read the description) shouldn't count as an "outside" click and
     dismiss it; while hidden it stays click-through so nothing underneath
     is blocked */
  pointer-events: auto;
}

.milestone-tooltip-year {
  font-size: 0.78rem;
  font-weight: 800;
  color: var(--milestone-color);
  letter-spacing: 0.04em;
}

.milestone-tooltip-title {
  margin-top: 0.2rem;
  font-family: var(--font-ui);
  font-size: 0.95rem;
  font-weight: 800;
  color: var(--ink);
  line-height: 1.25;
}

.milestone-tooltip-description {
  margin-top: 0.4rem;
  font-size: 0.82rem;
  font-weight: 500;
  line-height: 1.5;
  color: var(--ink-dim);
}

/* ---------- Theme Toggle ---------- */

.theme-toggle {
  position: fixed;
  right: 1.5rem;
  bottom: 1.5rem;
  z-index: 20;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--ink);
  box-shadow: 0 10px 30px var(--shadow);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform var(--transition-fast) ease, border-color var(--transition-fast) ease;
}

.theme-toggle:hover {
  transform: translateY(-2px);
  border-color: var(--ink);
}

.theme-toggle .icon {
  width: 22px;
  height: 22px;
  position: absolute;
  transition: opacity var(--transition-fast) ease, transform var(--transition-fast) ease;
}

.theme-toggle .icon-sun {
  opacity: 0;
  transform: scale(0.5) rotate(-45deg);
}

.theme-toggle .icon-moon {
  opacity: 1;
  transform: scale(1) rotate(0deg);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .icon-sun {
    opacity: 1;
    transform: scale(1) rotate(0deg);
  }
  :root:not([data-theme="light"]) .theme-toggle .icon-moon {
    opacity: 0;
    transform: scale(0.5) rotate(45deg);
  }
}

:root[data-theme="dark"] .theme-toggle .icon-sun {
  opacity: 1;
  transform: scale(1) rotate(0deg);
}

:root[data-theme="dark"] .theme-toggle .icon-moon {
  opacity: 0;
  transform: scale(0.5) rotate(45deg);
}

/* ---------- Film Detail Modal ---------- */

.film-modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1.5rem;
  /* the overlay stays in the DOM for ~500ms after closing while the
     fade-out plays — without this it would keep swallowing clicks for
     that window, making the page feel unresponsive right after closing */
  pointer-events: none;
}

.film-modal.open {
  pointer-events: auto;
}

.film-modal.hidden {
  display: none;
}

.film-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  opacity: 0;
  transition: opacity var(--transition-med) ease;
}

.film-modal.open .film-modal-backdrop {
  opacity: 1;
}

/* Opens with slide + fade only — deliberately no zoom. Scaling resizes
   the card's text mid-animation, which reads as the content shifting
   before it settles; even a subtle scale and a counter-scaled inner
   wrapper (tried both) stayed visible. Don't reintroduce a scale here. */
.film-modal-card {
  position: relative;
  width: 100%;
  max-width: 640px;
  overflow: hidden;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 14px;
  box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5);
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--transition-med) ease, transform var(--transition-med) ease;
}

/* scrolling lives here rather than on the card so an appearing scrollbar
   can never reflow the card's content mid-animation */
.film-modal-inner {
  max-height: calc(100vh - 4rem);
  overflow-y: auto;
}

.film-modal.open .film-modal-card {
  opacity: 1;
  transform: translateY(0);
}

.film-modal-close {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  z-index: 2;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
  transition: background var(--transition-fast) ease, transform var(--transition-fast) ease;
}

.film-modal-close:hover {
  background: rgba(0, 0, 0, 0.75);
  transform: scale(1.08);
}

.film-modal-close svg,
.film-modal-nav svg {
  width: 18px;
  height: 18px;
  display: block;
}

.film-modal-gallery {
  position: relative;
  background: #000;
}

.film-modal-image {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: contain;
  display: block;
}

.film-modal-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
  transition: background var(--transition-fast) ease, transform var(--transition-fast) ease;
}

.film-modal-nav:hover {
  background: rgba(0, 0, 0, 0.75);
  transform: translateY(-50%) scale(1.08);
}

.film-modal-nav.hidden {
  display: none;
}

.film-modal-prev {
  left: 0.75rem;
}

.film-modal-next {
  right: 0.75rem;
}

.film-modal-dots {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.75rem;
  display: flex;
  justify-content: center;
  gap: 0.4rem;
}

.film-modal-dots.hidden {
  display: none;
}

.film-modal-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.45);
  cursor: pointer;
  transition: background var(--transition-fast) ease, transform var(--transition-fast) ease;
}

.film-modal-dot:hover {
  background: rgba(255, 255, 255, 0.75);
}

.film-modal-dot.active {
  background: #fff;
  transform: scale(1.2);
}

.film-modal-body {
  padding: 1.6rem 1.8rem 1.8rem;
}

.film-modal-title {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 1.5rem;
  font-weight: 800;
  color: var(--ink);
  line-height: 1.2;
}

.film-modal-year {
  display: inline-block;
  margin-top: 0.4rem;
  font-size: 0.95rem;
  font-weight: 800;
  color: var(--era-color);
  letter-spacing: 0.02em;
}

.film-modal-director {
  margin: 0.2rem 0 0;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--ink-dim);
}

.film-modal-description {
  margin: 0.9rem 0 0;
  font-size: 0.95rem;
  font-weight: 500;
  line-height: 1.55;
  color: var(--ink-dim);
}

.film-modal-meta {
  margin-top: 1rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--hairline);
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ink-dim);
  display: grid;
  gap: 0.25rem;
}

.film-modal-meta span {
  color: var(--ink);
  font-weight: 700;
}

/* ---------- Milestone Detail Modal ---------- */

/* Reuses the film modal's frame, but the content follows the hover
   tooltip's layout: gold year on top, then title, then description. */
.milestone-modal .film-modal-card {
  max-width: 440px;
  --era-color: var(--milestone-color);
}

.milestone-modal-year {
  display: block;
  font-size: 0.85rem;
  font-weight: 800;
  color: var(--milestone-color);
  letter-spacing: 0.04em;
}

.milestone-modal-title {
  margin: 0.35rem 0 0;
  font-family: var(--font-ui);
  font-size: 1.3rem;
  font-weight: 800;
  color: var(--ink);
  line-height: 1.25;
}

/* ---------- About Link (index.html footer) ---------- */

/* a real footer, in normal document flow at the end of the page content —
   scrolls away with the page rather than staying pinned to the viewport */
.about-link {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  /* the footer lands flush against the viewport's bottom edge when the
     page fits (see centerAppVertically) — this bottom padding is the
     visual margin under the About link */
  padding: 1rem 0 2rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ink-dim);
  text-decoration: none;
  letter-spacing: 0.03em;
  transition: color var(--transition-fast) ease, opacity 0.25s ease;
  /* it's a sibling of #app, not a descendant, so hiding #app alone
     doesn't cover it — it needs the same data-ready gate, since it's
     positioned by the very same centerAppVertically() spacer math and
     would otherwise sit at its pre-load (wrong) position until that first
     real pass runs, then visibly jump to its actual spot */
  opacity: 0;
}

body.data-ready .about-link {
  opacity: 1;
}

/* only shown on the overview screen — hidden while the era detail view
   is open (toggled in script.js's openEra/closeEra) */
.about-link.hidden {
  display: none;
}

.about-link:hover {
  color: var(--ink);
  text-decoration: underline;
}

.about-link-logo {
  width: 26px;
  /* explicit ratio (matching the SVGs' own viewBox) rather than
     width+height:auto, so its height is always exact even before the
     fetched SVG has loaded in — an unknown/late-resolving height here
     would nudge the footer's measured height, which the vertical-
     centering math depends on */
  aspect-ratio: 90.91 / 81.37;
}

.about-link-logo svg,
.about-logo svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* ---------- About Page (about.html) ---------- */

.about-page {
  min-height: 100vh;
  padding: 4rem var(--gutter) 6rem;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.about-back {
  align-self: flex-start;
  display: inline-block;
  text-decoration: none;
  margin-bottom: 2rem;
}

.about-content {
  width: 100%;
  max-width: 640px;
  text-align: center;
  /* hidden until about.js's revealPage() adds .data-ready to <body> — see
     the matching #app rule for why: an empty-then-growing text block reads
     as a jump, so it stays invisible through that first pass instead */
  opacity: 0;
  transition: opacity 0.25s ease;
}

body.data-ready .about-content {
  opacity: 1;
}

/* standalone — no background plaque. Uses a separately pre-colored SVG
   per theme (swapped in script.js) rather than a fixed white backdrop, so
   there's no plaque needed to keep it legible in dark mode. */
.about-logo {
  width: 100px;
  aspect-ratio: 90.91 / 81.37;
  margin: 0 auto 1.8rem;
}

.about-title {
  font-family: var(--font-ui);
  font-size: clamp(1.6rem, 3.2vw, 2.2rem);
  font-weight: 800;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  margin: 0 0 1.2rem;
  color: var(--ink);
}

.about-intro {
  margin: 0.5rem 0 1.4rem;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.6;
  color: var(--ink);
}

.about-supervision {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 500;
  line-height: 1.6;
  color: var(--ink-dim);
}

.about-supervision strong,
.about-technical strong {
  color: var(--ink);
  font-weight: 800;
}

.about-section-title {
  margin: 2rem 0 1.1rem;
  padding-top: 1.6rem;
  border-top: 1px solid var(--hairline);
  font-size: 0.82rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-dim);
}

.about-contributors {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.55rem;
  font-size: 0.98rem;
  font-weight: 700;
  color: var(--ink);
}

.about-technical {
  margin-top: 2rem;
  padding-top: 1.6rem;
  border-top: 1px solid var(--hairline);
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--ink-dim);
}

.about-copyright {
  margin-top: 2.2rem;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--ink-dim);
  opacity: 0.75;
}

/* ---------- Responsive ---------- */

/* Scroll mode is applied by script.js when it measures an actual
   collision (year labels or film cards touching) that dropping the
   off-grid end tick can't solve; the wrap then gets an inline min-width
   grown until nothing touches. Full-bleed: span the app's whole width,
   then pad the content back in by the same gutter so the first/last year
   labels (which overhang their tick by half their width) aren't clipped
   at the scrollport's left edge. */
.timeline-wrap-outer.scrolling,
.era-timeline-wrap-outer.scrolling {
  overflow-x: auto;
  margin: 0 calc(-1 * var(--gutter));
  padding: 0 0 0 var(--gutter);
}

/* The trailing gutter can't be padding on the scroll container — browsers
   (Safari especially) don't count a scroller's right padding as scrollable
   overflow, so the content would sit flush against the right edge at the
   end of the scroll. A transparent border on the wrap is part of its own
   box, so it always scrolls into view; and since the absolutely positioned
   axis/lanes resolve against the padding box, they stay a gutter short of
   the border-box edge. */
.timeline-wrap-outer.scrolling .timeline-wrap,
.era-timeline-wrap-outer.scrolling .era-timeline-wrap {
  border-right: var(--gutter) solid transparent;
}

@media (max-width: 520px) {
  .era-panel {
    padding: 1.5rem 1.3rem;
  }

  .theme-toggle {
    right: 1rem;
    bottom: 1rem;
  }
}

/* Desktop screens: script.js (applyPageZoom) zooms the whole page by
   min(width/1920, height/1200), so every px in this file is effectively
   a fraction of that reference frame rather than a fixed on-screen size
   — Full HD, 1440p and 4K all render the identical composition, just
   scaled, with guaranteed vertical breathing room (the frame is ~180px
   taller than the composition needs), and wider screens spend the extra
   width on a wider timeline. Done in JS rather than media-query tiers
   so the effective size holds exactly at every viewport. Phones and
   small tablets (< 1024px) stay unzoomed. */
