/* About page — design.md: About page. Figma 505:75203.
   The page runs on a 968px column, narrower than the site's 1280px content width: that is the
   width Figma gives the testimonials, the "Outside the pixels" heading and the album, and the intro
   row is normalised onto it too (Figma insets that row ~26px further, which reads as drift). */

.about__inner {
  max-width: 968px;
  margin: 0 auto;
}

/* ── Portrait + bio ──────────────────────────────────────────────────────── */

/* A grid, not two independent flex columns — the reason is the second row. With two plain flex
   columns (each `column-gap` after its own first child), whichever column's first child is
   shorter has its second child start earlier: the bio (458px unnarrowed) was shorter than the
   portrait (499px), so the contact row sat ~41px above the toggle instead of level with it.
   Narrowing the bio (below) closes most of that gap, but a grid makes the alignment exact and
   keeps it exact regardless of any future content-length change, rather than depending on two
   heights happening to match. .about__portrait-col, .about__bio-col and .mode-toggle all become
   `display: contents` below — inert for layout — so their actual children place directly into
   this grid without changing the HTML's grouping. */
.about__intro {
  display: grid;
  grid-template-columns: 415px 450px;
  column-gap: var(--space-2xl);
  /* The base gap between every row; .mode-toggle__caption below pulls the row above it in
     further with its own margin, since that gap (portrait/bio to caption) is larger than this
     one (caption to the toggle switch / contact row). */
  row-gap: var(--space-sm);
  padding-top: var(--space-3xl);
}

/* The bio column's width is per-mode, not shared — the two bios wrap at different points for the
   same width (different words, different lengths), so matching each one's height to the
   portrait (499px) takes a different column width per bio, not one value that works for both:
   450px measures out to 500px for the Work bio, but only 542px for the (now six-paragraph) Fun
   bio at that same width. 500px brings Fun back down to 500px too. Both stay well inside 505px,
   the most this column can be before overflowing the page's own 968px column (415 portrait + 48
   gap + 505 = 968) — going wider than that would push the bio out past where the testimonials,
   "Outside the pixels" heading and the album all end. js/about.js keeps this attribute in step with
   the toggle's own [data-mode], which is what actually drives everything else. */
.about__intro[data-mode="fun"] {
  grid-template-columns: 415px 500px;
}

.about__portrait-col,
.about__bio-col,
.mode-toggle {
  display: contents;
}

/* Height is the image's own, not a fixed 499px: the two cut-outs have different aspect ratios
   (Figma places each at exactly half its export size — Work 394x474, Fun 410x457), and at this
   column's 415px the Work portrait comes out at 499px, which is where the old placeholder's
   height came from. Letting the image drive it keeps each mode's proportions honest. Row 1 of the
   grid is sized by the taller of portrait and bio — the bio, at ~500px in both modes — so the
   rows below don't move when the toggle swaps a shorter portrait in. */
/* `end`, not `start`: the two cut-outs are different heights (415 x 499 Work, 415 x 463 Fun), and
   they have to share a BOTTOM edge so the person doesn't appear to hover when the mode changes —
   the slack goes to the gap above the head instead. Row 1 is sized by the bio (~500px in both
   modes), which is taller than either portrait, so that bottom edge sits at a stable y. */
.about__portrait {
  grid-column: 1;
  grid-row: 1;
  align-self: end;
  position: relative;
  display: block;
  width: 415px;
  max-width: 100%;
}

.about__portrait-picture {
  display: block;
}

.about__portrait img {
  display: block;
  width: 100%;
  height: auto;
}

/* Cross-fade on toggle: js/about.js stacks a copy of the OUTGOING cut-out over the box, swaps the
   real <picture> underneath to the incoming one straight away, and then fades only this ghost —
   uncovering the new portrait already sitting behind it. See js/about.js for why just the one
   layer animates rather than both.

   Bottom-anchored, like .about__portrait's own `align-self: end`: the two cut-outs are different
   heights and share a bottom edge, so that is the edge the ghost has to keep. `height: auto`
   against its own width/height attributes is what holds it at the OUTGOING aspect ratio while the
   box around it has already resized to the incoming one.

   This fires on the toggle only — never on load. The 500ms is XFADE_MS in js/about.js; change
   both together. */
.about__portrait-ghost {
  position: absolute;
  left: 0;
  bottom: 0;
  pointer-events: none;
  opacity: 1;
  transition: opacity 500ms ease-in-out;
}

.about__portrait-ghost.is-out {
  opacity: 0;
}

.mode-toggle__caption {
  grid-column: 1;
  grid-row: 2;
  align-self: start;
  /* Recreates the larger portrait-to-caption gap on top of the grid's own --space-sm row-gap:
     --space-lg total, matching how much room this had under the portrait before. */
  margin-top: calc(var(--space-lg) - var(--space-sm));
}

.mode-toggle__row {
  grid-column: 1;
  grid-row: 3;
  align-self: start;
}

/* Row 3, same as .mode-toggle__row above — this is what actually lines the toggle switch up
   with the contact row: both are the only occupants of grid row 3, so both start at the same y
   regardless of what row 2 (the caption, present only in column 1) added above them. */
.about__bio {
  grid-column: 2;
  grid-row: 1;
  align-self: start;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  font-family: var(--font-sans);
  font-size: var(--text-body);
  line-height: var(--leading-normal);
  color: var(--color-text-primary);
}

/* Moved here from under the portrait — Figma 505:75203 and 531:103 both now place this row
   below the bio text, in the bio's own column, in LinkedIn / email / Resume order. The gap is
   normalised from Figma's ~72-80px spacing (an off-scale value, like several others in this
   system) to --space-3xl. */
.about__contact {
  grid-column: 2;
  grid-row: 3;
  align-self: start;
  display: flex;
  align-items: center;
  gap: var(--space-3xl);
}

.about__contact-link {
  font-family: var(--font-sans);
  font-size: var(--text-nav);
  font-weight: 400;
  color: var(--color-text-primary);
  text-decoration: none;
}

.about__contact-link:hover {
  text-decoration: underline;
}

/* Resume button, dark mode. Full AI-Powered pairing — background AND label.

   Correction while implementing this pass: this rule previously set color to
   --color-button-primary-label (#fafaf7, plain white) from an earlier, separate request that
   reverted the label off #ecc9bd — at the time the background hadn't been fixed yet, so the pink
   was sitting on the unchanged light-mode #695146, which did read as an odd, slightly-too-warm
   choice against an otherwise near-black page. design.md's own Resume button row (About page in
   dark mode — case-study palettes) already documents #ecc9bd as current, and this pass's own
   instructions confirmed it ("the label stays #ecc9bd") — so the code and the doc had drifted
   apart on this one value, and the code is what's wrong: restored to #ecc9bd here. With the
   background now also dark (#372A24, below), the pink sits on the same dark ground the brown
   testimonial card uses it against, at an even higher 8.99:1 (was 4.76:1 on the old light
   #695146) — the concern that motivated the white swap doesn't apply once the background is
   actually dark too.

   Background: was left reading --color-button-primary (#695146) in both themes — that token has
   no dark-mode value of its own, so the button stayed at full light-mode brightness against the
   otherwise near-black page. Now explicit here, at AI-Powered's dark band shade
   (--color-lib-ai-band-dark, #372A24) — the button's background IS AI-Powered's own #695146 in
   light mode, so this is the same case-study pairing the testimonial card and stats band already
   use, not a new relationship.

   --color-button-primary itself is deliberately untouched, not given a [data-theme="dark"] value
   of its own: it's also read by .album__nav (the photo album's arrows), which already has its
   own separate dark-mode override elsewhere in this file (reusable-lists' pairing, not this one)
   — changing the shared token would have been invisible there today (the more specific rule wins
   regardless) but would silently start leaking into any future consumer of --color-button-primary
   that doesn't happen to carry its own override, for a component this token was never scoped to.
   Scoping the fix to .about__contact .btn-pill's own rule avoids that. */
[data-theme="dark"] .about__contact .btn-pill {
  background: var(--color-lib-ai-band-dark); /* #372A24 */
  color: var(--color-button-primary-label); /* #fafaf7, same as the light-mode Resume button */
}

.about__bio p {
  margin: 0;
}

/* Figma sets these phrases SemiBold, not the browser's bold default.
   TODO: the "rainbow flair" treatment lands on these spans once its look is decided. */
.about__bio strong {
  font-weight: 600;
}

/* The fade-out js/about.js drives on toggle: text swaps while opacity is at 0, so the change
   itself is invisible, then the words arrive into the empty column. The 100ms is FADE_MS in
   js/about.js — change both together. */
.about__bio {
  transition: opacity 100ms ease-in-out;
}

.about__bio.is-fading {
  opacity: 0;
}

/* Word reveal on toggle: js/about.js wraps each word of the incoming bio in one of these and
   staggers their transition-delay, so the paragraph arrives a word at a time rather than all at
   once. The spans are removed again once it finishes. Toggle only — never on load. `inline-block` is required, not cosmetic — transform has no
   effect on a non-replaced inline box, so the translate would silently do nothing without it.
   The 300ms here is REVEAL_WORD_MS in js/about.js; change both together. */
.about__bio-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.3em);
  transition: opacity 300ms ease-out, transform 300ms ease-out;
}

.about__bio.is-revealed .about__bio-word {
  opacity: 1;
  transform: none;
}

/* ── Work/Fun mode toggle ────────────────────────────────────────────────────
   design.md: Work/Fun mode toggle (About page). Figma 532:222 (states) + 531:103 (Fun page).
   State lives in [data-mode] on .mode-toggle, set by js/about.js; only that attribute and the
   .is-morphing-to-* class below ever change here — the bio swap is handled separately, above.
   .mode-toggle's own display/gap are set once, above, alongside .about__portrait-col and
   .about__bio-col — a second `.mode-toggle { display: flex; ... }` rule used to live here and,
   being later in the cascade at equal specificity, silently won over `display: contents`, which
   put .mode-toggle__caption and .mode-toggle__row back inside their own flex box instead of
   directly in the grid — exactly the alignment bug this section fixes. */

.mode-toggle__caption {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-nav);
  font-weight: 400;
  color: var(--color-text-secondary);
}

.mode-toggle__row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.mode-toggle__mode-label {
  font-family: var(--font-sans);
  font-size: var(--text-nav);
  font-weight: 400;
  color: var(--color-text-primary);
}

/* Track: 64x32, this brown in light mode. design.md originally specified the track colour as
   fixed across both themes — only the knob and the two single-letter labels changed. On request,
   the track now gets its own dark-mode background too (see the [data-theme="dark"] rule below);
   nothing else about the toggle changed. */
.mode-toggle__track {
  position: relative;
  flex: none;
  width: 64px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 80px;
  background: #695146;
  cursor: pointer;
}

/* Dark mode, on request: AI-Powered's dark band shade — the same --color-lib-ai-band-dark the
   Resume button and the brown testimonial card use, not a lighter tint of the track's own light-
   mode brown.

   This reverses an earlier pass here that specifically tried #372a24 and rejected it: measured
   at the time as only 1.26:1 against the page (darkening tuned to sit on an accent-tinted case-
   study page, not this site's uniform near-black one), against the unchanged #695146's own
   2.38:1 — and picked #7a5f52 (a lightened tint, ≈2.97:1) instead. Both readings were correct;
   this is a values call, not a contrast bug being fixed. The lighter tint made the track read as
   a clearly distinct control against the page; the darker, case-study-matched shade makes it
   read as part of the same family as the Resume button and testimonial card next to it on this
   same page. Superseded on request in favour of the latter. */
[data-theme="dark"] .mode-toggle__track {
  background: var(--color-lib-ai-band-dark); /* #372a24 */
}

.mode-toggle__track:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: 3px;
}

/* Absolutely positioned rather than a flex child: the morph animates left/width/background
   freely, and the rest states below are just two fixed points on that same timeline. No
   transition on the rest-state rules themselves — the only visible motion is the keyframe
   animation triggered by .is-morphing-to-*; committing the final [data-mode] afterwards just
   lands exactly on the animation's own last frame, so there is nothing to snap.

   border-radius is a fixed 14px — half the knob's own 28px height — not 50%. 50% computes each
   corner's radius as a percentage of THAT corner's own width and height independently, which is
   fine on the 28x28 circle but draws an ELLIPSE once the knob is 60px wide mid-morph (a
   "stretched sphere," not a pill). A constant 14px radius is a true semicircle on every straight
   edge regardless of width, so both the 28px rest state and the 60px expanded state read as a
   proper stadium/pill shape with both ends rounded — no keyframe override needed for this. */
.mode-toggle__knob {
  position: absolute;
  top: 2px;
  width: 28px;
  height: 28px;
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
}

[data-mode="work"] .mode-toggle__knob { left: 2px; background: #ecc9bd; }
[data-mode="fun"] .mode-toggle__knob { left: 34px; background: #eae38b; }

.mode-toggle__knob-label,
.mode-toggle__outside-label {
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: 10px;
  line-height: 18px;
}

/* Constant regardless of mode — Figma sets the letter inside the knob to this brown in both
   Work and Fun. */
.mode-toggle__knob-label {
  color: #695146;
}

.mode-toggle__outside-label {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}

/* The outside label always previews the OTHER mode, in that mode's colour: pink ("w") while
   Fun is active, yellow ("f") while Work is active. */
[data-mode="work"] .mode-toggle__outside-label { right: 12px; color: #eae38b; }
[data-mode="fun"] .mode-toggle__outside-label { left: 10px; color: #ecc9bd; }

/* Both labels duck out while the knob is mid-morph and fade back in before it settles — a single
   keyframe animation, not a plain `transition`. A plain transition was tried first (fast fade out,
   then fade back in once js/about.js committed the new state) and never actually fired: something
   about a `display: contents` ancestor several levels up (.mode-toggle, for the grid layout — see
   "Row alignment" above) stopped the transition from starting on these descendants, even though
   the *rule* itself was confirmed to match (its declared duration/easing showed up correctly in
   getComputedStyle; the opacity value just never moved). The knob's own left/width/background
   already animate correctly in this exact tree via `@keyframes`, so the labels use the same
   mechanism rather than chasing the transition bug further. */
@keyframes mode-toggle-label-cycle {
  0% { opacity: 1; animation-timing-function: ease-in; }
  20% { opacity: 0; animation-timing-function: ease-out; }
  80% { opacity: 0; }
  100% { opacity: 1; }
}

.is-morphing-to-fun .mode-toggle__knob-label,
.is-morphing-to-fun .mode-toggle__outside-label,
.is-morphing-to-work .mode-toggle__knob-label,
.is-morphing-to-work .mode-toggle__outside-label {
  animation: mode-toggle-label-cycle 450ms linear;
}

/* The morph: a circle grows into a full-width pill, holds as a gradient, then contracts into a
   circle at the opposite end in the destination's solid colour. Duration (450ms) must match
   ANIMATION_MS in js/about.js, which removes this class and commits the final state on the same
   timer rather than waiting for animationend. border-radius is not touched here — the knob's own
   constant 14px (above) already draws a proper rounded-both-ends pill at every width the morph
   passes through, not just at its 28px rest size. */
@keyframes mode-toggle-to-fun {
  0% { left: 2px; width: 28px; background: #ecc9bd; }
  40%, 60% { left: 2px; width: 60px; background: linear-gradient(90deg, #ecc9bd 1.72%, #eae38b 105.46%); }
  100% { left: 34px; width: 28px; background: #eae38b; }
}

@keyframes mode-toggle-to-work {
  0% { left: 34px; width: 28px; background: #eae38b; }
  40%, 60% { left: 2px; width: 60px; background: linear-gradient(90deg, #ecc9bd 1.72%, #eae38b 105.46%); }
  100% { left: 2px; width: 28px; background: #ecc9bd; }
}

.is-morphing-to-fun .mode-toggle__knob {
  animation: mode-toggle-to-fun 450ms ease-in-out forwards;
}

.is-morphing-to-work .mode-toggle__knob {
  animation: mode-toggle-to-work 450ms ease-in-out forwards;
}

@media (prefers-reduced-motion: reduce) {
  /* .about__portrait itself no longer carries a transition — the cross-fade lives on the ghost
     layer, which is disabled below — so only the bio's fade-out is left to switch off here. */
  .about__bio {
    transition: none;
  }

  /* js/about.js skips the word reveal and the cross-fade entirely here, so neither the word spans
     nor the ghost layer are ever made — this is belt and braces in case any survive a mode change
     made before the setting was applied. */
  .about__bio-word {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .about__portrait-ghost {
    display: none;
  }

  .is-morphing-to-fun .mode-toggle__knob,
  .is-morphing-to-work .mode-toggle__knob,
  .is-morphing-to-fun .mode-toggle__knob-label,
  .is-morphing-to-fun .mode-toggle__outside-label,
  .is-morphing-to-work .mode-toggle__knob-label,
  .is-morphing-to-work .mode-toggle__outside-label {
    animation: none;
  }
}

/* ── Section headings ────────────────────────────────────────────────────── */

/* Figma sets both section headings at body size in SemiBold rather than on the display scale. */
.about__heading {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-body);
  font-weight: 600;
  line-height: var(--leading-normal);
  color: var(--color-text-primary);
}

/* ── Testimonials ────────────────────────────────────────────────────────── */

.about__testimonials-section {
  padding-top: var(--space-3xl);
}

/* align-items: flex-start, not the flex row's stretch default — see .testimonial-card__attribution
   below for why. Each card is now free to size to its own content ("reduce and increase" with
   whatever the viewport does to its own line-wrapping) rather than being forced to the row's
   tallest card. */
.about__testimonials {
  display: flex;
  align-items: flex-start;
  gap: 36px;
  margin-top: var(--space-xl);
}

/* Deliberately not the .quote component: that one is a --color-bg-band panel with 40px mono quote
   marks and mono-italic body. Figma's treatment here is a solid colour card with sans body and no
   marks, so it shares nothing but the idea. See design.md: About page. */
.testimonial-card {
  flex: 1 1 466px;
  min-width: 0;
  margin: 0;
  padding: 36px;
  border-radius: 5px;
  background: var(--card-bg);
  color: var(--card-text);
  font-family: var(--font-sans);
  font-size: var(--text-body);
  line-height: var(--leading-normal);
  display: flex;
  flex-direction: column;
}

.testimonial-card p {
  margin: 0;
}

.testimonial-card__quotes {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

/* A fixed --space-2xl (48px) gap above the attribution, not the margin-top: auto this used to
   carry. auto used to push the attribution to the foot of the CARD, which .about__testimonials'
   old align-items: stretch had force-equalised to the taller sibling's height — so the visible
   gap above a shorter card's attribution was really "however much stretch that card picked up",
   not the 48px it looked like at a glance. It held near 48px at some widths by coincidence (when
   the two cards' content happened to wrap to similar heights) and opened out past 200px at
   others (measured 210px at 900px wide, where the row's wrapping made one card's own content far
   shorter than its stretched neighbour). Fixed now: with the row no longer stretching cards to
   match (see .about__testimonials), each card is exactly as tall as its own content, so this
   padding-top IS the entire gap, unconditionally, at every width. */
.testimonial-card__attribution {
  padding-top: var(--space-2xl);
  font-style: normal;
}

.testimonial-card__name {
  font-weight: 600;
}

/* On-Demand Classification's olive. The accent is design.md's corrected #F0E9A0, not Figma's
   #eae38b, which is 4.39:1 on this background — the same failure design.md already records for
   the Work library cards. */
.testimonial-card--olive {
  --card-bg: var(--color-lib-ondemand-bg);
  --card-text: var(--color-lib-ondemand-accent);
}

.testimonial-card--brown {
  --card-bg: var(--color-lib-ai-bg);
  --card-text: var(--color-lib-ai-accent);
}

/* Dark mode: the cards now get their own background here, rather than reusing the light-mode
   --color-lib-*-bg — see design.md: About page in dark mode → case-study palettes. Text is
   untouched (the corrected accent colour already reads correctly on the new, darker ground; only
   the background needed a dark-specific value).

   --color-lib-*-band-dark is the same shade the case study pages use for their own dark-mode
   stats band (--cs-band-dark, aliased to these same tokens in tokens.css) — reused here rather
   than repeated as a literal, since the About page carries no .cs--* class to read --cs-band-dark
   from directly. One source of truth for the hex; two unrelated consumers. */
[data-theme="dark"] .testimonial-card--olive {
  --card-bg: var(--color-lib-ondemand-band-dark);   /* #36352A */
}

[data-theme="dark"] .testimonial-card--brown {
  --card-bg: var(--color-lib-ai-band-dark);         /* #372A24 */
}

/* ── Outside the pixels ──────────────────────────────────────────────────── */

/* Heading only. The lead-in is on this container rather than on the heading itself, which is why
   dropping the paragraph that used to follow it left the heading exactly where it was. */
.about__outside {
  padding-top: var(--space-3xl);
}

/* ── Photo album ─────────────────────────────────────────────────────────── */

/* 36px, off the spacing scale (--space-lg 24 / --space-xl 32 / --space-2xl 48) and so a literal —
   like several other measured values in this system. It is the whole gap between the heading and
   the album: .about__heading has `margin: 0`, neither box carries a margin for the other to
   collapse with, and .about__outside has no padding-bottom. Was --space-2xl when a paragraph sat
   in between. */
.about__album-section {
  padding-top: 36px;
  padding-bottom: var(--space-3xl);
}

.album {
  position: relative;
  width: 100%;
  max-width: 968px;
  margin: 0 auto;
}

.album__book {
  position: relative;
  aspect-ratio: 968 / 554;
  /* The cover is the two .album__cover rectangles below, not a background on this box. */
  perspective: 1800px;
}

/* Two covers rather than one rectangle — Figma 558:565 has the back cover standing a little proud
   of the front and very slightly off-square, and that mismatch is what keeps the object from
   reading as a flat rounded box. Both sit below the paper: the spread is a later sibling with the
   default z-index, so it paints over them without either needing one. */
.album__cover {
  position: absolute;
  border-radius: 14px;
  background: var(--color-album-cover);
}

/* Proud of the front on every edge, and off-square by a fraction of a degree. */
.album__cover--back {
  inset: -0.5% -0.6% -0.7% -0.4%;
  transform: rotate(0.22deg);
  background: color-mix(in srgb, var(--color-album-cover) 88%, #000);
  box-shadow: var(--shadow-book);
}

.album__cover--front {
  inset: 0;
  transform: rotate(-0.1deg);
}

/* The paper sits inset from the cover so the binding shows as a border all round, and the two
   pages are held apart by a real gutter rather than meeting in the middle. Widths are driven from
   the album, not the spread, so a page stays exactly 45.2% of the album (437.536px at full size) —
   the figure every scrapbook percentage is measured against. Spread inset 4.387% each side plus a
   0.826%-of-album gutter puts 4.387 + 45.2 + 0.826 + 45.2 + 4.387 back at 100. The gutter is that
   0.826% expressed against the spread's own width, hence 0.9055%. */
.album__spread {
  position: absolute;
  inset: 3.2% 4.387%;
  display: flex;
  gap: 0.9055%;
  /* Shows only through the gutter, the pages covering the rest: a near-black channel, exactly
     what the reference has between the two leaves. */
  background: linear-gradient(90deg, #15120d 0%, #2a241b 45%, #1a1610 100%);
  transform-style: preserve-3d;
}

.album__page {
  position: relative;
  flex: 1 1 50%;
  min-width: 0;
  overflow: hidden;
  background: var(--color-album-paper);
  /* Same procedural grain as the book's pages — no raster texture. */
  filter: url(#album-paper-grain);
}

/* The two inner edges are deliberately NOT mirror images. With the light coming from the upper
   left, the reference has the left page's cut edge as a narrow dark-tan band a few pixels wide,
   while the right page sits in the spine's shadow and carries a much broader, softer falloff that
   is still visibly darker than the paper ~30px in. Mirroring one shadow onto both read as a seam
   rather than as an open book. */
.album__page--left {
  border-radius: 8px 0 0 8px;
  box-shadow:
    inset -5px 0 0 -2px rgba(88, 62, 26, 0.5),
    inset -16px 0 14px -12px rgba(23, 23, 23, 0.5);
}

.album__page--right {
  border-radius: 0 8px 8px 0;
  box-shadow:
    inset 4px 0 0 -2px rgba(88, 62, 26, 0.35),
    inset 34px 0 30px -24px rgba(23, 23, 23, 0.6);
}

/* ── Scrapbook composition ───────────────────────────────────────────────────
   Each page is a free-form scattering of photos and captions — no shared grid. Every item's
   position, size and (for photos) rotation is a percentage set inline by js/about.js, read
   straight off Figma "About diary" (542:496). Percentages are of the PAGE's own box, which is a
   fixed 437.536 x 518.544 fraction of the 968 x 554 album at any width the album renders at, so a
   composition scales as one piece exactly like the album does — the same reasoning as the spiral
   binding and diagram geometry being exempt from the spacing scale (design.md: Spiral binding,
   Flow diagram). .album__page and .album__face are the positioned ancestors (already position:
   relative / absolute), so these percentages resolve against them directly. */
/* The positioned ancestor every item's percentages actually resolve against — not the page
   itself. On desktop the page box already is 437.536 x 518.544 (see above), so this sits flush
   with it and changes nothing. Below 768px the left page is dropped (see the narrow-screens
   rule) and the right page's flex item grows to fill the freed width without its height
   changing, which would otherwise stretch every item's width% against a box twice as wide as its
   height% assumes. Sizing the canvas from the page's height and deriving its width from the same
   fixed aspect ratio — then centering it — reproduces the desktop composition exactly at any
   page width, with the freed width simply showing as paper margin on both sides. */
.scrapbook__canvas {
  position: absolute;
  top: 0;
  left: 50%;
  height: 100%;
  aspect-ratio: 437.536 / 518.544;
  transform: translateX(-50%);
}

.scrapbook__photo {
  position: absolute;
  display: block;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

.scrapbook__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Figma's mockup shows plain cropped photos, hand-scattered and rotated — no polaroid border,
   tape or pins. The rounded corner and card shadow are the site's own existing tokens (design.md:
   Radius, Card shadow), not a new decoration. */
.scrapbook__caption {
  position: absolute;
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: var(--leading-normal);
  /* Figma sets this text pure #000000 — the same slip design.md already normalises everywhere
     else (see design.md: Colors) — to the site's #171717. On the album's cream paper
     (--color-album-paper, #eee6d9) that is 14.48:1, well past AA.

     --color-album-ink, NOT --color-text-primary: the paper is a physical surface and stays cream
     in dark mode, so ink following the page's text token flipped to #f2efe9 and left these
     captions at 1.08:1 — invisible. See tokens.css. */
  color: var(--color-album-ink);
}

/* Figma's inline emphasis spans (DM Mono Medium within an otherwise Regular caption). */
.scrapbook__caption strong {
  font-weight: 500;
}

/* The rare caption Figma sets in Medium for its *entire* line, not just a span within it. */
.scrapbook__caption--medium {
  font-weight: 500;
}

/* 32-unit pitch down the gutter, matching --binding-pitch. Width is a percentage (80 of the
   album's 968), not a fixed 48px as before: the album scales below 1024px while a pixel width
   would not, which left the rings oversized against shrunken pages. Because the album holds a
   fixed aspect ratio, this element's width and height scale by the same factor, so the
   `preserveAspectRatio="none"` stretch stays uniform and the rings keep their proportions. */
.album__coil {
  position: absolute;
  top: 3.2%;
  bottom: 3.2%;
  left: 50%;
  width: 8.264%;
  transform: translateX(-50%);
  pointer-events: none;
  z-index: 2;
}

/* ── Flip ────────────────────────────────────────────────────────────────── */

/* The turning leaf: one half-width panel hinged on the spine, carrying the outgoing page on its
   front and the incoming one on its back. Hidden until a turn starts. */
.album__flipper {
  position: absolute;
  top: 3.2%;
  bottom: 3.2%;
  width: 45.2%;
  transform-style: preserve-3d;
  transition: transform 0.62s ease-in-out;
  z-index: 1;
  visibility: hidden;
}

.album__flipper--next {
  right: 4.387%;
  transform-origin: left center;
  transform: rotateY(0deg);
}

.album__flipper--prev {
  left: 4.387%;
  transform-origin: right center;
  transform: rotateY(0deg);
}

.album.is-turning .album__flipper { visibility: visible; }
.album.is-turning-next .album__flipper--next { transform: rotateY(-180deg); }
.album.is-turning-prev .album__flipper--prev { transform: rotateY(180deg); }

.album__face {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--color-album-paper);
  backface-visibility: hidden;
  filter: url(#album-paper-grain);
}

.album__face--back { transform: rotateY(180deg); }

/* ── Arrows ──────────────────────────────────────────────────────────────── */

.album__nav {
  position: absolute;
  top: 50%;
  width: 32px;
  height: 32px;
  margin-top: -16px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--color-button-primary);
  color: var(--color-button-primary-label);
  cursor: pointer;
  z-index: 3;
}

.album__nav--prev { left: 7.5%; }
.album__nav--next { right: 7.5%; }

.album__nav svg { width: 24px; height: 24px; }

.album__nav[disabled] {
  opacity: 0.35;
  cursor: default;
}

/* Ink, not --color-text-primary, for the same reason as the captions: `outline-offset` puts this
   ring on the album's cream paper, where a dark-mode near-white outline was invisible — and a
   focus indicator nobody can see is the one that matters most. Ink works on the paper in both
   modes, including over the dark-mode arrows below. */
.album__nav:focus-visible {
  outline: 2px solid var(--color-album-ink);
  outline-offset: 2px;
}

/* Dark mode: the arrows take the album's OWN palette instead of the shared primary-button brown.
   The cover is the Reusable Lists book's blue, so in dark mode its controls carry that case
   study's pairing from design.md — #3c465a with #E4AC71, 4.71:1 on each other and 7.65:1 against
   the cream paper they sit on.

   Dark mode only, matching how the case study pages work: light mode keeps the site-wide primary
   button (#695146 / #fafaf7, 7.01:1), which is untouched here and on the Resume button. The
   album's cover, paper and ink do not change between modes — it is a physical object — so this is
   the one piece of it that responds to the theme at all. */
[data-theme="dark"] .album__nav {
  background: var(--color-lib-lists-bg);
  color: var(--color-lib-lists-accent);
}

/* ── Narrow screens ──────────────────────────────────────────────────────── */

@media (max-width: 1023px) {
  /* The grid above only pays for itself once there's a second column to align against. Below
     this width everything reverts to plain top-to-bottom stacking: the three "display: contents"
     wrappers become real flex columns again, and every explicit grid-column/grid-row placement
     is cleared so source order (portrait, caption, switch, bio, contact) governs instead. */
  .about__intro {
    display: flex;
    flex-direction: column;
    gap: var(--space-xl);
  }

  .about__portrait-col,
  .about__bio-col,
  .mode-toggle {
    display: flex;
    flex-direction: column;
  }

  .about__portrait-col { gap: var(--space-lg); }
  .about__bio-col { gap: var(--space-lg); }
  .mode-toggle { gap: var(--space-sm); }

  .about__portrait,
  .mode-toggle__caption,
  .mode-toggle__row,
  .about__bio,
  .about__contact {
    grid-column: auto;
    grid-row: auto;
  }

  .mode-toggle__caption { margin-top: 0; }

  /* No aspect-ratio here any more — the portrait image carries its own, and each mode's differs.
     align-self reverts too: this column is a plain flex stack here, where `end` would mean the
     cross axis (right edge) rather than the bottom-alignment it buys in the grid above. */
  .about__portrait { width: 100%; align-self: auto; }
  .about__bio-col { flex: 0 1 auto; }

  /* .testimonial-card's flex: 1 1 466px (below) is a WIDTH value from Figma, calibrated for the
     row layout above. Once the row flips to a column here, "466px" becomes the main axis's
     preferred size, i.e. HEIGHT instead of width — locking every card to exactly 466px tall
     regardless of its own content and leaving dead space below a shorter card's attribution.
     Reset to flex: 0 1 auto so each card's height is just its content's, which is what actually
     "reduces and increases" the card as the viewport (and its line-wrapping) changes. */
  .about__testimonials { flex-direction: column; gap: var(--space-lg); }

  .testimonial-card { flex: 0 1 auto; }
}

@media (max-width: 767px) {
  .about__contact { flex-wrap: wrap; gap: var(--space-md); }
  .testimonial-card { padding: var(--space-lg); }
}

/* The spread stops being readable at two pages wide well before the site's usual 768px content
   breakpoint — checked by sweeping every page's caption against its photo across the full width
   range; the earliest GENUINE (not pre-existing) overlap starts around album width ~640-740px,
   which is viewport ~780-900px. 779px is a deliberately conservative choice within that range,
   close to the old 767px cutover rather than at the earliest onset — so the two-page "open book"
   view is still available on more screens, at the cost of a little caption/photo crowding on a
   couple of pages in the ~780-900px window just above this. See design.md: Photo album for the
   sweep and the trade-off.

   js/about.js switches its OWN pagination model (spread-of-2 vs one-page-at-a-time) at the exact
   same width, via ALBUM_SINGLE_MQ — that constant and this max-width MUST stay in lockstep, or
   the DOM here and the JS's page-index math disagree about which mode is active. */
@media (max-width: 779px) {
  .album__page--left { display: none; }
  .album__page--right { border-radius: 8px; }
  .album__coil { display: none; }
  .album__nav--prev { left: 2%; }
  .album__nav--next { right: 2%; }

  /* One page at a time turns with a plain cross-fade, not the flipper's 3D hinge — there is no
     spine here for a leaf to hinge onto once the coil is gone, and reusing the flip illusion for
     a single page would still look like it was miming a book the layout no longer is. js/about.js
     toggles is-page-fading; PAGE_FADE_MS there must match this duration. */
  .album__page--right {
    transition: opacity 180ms ease-in-out;
  }

  .album__page--right.is-page-fading {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .album__flipper { transition: none; }

  /* js/about.js already skips straight to the swap under reduced motion (see turnPage()), so
     is-page-fading is never added — this is belt and braces to match. */
  .album__page--right { transition: none; }
}
