/* ==========================================================================
   COMPONENTS
   Reusable pieces you'll duplicate across pages. Each one is its own
   labelled section below — find the one you want, copy the matching
   HTML from style-guide.html, and edit the text/content.

   Everything here reads its colours/fonts/spacing from variables.css.
   Don't hard-code a colour or font here — use a var(--...) instead, so
   the whole site stays consistent.

   Contents:
     1. Layout containers
     2. Skip link (accessibility)
     3. Global navigation
     4. Buttons
     5. Display title (calligraphy font, big moments only)
     6. Page frame (wraps chapters / long reading content)
     7. Book illustration wrapper
     8. Catalogue list (homepage "8 sections" list)
     9. Cross-reference note
     10. Publication line (page numbers / edition details)
     11. Pull quote
     12. Ornament divider
     13. Tag / badge
     14. Accordion
     15. Return-to-shelf link
     16. Site footer
     17. Torn paper
     18. Icon box
     19. Text box
     20. Split panel (dark, text + image)
     21. Shelf hero (homepage bookshelf + hotspots)
     22. Book info panel
   ========================================================================== */


/* --- 1. LAYOUT CONTAINERS ----------------------------------------------
   Wrap page content in one of these so it doesn't stretch edge-to-edge.
   .container       = narrow, for reading (chapters, CV text, etc.)
   .container-wide  = wide, for the homepage bookshelf / grids */

.container {
  max-width: var(--container-width);
  margin: 0 auto;
  padding: 0 var(--page-gutter);
}

.container-wide {
  max-width: var(--container-wide);
  margin: 0 auto;
  padding: 0 var(--page-gutter);
}


/* --- 2. SKIP LINK --------------------------------------------------------
   Hidden until a keyboard user tabs to it — lets them jump straight to
   the main content instead of tabbing through the whole nav every time.
   Put <a href="#main" class="skip-link">Skip to content</a> as the very
   first thing inside <body>, and give your main content id="main". */

.skip-link {
  position: absolute;
  top: var(--space-md);
  left: var(--space-md);
  background-color: var(--color-ink);
  color: var(--color-paper);
  padding: 0.75em 1.25em;
  z-index: 100;
  text-decoration: none;
  border-radius: var(--radius-sm);
  /* translateY (a % of the element's OWN height) instead of a fixed
     top offset — a fixed offset like "-3rem" has to guess the link's
     rendered height, and if it guesses too small, a sliver of the
     link peeks into view permanently. -150% always clears it. */
  transform: translateY(-150%);
  transition: transform var(--transition-fast);
}

.skip-link:focus {
  transform: translateY(0);
}


/* --- 3. GLOBAL NAVIGATION ------------------------------------------------
   Dark charcoal bar used identically on every page (one of the two
   "bookends" of dark colour — see variables.css). Wraps naturally onto
   a second line on narrow screens, so no JS menu toggle is needed. */

.site-nav {
  background-color: var(--color-charcoal);
}

.site-nav__inner {
  max-width: var(--container-wide);
  margin: 0 auto;
  padding: var(--space-md) var(--page-gutter);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.site-nav__home {
  font-family: var(--font-secondary);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.95rem;
  color: var(--color-paper);
  text-decoration: none;
}

.site-nav__links {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md) var(--space-lg);
  margin: 0;
  padding: 0;
}

.site-nav__links a {
  font-family: var(--font-secondary);
  font-size: 0.85rem;
  letter-spacing: 0.03em;
  color: var(--color-paper);
  opacity: 0.8;
  text-decoration: none;
}

.site-nav__links a:hover,
.site-nav__links a[aria-current="page"] {
  opacity: 1;
  text-decoration: underline;
  text-underline-offset: 0.25em;
}


/* --- 4. BUTTONS -----------------------------------------------------------
   Flat and ink-bordered on purpose — no gradients, bevels or drop
   shadows. Charcoal only — the per-genre accent colour is kept out of
   buttons and links so they stay identical and predictable everywhere;
   the accent is reserved for small decorative touches instead. */

.btn {
  display: inline-block;
  padding: 0.65em 1.6em;
  border: 1.5px solid var(--color-ink);
  border-radius: var(--radius-sm);
  background-color: transparent;
  color: var(--color-ink);
  font-family: var(--font-secondary);
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--transition-fast), color var(--transition-fast);
}

.btn:hover,
.btn:focus-visible {
  background-color: var(--color-ink);
  color: var(--color-paper);
}

/* For a .btn sitting on a dark background (e.g. inside .text-box--dark
   or .book-info-panel) — same shape, paper-coloured instead of ink. */
.btn--light {
  border-color: var(--color-paper);
  color: var(--color-paper);
}

.btn--light:hover,
.btn--light:focus-visible {
  background-color: var(--color-paper);
  color: var(--color-charcoal);
}


/* --- 5. DISPLAY TITLE -----------------------------------------------------
   The calligraphy font (--font-display), for rare "big moment" titles
   only — a homepage headline or a chapter opener, never body content or
   navigation, because it's hard to read at small sizes or in long runs. */

.display-title {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: 0.03em;
  word-spacing: 0.25em;
  font-size: 3.5rem;
  line-height: 1.5;
  margin: 0 0 var(--space-md);
  color: var(--color-ink);
}

@media (max-width: 640px) {
  .display-title {
    font-size: 2.4rem;
  }
}


/* --- 6. PAGE FRAME ---------------------------------------------------------
   A quiet double-ruled border for wrapping a block of reading content —
   a chapter, a CV section, an excerpt. Built with plain CSS (not the
   book illustration) so it stays crisp and reliable at any length or
   screen size. */

.page-frame {
  position: relative;
  max-width: var(--container-width);
  margin: 0 auto;
  padding: var(--space-2xl) var(--space-xl);
  background-color: var(--color-paper);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-page);
}

.page-frame::before {
  content: "";
  position: absolute;
  inset: var(--space-sm);
  border: 1px solid var(--color-border);
  pointer-events: none;
}

@media (max-width: 640px) {
  .page-frame {
    padding: var(--space-xl) var(--space-md);
  }
  .page-frame::before {
    inset: var(--space-xs);
  }
}


/* --- 7. BOOK ILLUSTRATION WRAPPER ------------------------------------------
   For showing the hand-drawn artwork itself (bookshelf, open book) —
   just centres it and caps its width. Doesn't try to place text inside
   the drawing, which would break on resize; use .page-frame for that. */

.book-illustration {
  max-width: 640px;
  margin: 0 auto;
}

.book-illustration img {
  width: 100%;
  height: auto;
}


/* --- 8. CATALOGUE LIST ------------------------------------------------------
   The "card catalogue" style list of the 8 sections on the homepage —
   a full-width list of rows (like a library ledger), each with a book
   cover on the left. Structure: cover → title → what it contains →
   "Read the book" link.

   Cover images: use .catalogue-row__cover with an <img> once you have
   the real covers. Until then, leave it as an empty placeholder box —
   see style-guide.html for the placeholder markup. */

.catalogue-list {
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--color-border);
}

.catalogue-row {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  /* Vertical padding is --space-xl, not the smaller --space-lg used
     elsewhere — the hover lift + 20% grow on the cover (below) needs
     more headroom than --space-lg gives, or the raised cover pops
     over the divider line of the row above it. */
  padding: var(--space-xl) 0;
  border-bottom: 1px solid var(--color-border);
  text-decoration: none;
  color: inherit;
}

.catalogue-row__cover {
  flex-shrink: 0;
  width: 180px; /* 100px → 120px → 150px → 120px → 180px (+50%) */
  aspect-ratio: 8 / 9; /* matches the covers' own artboard proportions — they show the whole angled book, not a flat rectangle, so don't force a taller "book cover" ratio or it'll crop the art */
  background-color: transparent; /* the art sits directly on the page — no mat behind it */
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  overflow: hidden;
  font-family: var(--font-secondary);
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-ink-soft);
}

/* Once a cover is placeholder-only (no image yet), add this modifier so
   the box reads as "not real content" rather than a filled card. */
.catalogue-row__cover--empty {
  border: 1px dashed var(--color-border);
}

.catalogue-row__cover img {
  width: 100%;
  height: 100%;
  object-fit: contain; /* shows the whole illustration — never crops it */
  display: block;
  transition: transform 450ms cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}

/* Lifts the book up 20% larger on hover/focus of the whole row — the
   transform lives on the IMAGE, not on .catalogue-row__cover (which
   stays a fixed SIZE always, so the row's hoverable area never
   changes — scaling the box itself was the earlier approach, and it
   let the enlarged box spill outside its own hoverable area, causing
   the hover state to flicker on/off as the mouse crossed that
   boundary).

   Both the rise (hover in) and settle (hover out) use the SAME easing
   curve — cubic-bezier(0.22, 1, 0.36, 1), a gentle "ease-out" shape —
   so the motion looks like one continuous lift/lower rather than a
   fast snap in one direction. Plain "ease-out" was tried first and
   still read as jerky, because its curve is too aggressive at the
   start of the reverse (lowering) direction.

   .catalogue-row__cover's overflow switches to visible during hover so
   the zoomed image isn't clipped back down to the original frame (it
   was already filling that frame via object-fit:contain, so scaling
   it up with overflow still hidden cropped the corners off). This is
   safe from the same flicker bug because overflow doesn't change the
   box's own size or hit-test area — only .catalogue-row's hover state
   (unaffected by any of this) controls when the zoom happens. */
.catalogue-row:hover .catalogue-row__cover,
.catalogue-row:focus-visible .catalogue-row__cover {
  overflow: visible;
}

.catalogue-row:hover .catalogue-row__cover img,
.catalogue-row:focus-visible .catalogue-row__cover img {
  transform: translateY(-10px) scale(1.2);
}

.catalogue-row__content {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.35em;
}

.catalogue-row__meta {
  font-family: var(--font-secondary);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-ink-soft);
}

.catalogue-row__title {
  font-family: var(--font-secondary);
  font-size: 1.3rem;
  margin: 0;
  color: var(--color-ink);
}

.catalogue-row__desc {
  margin: 0;
  font-size: 0.9rem;
  color: var(--color-ink-soft);
  max-width: 42em;
}

.catalogue-row__cta {
  align-self: flex-start;
  margin-top: 0.35em;
  font-family: var(--font-secondary);
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-ink);
}

.catalogue-row:hover .catalogue-row__title,
.catalogue-row:focus-visible .catalogue-row__title {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

@media (max-width: 480px) {
  .catalogue-row {
    align-items: flex-start;
  }
  .catalogue-row__cover {
    width: 116px;
  }
}

/* Dark variant — a full-width charcoal strip, for breaking up a page
   that's otherwise all cream. This is just for a section HEADING (like
   the homepage's "The Card Catalogue" title + intro line) — not for
   wrapping the book list itself. The list's cover art is black
   linework on transparent PNGs, which would nearly disappear on a dark
   background, so keep .catalogue-list on the normal cream page outside
   this wrapper.

   Markup: wrap just the heading/intro in it, e.g.
   <section class="catalogue-section--dark">
     <div class="container">
       <h2>…</h2>
       <p class="catalogue-section__intro">…</p>
     </div>
   </section> */

.catalogue-section--dark {
  background-color: var(--color-charcoal);
  padding: var(--space-2xl) 0;
}

.catalogue-section--dark h2 {
  color: var(--color-paper);
}

.catalogue-section--dark .catalogue-section__intro {
  color: var(--color-paper);
  opacity: 0.75;
}


/* --- 9. CROSS-REFERENCE NOTE -----------------------------------------------
   For lines like "See also: Guide to Systems and Processes." */

.cross-reference {
  font-family: var(--font-secondary);
  font-style: italic;
  font-size: 0.9rem;
  color: var(--color-ink-soft);
  border-left: 2px solid var(--color-accent);
  padding-left: var(--space-sm);
  margin: var(--space-lg) 0;
}

/* Collected Works specifically doesn't want the italic + side-rule
   treatment (Kathryn's call, from the 3-option link-style artifact,
   "Option A" — plain small-caps text link) — scoped to this page's
   own genre attribute so Resume's "Further reading → LinkedIn" links
   keep the original style untouched. */
[data-genre="curiosities"] .cross-reference {
  font-style: normal;
  border-left: none;
  padding-left: 0;
  font-weight: 700;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-ink);
}

[data-genre="curiosities"] .cross-reference a {
  color: inherit;
  text-decoration: none;
}

[data-genre="curiosities"] .cross-reference a:hover {
  color: var(--color-accent-deep);
}


/* --- 10. PUBLICATION LINE ---------------------------------------------------
   Small-caps meta line for subtle "page numbers / publication detail"
   flourishes at the bottom of a section — the two spans (volume title,
   "End of Volume") sit centered together as one line, under a rule
   that's only 75% of the container's width rather than the full
   measure, so it reads as a deliberate closing flourish instead of
   just another full-width divider. */

.publication-line {
  max-width: none; /* overrides the global p{max-width:38em} readability cap — at this element's own 0.75rem font-size that cap resolves to just 456px, which silently left-aligned this whole block instead of letting text-align:center work across its actual container width */
  text-align: center;
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-ink-soft);
  margin-top: var(--space-xl);
}

.publication-line::before {
  content: "";
  display: block;
  width: 75%;
  margin: 0 auto var(--space-sm);
  border-top: 1px solid var(--color-border);
}

.publication-line span:not(:last-child)::after {
  content: "·";
  margin: 0 0.6em;
}


/* --- 10a. END OF VOLUME ------------------------------------------------------
   The closing block used at the bottom of every book page: the open-
   book ornament (.ornament-divider, section 5) sitting above
   .publication-line (section 10). Grouped into one named component —
   rather than each page copying the image and the line separately —
   so "what a volume's ending looks like" is one thing to edit, not
   four drifting copies.

   <section class="end-of-volume">
     <img src="../images/illus-open-book-pages.png" class="ornament-divider" alt="">
     <p class="publication-line">
       <span>Vol. III — Writing and Editing</span>
       <span>End of Volume</span>
     </p>
   </section> */

.end-of-volume {
  margin-top: var(--space-2xl);
  margin-bottom: var(--space-2xl);
  text-align: center;
}


/* --- 11. PULL QUOTE ----------------------------------------------------------
   A large charcoal quote mark instead of a coloured rule — keeps the
   genre accent colour out of it, same as buttons and links. */

.pull-quote {
  position: relative;
  font-family: var(--font-secondary);
  font-style: italic;
  font-size: 1.3rem;
  line-height: 1.5;
  padding-left: 1.75rem;
  margin: var(--space-xl) 0;
  color: var(--color-ink);
}

.pull-quote::before {
  content: "\201C";
  position: absolute;
  left: 0;
  top: 0.1em;
  font-family: var(--font-secondary);
  font-style: normal;
  font-size: 2.5rem;
  line-height: 1;
  color: var(--color-ink);
}

.pull-quote cite {
  display: block;
  margin-top: var(--space-sm);
  font-size: 0.8rem;
  font-style: normal;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-ink-soft);
}


/* --- 12. ORNAMENT DIVIDER ----------------------------------------------------
   The hand-drawn vine, used as a section break. Markup:
   <img src="images/divider-vine.png" class="ornament-divider" alt=""> —
   alt is empty and the image is decorative, so screen readers skip it. */

.ornament-divider {
  display: block;
  max-width: 200px;
  width: 45%;
  height: auto;
  margin: var(--space-md) auto;
}


/* --- 13. TAG / BADGE ----------------------------------------------------------
   For tool names, skill tags, categories. */

.tag {
  display: inline-block;
  padding: 0.25em 0.75em;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-ink-soft);
  background-color: var(--color-paper-deep);
  text-decoration: none; /* harmless on a <span>; needed so a linked <a class="tag"> doesn't show an underline under the pill */
}

/* Wrapper for a wrapped row of .tag elements — e.g. a tools/skills
   cloud (Key Skills, Resume and Experience). */
.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0 0 var(--space-md);
}

/* Accent-colored variant, for when the tags are the point (e.g. named
   tools) rather than incidental labels. */
.tag--accent {
  border-color: var(--color-accent);
  color: var(--color-accent-deep);
  background-color: var(--color-paper);
}

a.tag:hover,
a.tag:focus-visible {
  border-color: var(--color-ink-soft);
}


/* --- 14. ACCORDION -------------------------------------------------------------
   Accessible expand/collapse for the CV sections. Needs a little JS —
   see js/main.js. Markup pattern:

   <div class="accordion-item">
     <h3>
       <button class="accordion-trigger" aria-expanded="false" aria-controls="panel-1" id="trigger-1">
         Section title
         <span class="accordion-icon" aria-hidden="true">+</span>
       </button>
     </h3>
     <div class="accordion-panel" id="panel-1" role="region" aria-labelledby="trigger-1" hidden>
       <p>Panel content…</p>
     </div>
   </div> */

.accordion-item {
  border-radius: var(--radius-sm);
  overflow: hidden; /* keeps the trigger's dark background inside the rounded corners */
  margin-bottom: var(--space-sm);
}

/* --- 14a. CAREER RAIL (TIMELINE OPTION B) -----------------------------------------
   A left-rail timeline wrapper around a plain stack of .accordion-item
   blocks (section 14 above, unchanged) — a connecting vertical line
   plus a dot per entry, purely additive so every entry stays exactly
   as collapsible as before. Dot color is --color-focus (dark blue),
   deliberately NOT the resume page's own green --color-accent, per
   Kathryn's explicit request.

   Markup:
   <div class="career-rail">
     <div class="accordion-item">…unchanged…</div>
     … one per role …
   </div> */

.career-rail {
  position: relative;
  max-width: 44rem;
  margin: 0 auto;
  padding-left: 2rem;
}

.career-rail::before {
  content: "";
  position: absolute;
  left: 0.4rem;
  top: 0.6rem;
  bottom: 0.6rem;
  width: 1px;
  background: var(--color-sepia-light);
}

.career-rail .accordion-item {
  position: relative;
  /* the base rule's overflow:hidden (used everywhere else to keep the
     trigger's dark bar inside its own rounded corners) was clipping
     this dot, since left:-2rem below intentionally sits outside the
     item's own box. Losing the 2px corner-rounding here is unnoticeable;
     losing the dot entirely was not. */
  overflow: visible;
}

.career-rail .accordion-item::before {
  content: "";
  position: absolute;
  left: -2rem;
  top: 1.1rem;
  width: 0.8rem;
  height: 0.8rem;
  border-radius: 50%;
  background: var(--color-focus);
  border: 2px solid var(--color-paper);
  box-shadow: 0 0 0 1px var(--color-focus);
  z-index: 1;
}

/* Plain trigger to match the artifact's Left Rail option — no dark
   bar, just text sitting directly on the page beside the rail. */
.career-rail .accordion-trigger {
  background: none;
  color: var(--color-ink);
  padding: 0 0 var(--space-sm);
}

.career-rail .accordion-trigger:hover {
  background: none;
  color: var(--color-ink-soft);
}

.career-rail .accordion-trigger__dates {
  color: var(--color-ink-soft);
  opacity: 1;
}

.career-rail .accordion-icon {
  color: var(--color-focus);
}

.career-rail .accordion-panel {
  padding: 0 0 var(--space-md);
  background-color: transparent; /* the base .accordion-panel paper fill reads as a boxed patch against this page's textured background once opened — career-rail's plain trigger already sits directly on the page, so the panel should too */
}

@media (max-width: 480px) {
  .career-rail {
    padding-left: 1.4rem;
  }
  .career-rail .accordion-item::before {
    left: -1.4rem;
  }
}

.accordion-trigger {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  background-color: var(--color-charcoal);
  border: none;
  cursor: pointer;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 1rem;
  text-align: left;
  color: var(--color-paper);
}

.accordion-trigger:hover {
  background-color: var(--color-ink-soft);
}

.accordion-icon {
  flex-shrink: 0;
  font-weight: 700;
  font-size: 1.4rem;
  line-height: 1;
  transition: transform var(--transition-fast);
}

.accordion-trigger[aria-expanded="true"] .accordion-icon {
  transform: rotate(45deg);
}

.accordion-panel {
  padding: var(--space-lg);
  background-color: var(--color-paper);
  color: var(--color-ink);
}

.accordion-panel[hidden] {
  display: none;
}

/* Two-line trigger content (role + dates), for accordions where the
   collapsed state needs to show more than a plain title — e.g. a CV's
   Career History. Wrap the two lines in .accordion-trigger__text so
   the icon still sits flush right via the trigger's own flex layout:

   <button class="accordion-trigger" aria-expanded="false" aria-controls="…" id="…">
     <span class="accordion-trigger__text">
       <span class="accordion-trigger__role">Company — Role Title</span>
       <span class="accordion-trigger__dates">Month Year – Month Year</span>
     </span>
     <span class="accordion-icon" aria-hidden="true">+</span>
   </button> */

.accordion-trigger__text {
  display: flex;
  flex-direction: column;
  gap: 0.2em;
}

.accordion-trigger__role {
  font-family: var(--font-secondary);
  font-size: 1rem;
}

.accordion-trigger__dates {
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-paper);
  opacity: 0.7;
}


/* --- 15. RETURN-TO-SHELF LINK --------------------------------------------------
   Consistent "back to the bookshelf" link, near the top of every book
   page. Went from a fixed pill → an in-flow plain-text link → and now
   (testing) a pill again, this time inside a position:sticky header
   (15a) rather than position:fixed. Sticky still sits in normal page
   flow until you scroll past it, unlike fixed which overlays from the
   very first frame — so the pill needs its own solid background/shadow
   again, since once it sticks it's no longer just sitting on the
   hero's own background, it's floating over whatever page content has
   scrolled underneath it.
   <a href="index.html" class="return-link">Return to the Bookshelf</a> */

.return-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-family: var(--font-secondary);
  font-size: 1.02rem; /* +20% over the original 0.85rem */
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-decoration: none;
  color: var(--color-ink-soft);
  background-color: var(--color-paper);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  padding: 0.55em 1.15em;
  box-shadow: var(--shadow-page);
  transition: background-color var(--transition-fast), color var(--transition-fast);
}

.return-link::before {
  content: "←";
}

.return-link:hover {
  color: var(--color-ink);
  background-color: var(--color-paper-deep);
}

/* Same pill, pointing forward instead of back — sits opposite
   "Return to the Bookshelf" in .book-header (section 15a), on the
   right. Swaps the leading "←" for a trailing "→"; everything else
   (pill shape, hover state) stays identical since it's the same base
   .return-link class.
   <a href="story.html" class="return-link return-link--next">Read Next Volume</a> */
.return-link--next::before {
  content: none;
}

.return-link--next::after {
  content: "→";
}

/* Same pill again, this time a <button> that opens the Contact Me
   lightbox (section 44) instead of a link — no arrow either
   direction, just the plain pill shape/hover state.
   <button type="button" class="return-link return-link--contact" data-lightbox-open="contact-lightbox">Contact Me</button> */
.return-link--contact {
  cursor: pointer;
}

.return-link--contact::before {
  content: none;
}


/* --- 15a. BOOK HEADER ------------------------------------------------------------
   Sits at the very top of <main>, holding "Return to the Bookshelf" on
   the left and, per Kathryn's request, a matching "Read Next Volume"
   pill on the right (.return-link--next just above) — a flex row,
   space-between, so a page with only one of the two (the last volume
   has no next) still lands it on the correct side. TESTING
   position:sticky here — the header itself stays transparent (it's
   just a positioning wrapper; each pill carries its own background),
   so it never covers the hero with a full-width bar, it just keeps
   both pills pinned to the top corners once you scroll past their
   normal-flow position. No divider rule beneath it.

   <header class="book-header">
     <a href="../index.html" class="return-link">Return to the Bookshelf</a>
     <a href="story.html" class="return-link return-link--next">Read Next Volume</a>
   </header> */

.book-header {
  position: sticky;
  top: var(--space-md); /* only affects the STUCK position once scrolled past — initial placement still comes from this element's own padding/margin below, same as before sticky was added */
  z-index: 50;
  margin-bottom: calc(var(--space-xl) - 50px);
  padding: var(--space-lg) var(--page-gutter);
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-md) var(--space-lg);
  flex-wrap: wrap;
}


/* --- 16. SITE FOOTER ------------------------------------------------------------
   The second dark "bookend" — identical on every page. */

.site-footer {
  background-color: var(--color-charcoal);
  color: var(--color-paper);
  margin-top: var(--space-2xl);
  padding: var(--space-xl) 0;
}

.site-footer__inner {
  max-width: var(--container-wide);
  margin: 0 auto;
  padding: 0 var(--page-gutter);
  text-align: center;
  font-size: 0.8rem;
  opacity: 0.75;
}

.site-footer a {
  color: var(--color-paper);
}


/* --- 16a. BOOK FOOTER ------------------------------------------------------------
   Unlike .site-footer above (the dark charcoal bookend), this one is
   the same background as the rest of the page — it's not a visual
   "ending," just where the prev/next volume navigation lives. Replaces
   the old .prev-link--fixed / .next-link--fixed pills that used to
   float over the page: those could overlap each other at narrow
   widths (long volume titles had no max-width) and always sat on top
   of content wherever you'd scrolled to. One footer at the true
   bottom of the page fixes both. ("Return to the Bookshelf" stays at
   the top of the page instead — section 15 — it never had the
   overlap problem the prev/next pair did.)

   Reuses the plain .prev-link / .next-link classes (sections 30, 36)
   as-is — only the wrapper is new.

   <footer class="book-footer">
     <div class="book-footer__row">
       <a href="resume.html" class="prev-link">Read Previous Volume — Resume and Experience</a>
       <a href="story.html" class="next-link">Read Next Volume — The Story So Far</a>
     </div>
   </footer>

   Omit whichever of .book-footer__row's two links doesn't apply (e.g.
   Volume One has no previous volume) — .book-footer__row's flexbox
   still lands the remaining single link on the correct side via
   margin-left/right:auto on .prev-link/.next-link below. */

.book-footer {
  margin-top: var(--space-2xl);
  padding: var(--space-xl) var(--page-gutter);
  border-top: 1px solid var(--color-border);
  text-align: center;
}

.book-footer__row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-lg) var(--space-xl);
  flex-wrap: wrap;
}

.book-footer__row .prev-link {
  margin-right: auto;
}

.book-footer__row .next-link {
  margin-left: auto;
  text-align: right;
}

@media (max-width: 640px) {
  .book-footer__row {
    flex-direction: column;
    align-items: center;
  }
  .book-footer__row .prev-link,
  .book-footer__row .next-link {
    margin-left: 0;
    margin-right: 0;
  }
}


/* --- 17. TORN PAPER ----------------------------------------------------------
   A soft, organic torn-edge treatment for panels that should feel like a
   loose scrap of paper — excerpts, notes, pull-out content. The edge is
   a real irregular shape (an organic wobbled outline, blurred for a
   feathered boundary) used as a CSS mask — not a CSS zigzag.

   The masks are baked PNGs (images/masks/torn-edge-*.png), not live SVG
   filters. An SVG filter (feTurbulence + feDisplacementMap + blur) gives
   the exact same look and was the first approach here, but browsers are
   inconsistent about running that kind of filter *inside* a CSS mask
   specifically — it can silently fail and show a plain rectangle with
   no warning. A PNG mask is just alpha transparency, so it always
   renders the same way everywhere. The shape/softness is identical
   either way; only how it's stored changed.

   The shadow uses `filter: drop-shadow(...)`, not `box-shadow`.
   box-shadow always follows the element's rectangular box, ignoring the
   mask — it would draw a rectangular shadow behind an irregular shape.
   drop-shadow follows the masked (visible) silhouette instead, so the
   shadow actually traces the torn edge. Two drop-shadows are layered —
   a tight one for contact, a softer wider one for ambient lift — for a
   bit of physical depth without either shadow reading as heavy.

   USE SPARINGLY. One or two torn-paper elements on a page reads as a
   deliberate accent; covering everything in it reads as a scrapbook,
   which is the opposite of what this is for. Don't apply it to
   structural components (nav, page-frame, catalogue rows) — reserve it
   for content that's meant to feel like a loose, found object: a pull
   quote, a short note, an excerpt.

   Content needs room to breathe before it reaches the torn edge — the
   padding below is already tuned for that; don't shrink it.

   Variants (swap the modifier class, base .torn-paper stays):
     .torn-paper            → irregular tear on all four sides
     .torn-paper--alt       → a different tear pattern — use this when a
                               second torn-paper element sits near the
                               first, so the two don't look identically cut
     .torn-paper--top       → straight on three sides, torn only along
                               the top, like a page pulled from a pad
     .torn-paper--pale      → add alongside any of the above for the
                               lighter paper tone instead of the base one
                               — only use this if the torn-paper sits on
                               top of something darker than the page
                               background (e.g. --color-paper-deep), or
                               it'll disappear the same way the default
                               would on the page itself */

.torn-paper {
  position: relative;
  padding: var(--space-xl) var(--space-lg);
  /* --color-parchment, not --color-paper-deep — this needs to actually
     read as warm cream paper. --color-paper-deep is deliberately cool
     grey-mauve now (matching the rest of the palette), which would make
     a torn "paper" shape look like grey card instead. */
  background-color: var(--color-parchment);
  -webkit-mask-image: url("../images/masks/torn-edge-a.png");
  mask-image: url("../images/masks/torn-edge-a.png");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-mode: alpha;
  mask-mode: alpha;
  filter:
    drop-shadow(0 2px 3px rgba(39, 38, 54, 0.14))
    drop-shadow(0 10px 18px rgba(39, 38, 54, 0.10));
}

.torn-paper--alt {
  -webkit-mask-image: url("../images/masks/torn-edge-b.png");
  mask-image: url("../images/masks/torn-edge-b.png");
}

.torn-paper--top {
  -webkit-mask-image: url("../images/masks/torn-edge-top.png");
  mask-image: url("../images/masks/torn-edge-top.png");
}

.torn-paper--pale {
  background-color: var(--color-paper);
}


/* --- 18. ICON BOX ----------------------------------------------------------
   A small hand-drawn icon with a short label beneath it — for things
   like a "reasons to work with me" row, skill highlights, or a set of
   small decorative markers (moth, key, stamp, bookplate…). One icon-box
   is one item; use .icon-box-row to lay several out side by side.

   Markup:
   <div class="icon-box">
     <img src="images/icon-moth.png" alt="" class="icon-box__icon">
     <p class="icon-box__label">Label text</p>
   </div>

   The icon image should be a transparent PNG so it sits directly on the
   page like the book covers do — no background box behind it. */

.icon-box-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-xl) var(--space-lg);
  margin: var(--space-xl) 0;
}

.icon-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-sm);
  width: 9rem;
}

.icon-box__icon {
  width: 72px;
  height: 72px; /* fixed on both axes (was height:auto) — source icons vary a lot in their own aspect ratio (a tall narrow "H" vs. a wide "a" vs. a square badge), which left each icon a different actual height and pushed the labels below out of alignment with each other */
  object-fit: contain; /* centers each icon within that fixed box without stretching/distorting it, whatever its native proportions */
  display: block;
}

.icon-box__label {
  margin: 0;
  font-family: var(--font-secondary);
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-ink-soft);
}

/* A second, smaller line under .icon-box__label — added for Languages
   (Option B), where each box needs a proficiency level under the
   language name, not just the name alone. */
.icon-box__level {
  margin: 0;
  font-family: var(--font-body);
  font-size: 0.7rem;
  color: var(--color-ink-soft);
  opacity: 0.8;
}


/* --- 19. TEXT BOX ------------------------------------------------------------
   A simple heading + body content block, in a few alignment/heading
   combinations. No border or background by default — it's just text,
   positioned and styled consistently — except .text-box--dark, which is
   a genuine dark panel (charcoal background, paper-coloured text).

   Pick ONE alignment modifier and, if you want the calligraphy
   treatment, add --display to the heading element itself (not the box):

   <div class="text-box text-box--center">
     <h3 class="text-box__heading text-box__heading--display">Heading</h3>
     <p class="text-box__subtext">Optional small-caps line</p>
     <p class="text-box__body">Body text…</p>
   </div>

   <div class="text-box text-box--left">
     <h3 class="text-box__heading">Heading</h3>
     <p class="text-box__body">Body text…</p>
   </div>

   <div class="text-box text-box--right"> … same, right-aligned … </div>

   <div class="text-box text-box--dark">
     <h3 class="text-box__heading text-box__heading--display">Heading</h3>
     <p class="text-box__body">Body text…</p>
   </div>

   The calligraphy heading (--display) is deliberately smaller here than
   the site-wide .display-title — that one is for full hero moments,
   this is for a heading inside a normal content block. */

.text-box {
  max-width: 26rem;
}

.text-box--center {
  margin-left: auto;
  margin-right: auto;
  text-align: center;
}

.text-box--left {
  text-align: left;
}

.text-box--right {
  margin-left: auto;
  text-align: right;
}

.text-box__heading {
  font-family: var(--font-secondary);
  font-size: 1.5rem;
  margin: 0 0 var(--space-sm);
  color: var(--color-ink);
}

.text-box__heading--display {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: 0.03em;
  word-spacing: 0.25em;
  font-size: 2.2rem;
  line-height: 1.5;
}

/* The centered box's calligraphy heading reads as more of a moment —
   sized up 30% from the shared --display size above (2.2rem → 2.86rem)
   just for this variant, not the dark box, which keeps the smaller one. */
.text-box--center .text-box__heading--display {
  font-size: 2.86rem;
}

.text-box__subtext {
  margin: 0 0 var(--space-md);
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-ink-soft);
}

/* Every book page's title hero ("Volume X" -> book title) gets 30px
   more breathing room here than .text-box__subtext's shared default —
   scoped to the hero specifically so the many OTHER things using
   .text-box__subtext elsewhere (the homepage welcome panel, chapter
   dates, etc.) don't also grow. */
.split-panel__text .text-box__subtext {
  margin-bottom: calc(var(--space-md) + 30px);
  font-size: 0.825rem; /* +10% over the shared .text-box__subtext 0.75rem base, per Kathryn's request for the hero "Volume X" label specifically — scoped here so every other .text-box__subtext use (homepage welcome panel, chapter dates, etc.) keeps the smaller default */
}

.split-panel__text .text-box__heading--display {
  margin-bottom: var(--space-lg); /* extra room between the hero's book-title heading and the paragraph below it — was relying on .text-box__heading's much tighter shared default (--space-sm) */
}

.text-box__body {
  margin: 0;
  color: var(--color-ink-soft);
}

/* Dark variant — its own panel, not just a colour swap, so it always
   centers itself and has real padding regardless of which alignment
   class you also add. */
.text-box--dark {
  max-width: 30rem;
  margin-left: auto;
  margin-right: auto;
  padding: var(--space-xl) var(--space-lg);
  background-color: var(--color-charcoal);
  color: var(--color-paper);
  text-align: center;
}

.text-box--dark .text-box__heading {
  color: var(--color-paper);
}

.text-box--dark .text-box__body {
  color: var(--color-paper);
  opacity: 0.85;
}

.text-box--dark .text-box__subtext {
  color: var(--color-paper);
  opacity: 0.75;
}

/* Languages' icon boxes, folded into the bottom of Personal Profile —
   their SVGs, label and level line were authored for a paper
   background (components.css section 18), so recolor them for this
   dark panel. A stylesheet rule for stroke/fill beats an SVG's own
   presentation attribute regardless of specificity, so this is enough
   even though the markup no longer sets stroke/fill inline. */
.text-box--dark .icon-box-row svg path {
  stroke: var(--color-paper);
}

.text-box--dark .icon-box-row svg text {
  fill: var(--color-paper);
}

.text-box--dark .icon-box__label {
  color: var(--color-paper);
  opacity: 0.9;
}

.text-box--dark .icon-box__level {
  color: var(--color-paper);
  opacity: 0.65;
}


/* --- 20. SPLIT PANEL -----------------------------------------------------------
   A full-width band with text on one side and an image on the other —
   text on the left, image on the right, by default.

   Reuses the .text-box__heading / __heading--display / __subtext /
   __body classes from section 19 for the text side, so it looks
   consistent with every other text block on the site and you don't
   need to learn new typography classes for it. Only the colour changes
   (handled below) — the heading/body sizing is identical to a normal
   text box.

   Markup:
   <div class="split-panel split-panel--dark">
     <div class="split-panel__inner">
       <div class="split-panel__text">
         <h3 class="text-box__heading">Heading</h3>
         <p class="text-box__subtext">Optional small-caps line</p>
         <p class="text-box__body">Body text…</p>
       </div>
       <div class="split-panel__media">
         <img src="images/…" alt="…">
       </div>
     </div>
   </div>

   Add .split-panel--reverse to flip it (image left, text right).
   Stacks to a single column (text above image) below 640px. */

.split-panel {
  background-color: var(--color-paper);
}

.split-panel--dark {
  background-color: var(--color-charcoal);
}

.split-panel__inner {
  max-width: var(--container-wide);
  margin: 0 auto;
  padding: var(--space-2xl) var(--page-gutter);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2xl);
  align-items: center;
}

.split-panel__text {
  text-align: left;
  max-width: calc(26rem + 30px); /* the shared .text-box's 26rem was wrapping the hero paragraph too tight, cutting lines short — first +20px wasn't enough, now +30px total, scoped to the hero so other .text-box uses (dark panels, book info panel, etc.) keep their own widths */
}

.split-panel__media img {
  width: 60%; /* book covers were filling the whole column — 40% smaller reads better as a cover, not a poster */
  height: auto;
  display: block;
  margin: 0 auto;
  transition: transform var(--transition-fast);
}

.split-panel__media img:hover {
  transform: scale(1.25); /* the cover book grows on hover, on every book page's title hero */
}

.split-panel--reverse .split-panel__media {
  order: -1;
}

/* Dark background → the reused text-box typography needs paper-coloured
   text instead of its normal ink colour, same overrides as .text-box--dark. */
.split-panel--dark .text-box__heading {
  color: var(--color-paper);
}

.split-panel--dark .text-box__body {
  color: var(--color-paper);
  opacity: 0.85;
}

.split-panel--dark .text-box__subtext {
  color: var(--color-paper);
  opacity: 0.75;
}

@media (max-width: 640px) {
  .split-panel__inner {
    grid-template-columns: 1fr;
  }
  .split-panel--reverse .split-panel__media {
    order: 0; /* stacked layout always reads text-first, image-second */
  }
}


/* --- 21. SHELF HERO ----------------------------------------------------------
   The homepage bookshelf illustration with an invisible clickable button
   over each book spine. The image itself is never redrawn or cropped —
   the buttons are positioned as percentages over it, so they track the
   art correctly at any screen size (percentages scale with the image
   automatically, since both live in the same relative-positioned box).

   Each button just needs data-book="<id>" matching an entry in the
   BOOKS list at the top of js/main.js, plus a left/top/width/height
   inline style — those four numbers are specific to exactly where that
   one spine sits in bookshelf.png, so they're not something a shared
   CSS class could express; that's the one deliberate use of inline
   style in this whole project. If you ever replace the artwork, these
   percentages will need re-measuring against the new image.

   Markup (repeat per book, all 8 already in index.html):
   <button class="shelf-hero__hotspot" data-book="resume"
           style="left:2.2%; top:3.6%; width:10.7%; height:94.1%;">
     <span class="visually-hidden">Resume and Experience</span>
   </button>

   js/main.js listens for clicks on .shelf-hero__hotspot and fills in
   .book-info-panel (section 22) with that book's details.

   .shelf-layout is the wrapper around BOTH the panel and the shelf —
   it puts the (initially hidden) info panel to the left of the shelf
   on desktop, so once a book is clicked, the answer appears right next
   to it with no scrolling. Below 900px there's no room for two
   columns, so it stacks — shelf on top, panel below, matching the
   order you naturally interact with them on a touch screen. */

.shelf-layout {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-xl);
}

.shelf-layout .book-info-panel {
  flex: 0 0 26.4rem; /* 22rem, sized up 20% */
  width: 26.4rem;
  height: 21.5rem; /* fixed so the panel doesn't resize as each book's title/description length changes */
  margin-top: 0;
}

.shelf-layout .shelf-hero__frame {
  flex: 1 1 420px;
}

@media (max-width: 900px) {
  .shelf-layout {
    flex-direction: column;
  }
  .shelf-layout .shelf-hero__frame {
    order: 1;
  }
  .shelf-layout .book-info-panel {
    order: 2;
    flex-basis: auto;
    width: 100%;
    max-width: 30rem;
    height: 24rem; /* fixed for the same reason as the desktop rule above — narrower widths here wrap more text, so this needs to be taller than the desktop box */
    margin: var(--space-xl) auto 0;
  }
}

.shelf-hero__frame {
  position: relative;
  max-width: 850px; /* the hero section now fills the viewport height (index.html) and centers its content, so there's room for a bigger shelf than the earlier compact layout allowed */
  margin: 0 auto;
}

.shelf-hero__image {
  width: 100%;
  height: auto;
  display: block;
}

.shelf-hero__caption {
  /* --font-display's swashes overshoot standard font-metric ascent
     noticeably (the capital "C" here reaches well above the line box
     browsers compute), so this needs more headroom than a normal
     caption would to actually clear the shelf art below the image.
     Net to plain --space-2xl with no extra offset: pushed down +40px
     for clearance from the shelf's floor line, then pulled back up
     20px, then 10px, then another 10px, per Kathryn's follow-ups. Font
     size bumped +5pt on top of the original 1.6rem for the same
     request. */
  margin: var(--space-2xl) 0 0;
  text-align: center;
  font-family: var(--font-display);
  letter-spacing: 0.03em;
  word-spacing: 0.25em;
  font-size: calc(1.6rem + 5pt);
  line-height: 1.5;
  color: var(--color-ink);
}

/* Each hotspot previously carried its own cropped "slice" of the
   shared bookshelf.png as its own background (sized/positioned via
   --bg-size/--bg-pos custom properties, still set inline per hotspot
   even though unused now — harmless leftovers, safe to ignore), scaled
   up 1.25x on hover to make "just this book" pop out. In practice the
   slice never lined up pixel-for-pixel with the real artwork
   underneath — even at rest, sub-pixel rounding in the percentage
   math visibly doubled/ghosted spine linework and lettering, and
   scaling that already-misaligned slice on hover made it worse,
   overlapping neighboring spines. Replaced with a plain highlight
   overlay (tint + inset ring) that needs no duplicate image layer at
   all, so there's nothing left to misalign. */
.shelf-hero__hotspot {
  position: absolute;
  background-color: transparent;
  border: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
}

/* Deliberately no :hover state on the artwork itself — Kathryn's call:
   the shelf illustration shouldn't visually change on hover, even
   though hovering (js/main.js) now does preview that book's details in
   the side panel. The cursor:pointer above plus base.css's
   :focus-visible outline (color scoped just below) are the only
   affordances a hotspot gets directly. */

/* base.css's global :focus-visible rule (2px solid --color-focus,
   3px offset) is the right call everywhere else on the site, but on
   this plain pencil illustration with no genre accent the blue ring
   reads as a stray, out-of-place box rather than a focus indicator —
   recolored to match the ink tint above instead of removing it
   outright (keyboard focus still needs a visible outline). */
.shelf-hero__hotspot:focus-visible {
  outline-color: var(--color-ink);
}


/* --- 22. BOOK INFO PANEL ------------------------------------------------------
   The dark panel beside the shelf. This is literally the same dark
   text box from section 19 — not a lookalike.

   PERMANENT, not hidden — it used to only appear after a click, which
   shoved the shelf sideways to make room and read as a layout glitch.
   Now its space is always reserved, and it does double duty: before
   any book is picked, its HTML already contains a welcome state (name,
   tagline, instructions); js/main.js overwrites that with the
   previewed book's details on hover/focus, and un-hides the
   "book-info-panel__link" (that link — and only that link — starts
   with the HTML `hidden` attribute, since there's nothing to link to
   until a book is previewed). On index.html that link is styled as
   plain calligraphy text ("Click to read", same --font-display as the
   panel's own title) instead of the shared .btn/.btn--light pill —
   see that page's own <style> block, since this is a per-page choice,
   not this shared component's default.

   Markup — content starts as the welcome state and is overwritten by
   JS, but the elements need to exist with these exact classes for
   main.js to find them. Note every text element carries BOTH its
   .text-box__… class (for the shared look) AND its .book-info-panel__…
   class (a stable hook for JS/CSS that has nothing to do with which
   component supplies the typography):

   <div class="book-info-panel text-box text-box--dark" id="book-info-panel">
     <p class="text-box__subtext book-info-panel__volume">An Illustrated History</p>
     <h1 class="text-box__heading text-box__heading--display book-info-panel__title">Kathryn Donoghue</h1>
     <p class="text-box__body book-info-panel__desc">Hover over a spine to peek inside, click to read more.</p>
     <a href="#" class="btn btn--light book-info-panel__link" hidden>Read the Book →</a>
   </div>

   main.js still sets data-genre on the panel per selected book, even
   though nothing currently reads it here — harmless, and ready to use
   again if a genre-coloured touch gets added back later. The link
   stays .btn--light rather than accent-coloured, per the "no accent
   colour on interactive elements" rule. */

.book-info-panel {
  max-width: 75%;
  margin-top: var(--space-xl);
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
}

/* Extra breathing room before the button — more than the default
   .text-box__body gives, specifically for this panel. */
.book-info-panel .text-box__body {
  margin-bottom: var(--space-xl);
}

/* Sized up from the shared --display size: 2.2rem → 2.64rem (+20%)
   → 3.3rem (+25% more). This ONLY applies to the welcome-state "Kathryn
   Donoghue" heading, not book titles — js/main.js adds/removes this
   class when swapping the heading's content, since both states reuse
   the same element (see section 22). */
.book-info-panel__title--welcome {
  font-size: 3.3rem;
}


/* --- 23. CV ENTRY ------------------------------------------------------------
   One reusable block for anything shaped like "title + a meta line +
   some bullets" — a role's responsibilities (inside an accordion panel,
   section 14), an education entry, a personal project. Copy this same
   markup wherever that shape repeats, on this page or a future one.

   Markup (all three pieces are optional except __list, so a short entry
   can skip __title or __meta if there's nothing to put there):

   <div class="cv-entry">
     <h4 class="cv-entry__title">Degree or Project Name</h4>
     <p class="cv-entry__meta">Institution or context · Date range</p>
     <ul class="cv-entry__list">
       <li>One point…</li>
     </ul>
   </div> */

.cv-entry {
  margin-bottom: var(--space-lg);
}

.cv-entry__title {
  font-family: var(--font-secondary);
  font-size: 1.05rem;
  margin: 0 0 0.2em;
  color: var(--color-ink);
}

.cv-entry__meta {
  margin: 0 0 var(--space-sm);
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-ink-soft);
}

.cv-entry__list {
  margin: 0;
  padding-left: 1.1em;
  color: var(--color-ink-soft);
}

.cv-entry__list li {
  margin-bottom: 0.5em;
}

.cv-entry__list li:last-child {
  margin-bottom: 0;
}

/* Ledger Entries — "Ledger Index" option from the Career Achievements
   style comparison. Italic roman-numeral markers with a dashed rule
   between entries, index-page style, instead of a plain bullet list.
   Markup:
   <div class="ledger-entries">
     <div class="ledger-entry"><span class="ledger-entry__num">I.</span><p class="ledger-entry__body">…</p></div>
     …
   </div> */
.ledger-entries {
  margin: var(--space-md) 0;
}

.ledger-entry {
  padding: 14px 0;
  border-bottom: 1px dashed var(--color-sepia-light);
}

.ledger-entry:first-child {
  padding-top: 0;
}

.ledger-entry:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

/* Inline, not a reserved flex column — a fixed-width numeral column
   (the previous approach) narrows the text's available width versus
   Key Skills' plain <ul>, which has no such column and just fills the
   container. Flowing the numeral inline, as part of the same <p>, is
   the only way to guarantee an identical wrapping width to that <ul>. */
.ledger-entry__num {
  font-family: var(--font-secondary);
  font-style: italic;
  font-weight: 600;
  color: var(--color-accent-deep);
  font-size: 1.1rem;
  margin-right: 6px;
}

.ledger-entry__body {
  font-size: 1rem;
  line-height: 1.6;
  color: var(--color-ink);
  margin: 0;
  max-width: none; /* overrides the global p{max-width:38em} readability cap, which was wrapping this narrower than Key Skills' plain <ul> (not subject to that cap) in the same .container */
}

.cv-entry__link {
  display: inline-block;
  margin-top: 0.3em;
  font-size: 0.9rem;
}

/* Lays several short .cv-entry blocks out side by side instead of
   stacked — e.g. a Languages section (English / Afrikaans / Spanish).
   Not needed for longer entries like roles or education, which read
   better stacked. */
.cv-entry-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xl);
}

/* A strict two-column layout for a pair of longer .cv-entry blocks
   (e.g. Personal Projects) — unlike .cv-entry-row's flex-wrap, this
   guarantees two real columns rather than collapsing to one whenever
   the available width is too narrow to fit both at their preferred
   size (which .container's 46rem reading width often is once a
   flex-shrink:0 image sits alongside it). Drops to one column on
   mobile, where two would be too cramped to read. */
.cv-entry-columns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-lg);
}

@media (max-width: 640px) {
  .cv-entry-columns {
    grid-template-columns: 1fr;
  }
}


/* --- 24. PHOTO PLACEHOLDER ---------------------------------------------------
   A stand-in for a photo or illustration that hasn't been supplied yet
   — same "not real content" dashed-box language as
   .catalogue-row__cover--empty (section 8). Swap the whole element for
   a real <img> once you have one; .photo-caption can stay underneath
   either way.

   Markup:
   <div class="photo-placeholder">Photo placeholder</div>
   <p class="photo-caption">Caption placeholder — a short line about this image.</p> */

.photo-placeholder {
  aspect-ratio: 4 / 3;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: var(--space-md) 0 var(--space-sm);
  border: 1px dashed var(--color-border);
  background-color: var(--color-paper-deep);
  color: var(--color-ink-soft);
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* Same idea as .photo-placeholder, for video — a small play-button
   glyph instead of just text, so it reads as "video goes here" at a
   glance rather than looking like a mislabeled photo box. Swap the
   whole element for a real <video> (or an embed) once footage exists.

   Markup:
   <div class="video-placeholder">
     <span class="video-placeholder__icon" aria-hidden="true">▶</span>
     <span>Video placeholder</span>
   </div>
   <p class="photo-caption">Caption placeholder — a short line about this video.</p> */

.video-placeholder {
  aspect-ratio: 16 / 9;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  margin: var(--space-md) 0 var(--space-sm);
  border: 1px dashed var(--color-border);
  background-color: var(--color-paper-deep);
  color: var(--color-ink-soft);
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.video-placeholder__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5em;
  height: 2.5em;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  font-size: 1.1rem;
  color: var(--color-ink-soft);
  /* nudge the glyph so it looks visually centered inside the circle,
     since ▶ has more whitespace on its own left than its right */
  padding-left: 0.2em;
}

.photo-caption {
  margin: 0 0 var(--space-lg);
  font-family: var(--font-secondary);
  font-style: italic;
  font-size: 0.8rem;
  color: var(--color-ink-soft);
}


/* --- 25. READING SECTION ------------------------------------------------------
   A full-bleed band for a repeating run of content — chapters, entries,
   anything that would otherwise be several near-identical sections in a
   row with no visual rhythm. Alternate the plain version with
   .reading-section--dark every other one (the same "dark bookend" idea
   as .catalogue-section--dark in section 8, just reusable for any
   repeating content, not only a heading strip):

   <section class="reading-section"><div class="container">…chapter 1…</div></section>
   <section class="reading-section reading-section--dark"><div class="container">…chapter 2…</div></section>
   <section class="reading-section"><div class="container">…chapter 3…</div></section>

   The dark variant overrides every component that might appear inside
   it (headings, text-box body/subtext, pull-quote, cross-reference,
   photo-placeholder) so you can drop normal content in without
   thinking about colour — it just switches to the paper-on-charcoal
   treatment used everywhere else on the site. */

.reading-section {
  padding: var(--space-2xl) 0;
}

.reading-section--dark {
  background-color: var(--color-charcoal);
  color: var(--color-paper);
}

.reading-section--dark h1,
.reading-section--dark h2,
.reading-section--dark h3,
.reading-section--dark h4 {
  color: var(--color-paper);
}

.reading-section--dark .text-box__subtext {
  color: var(--color-paper);
  opacity: 0.75;
}

.reading-section--dark .text-box__body {
  color: var(--color-paper);
  opacity: 0.9;
}

.reading-section--dark .pull-quote,
.reading-section--dark .pull-quote::before {
  color: var(--color-paper);
}

.reading-section--dark .pull-quote cite {
  color: var(--color-paper);
  opacity: 0.75;
}

.reading-section--dark .cross-reference {
  color: var(--color-paper);
  opacity: 0.85;
}

.reading-section--dark .cross-reference a {
  color: var(--color-paper);
}

.reading-section--dark .photo-placeholder {
  background-color: var(--color-ink-soft);
  border-color: rgba(244, 234, 217, 0.3);
  color: var(--color-paper);
}

.reading-section--dark .photo-caption {
  color: var(--color-paper);
  opacity: 0.75;
}

.reading-section--dark .open-letter__prompt {
  color: var(--color-paper);
  opacity: 0.75;
}

.reading-section--dark .read-more-trigger {
  color: var(--color-paper);
}

.reading-section--dark .read-more-trigger:hover,
.reading-section--dark .read-more-trigger:focus-visible {
  color: #f0c4c9; /* pale pink, requested specifically for this hover state */
}

.reading-section--dark a {
  color: var(--color-paper);
}

.reading-section--dark a:hover {
  opacity: 0.8;
}


/* --- 26. CHAPTER LAYOUT --------------------------------------------------------
   Puts a chapter's text beside its photo instead of stacked — text
   wider than the photo, same "text + media" idea as .split-panel
   (section 20) but sized for a photo placeholder sitting next to a
   couple of paragraphs, not a full-bleed band.

   Add .chapter-layout--reverse to put the photo on the LEFT instead of
   the right — alternate it chapter to chapter (plain, --reverse,
   plain, --reverse…) so the page zigzags instead of every photo
   landing on the same side. Below 640px both stack text-first,
   photo-second regardless of --reverse, same as .split-panel.

   Markup:
   <div class="chapter-layout"> (or "chapter-layout chapter-layout--reverse")
     <div class="chapter-layout__text">…heading, body, pull-quote…</div>
     <div class="chapter-layout__media">
       <div class="photo-placeholder">Photo placeholder</div>
       <p class="photo-caption">…</p>
     </div>
   </div> */

.chapter-layout {
  display: flex;
  gap: var(--space-xl);
  align-items: stretch;
  flex-wrap: wrap;
}

.chapter-layout__text {
  flex: 3 1 320px;
  min-width: 0;
}

.chapter-layout__media {
  flex: 2 1 220px;
  max-width: 320px;
  display: flex;
  flex-direction: column;
}

/* .chapter-layout has align-items:stretch, so this column is always
   exactly as tall as .chapter-layout__text beside it — the photo
   placeholder (flex:1) fills that height, growing or shrinking with
   however long the chapter's text runs, and the caption keeps its own
   natural height underneath it. */
.chapter-layout__media .photo-placeholder {
  flex: 1;
  aspect-ratio: auto;
  min-height: 200px;
  margin-bottom: var(--space-sm);
}

.chapter-layout--reverse .chapter-layout__media {
  order: -1;
}

/* A real illustration in place of .photo-placeholder (same flex:1
   stretch-to-match-text-height behaviour). These are the site's usual
   black-alpha line art — ink drawn only in the alpha channel — so by
   default this gets its own --color-paper card to stay visible on any
   background darker than paper.

   --on-dark is for a second version of the same source file, recoloured
   cream instead of black (alpha channel kept, RGB swapped to
   --color-paper — see images/illus-*-cream.png, made with a one-off
   Pillow script, not a CSS filter) and shown with NO card at all. Only
   use it inside .reading-section--dark: the cream ink reads as an
   engraving straight on the dark ground there, but would nearly vanish
   against the page's own light paper background. */
.chapter-layout__media .chapter-layout__illustration {
  flex: 1;
  min-height: 200px;
  margin-bottom: var(--space-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--color-paper);
  padding: var(--space-md);
  box-sizing: border-box;
}

.chapter-layout__media .chapter-layout__illustration.chapter-layout__illustration--on-dark {
  background-color: transparent;
}

.chapter-layout__illustration img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

@media (max-width: 640px) {
  .chapter-layout__media .chapter-layout__illustration {
    flex: none;
    aspect-ratio: 4 / 3;
    min-height: 0;
  }
}

@media (max-width: 640px) {
  .chapter-layout__media {
    max-width: none;
  }
  /* Matches .chapter-layout--reverse .chapter-layout__media's own
     specificity (section above) so this actually wins on narrow
     screens — a plain ".chapter-layout__media { order: 0 }" here lost
     to that more specific selector and stacking stayed reversed. */
  .chapter-layout--reverse .chapter-layout__media {
    order: 0;
  }
  /* Stacked layout has nothing to stretch to match, so go back to a
     normal fixed-ratio photo block instead of an arbitrary tall box. */
  .chapter-layout__media .photo-placeholder {
    flex: none;
    aspect-ratio: 4 / 3;
    min-height: 0;
  }
}


/* --- 27. BLOG CARD ------------------------------------------------------------
   For an external piece — a WordPress post, a published article
   somewhere else — presented as a featured image, source line, title,
   excerpt and a "read" cue. The WHOLE card is the link (same idea as
   .catalogue-row, section 8: one <a> wrapping everything, not a
   separate "read more" link inside it).

   Markup:
   <a href="https://example.com/the-real-post" class="blog-card" target="_blank" rel="noopener">
     <div class="blog-card__image">Featured image placeholder</div>
     <div class="blog-card__content">
       <span class="blog-card__source">Placeholder Blog · placeholderblog.com</span>
       <h4 class="blog-card__title">Post Title</h4>
       <p class="blog-card__excerpt">Excerpt or summary text…</p>
       <span class="blog-card__cta">Read the article →</span>
     </div>
   </a>

   Once a real featured image exists, swap .blog-card__image's content
   for an <img> — the box's padding/border/aspect-ratio stay on the
   element itself so nothing else needs to change.

   No accent colour on hover, same rule as buttons/links elsewhere
   (section 4) — the border just darkens instead. */

.blog-card {
  display: flex;
  flex-direction: column;
  height: 100%; /* fills its grid row so every card in a row matches height, however long the excerpt runs */
  overflow: hidden; /* clips the image to the card's own rounded corners */
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  /* No fill — just the border and the image inside it carry the card,
     same restrained look as the rest of the site's boxed components. */
  background-color: transparent;
  color: inherit;
  text-decoration: none;
  transition: border-color var(--transition-fast);
}

.blog-card:hover,
.blog-card:focus-visible {
  border-color: var(--color-ink-soft);
}

.blog-card__image {
  aspect-ratio: 3 / 2;
  display: flex;
  align-items: center;
  justify-content: center;
  border-bottom: 1px solid var(--color-border);
  background-color: var(--color-paper-deep);
  color: var(--color-ink-soft);
  font-family: var(--font-secondary);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* Once a card has a real featured image, use <img class="blog-card__image">
   instead of the placeholder <div> — same class, so the aspect-ratio
   and border-bottom above still apply; these two rules just make an
   actual <img> fill that box instead of leaving it at its natural
   size (object-fit:cover crops rather than squashing/stretching). */
img.blog-card__image {
  width: 100%;
  object-fit: cover;
}

.blog-card__content {
  padding: var(--space-md) var(--space-md) var(--space-lg);
}

.blog-card__title {
  /* Top margin stands in for the removed .blog-card__source line
     (SEO Articles tab) — a blank line where the source text used to
     sit, rather than the title jumping up to close the gap. Bottom
     margin is a second full line, separating the title from
     .blog-card__excerpt below (was a tighter 0.4em). */
  margin: 1.3em 0 1.3em;
  font-family: var(--font-secondary);
  font-size: 1.05rem;
  line-height: 1.3;
  color: var(--color-ink);
}

.blog-card:hover .blog-card__title,
.blog-card:focus-visible .blog-card__title {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.blog-card__excerpt {
  margin: 0 0 var(--space-sm);
  color: var(--color-ink-soft);
  font-size: 0.88rem;
  /* Clamps to 2 lines with an ellipsis, so a short excerpt and a long
     one still leave every card in a row the same height — the
     -webkit- prefix is required, but every current browser (Chrome,
     Safari, Firefox) honours it even outside WebKit. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.blog-card__cta {
  margin-top: auto; /* pins the CTA to the card's bottom edge regardless of how tall the title/excerpt above it are */
  padding-top: var(--space-sm);
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-ink);
}


/* --- 28. BLOG CARD GRID --------------------------------------------------------
   Lays .blog-card items out three to a row (the way a real WordPress
   blog listing usually looks), narrowing to two and then one as the
   screen shrinks. Wrap any number of .blog-card links in this — one
   card just sits in the first column with room to its right for more.

   <div class="blog-card-grid">
     <a class="blog-card">…</a>
     <a class="blog-card">…</a>
   </div> */

.blog-card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-lg);
  align-items: stretch;
  margin-top: var(--space-lg);
}

@media (max-width: 900px) {
  .blog-card-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 640px) {
  .blog-card-grid {
    grid-template-columns: 1fr;
  }
}

/* Modifier for a denser grid (2 rows of 4) — used by Edited Works,
   where entries are site-level links rather than full article cards
   and don't need the extra width the 3-column default gives them. */
.blog-card-grid--four {
  grid-template-columns: repeat(4, 1fr);
}

@media (max-width: 900px) {
  .blog-card-grid--four {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 480px) {
  .blog-card-grid--four {
    grid-template-columns: 1fr;
  }
}


/* --- 29. LINK LIST --------------------------------------------------------------
   A compact alternative to .blog-card-grid for when there are too many
   links for full cards to make sense (a couple of dozen pieces for one
   client, say) — no image, just a plain divided list of titles. Pair
   it with a .tag (section 13) as a small label above each group:

   <span class="tag">Client Name</span>
   <ul class="link-list">
     <li><a href="https://example.com/post" target="_blank" rel="noopener">Post Title</a></li>
   </ul> */

.link-list {
  list-style: none;
  margin: var(--space-sm) 0 var(--space-xl);
  padding: 0;
  border-top: 1px solid var(--color-border);
}

.link-list li {
  border-bottom: 1px solid var(--color-border);
}

.link-list a {
  display: block;
  padding: 0.6em 0;
  color: var(--color-ink);
  text-decoration: none;
}

.link-list a:hover,
.link-list a:focus-visible {
  color: var(--color-ink-soft);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}


/* --- 30. NEXT VOLUME LINK -------------------------------------------------------
   Sits inside .book-footer's __row (section 16a) opposite "Read
   Previous Volume", arrow pointing forward instead of back. Only add
   this to a page once the NEXT volume in the fixed 8-book order
   actually exists — don't link to a page that isn't built yet.

   <a href="story.html" class="next-link">Read Next Volume</a> */

.next-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-family: var(--font-secondary);
  font-weight: 700;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-decoration: none;
  color: var(--color-ink-soft);
}

.next-link::after {
  content: "→";
}

.next-link:hover {
  color: var(--color-ink);
}


/* --- 31. IMAGE TABS -------------------------------------------------------------
   Tabbed sections whose tab titles ARE images (ornate title-plate PNGs,
   transparent background, ~1.41:1 ratio) rather than text — used by
   Writing and Editing in place of an accordion. Needs a little JS, see
   js/main.js.

   .image-tabs__list is a full-bleed strip — sits directly in <main>,
   outside any .container/padding, so it runs edge to edge like a
   shelf ledge rather than a boxed component (Kathryn's "Option 3, full
   width" pick from three tab-design mockups; this replaced an earlier
   version that filled the available width and scaled the plates up to
   320px, which read as a tall, thick block rather than a thin shelf).
   .image-tabs__panel can still live in a normal padded/contained
   section elsewhere in the page — main.js finds panels by id, not by
   DOM nesting under the list. Markup pattern:

   <div class="image-tabs__list" role="tablist" aria-label="…">
     <button type="button" class="image-tabs__tab" role="tab" id="tab-1"
             aria-selected="true" aria-controls="panel-1">
       <img src="…" alt="Section title">
     </button>
     <button type="button" class="image-tabs__tab" role="tab" id="tab-2"
             aria-selected="false" aria-controls="panel-2" tabindex="-1">
       <img src="…" alt="Section title">
     </button>
   </div>

   <div class="image-tabs">
     <div class="image-tabs__panel" id="panel-1" role="tabpanel" aria-labelledby="tab-1">
       Panel content…
     </div>
     <div class="image-tabs__panel" id="panel-2" role="tabpanel" aria-labelledby="tab-2" hidden>
       Panel content…
     </div>
   </div> */

.image-tabs__list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 100px;
  width: 100%;
  background-color: var(--color-ink-soft);
  padding: var(--space-sm) var(--page-gutter);
  box-sizing: border-box;
}

.image-tabs__tab {
  position: relative; /* so ::before below can position the divider against this tab specifically */
  flex: 0 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-bottom: 3px solid transparent;
  cursor: pointer;
  padding: 0 0 var(--space-xs);
  transition: border-color var(--transition-fast);
}

/* A light divider between tabs, centered in the 50px gap — skipped on
   the first tab since there's nothing to its left to separate from. */
.image-tabs__tab:not(:first-child)::before {
  content: "";
  position: absolute;
  left: -50px; /* centered in the 100px gap between tabs */
  top: 15%;
  bottom: 15%;
  width: 1px;
  background-color: rgba(244, 234, 217, 0.25);
}

.image-tabs__tab img {
  width: 100%;
  max-width: 228px; /* +20% over the original 190px */
  height: auto;
  display: block;
  transition: transform var(--transition-fast);
  /* These plates are dark line art on a transparent background, so
     against the dark shelf background they'd disappear — recolor the
     linework to the site's cream paper tone instead. Applied equally
     regardless of selected state, since every tab reads as the same
     cream color now — only size and the underline distinguish the
     active one. */
  filter: invert(1) sepia(0.2) saturate(150%) brightness(0.98);
}

.image-tabs__tab[aria-selected="true"] {
  border-bottom-color: var(--color-paper);
}

.image-tabs__tab[aria-selected="true"] img {
  transform: scale(1.1); /* the active tab's plate runs 10% bigger still — transform instead of a bigger max-width so it doesn't reflow neighboring tabs */
}

/* Plain-text stand-in for a tab's usual hand-drawn title-plate image —
   same .image-tabs__tab shell (dark shelf, underline-on-select all
   still apply automatically since those rules target the shared base
   class), just a text label instead of an <img> child until the real
   artwork exists. Swap the whole element for <img src="…" alt="…">
   once it does; no other markup needs to change.

   <button type="button" class="image-tabs__tab" role="tab" id="…" aria-selected="…" aria-controls="…">
     <span class="image-tabs__tab-label">Section Name</span>
   </button> */
.image-tabs__tab-label {
  font-family: var(--font-secondary);
  font-weight: 700;
  font-size: 1rem;
  letter-spacing: 0.04em;
  color: var(--color-paper);
  padding: 0 var(--space-xs);
  transition: transform var(--transition-fast);
}

.image-tabs__tab[aria-selected="true"] .image-tabs__tab-label {
  transform: scale(1.1);
}

.image-tabs__panel {
  padding-top: var(--space-2xl);
}

.image-tabs__panel[hidden] {
  display: none;
}

@media (max-width: 480px) {
  .image-tabs__list {
    gap: var(--space-md);
  }

  .image-tabs__tab img {
    max-width: 156px; /* +20% over the original 130px, matching the desktop increase */
  }
}

/* --- 31a. NESTED TEXT TABS (--sub / --text modifiers) ---------------------------
   For grouping content by, e.g., client site WITHIN one of the image
   tabs above (Writing and Editing's AI Editing panel groups its five
   client sites this way) — plain text labels instead of title-plate
   images, sitting inline in the panel's own padded column instead of
   the full-bleed dark shelf. Same .image-tabs__list/__tab/__panel
   classes and ARIA pattern as the top-level tabs (js/main.js already
   wires up every .image-tabs__list on the page independently, so this
   needed no script changes) — these two modifiers only override the
   look.

   <div class="image-tabs__list image-tabs__list--sub" role="tablist" aria-label="…">
     <button type="button" class="image-tabs__tab image-tabs__tab--text" role="tab" id="subtab-1" aria-selected="true" aria-controls="subpanel-1">Site Name</button>
     <button type="button" class="image-tabs__tab image-tabs__tab--text" role="tab" id="subtab-2" aria-selected="false" aria-controls="subpanel-2" tabindex="-1">Other Site</button>
   </div>
   <div class="image-tabs__panel" id="subpanel-1" role="tabpanel" aria-labelledby="subtab-1">…</div>
   <div class="image-tabs__panel" id="subpanel-2" role="tabpanel" aria-labelledby="subtab-2" hidden>…</div> */

.image-tabs__list--sub {
  width: auto;
  background-color: transparent;
  padding: 0;
  justify-content: flex-start;
  border-bottom: 1px solid var(--color-border);
  gap: var(--space-lg);
}

.image-tabs__tab--text {
  padding: 0 0 var(--space-sm);
  font-family: var(--font-secondary);
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-ink-soft);
}

.image-tabs__tab--text:hover {
  color: var(--color-ink);
}

.image-tabs__tab--text[aria-selected="true"] {
  color: var(--color-ink);
  border-bottom-color: var(--color-accent); /* overrides the dark-shelf tabs' cream underline — this variant sits on the page's own light background, where cream wouldn't show up */
}

@media (max-width: 480px) {
  .image-tabs__list--sub {
    gap: var(--space-md);
  }
}


/* --- 31b. CLIENT DOSSIER (SIDEBAR NAV + DETAIL) ----------------------------------
   Option C from the "AI Editing" client-tabs comparison artifact — a
   narrow name-rail down the left showing every client and its post
   count at once, with the selected client's real .link-list on the
   right. Replaces --sub/--text's horizontal strip for the AI Editing
   & Writing panel's five client sites. Still the exact same
   .image-tabs__list/__tab/__panel classes and ARIA pattern — js/main.js
   only cares about role=tab/aria-controls, not layout, so this needed
   no JS changes — just a two-column grid instead of a row above one
   panel.

   <div class="client-dossier">
     <div class="image-tabs__list image-tabs__list--sidebar" role="tablist" aria-label="…">
       <button type="button" class="image-tabs__tab image-tabs__tab--sidebar" role="tab" id="…" aria-selected="true" aria-controls="…">
         <span class="image-tabs__tab-name">Client Name</span>
         <span class="count-tag">N posts</span>
       </button>
       … tabindex="-1" on the rest …
     </div>
     <div class="client-dossier__detail">
       <div class="image-tabs__panel" id="…" role="tabpanel" aria-labelledby="…">
         <h4 class="client-dossier__name">Client Name</h4>
         <ul class="link-list">…</ul>
       </div>
       … one per client, all but the first hidden …
     </div>
   </div> */

.client-dossier {
  display: grid;
  grid-template-columns: 13rem 1fr;
  gap: 2rem;
  align-items: start;
  margin-top: var(--space-xl);
}

/* .image-tabs__panel's generic padding-top:var(--space-2xl) (40px) is
   meant for a panel sitting BELOW a tab strip — every subpanel here
   inherits that same class and was getting shoved 40px down from the
   top of .client-dossier__detail, while the rail beside it starts
   right at the top of .client-dossier. Zeroed here so both columns'
   content starts at the same line, like the artifact. */
.client-dossier__detail .image-tabs__panel {
  padding-top: 0;
}

.image-tabs__list--sidebar {
  width: auto;
  background: none;
  padding: 0;
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: stretch;
  justify-content: flex-start;
  gap: 0;
  border-left: 2px solid var(--color-border);
}

.image-tabs__tab--sidebar {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  gap: 0.15em;
  border-left: 2px solid transparent;
  border-bottom: none;
  margin-left: -2px;
  padding: 0.6rem 0 0.6rem 1rem;
  text-align: left;
  font-family: var(--font-body); /* plain body mono, matching the artifact's nav (font: inherit off its mono body) — not the serif heading font used elsewhere on this rail's detail heading */
  font-size: 0.88rem;
  color: var(--color-ink-soft);
}

.image-tabs__tab--sidebar:not(:first-child)::before {
  display: none; /* suppresses the base tab's horizontal-shelf divider (absolutely positioned for a 100px row gap) — meaningless in this vertical layout */
}

.image-tabs__tab--sidebar:hover {
  color: var(--color-ink);
}

.image-tabs__tab--sidebar[aria-selected="true"] {
  color: var(--color-ink);
  font-weight: 700;
  border-left-color: var(--color-accent-deep);
}

.image-tabs__tab--sidebar .count-tag {
  font-family: var(--font-body);
  font-size: 0.68rem;
  font-weight: 400;
  color: var(--color-ink-soft);
  opacity: 0.75;
}

.client-dossier__name {
  font-family: var(--font-secondary);
  font-weight: 700;
  font-size: 1.15rem;
  margin: 0 0 var(--space-xs);
  /* base.css's global h4 rule (uppercase, letter-spacing) doesn't match
     the artifact this was built from — that used a plain title-case
     serif heading, not small-caps. Cancel it here specifically rather
     than touch the shared h4 rule other pages rely on. */
  text-transform: none;
  letter-spacing: normal;
}

@media (max-width: 640px) {
  .client-dossier {
    grid-template-columns: 1fr;
  }

  .image-tabs__list--sidebar {
    flex-direction: row;
    flex-wrap: wrap;
    border-left: none;
    border-bottom: 1px solid var(--color-border);
    padding-bottom: var(--space-xs);
    gap: var(--space-sm) var(--space-md);
  }

  .image-tabs__tab--sidebar {
    border-left: none;
    margin-left: 0;
    border-bottom: 2px solid transparent;
    padding: 0 0 var(--space-xs);
  }

  .image-tabs__tab--sidebar[aria-selected="true"] {
    border-bottom-color: var(--color-accent-deep);
  }
}


/* --- 32. TABLE OF CONTENTS -------------------------------------------------------
   An old-book-style contents page: numbered entries, a dotted leader
   running from each title out to an arrow, jumping to the matching
   in-page heading. Sits in a .page-frame (section 6) for the
   double-ruled "block of reading content" look. Markup pattern:

   <div class="page-frame toc">
     <p class="toc__label">Table of Contents</p>
     <div class="toc__rule" aria-hidden="true"></div>
     <ol class="toc__list">
       <li><a href="#profile" class="toc__row">
         <span class="toc__num">01</span>
         <span class="toc__title">Personal Profile</span>
         <span class="toc__leader" aria-hidden="true"></span>
         <span class="toc__arrow" aria-hidden="true">→</span>
       </a></li>
     </ol>
   </div>

   Every linked heading needs a matching id (h1-h4 already carry
   scroll-margin-top globally, base.css, so the jump doesn't land the
   heading flush against the viewport edge). */

.toc__label {
  margin: 0 0 var(--space-md);
  font-family: var(--font-secondary);
  font-size: 1.6rem;
  font-weight: 700;
  text-align: center;
  color: var(--color-ink);
}

.toc__rule {
  display: flex;
  align-items: center;
  gap: 0.6em;
  max-width: 220px;
  margin: 0 auto var(--space-lg);
  color: var(--color-accent);
}

.toc__rule::before,
.toc__rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background-color: var(--color-sepia-light);
}

.toc__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.toc__row {
  display: flex;
  align-items: baseline;
  gap: 0.6em;
  padding: 0.5em 0;
  text-decoration: none;
  color: var(--color-ink);
}

.toc__num {
  flex-shrink: 0;
  min-width: 1.6em;
  font-family: var(--font-secondary);
  font-size: 0.8rem;
  color: var(--color-accent);
}

.toc__title {
  flex-shrink: 0;
  font-family: var(--font-secondary);
  white-space: nowrap;
}

.toc__leader {
  flex: 1 1 auto;
  margin-bottom: 0.3em;
  border-bottom: 1px dotted var(--color-sepia-light);
  transition: border-color var(--transition-fast);
}

.toc__arrow {
  flex-shrink: 0;
  color: var(--color-ink-soft);
  transition: transform var(--transition-fast), color var(--transition-fast);
}

.toc__row:hover .toc__leader,
.toc__row:focus-visible .toc__leader {
  border-bottom-color: var(--color-accent);
}

.toc__row:hover .toc__arrow,
.toc__row:focus-visible .toc__arrow {
  color: var(--color-accent);
  transform: translateX(3px);
}

@media (max-width: 640px) {
  .toc__title {
    white-space: normal;
  }
}


/* --- 33. CHAPTER HEADER -----------------------------------------------------------
   A decorative banner for opening a chapter — the illustrated dagger
   art (images/chapter header.png), running the full width of the
   container, with the chapter label, title AND the opening paragraph
   nested together on top of it, tucked up near the crossguard/blade
   and to the right of the apple-and-rose column, per Kathryn's
   reference. The art is a transparent PNG, so the blank ~23% at the
   bottom of its bounding box shows the page's own cream background
   through it — nothing to paint there, the overlay is free to run
   past the visible linework. Label keeps the same small-caps
   treatment as .text-box__subtext elsewhere on the site; the title
   and paragraph are normal elements so they stay real, readable/
   selectable text rather than being baked into the image. Used on
   Chapters 1, 3 and 5 of The Story So Far — all non-reversed odd
   chapters. The art is asymmetric (all the ornament sits top-left,
   tapering off to the right), so it can't just be flipped for
   even-numbered/reversed chapters without a mirror image; that's still
   to be figured out. Markup:

   <div class="chapter-header">
     <div class="chapter-header__art-frame">
       <img src="../images/chapter header.png" alt="" class="chapter-header__art">
     </div>
     <div class="chapter-header__overlay">
       <p class="chapter-header__label">Chapter 01</p>
       <h2 class="chapter-header__title">Chapter Title</h2>
       <p class="text-box__body drop-cap chapter-header__intro">Opening paragraph…</p>
     </div>
   </div> */

/* Full-bleed section (outside .container), but still needs the same
   edge margin every other section on the site gets (var(--page-gutter),
   defined in variables.css) — nothing should touch the true viewport
   edge with zero margin just because it happens to live outside
   .container. Sized via width/margin rather than padding, since
   percentage margins on the overlay below are resolved against the
   padding box — padding here would let it ignore the gutter it's
   meant to respect. */
.chapter-header {
  width: calc(100% - 2 * var(--page-gutter));
  margin: 0 var(--page-gutter) var(--space-md);
}

/* The source art (images/chapter header.png) has a large fully-
   transparent margin along its bottom (~23%) AND its left (~11%) —
   cropping both out instead of showing the art at its full native
   canvas removes that dead space instead of just leaving it blank on
   the page, and pulls the actual linework further left/down into the
   frame. Sized to 70% of the container (left-anchored, since a block
   element narrower than its parent sits at the left by default) so it
   reads as an illustration tucked into the corner rather than a
   full-bleed banner. overflow:hidden on the frame + an oversized,
   right/top-anchored inner <img> is what performs the crop — a single
   object-fit only crops one axis, and here both need it. Stays in
   normal flow (not absolutely positioned within .chapter-header) so it
   always contributes its own real height to the page. */
.chapter-header__art-frame {
  width: 70%;
  aspect-ratio: 1630 / 1000;
  overflow: hidden;
  position: relative;
}

.chapter-header__art {
  position: absolute;
  top: 0;
  right: 0;
  width: 112.2%;
  max-width: none; /* overrides the global img{max-width:100%} rule, which was clamping this back down and silently cancelling the crop */
  height: auto;
  display: block;
}

/* Pulled UP over the art with a negative margin instead of being
   absolutely positioned on top of it — that earlier approach placed
   the overlay at a fixed top:49% of .chapter-header's height, but
   .chapter-header's height was itself only ever the art-frame's
   height (an absolutely-positioned child contributes no height to its
   parent), so any overlay content taller than "49% of the art" had
   nowhere accounted-for to go: it just spilled past .chapter-header's
   bottom edge and overlapped whatever section came next. A chapter
   with one short paragraph (Chapter 1) never revealed this; two
   paragraphs, or a paragraph plus a pull-quote (Chapters 3 and 5),
   did.

   Normal flow fixes it at the source: the overlay is a real sibling
   after .chapter-header__art-frame, pulled up by a negative margin
   sized to land its first line at the same visual spot (49% down the
   art) as before, but its ACTUAL height — however much text it ends
   up holding — now correctly pushes down whatever comes after it, at
   any width, with no per-chapter tuning needed.

   The negative margin is a percentage because CSS resolves vertical
   margins against the containing block's WIDTH, not height — so this
   still has to be re-derived in terms of .chapter-header's width, not
   simply "-51% of the art-frame's height":
     art-frame height  = chapter-header width × 0.70 × (1000/1630)
     pull-up distance  = art-frame height × (1 − 0.49) = × 0.51
     as % of chapter-header width = 0.70 × 0.6135 × 0.51 ≈ 21.9% */
.chapter-header__overlay {
  margin-top: -21.9%;
  margin-left: 17%;
}

.chapter-header__label {
  margin: 0 0 0.2em;
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-ink-soft);
}

.chapter-header__title {
  margin: 0 0 var(--space-xl);
}

.chapter-header__intro {
  margin: 0;
  max-width: none; /* overrides the global p{max-width:38em} readability cap — this needs to use the overlay's full width, out to the right edge */
}

.chapter-header__overlay .pull-quote {
  max-width: none;
}

/* Below tablet, the overlaid text has too little width to stay short
   enough to fit inside the art's height — it would run on and
   overlap the art itself. Drop it back below the art entirely instead
   of pulling it up on top. */
@media (max-width: 640px) {
  .chapter-header__overlay {
    margin-top: var(--space-md);
    margin-left: 0;
  }
}


/* --- 34. DROP CAP -----------------------------------------------------------------
   A large decorative first letter for the opening paragraph of a
   chapter, like a printed book. Add the class to the <p> itself —
   ::first-letter handles the rest, no need to wrap the letter in its
   own span. Markup:

   <p class="text-box__body drop-cap">Lorem ipsum…</p> */

.drop-cap::first-letter {
  float: left;
  font-family: var(--font-secondary);
  font-weight: 700;
  /* Scales down with viewport width instead of staying fixed — at
     4.8rem flat, the cap towers over the much narrower line of text
     it sits beside on small screens. Caps at 4.8rem (~3 lines of body
     text at the 1.7 line-height) on desktop. */
  font-size: clamp(2.4rem, 9vw, 4.8rem);
  line-height: 1;
  padding-right: 0.5rem;
  /* Playfair Display carries extra internal leading above its
     cap-height even at line-height:1, so the glyph sits visibly below
     the top of the first line instead of level with it — nudge it up
     to compensate. */
  margin-top: -0.15em;
  color: var(--color-ink);
}


/* --- 35. OPEN LETTER -----------------------------------------------------------
   The envelope (images/envelope.png) that introduces The Story So Far.
   Now a button rather than a static image beside always-visible text:
   clicking it opens the note as a lightbox (section 44) instead.
   Added once the Prologue above became the page's real opening
   narrative — the short bio note reads better now as something a
   reader can choose to open than as more always-visible text
   competing with it.

   Markup:
   <div class="open-letter">
     <button type="button" class="open-letter__trigger" data-lightbox-open="open-letter-lightbox" aria-label="Open a short note from Kathryn">
       <img src="../images/envelope.png" alt="" class="open-letter__envelope">
     </button>
     <p class="open-letter__prompt">Click the envelope for a short note from Kathryn</p>
   </div>
   …with the note's own content living in the matching .lightbox
   markup elsewhere on the page (section 44 shows that shape). */

.open-letter {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.open-letter__trigger {
  display: block;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  transition: transform var(--transition-fast);
}

.open-letter__trigger:hover,
.open-letter__trigger:focus-visible {
  transform: scale(1.04);
}

.open-letter__envelope {
  width: 100%;
  max-width: 380px;
  height: auto;
  display: block;
}

.open-letter__prompt {
  margin: var(--space-md) 0 0;
  font-family: var(--font-secondary);
  font-style: italic;
  font-size: 0.85rem;
  color: var(--color-ink-soft);
}

@media (max-width: 640px) {
  .open-letter__envelope {
    max-width: 280px;
  }
}


/* --- 44. LIGHTBOX -----------------------------------------------------------------
   Generic reusable modal — a dimmed full-viewport backdrop plus a
   centered paper panel. Not tied to the open letter specifically: any
   trigger with data-lightbox-open="some-id" opens the .lightbox with
   that id, and js/main.js wires up however many .lightbox elements a
   page has the same generic way (same pattern as .image-tabs__list —
   markup drives it, no per-instance JS needed). Closes on the
   backdrop, the × button, or Escape.

   The panel unfolds open (scaleY from a flat sliver up to full height)
   and folds back closed the same way in reverse — reads as the letter
   itself unfolding/refolding, which is the effect Kathryn asked for on
   the open letter specifically, but it's on the generic component so
   any future lightbox gets it too. [hidden] can't be transitioned (it's
   display:none), so js/main.js adds/removes .lightbox--visible a frame
   after toggling [hidden] on open, and waits out this same transition
   duration before setting [hidden] back on close — the two must stay
   in sync if this duration changes.

   Markup:
   <button type="button" data-lightbox-open="my-lightbox">Open</button>

   <div class="lightbox" id="my-lightbox" hidden>
     <div class="lightbox__backdrop" data-lightbox-close></div>
     <div class="lightbox__panel" role="dialog" aria-modal="true" aria-labelledby="my-lightbox-title" tabindex="-1">
       <button type="button" class="lightbox__close" data-lightbox-close aria-label="Close">&times;</button>
       …content, with some element carrying id="my-lightbox-title"…
     </div>
   </div> */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--page-gutter);
}

.lightbox__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(32, 31, 43, 0.75);
  opacity: 0;
  transition: opacity 0.3s ease;
}

.lightbox__panel {
  position: relative;
  max-width: 38rem;
  max-height: 85vh;
  overflow-y: auto;
  background: var(--color-paper);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-page);
  padding: var(--space-xl) var(--space-lg);
  outline: none; /* focused programmatically on open so screen readers land here, not because a visible ring is needed on the whole panel */
  /* Unfolds via clip-path (masking the box open from a thin center
     band) rather than scaleY — an earlier version scaled the whole
     panel, which squashed and stretched the letter text itself while
     animating and just looked broken. clip-path reveals the same box
     at its real, undistorted size the whole time. */
  clip-path: inset(50% 0 50% 0);
  opacity: 0;
  transition: clip-path 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
}

.lightbox--visible .lightbox__backdrop {
  opacity: 1;
}

.lightbox--visible .lightbox__panel {
  clip-path: inset(0 0 0 0);
  opacity: 1;
}

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

.lightbox__close {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
  width: 2rem;
  height: 2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  font-size: 1.4rem;
  line-height: 1;
  color: var(--color-ink-soft);
  cursor: pointer;
}

.lightbox__close:hover,
.lightbox__close:focus-visible {
  color: var(--color-ink);
}

@media (max-width: 480px) {
  .lightbox__panel {
    padding: var(--space-lg) var(--space-md);
  }
}

/* Video variant — a plain wide panel sized for comfortable viewing
   instead of the 38rem letter-reading width. Used by the moodboard's
   video tiles (section 48): each tile is a poster image that opens
   one of these instead of playing inline, so the tile itself can be
   sized freely without an <video controls> element dictating its
   proportions. Same folded-paper texture (images/background
   folded.jpg) as the other textured lightboxes below, framing the
   video like a printed photo rather than filling the panel edge to
   edge — that's why this needs real padding instead of the 0 a plain
   dark backdrop could get away with. */
/* Wide variant — for a lightbox holding a stack of full-width
   components (pill-cloud, rolling-bar, icon-box-row) rather than a
   letter's worth of prose. First used by The Art of Websites and
   SEO's #seo-experience-lightbox. */
.lightbox--wide .lightbox__panel {
  max-width: 52rem;
  width: 100%;
}

.lightbox--video .lightbox__panel {
  max-width: 56rem;
  width: 100%;
  padding: var(--space-md);
  background-image: url("../images/background folded.jpg");
  background-size: cover;
  background-position: center;
}

.lightbox--video video {
  display: block;
  width: 100%;
  max-height: 75vh;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
}

/* A calligraphy salutation/signature line for a letter-style lightbox
   — sized between .text-box__heading--display (2.2rem, too heavy for
   a single line inside a modest 38rem panel) and body text. Not a
   generic .lightbox part; only used where the content itself is a
   letter, e.g. #open-letter-lightbox. */
.lightbox__signature {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: 0.03em;
  word-spacing: 0.25em;
  font-size: 1.8rem;
  line-height: 1.5;
  margin: 0 0 var(--space-md);
  color: var(--color-ink);
}

/* The open letter specifically reads as a physical letter, so its
   lightbox panel gets the same folded-paper texture used on Collected
   Works (images/background folded.jpg) — tried on the page section
   around the envelope first, but Kathryn wanted it on the letter
   itself instead, not the section that holds the envelope. */
#open-letter-lightbox .lightbox__panel,
#contact-lightbox .lightbox__panel,
#case-studies-note-lightbox .lightbox__panel,
#rocknco-lightbox .lightbox__panel,
#counselling-course-lightbox .lightbox__panel,
#working-with-children-lightbox .lightbox__panel,
#art-therapy-lightbox .lightbox__panel,
#aerial-arts-lightbox .lightbox__panel,
#rocknco-photography-lightbox .lightbox__panel,
#fairy-makeup-lightbox .lightbox__panel,
#book-of-creatives-lightbox .lightbox__panel,
#dried-up-lightbox .lightbox__panel,
#human-nature-font-lightbox .lightbox__panel,
#human-nature-drawings-lightbox .lightbox__panel,
#ink-heart-lightbox .lightbox__panel,
#skull-string-art-lightbox .lightbox__panel,
#tactile-typography-lightbox .lightbox__panel,
#beneath-the-surface-lightbox .lightbox__panel,
#bubble-planet-lightbox .lightbox__panel {
  background-image: url("../images/background folded.jpg");
  background-size: cover;
  background-position: center;
}


/* --- 45. READ MORE TOGGLE ----------------------------------------------------
   Expand/collapse for long-form text that's too long to show in full
   by default — first used on the Prologue, which ran to seven
   paragraphs. Same aria-expanded/aria-controls/hidden mechanism as the
   accordion (section 14), but wired separately in js/main.js and
   styled as a plain inline text link rather than a bordered accordion
   row, since this sits mid-paragraph rather than as its own header.

   Markup:
   <p>…visible text…</p>
   <button type="button" class="read-more-trigger" aria-expanded="false" aria-controls="some-id" data-less-label="Close the Story">
     <span class="read-more-trigger__label">Continue the Story</span>
     <span class="read-more-trigger__arrow" aria-hidden="true">↓</span>
   </button>
   <div id="some-id" hidden>…more text…</div> */

.read-more-trigger {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
  margin-top: var(--space-md);
  background: none;
  border: none;
  padding: 0;
  font-family: var(--font-secondary);
  font-style: italic;
  font-size: 1.2rem;
  color: var(--color-accent-deep);
  cursor: pointer;
}

.read-more-trigger__label {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.read-more-trigger__arrow {
  font-family: Arial, sans-serif; /* Playfair Display renders the ↓/↑ glyph as a tiny, barely-visible mark at this size — a plain system sans draws a proper arrow */
  font-style: normal;
  font-size: 1.4rem;
  line-height: 1;
}

.read-more-trigger:hover,
.read-more-trigger:focus-visible {
  color: var(--color-ink);
}

/* Modifier: renders the trigger as an actual button (paper-bordered,
   for a dark panel like .text-box--dark) instead of the plain italic
   underlined link above — same toggle behavior/JS, different chrome.
   Defined after .read-more-trigger so it wins on the properties both
   set. */
.read-more-trigger--btn {
  display: inline-block;
  margin-top: var(--space-md);
  padding: 0.65em 1.6em;
  border: 1.5px solid var(--color-paper);
  border-radius: var(--radius-sm);
  background-color: transparent;
  color: var(--color-paper);
  font-family: var(--font-secondary);
  font-style: normal;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  transition: background-color var(--transition-fast), color var(--transition-fast);
}

.read-more-trigger--btn .read-more-trigger__label {
  text-decoration: none;
}

.read-more-trigger--btn:hover,
.read-more-trigger--btn:focus-visible {
  background-color: var(--color-paper);
  color: var(--color-charcoal);
}

.read-more-content {
  margin-top: var(--space-md);
}


/* --- 36. PREV VOLUME LINK -------------------------------------------------------
   Sits inside .book-footer's __row (section 16a) opposite "Read Next
   Volume", arrow pointing back instead of forward. Only add this to a
   page once the PREVIOUS volume in the fixed 8-book order actually
   exists (so never on Vol. I). Includes the target volume's actual
   title, not just "Previous", so it's clear which title you'd land on.

   <a href="resume.html" class="prev-link">Resume and Experience</a> */

.prev-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-family: var(--font-secondary);
  font-weight: 700;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-decoration: none;
  color: var(--color-ink-soft);
}

.prev-link::before {
  content: "←";
}

.prev-link:hover {
  color: var(--color-ink);
}


/* --- 37. WRITING ARCHIVE (PINTEREST BOARD) ---------------------------------------
   Pinterest's official board-embed widget (pinit.js), sitting directly
   on the normal page background — no card/panel treatment, full width
   — currently used at the bottom of Writing and Editing's SEO Articles
   tab, but built generic/reusable so it can be dropped into another
   page later. Heading/description/CTA all reuse existing typography
   so only the embed itself is foreign markup. Add pinit.js ONCE per
   page, anywhere in the body — it scans the whole page for
   data-pin-do elements itself, so it doesn't matter whether the
   script tag comes before or after this markup. data-pin-board-width
   is a base size Pinterest's widget needs to lay itself out at all
   (omitting it renders nothing); data-pin-scale-width="100" is what
   actually stretches it to fill its container instead of sitting at
   that fixed base size. Markup:

   <section class="writing-archive">
     <h3 style="text-align:center;">Pinterest Board: Writing Archive</h3>
     <p class="text-box__body" style="text-align:center; max-width:34rem; margin:0 auto var(--space-lg);">Short description…</p>
     <div class="writing-archive__embed">
       <a data-pin-do="embedBoard" data-pin-board-width="1100"
          data-pin-scale-height="240" data-pin-scale-width="100"
          href="https://www.pinterest.com/kathryndonoghue/writing-portfolio/"></a>
     </div>
     <p style="text-align:center; margin-top:var(--space-md);">
       <a href="https://www.pinterest.com/kathryndonoghue/writing-portfolio/"
          class="writing-archive__cta" target="_blank" rel="noopener">Browse the full collection →</a>
     </p>
   </section>

   <script async defer src="https://assets.pinterest.com/js/pinit.js"></script> */

.writing-archive {
  margin-top: var(--space-2xl);
  padding: 0 var(--page-gutter); /* standard site edge gutter — this section has no .container wrapper to provide it */
}

.writing-archive__embed {
  margin: var(--space-lg) auto;
  width: 100%;
  max-width: 1400px; /* matches data-pin-board-width so the widget renders at full size instead of shrinking to fit a narrower default */
  display: flex;
  justify-content: center; /* Pinterest's own iframe replaces the <a> with a fixed inline width and no centering of its own — without this it hugs the left edge */
}

.writing-archive__cta {
  display: inline-block;
  margin-top: var(--space-md);
  font-family: var(--font-secondary);
  color: var(--color-accent);
  text-decoration: none;
}

.writing-archive__cta:hover {
  color: var(--color-accent-deep);
  text-decoration: underline;
}


/* --- 38. ILLUSTRATED FRAME (COMPARISON OPTION) -----------------------------------
   An alternative to .page-frame (section 6) for framing .toc content —
   uses the hand-drawn border art (images/frame.png) as a background
   instead of the plain double-ruled box. Currently sitting side by
   side with the original .page-frame version on the Resume page
   purely so Kathryn can compare the two; remove whichever one doesn't
   get kept once she decides.

   The source art is portrait (2480×3508, ratio ≈ 0.707). Rather than
   stretching it to match however tall the content happens to be
   (which distorts the vine art), this box is locked to that exact
   aspect ratio via aspect-ratio, so background-size:100% 100% never
   has to stretch non-uniformly — content is centered inside instead,
   so it fits the frame rather than the frame being warped to fit it.
   Generous padding keeps it well clear of the vines. Narrower than
   the plain .page-frame version because a wide box at this aspect
   ratio would be impractically tall.

   <div class="toc toc-illustrated-frame">…the same .toc__label / .toc__list content as .page-frame.toc…</div> */

.toc-illustrated-frame {
  position: relative;
  max-width: 34rem;
  margin: 0 auto;
  aspect-ratio: 2480 / 3508;
  box-sizing: border-box;
  padding: 4.5rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  background-image: url("../images/frame.png");
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

@media (max-width: 640px) {
  .toc-illustrated-frame {
    max-width: 22rem;
    padding: 3.5rem 3rem;
  }
}

/* --- 38b. ILLUSTRATED FRAME, HORIZONTAL (OPTION C) -------------------------------
   Same idea as .toc-illustrated-frame, but using images/frame-horizontal.png
   (frame.png pre-rotated 90° as its own file, made once with a Python/
   Pillow one-off).

   Originally built with border-image so the frame could grow to fit
   whatever content height (see git history) — but frame-horizontal.png's
   detail sits almost entirely in the corners, with only a sparse
   connecting line along the straight edges. Any border-image width
   that made the corners read cleanly stretched that sparse edge line
   into a smeared, illegible mess (the same failure mode #book-info-panel
   hit on the homepage with frame ornate.png, see index.html). Switched
   to that same fix: use the frame as ONE whole background image,
   uniformly scaled by locking the box to the source art's own aspect
   ratio (3508:2480 — frame.png's 2480:3508 rotated 90°) so
   background-size:100% 100% never stretches non-uniformly. Padding is
   sized so the 7-row Resume TOC sits inside the frame's actual inner
   window at this width; if a future page uses this with taller/shorter
   content, that's a real gap to watch (this trades "flexible height,
   distorted art" for "fixed height, correct art").

   <div class="toc toc-illustrated-frame--horizontal">…the same .toc__label / .toc__list content…</div> */

.toc-illustrated-frame--horizontal {
  position: relative;
  width: 100%;
  max-width: 68rem;
  margin: 0 auto;
  box-sizing: border-box;
  aspect-ratio: 3508 / 2480;
  background-image: url("../images/frame-horizontal.png");
  background-size: 100% 100%;
  background-repeat: no-repeat;
  padding: 9% 13% 11%;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* A plain 20px border sitting immediately around the TOC content,
   inside the vine frame's own clearance — a second, functional frame
   between the text and the ornate floral one, per Kathryn's request.
   Transparent per her follow-up (matches the page background instead
   of reading as a visible ink-toned ring) — it's pure spacing now,
   not a decorative line. Its own padding keeps the text from touching
   that border directly. */
.toc-illustrated-frame__inner {
  border: 20px solid transparent;
  padding: var(--space-md);
  box-sizing: border-box;
}

@media (max-width: 640px) {
  .toc-illustrated-frame--horizontal {
    max-width: 26rem;
    aspect-ratio: auto;
    background-image: none;
    border: 1px solid var(--color-ink-soft);
    padding: 2rem 1.5rem;
  }

  .toc-illustrated-frame__inner {
    border-width: 10px;
  }
}


/* --- 39. TIMELINE NAV -------------------------------------------------------------
   A clickable horizontal timeline used as a chapter-jump opener in
   place of a text .toc — same idea as the reference "HISTORY
   TIMELINE" mockup (dated points strung along a line, each with a
   photo), but in the site's own plain-cream/ink language instead of
   the reference's dark sepia-photo background, and with the reference's
   layout flipped: photo on top, dotted marker + line, then year/label
   below (the reference has the date above and the photo below).
   Currently used on A History of Yoga and Aerial in place of a
   Table of Contents. Items are plain links (<a href="#id">), same
   anchor-jump pattern as .toc__row — no JS needed. Markup:

   <div class="timeline-nav">
     <a href="#realyouspace" class="timeline-nav__item">
       <div class="photo-placeholder timeline-nav__photo">Photo placeholder</div>
       <span class="timeline-nav__marker-row" aria-hidden="true"><span class="timeline-nav__marker">+</span></span>
       <span class="timeline-nav__year">2026 – Present</span>
       <span class="timeline-nav__label">Real You Space</span>
     </a>
     <!-- repeat per chapter — no gap between items, so each item's own
          half-line segments (marker-row ::before/::after) touch the
          next item's and read as one continuous line across the row -->
   </div> */

.timeline-nav {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
}

.timeline-nav__item {
  flex: 1 1 200px; /* +25% over the original 160px, per Kathryn's request */
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  text-decoration: none;
  color: var(--color-ink);
  padding: 0 var(--space-sm);
}

.timeline-nav__photo {
  width: 100%;
  max-width: 270px; /* +25% over the original 216px */
  aspect-ratio: 4 / 3; /* matches .photo-placeholder's own default, so a real <img> using this class lines up with the other still-placeholder items */
  object-fit: cover;
  display: block;
  margin: 0 0 var(--space-md);
  transition: opacity var(--transition-fast);
}

.timeline-nav__item:hover .timeline-nav__photo {
  opacity: 0.8;
}

.timeline-nav__marker-row {
  display: flex;
  align-items: center;
  width: 100%;
  margin-bottom: var(--space-sm);
}

.timeline-nav__marker-row::before,
.timeline-nav__marker-row::after {
  content: "";
  flex: 1;
  height: 1px;
  background-color: var(--color-border);
}

.timeline-nav__marker {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.8em;
  height: 1.8em;
  margin: 0 0.4em;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background-color: var(--color-paper);
  color: var(--color-accent);
  font-size: 0.9rem;
  transition: background-color var(--transition-fast), color var(--transition-fast);
}

.timeline-nav__item:hover .timeline-nav__marker {
  background-color: var(--color-accent);
  color: var(--color-paper);
}

.timeline-nav__year {
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-ink-soft);
}

.timeline-nav__label {
  font-family: var(--font-secondary);
  font-weight: 700;
  font-size: 1rem;
  margin-top: 0.2em;
}

.timeline-nav__text {
  display: flex;
  flex-direction: column;
}

@media (max-width: 640px) {
  .timeline-nav {
    flex-direction: column;
  }
  .timeline-nav__item {
    flex-direction: row;
    text-align: left;
    align-items: center;
    padding: var(--space-sm) 0;
  }
  .timeline-nav__photo {
    max-width: 126px; /* +25% over the original 101px */
    margin: 0 var(--space-md) 0 0;
  }
  .timeline-nav__marker-row {
    display: none; /* the connecting line reads as a row-spanning device on desktop; stacked vertically it has no clean equivalent, so it's dropped rather than faked */
  }
}


/* --- 40a. VIDEO EMBED -------------------------------------------------------------
   A real, self-hosted video (native <video controls>) — used in place
   of the Instagram embed once actual footage exists, since Instagram's
   widget only offers a click-through preview, not true inline
   playback (see the .instagram-post-placeholder note below, kept for
   any chapter that doesn't have real footage yet). No fixed
   aspect-ratio here deliberately — height:auto lets the browser use
   the file's own real intrinsic ratio, whatever it is, so nothing
   ever gets stretched. Markup:

   <video class="video-embed" controls preload="metadata">
     <source src="../videos/example.mp4" type="video/mp4">
   </video> */

.video-embed {
  width: 100%;
  height: auto;
  display: block;
  margin: var(--space-md) 0 var(--space-sm);
  background-color: var(--color-ink);
}

/* A real YouTube embed (youtube-nocookie.com, the privacy-enhanced
   domain) rather than just linking out — aspect-ratio:16/9 keeps a
   proper widescreen frame at any container width without the old
   padding-bottom-percentage hack. Markup:

   <iframe class="youtube-embed" src="https://www.youtube-nocookie.com/embed/VIDEO_ID" title="…" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> */
.youtube-embed {
  width: 100%;
  aspect-ratio: 16 / 9;
  display: block;
  margin: var(--space-md) 0 var(--space-sm);
  border: none;
}


/* --- 40. INSTAGRAM POST PLACEHOLDER -----------------------------------------------
   A stand-in for Instagram's official post embed (blockquote.instagram-media
   + //www.instagram.com/embed.js) — same "not real content yet"
   language as .photo-placeholder / .video-placeholder. Swap for the
   real embed once a post URL exists:

   <blockquote class="instagram-media" data-instgrm-permalink="https://www.instagram.com/p/POST_ID/" data-instgrm-version="14"></blockquote>

   ...with <script async src="//www.instagram.com/embed.js"></script>
   once per page (anywhere in the body — it scans the whole page). */

.instagram-post-placeholder {
  aspect-ratio: 9 / 16;
  max-width: 320px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  margin: var(--space-md) 0 var(--space-sm);
  border: 1px dashed var(--color-border);
  background-color: var(--color-paper-deep);
  color: var(--color-ink-soft);
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* Instagram's embed.js hard-codes an inline min-width:326px on the
   rendered iframe (and the blockquote before it's processed) —
   wider than several of the columns this sits in (e.g.
   .chapter-layout__media caps at 320px), so it was overflowing its
   container instead of shrinking to fit. Inline styles normally beat
   a stylesheet, so this needs !important to actually win. */
.instagram-media {
  min-width: 0 !important;
  max-width: 100% !important;
}


/* --- 41. IMAGE GALLERY -------------------------------------------------------------
   A small arrow-navigated slideshow for a handful of real photos (e.g.
   several shots of one performance) — one image visible at a time,
   prev/next arrows cycle through them. Needs a little JS, see
   js/main.js. Set the viewport's aspect-ratio inline to match
   whatever the source photos actually are (square, portrait, etc.) —
   there's no default since that varies per use. Markup:

   <div class="image-gallery">
     <div class="image-gallery__viewport" style="aspect-ratio: 1;">
       <img src="…" class="image-gallery__slide" alt="…">
       <img src="…" class="image-gallery__slide" alt="…" hidden>
       <img src="…" class="image-gallery__slide" alt="…" hidden>
     </div>
     <button type="button" class="image-gallery__arrow image-gallery__arrow--prev" aria-label="Previous image">←</button>
     <button type="button" class="image-gallery__arrow image-gallery__arrow--next" aria-label="Next image">→</button>
   </div> */

.image-gallery {
  position: relative;
}

.image-gallery__viewport {
  position: relative;
  overflow: hidden;
  background-color: var(--color-paper-deep);
}

.image-gallery__slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.image-gallery__slide[hidden] {
  display: none;
}

.image-gallery__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2em;
  height: 2em;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background-color: var(--color-paper);
  color: var(--color-ink);
  cursor: pointer;
  font-size: 1rem;
}

.image-gallery__arrow:hover {
  background-color: var(--color-accent);
  color: var(--color-paper);
  border-color: var(--color-accent);
}

.image-gallery__arrow--prev {
  left: var(--space-sm);
}

.image-gallery__arrow--next {
  right: var(--space-sm);
}


/* --- 42. POLAROID FIELD -----------------------------------------------------------
   A scattered, overlapping collage of "snapshot" cards for Collected
   Works' gallery opener — each a white polaroid-style border (thicker
   at the bottom) with a caption in that bottom strip, set in
   --font-display (Homemade Apple) for a handwritten feel distinct
   from the rest of the page's typography.

   Each .polaroid is a real link, positioned with its own inline
   top/left/width/height/transform (rotation + scatter varies per
   photo, so there's nothing to share in a class) — same one-off-inline
   convention already used for chapter-header art placement elsewhere.
   Swap the inner .photo-placeholder for a real <img> once photos
   exist; nothing else about the markup needs to change.

   <div class="polaroid-field">
     <a href="#" class="polaroid" style="top:0; left:40px; width:190px; height:230px; transform:rotate(-7deg);">
       <div class="photo-placeholder">Photo placeholder</div>
       <p class="polaroid__caption">Caption placeholder</p>
     </a>
     …
   </div>

   Add the --wide modifier to fill a full .container-wide measure
   instead of the default narrower field — Collected Works scales
   every polaroid's own inline position/size by the same ~1.4x rather
   than just spreading the narrower layout's gaps wider.

   Below 700px the scatter doesn't reflow cleanly (it's hand-tuned for
   one field width), so it collapses to a plain stacked column instead
   — the !important overrides are there specifically to beat each
   polaroid's own inline style at that breakpoint. */

.polaroid-field {
  position: relative;
  max-width: 820px;
  height: 780px;
  margin: 0 auto;
}

/* Fills the full container-wide measure instead of the narrower
   default. No longer used on Collected Works — its 20-polaroid
   collage grew unwieldy as more cards were added, so it moved to the
   tidy fixed-row .polaroid-grid below — but kept for any future page
   that wants the original scattered-collage look at a handful of
   items. */
.polaroid-field--wide {
  max-width: var(--container-wide);
  height: 2060px;
}


/* --- 42a. POLAROID GRID (FIXED ROWS) ----------------------------------------------
   A tidy alternative to the scattered .polaroid-field collage above —
   used once Collected Works grew to 20 polaroids and the freeform
   collage became hard to scan. Each .polaroid-row is its own flex row
   holding however many cards Kathryn wants in it (3/4/3/4/3/3 on
   Collected Works); .polaroid--grid strips the collage's absolute
   positioning/rotation down to a plain static, upright card at a
   consistent size, so every row lines up regardless of how many cards
   are in it. Add .polaroid--featured to a card for the larger size
   reserved for Kathryn's picks (section below).

   Markup:
   <div class="polaroid-grid">
     <div class="polaroid-row">
       <button type="button" class="polaroid polaroid--grid" data-lightbox-open="…">
         <img src="…" alt="">
         <p class="polaroid__caption">…</p>
       </button>
       … 3 or 4 per row …
     </div>
     … one .polaroid-row per row …
   </div> */

.polaroid-grid {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xl);
  max-width: var(--container-wide);
  margin: 0 auto;
}

.polaroid-row {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: var(--space-md);
  width: 100%;
}

/* A slight rotate + vertical stagger per card, cycling by position
   within its row (nth-child resets per .polaroid-row, so this pattern
   repeats the same way whether a row holds 2, 3 or 4 cards) — keeps
   the requested fixed row counts (3/4/3/4/3/3) while bringing back the
   hand-scattered feel of the original collage instead of a perfectly
   flush grid. .polaroid:hover's own transform (rotate(0deg), !important)
   still levels each card and lifts it on hover, same as before. */
.polaroid-row > .polaroid--grid:nth-child(1) { transform: rotate(-4deg) translateY(6px); }
.polaroid-row > .polaroid--grid:nth-child(2) { transform: rotate(3deg) translateY(-8px); }
.polaroid-row > .polaroid--grid:nth-child(3) { transform: rotate(-3deg) translateY(4px); }
.polaroid-row > .polaroid--grid:nth-child(4) { transform: rotate(5deg) translateY(-5px); }
.polaroid-row > .polaroid--grid:nth-child(5) { transform: rotate(-2deg) translateY(7px); }

.polaroid.polaroid--grid {
  /* Doubled-up selector (not just .polaroid--grid) so this reliably
     beats the base .polaroid rule below regardless of source order —
     as a single class it tied .polaroid's own specificity and lost
     the position override in testing, collapsing every row to
     height:0 since absolutely-positioned children don't contribute to
     their flex parent's height. relative, not static: .polaroid__caption
     is itself position:absolute (left:0;right:0;bottom:8px) expecting
     .polaroid to be its positioned ancestor — static would have broken
     that entirely, letting the caption escape to the viewport instead
     (this shipped briefly and produced a huge, garbled-looking
     caption on every grid card before being caught here). */
  position: relative;
  /* 10% up from the original 195×240 fit, per Kathryn's request —
     .container-wide's own side padding was zeroed on the gallery
     section (inline style) to reclaim the full 1152px box, since
     195×240 already used nearly all the width .container-wide's
     padding otherwise left for a 5-item row; sizing up without that
     would have wrapped the 5th card onto its own line, breaking the
     requested 4/5/4/5/2. */
  width: 215px;
  height: 264px;
}

/* Reserved for Kathryn's picks (Book of Creatives, Human Nature
   Drawings, Beneath the Surface, Rock & Co Event, Aerial Arts, Fairy
   Make-Up Tutorial, Bubble Planet Review, Aerial Yoga TV Interview,
   Dungeons & Dragons Journal) — 25% bigger than the rest of the grid
   to read as more important at a glance. Sits in the same flex row as
   the regular-size cards (align-items: flex-start on .polaroid-row),
   so it just stands taller/wider in place. */
.polaroid.polaroid--grid.polaroid--featured {
  width: 269px;
  height: 330px;
}

/* Bigger still than .polaroid--featured — for the two cards Kathryn
   wants to read as more important than even the other featured picks
   (Bubble Planet Review, Rock & Co Event). +25% over --featured's own
   size, same ratio --featured itself is up from the base card. */
.polaroid.polaroid--grid.polaroid--xl {
  width: 336px;
  height: 413px;
}

@media (max-width: 700px) {
  .polaroid.polaroid--grid {
    width: 150px;
    height: 190px;
  }

  .polaroid.polaroid--grid.polaroid--featured {
    width: 188px;
    height: 238px;
  }

  .polaroid.polaroid--grid.polaroid--xl {
    width: 235px;
    height: 298px;
  }
}

.polaroid {
  position: absolute;
  display: block;
  background-color: var(--color-paper);
  padding: 10px 10px 73px; /* first bumped 20% over a plain polaroid's usual ~34px, then given one more full line of room on top of that so the handwritten caption always has space to wrap without crowding the photo, then +5px more for longer two-line captions */
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18);
  border-radius: 2px;
  text-decoration: none;
  border: none; /* no-op on the usual <a>; resets a <button>.polaroid (e.g. one that opens a lightbox instead of linking out) to look identical */
  font: inherit;
  text-align: inherit;
  cursor: pointer;
  transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}

.polaroid:hover,
.polaroid:focus-visible {
  transform: scale(1.1) rotate(0deg) !important;
  box-shadow: 0 14px 28px rgba(0, 0, 0, 0.28);
  z-index: 50 !important;
}

.polaroid .photo-placeholder,
.polaroid img {
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  margin: 0;
  display: block;
  object-fit: cover;
}

.polaroid__caption {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 73px; /* matches .polaroid's bottom padding — the caption's whole reserved band, not just a line pinned to its bottom edge */
  margin: 0;
  padding: 0 10px; /* left:0/right:0 above positions this against .polaroid's padding-box edge, i.e. the FULL card width — with no padding of its own, longer captions ("Fairy Make-Up Tutorial", "Human Nature Font") ran flush to, and past, the card's edges instead of wrapping with room to spare */
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-family: var(--font-display);
  letter-spacing: 0.03em;
  word-spacing: 0.25em;
  font-size: 1.15rem;
  line-height: 1.5;
  color: var(--color-ink);
  overflow-wrap: break-word;
}

@media (max-width: 700px) {
  .polaroid-field {
    position: static;
    height: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-lg);
  }

  .polaroid {
    position: static !important;
    transform: none !important;
    width: 220px !important;
    height: 260px !important;
  }
}


/* --- 43a. WORKFLOW META (GOAL / STARTING POINT / APPROACH / OUTCOME) -------------
   A compact four-part case-study frame that sits above each workflow
   diagram below (43 and 43b) — same four questions for both projects,
   so Travel Blog Content Management and the YouTube pipeline read as
   a matched pair even though their diagrams are two different shapes.

   Markup:
   <dl class="workflow-meta">
     <div class="workflow-meta__item">
       <dt>Goal</dt>
       <dd>…</dd>
     </div>
     … four items total …
   </dl> */

.workflow-meta {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-lg);
  max-width: 62rem;
  margin: var(--space-lg) auto var(--space-xl);
  padding: var(--space-lg) 0;
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}

.workflow-meta__item dt {
  font-family: var(--font-body);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-accent-deep);
  margin: 0 0 0.4rem;
}

.workflow-meta__item dd {
  font-size: 0.85rem;
  line-height: 1.6;
  color: var(--color-ink-soft);
  margin: 0;
}

@media (max-width: 700px) {
  .workflow-meta {
    grid-template-columns: 1fr 1fr;
  }
}

@media (max-width: 460px) {
  .workflow-meta {
    grid-template-columns: 1fr;
  }
}


/* --- 43. WORKFLOW DIAGRAM — SPINE TIMELINE ----------------------------------------
   One of three layout options put to Kathryn as a published Artifact
   comparison; she picked this one ("Option A") for Travel Blog Content
   Management specifically because that workflow really is a straight
   one-way line, brief to published post. Steps alternate left/right
   off a center line, like reign dates down an illustrated history
   page. Colour pulls from --color-accent, so it follows whichever
   [data-genre] the page is on rather than being hard-coded to systems'
   olive. Kathryn has already flagged this as a first pass due another
   design tweak — don't treat the current spacing/sizing as final.

   Markup:
   <div class="workflow-spine">
     <div class="workflow-spine__row">
       <span class="workflow-spine__num">01</span>
       <div class="workflow-spine__card">
         <p class="workflow-spine__title">Step title</p>
         <p class="workflow-spine__desc">Step description.</p>
       </div>
     </div>
     … one row per step; alternating sides is automatic via :nth-child,
     so rows don't need an "odd/even" class of their own …
   </div> */

.workflow-spine {
  position: relative;
  max-width: 52rem;
  margin: var(--space-xl) auto 0;
  padding: var(--space-sm) 0 0;
}

.workflow-spine::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--color-sepia-light);
  transform: translateX(-50%);
}

.workflow-spine__row {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 2.4rem 1fr;
  column-gap: var(--space-md);
  align-items: start;
  margin-bottom: var(--space-sm);
}

.workflow-spine__card {
  grid-row: 1;
  background: var(--color-paper);
  border: 1px solid var(--color-border);
  padding: var(--space-md) var(--space-lg);
  position: relative;
}

.workflow-spine__row:nth-child(odd) .workflow-spine__card { grid-column: 1; text-align: right; }
.workflow-spine__row:nth-child(even) .workflow-spine__card { grid-column: 3; text-align: left; }

/* Optional per-row illustration, placed directly opposite the card on
   whichever side is empty for that row (e.g. row 3's card sits in
   column 1, so its illustration sits in column 3). Add the mirrored
   rule (grid-column: 1) on a per-row basis if an even row ever needs one.
   grid-row: 1 on num/card/illus-slot alike is the actual fix for the
   "too much space" bug: on even rows, this item's column (1) comes
   after items already auto-placed at higher columns (num=2, card=3) in
   DOM order, so the browser's grid auto-placement pushed it into a new
   implicit row below the card instead of sharing the row — that's what
   was actually causing the huge gap, not any centering math. Pinning
   every item to grid-row: 1 removes the ambiguity entirely. */
.workflow-spine__illus-slot {
  grid-row: 1;
  grid-column: 3;
  align-self: center;
  display: flex;
  align-items: center;
  justify-content: center;
}

.workflow-spine__illus {
  width: 100%;
  max-width: 170px;
  height: auto;
}

/* Even rows carry the card in column 3 (see the card rule above), so
   their illustration slot mirrors to the empty column 1 side instead. */
.workflow-spine__row:nth-child(even) .workflow-spine__illus-slot { grid-column: 1; }

/* Rows 3 and 5's illustrations (crossed quills, quill & inkwell) read as
   cramped against their neighbors — extra symmetric padding here grows
   the row to give them more breathing room above and below, without
   touching the shared spacing every other row still uses. */
.workflow-spine__row:nth-child(3) .workflow-spine__illus-slot,
.workflow-spine__row:nth-child(5) .workflow-spine__illus-slot {
  padding: var(--space-md) 0;
}

/* Row 7's illustration (open book) needs a smaller, exact 10px
   above/below rather than rows 3/5's larger space-md bump. */
.workflow-spine__row:nth-child(7) .workflow-spine__illus-slot {
  padding: 10px 0;
}

/* Row 6's illustration (raven) needed the same treatment, then another
   10px on top — 20px above/below in total. */
.workflow-spine__row:nth-child(6) .workflow-spine__illus-slot {
  padding: 20px 0;
}

/* Row 4's illustration (open book pages) needed 10px above/below too. */
.workflow-spine__row:nth-child(4) .workflow-spine__illus-slot {
  padding: 10px 0;
}

/* Row 9 (moth) sits too far below row 8 — pull it up 10px. */
.workflow-spine__row:nth-child(9) {
  margin-top: -10px;
}

/* Row 1's illustration needs to read larger than the shared --lg bump
   (used here and on row 8) without enlarging row 8 too — 187px + 15%. */
.workflow-spine__row:nth-child(1) .workflow-spine__illus {
  max-width: 215px;
}

/* One-off 10% size bump for specific illustrations (Content Planning,
   Tracking & Distribution) rather than the shared 170px default. */
.workflow-spine__illus--lg { max-width: 187px; }

.workflow-spine__num {
  grid-row: 1;
  grid-column: 2;
  justify-self: center;
  width: 2.4rem;
  height: 2.4rem;
  border-radius: 50%;
  background: var(--color-paper);
  border: 1px solid var(--color-accent);
  color: var(--color-accent-deep);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
  font-weight: 700;
  font-family: var(--font-body);
  z-index: 1;
}

.workflow-spine__title {
  font-family: var(--font-secondary);
  font-weight: 600;
  font-size: 1.02rem;
  margin: 0 0 0.25rem;
}

.workflow-spine__desc {
  font-size: 0.85rem;
  color: var(--color-ink-soft);
  line-height: 1.55;
  margin: 0;
}

@media (max-width: 700px) {
  .workflow-spine::before { left: 1.2rem; }
  .workflow-spine__row { grid-template-columns: 2.4rem 1fr; }
  .workflow-spine__num { grid-column: 1; }
  .workflow-spine__row:nth-child(odd) .workflow-spine__card,
  .workflow-spine__row:nth-child(even) .workflow-spine__card {
    grid-column: 2;
    text-align: left;
  }
  .workflow-spine__illus-slot { display: none; }
}


/* --- 43b. WORKFLOW DIAGRAM — RADIAL CYCLE -----------------------------------------
   The other option from the same Artifact comparison ("Option C"),
   picked for the YouTube pipeline specifically because that one really
   does loop — publishing feeds view/retention data back into the next
   video's concept, so the diagram itself should show a ring rather
   than a line. js/main.js positions each .workflow-wheel__item evenly
   around the ring (angle = 360/n × index), so the HTML just lists the
   steps in order and isn't hard-coded to 8 of them. Also flagged by
   Kathryn as a first pass due a design tweak.

   Markup:
   <div class="workflow-wheel-frame">
     <div class="workflow-wheel">
       <div class="workflow-wheel__hub"><span>Hub label</span></div>
       <div class="workflow-wheel__item">
         <div class="workflow-wheel__num">01</div>
         <div class="workflow-wheel__title">Step title</div>
       </div>
       … one .workflow-wheel__item per step …
     </div>
     <p class="workflow-wheel__legend">Caption explaining the loop.</p>
     <div class="workflow-wheel__fallback">
       <ol class="workflow-wheel__fallback-list">…same steps, plain list…</ol>
     </div>
   </div>

   The ring depends on JS-measured absolute positioning, which doesn't
   translate to a narrow single-column phone screen — .workflow-wheel__fallback
   swaps in a plain numbered list below 700px instead of shrinking the
   ring illegibly small. */

.workflow-wheel-frame {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xl);
  margin: var(--space-xl) auto;
}

.workflow-wheel {
  position: relative;
  width: min(560px, 90vw);
  aspect-ratio: 1;
  border-radius: 50%;
  border: 1px solid var(--color-sepia-light);
}

/* Inner ring — the "wax seal" pressed-medallion look, approved over the
   plain dashed-circle original. */
.workflow-wheel::before {
  content: "";
  position: absolute;
  inset: 8px;
  border-radius: 50%;
  border: 1px solid rgba(107, 107, 63, 0.25);
  pointer-events: none;
}

.workflow-wheel__hub {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 34%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--color-paper-deep);
  border: 1px solid var(--color-border);
  box-shadow: 0 3px 0 var(--color-border), 0 6px 14px rgba(39, 38, 54, 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--space-sm);
}

.workflow-wheel__hub span {
  font-family: var(--font-secondary);
  font-weight: 600;
  font-size: 0.95rem;
  line-height: 1.3;
}

.workflow-wheel__item {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 30%;
  text-align: center;
}

/* Each item's own transform gives it a separate stacking context, so a
   later item in the markup always paints over an earlier one — the
   tooltip's own z-index can't escape that. Raising the hovered/focused
   item itself puts its whole stacking context (tooltip included) above
   every sibling, regardless of DOM order. */
.workflow-wheel__item:hover,
.workflow-wheel__item:focus-within {
  z-index: 20;
}

.workflow-wheel__num {
  position: relative;
  width: 1.9rem;
  height: 1.9rem;
  margin: 0 auto 0.35rem;
  border-radius: 50%;
  background: var(--color-paper);
  border: 1px solid var(--color-accent);
  color: var(--color-accent-deep);
  box-shadow: 0 2px 0 var(--color-accent), 0 3px 6px rgba(39, 38, 54, 0.3);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.72rem;
  font-weight: 700;
  font-family: var(--font-body);
  cursor: default;
  transition: transform 0.15s ease;
}

.workflow-wheel__num:hover,
.workflow-wheel__num:focus-visible {
  transform: scale(1.2);
}

/* Hover/focus tooltip on each wheel number, explaining that step.
   .workflow-wheel__num is the positioning context (not .workflow-wheel__item,
   which already carries its own centering transform) so the tooltip anchors
   to the circle itself. Styled as a small paper note (not a dark web-app
   tooltip) to match the book's aesthetic — border + fill pseudo-elements
   give the pointer an outline instead of a flat triangle. */
.workflow-wheel__tooltip {
  position: absolute;
  bottom: calc(100% + 0.7rem);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  width: max-content;
  max-width: 11rem;
  background: var(--color-paper);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-style: italic;
  font-weight: 400;
  font-size: 0.72rem;
  line-height: 1.45;
  text-align: left;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-page);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease;
  z-index: 5;
}

.workflow-wheel__tooltip::after,
.workflow-wheel__tooltip::before {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
}

.workflow-wheel__tooltip::after { border-top-color: var(--color-accent); }
.workflow-wheel__tooltip::before {
  border-width: 4px;
  transform: translateX(-50%) translateY(-1px);
  border-top-color: var(--color-paper);
}

.workflow-wheel__num:hover .workflow-wheel__tooltip,
.workflow-wheel__num:focus-visible .workflow-wheel__tooltip {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
}

.workflow-wheel__title {
  font-family: var(--font-secondary);
  font-weight: 600;
  font-size: 0.82rem;
  line-height: 1.25;
}

.workflow-wheel__legend {
  max-width: 30rem;
  font-size: 0.85rem;
  color: var(--color-ink-soft);
  text-align: center;
  line-height: 1.6;
}

.workflow-wheel__fallback {
  display: none;
}

.workflow-wheel__fallback-list {
  font-family: var(--font-body);
  font-size: 0.85rem;
  color: var(--color-ink);
  line-height: 2;
  max-width: 24rem;
  margin: 0 auto;
  padding-left: 1.4rem;
}

@media (max-width: 700px) {
  .workflow-wheel,
  .workflow-wheel__legend {
    display: none;
  }
  .workflow-wheel__fallback {
    display: block;
  }
}


/* --- 46. FLIP BOX -----------------------------------------------------------------
   A framed photo that flips on hover/focus to reveal project details
   on its back face — built for Personal Projects, but generic enough
   to reuse anywhere a "photo up front, details on the back" shape
   fits better than two stacked blocks. Chosen from a 4-option artifact
   comparison (plain vine frame beat the ornate one for this use).

   Uses the same hand-drawn frame art as .toc-illustrated-frame--horizontal
   (images/frame-horizontal.png) and the same "transparent border
   between content and the frame's inner rule line" fix as that
   component — the frame's real inner rule sits at roughly 10-13% inset
   (measured directly from the image's alpha channel, not guessed;
   guessing it at 9%/8% first is what caused the photo to cover part of
   the frame). .flip-box__frame-inset is that transparent border, used
   on BOTH faces so the card reads as the same size flipped either way.

   Sized to fit the densest real content (title, meta line, several
   bullets, a link) without the back face ever needing to scroll.
   aspect-ratio locked to the frame art's own ratio so
   background-size:100% 100% never stretches it unevenly. If a future
   use case has noticeably more back-face text than this, size up
   further rather than shrinking the type or bringing back a scrollbar.

   The project name sits ABOVE the box as its own calligraphy heading
   (.flip-box-heading), not overlaid on the front photo — cleaner than
   text-on-image, and it means the front face is just the photo, full
   stop, no legibility fight with whatever's underneath.

   Markup — .cv-entry__title/__meta/__list/__link (section 23) inside
   the back face for consistent CV-style typography:

   <div class="flip-box-item">
     <p class="flip-box-heading">Project Name</p>
     <div class="flip-box" tabindex="0">
       <div class="flip-box__inner">
         <div class="flip-box__face flip-box__face--front">
           <div class="flip-box__frame-inset"><img src="…" alt=""></div>
         </div>
         <div class="flip-box__face flip-box__face--back">
           <div class="flip-box__frame-inset">
             <h3 class="cv-entry__title">Project Name</h3>
             <p class="cv-entry__meta">example.com</p>
             <ul class="cv-entry__list">…</ul>
             <a href="#" class="cv-entry__link">Visit the site →</a>
           </div>
         </div>
       </div>
     </div>
   </div>

   Wrap a row of .flip-box-item in .flip-box-grid. tabindex="0" on the
   box itself (there's no single link/button to focus instead) lets
   keyboard users trigger the flip via :focus-within/:focus-visible. */

.flip-box-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
  gap: var(--space-xl);
  max-width: 98.7rem; /* +5% over the original 94rem, per Kathryn's request to give the card text more room without changing its font size */
  margin: 0 auto;
}

.flip-box-heading {
  font-family: var(--font-display);
  letter-spacing: 0.03em;
  word-spacing: 0.25em;
  font-size: 1.9rem;
  text-align: center;
  color: var(--color-ink);
  margin: 0 0 20px;
}

.flip-box {
  position: relative;
  perspective: 1600px;
  aspect-ratio: 3508 / 2480;
  max-width: 58.8rem; /* +5% over the original 56rem */
  width: 100%;
  margin: 0 auto;
  cursor: pointer;
}

.flip-box__inner {
  position: relative;
  width: 100%;
  height: 100%;
  transition: transform var(--transition-slow, 0.6s cubic-bezier(0.4, 0, 0.2, 1));
  transform-style: preserve-3d;
}

.flip-box:hover .flip-box__inner,
.flip-box:focus-within .flip-box__inner {
  transform: rotateY(180deg);
}

.flip-box__face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  background-image: url("../images/frame-horizontal.png");
  background-size: 100% 100%;
  background-repeat: no-repeat;
  padding: 11% 13%;
  box-sizing: border-box;
}

.flip-box__face--front {
  display: flex;
  flex-direction: column;
}

.flip-box__face--back {
  transform: rotateY(180deg);
  display: flex;
  flex-direction: column;
  justify-content: flex-start; /* NOT center: a centered flex container that overflows clips content at BOTH the start and end with no way to scroll to the start — top-aligning means any overflow (there shouldn't be any, see sizing above) only ever happens at the bottom, where overflow-y:auto can actually reach it */
  overflow-y: auto;
}

/* The transparent-border fix, doing double duty as both the image
   inset (front) and the text inset (back) — see the section note. */
/* Same two-part technique as .toc-illustrated-frame__inner (section 38):
   a transparent BORDER plus its own PADDING inside that, not just the
   border alone — the border by itself was still only ~10px of real
   clearance, not enough once measured against the frame art's actual
   inner rule position. */
.flip-box__frame-inset {
  border: 0.6rem solid transparent;
  padding: var(--space-sm);
  box-sizing: border-box;
}

.flip-box__face--front .flip-box__frame-inset {
  flex: 1;
  overflow: hidden;
}

.flip-box__face--front img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: grayscale(1) contrast(1.05);
}

.flip-box__face--back .cv-entry__title {
  font-size: 1.35rem; /* +3pt (0.25rem) over the base 1.1rem, per Kathryn's request to size up the flip-box back text */
  margin-bottom: 0.15em;
}

.flip-box__face--back .cv-entry__meta {
  font-size: 1rem; /* +3pt over .cv-entry__meta's base 0.75rem */
  font-weight: 700; /* the role/studio line reads as this card's heading, so it's bold like one */
  margin-top: 0.4em; /* a little more breathing room above the URL than the original 0, without the card's fixed height overflowing into its bottom border */
  margin-bottom: 0.5em;
}

.flip-box__face--back .cv-entry__list {
  font-size: 1.05rem; /* +3pt over the base 0.8rem */
  line-height: 1.4;
}

.flip-box__face--back .cv-entry__list li {
  margin-bottom: 0.3em;
}

/* Where a back face uses stacked <p class="cv-entry__list"> lines
   instead of a <ul> (e.g. Where It Began's two studio lines) —
   .cv-entry__list's shared margin:0 otherwise runs them together with
   no gap. */
.flip-box__face--back p.cv-entry__list + p.cv-entry__list {
  margin-top: 0.6em;
}

/* Matches .next-link/.prev-link's "Read Next Volume" treatment
   (section 30) — small uppercase caps, no underline — instead of
   .cv-entry__link's plain underlined default. */
.flip-box__face--back .cv-entry__link {
  display: block;
  font-family: var(--font-secondary);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-decoration: none;
  color: var(--color-ink-soft);
  margin-top: 0.4rem; /* a little more than the original 0.15rem, short of the card's bottom border */
  margin-left: 1.1em; /* lines up with the bullet TEXT above, not the flush-left title/meta */
}

.flip-box__face--back .cv-entry__link:hover {
  color: var(--color-ink);
}

@media (max-width: 480px) {
  .flip-box {
    max-width: 100%;
  }
}


/* --- 47. ORNATE PHOTO FRAME -------------------------------------------------------
   A photo set inside the same floral frame (images/frame ornate.png)
   as the homepage's #book-info-panel — that one used fixed-px padding
   calibrated to its own 560px width; this version uses the equivalent
   PERCENTAGES (measured from the frame's actual inner-opening bounds:
   top 21.8%, right 21.7%, bottom 26.9%, left 22.4%) so it scales
   correctly at whatever width it's actually used at. Locked to the
   source art's own aspect ratio (2480:3508, portrait) so
   background-size:100% 100% never stretches it unevenly — same fix as
   .toc-illustrated-frame (section 38) and .flip-box (section 46).

   Don't put .ornate-photo-frame itself directly on a flex item with
   flex:1 (or any flex-basis) — combining a flex-basis with
   aspect-ratio on the SAME element is a real Chrome bug (see .flip-box,
   section 46): width gets derived from content-driven height instead
   of the other way around and can collapse to 0. Wrap it in a plain
   sizing div instead:

   <div style="flex:1; …"> <!-- sizing only, no aspect-ratio -->
     <div class="ornate-photo-frame">
       <div class="ornate-photo-frame__inner">
         <img src="…" alt="">
       </div>
     </div>
   </div> */

.ornate-photo-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 2480 / 3508;
  box-sizing: border-box;
  background-image: url("../images/frame ornate.png");
  background-size: 100% 100%;
  background-repeat: no-repeat;
  /* Pixel-measured inner window (found by scanning frame ornate.png for
     its largest contiguous blank rectangle): top 764px, right-inset
     539px, bottom-inset 944px, left 559px, out of the 2480×3508 source.
     Left/right convert straight to %-of-width (22.5%/21.7%). Top/bottom
     do NOT — CSS resolves ALL padding percentages (including top and
     bottom) against the containing block's WIDTH, never its height, so
     764/3508 (21.8%, the pixel fraction of HEIGHT) is the wrong number
     to hand padding-top. The correct conversion multiplies by the
     element's own height:width ratio (3508/2480 = 1.4145) first:
     (764/3508) × 1.4145 = 30.8%, (944/3508) × 1.4145 = 38.1%. This was
     first caught because it fixed the horizontal variant (47a) below,
     which showed it clearly — but the bug was here originally, just
     subtle enough on a small logo to go unnoticed. Verified against a
     rendered screenshot (headless Chrome is available in this
     environment via the Bash tool — see 47a's note). */
  padding: 30.8% 21.73% 38.06% 22.54%;
}

.ornate-photo-frame__inner {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

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

/* --- 47a. ORNATE PHOTO FRAME — HORIZONTAL VARIANT ---------------------------------
   The frame art itself is a fixed PORTRAIT asset (2480×3508) — fine
   for a logo, wrong for a landscape website screenshot, which is what
   this variant is for (first used on Personal Projects, once Kathryn
   is sharing real screenshots there). Rather than stretch the art
   non-uniformly into a landscape box (the original "distortion, not a
   tuning problem" problem noted in section 47), this rotates the
   frame art itself 90° so it reads as a horizontal frame, while the
   screenshot inside stays upright — achieved by separating the two
   layers that section 47's plain version keeps combined:

     .ornate-photo-frame--h__art   — carries ONLY the frame artwork,
       pre-sized to the ORIGINAL portrait ratio then rotated 90° and
       centered, so after rotation its bounding box exactly fills this
       (landscape) element. The math: for a landscape box of aspect
       3508:2480, the pre-rotation art needs width = 70.69% of the
       box (2480/3508) and its own aspect-ratio locked at 2480/3508 —
       work through the rotation geometry before touching these
       numbers again, they're not arbitrary.
     .ornate-photo-frame--h__inner — carries the actual <img>, NOT
       rotated, positioned with its own padding re-derived from
       section 47's four percentages for this swapped orientation
       (each old side's padding becomes the next side's, converted
       from "% of the old portrait width" to "% of this new landscape
       width" — see the values below, not simply the original four
       renumbered).

   Verified with a real screenshot (headless Chrome is actually
   available in this environment — `/Applications/Google Chrome.app/…
   --headless --disable-gpu --screenshot=out.png --window-size=W,H
   file:///…` — no npm/Playwright install needed). The first version of
   these numbers overlapped the frame badly because they inherited
   section 47's own top/bottom bug (percentages measured as a fraction
   of the portrait art's HEIGHT, not converted to width-relative %,
   which is what CSS padding always resolves against). Re-derived here
   straight from the pixel-measured window on the un-rotated art (top
   764, right-inset 539, bottom-inset 944, left 559, out of 2480×3508),
   mapped through a 90° clockwise rotation (old left→new top, old
   top→new right, old right→new bottom, old bottom→new left), each
   fraction then expressed against the RIGHT axis (new top/bottom need
   an extra ×(height:width ratio) conversion, new left/right don't —
   same width-basis rule as section 47):
     new top    = old-left-frac(559/2480)   × (Hh/Wh, 0.707) = 15.94%
     new right  = old-top-frac(764/3508)                      = 21.78%
     new bottom = old-right-frac(539/2480)  × (Hh/Wh, 0.707) = 15.37%
     new left   = old-bottom-frac(944/3508)                   = 26.91%

   Markup:
   <div class="ornate-photo-frame--h">
     <div class="ornate-photo-frame--h__art" aria-hidden="true"></div>
     <div class="ornate-photo-frame--h__inner">
       <img src="…" alt="…">
     </div>
   </div> */

.ornate-photo-frame--h {
  position: relative;
  width: 100%;
  aspect-ratio: 3508 / 2480;
  box-sizing: border-box;
}

.ornate-photo-frame--h__art {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 70.69%;
  aspect-ratio: 2480 / 3508;
  background-image: url("../images/frame ornate.png");
  background-size: 100% 100%;
  background-repeat: no-repeat;
  transform: translate(-50%, -50%) rotate(90deg);
  pointer-events: none;
}

.ornate-photo-frame--h__inner {
  position: absolute;
  inset: 0;
  padding: 15.94% 21.78% 15.37% 26.91%;
  box-sizing: border-box;
  overflow: hidden;
}

.ornate-photo-frame--h__inner img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}


/* --- 48. MOODBOARD -----------------------------------------------------------------
   A scattered masonry of real photos/videos with hand-drawn cursive
   labels and curved SVG arrows pointing into specific tiles — built
   for A History of Yoga and Aerial's four eras, approved via a
   dedicated artifact comparison before landing here.

   This is a fixed-canvas layout (1240px native), NOT a fluid grid —
   the labels and arrows are hand-positioned to point at exact tiles,
   which only works with known, fixed coordinates. Rather than hand-
   recompute every coordinate to shrink it, .moodboard-frame reserves a
   smaller box (992×688, 0.8×) and .moodboard is scaled down into it
   with a plain CSS transform — the whole subtree (tiles, labels,
   arrows) shrinks together, so nothing drifts out of alignment. This
   fits inside .container-wide without needing to scroll. A future pass
   could swap in a real mobile-specific fallback if this needs to work
   well on phones.

   Two arrow bugs already fixed once, worth not repeating:
   1. A curve whose control points ever reverse direction (x decreasing
      then increasing, or vice versa) reads as a kinked/hooked line, not
      a smooth stroke — keep each cubic's x (or y) monotonic end to end.
   2. Labels/arrows need a real dead-zone between the label's own text
      box and the nearest tile — text width plus font overhang can
      still clip a photo if that gap is too tight (learned the hard way
      at 10px; 60px is comfortable).

   Markup: one .moodboard-tile per photo/video (add .moodboard-tile--video
   for the play-button overlay), absolutely positioned via inline
   left/top/width/height; one .moodboard-label per callout (add
   .moodboard-label--right for the right-hand side, which right-aligns
   the text); one <svg class="moodboard-arrows"> holding all the curved
   paths + the shared arrowhead marker. */

.moodboard-scroll {
  width: 992px;
  max-width: 100%;
  height: 752px;
  margin: 0 auto;
  padding: 0.5rem 0 1rem;
  overflow: hidden;
}

.moodboard {
  position: relative;
  width: 1240px;
  max-width: none;
  height: 940px;
  background-image: url("../images/systems background.jpg");
  background-size: cover;
  background-position: center;
  border-radius: 20px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 16px 40px rgba(0, 0, 0, 0.12);
  transform: scale(0.8);
  transform-origin: top left;
}

.moodboard-tile {
  position: absolute;
  overflow: hidden;
  border-radius: 3px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}

.moodboard-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: saturate(0.9);
}

.moodboard-tile--video::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: rgba(244, 234, 217, 0.92);
}

.moodboard-tile--video::before {
  content: "▶";
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-38%, -50%);
  font-size: 0.95rem;
  color: var(--color-ink);
  z-index: 1;
}

.moodboard-label {
  position: absolute;
  font-family: var(--font-display);
  letter-spacing: 0.03em;
  word-spacing: 0.25em;
  font-size: 1.7rem;
  line-height: 1.15;
  color: var(--color-ink);
  width: 140px;
  margin: 0;
}

.moodboard-label--right {
  text-align: right;
}

.moodboard-arrows {
  position: absolute;
  inset: 0;
  overflow: visible;
  pointer-events: none;
}

.moodboard-arrows path {
  fill: none;
  stroke: var(--color-ink-soft);
  stroke-width: 1.6;
  stroke-linecap: round;
}

.moodboard-footer {
  position: absolute;
  left: 50px;
  bottom: 34px;
  font-family: var(--font-body);
  font-size: 0.95rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-ink);
  margin: 0;
}


/* --- 49. TIMELINE SPINE (ALTERNATING SIDES, FLIP-BOX PHOTOS) ---------------------
   Replaces .timeline-nav's horizontal row of small photos (section 39)
   — first tried as a single-side vertical rail (.timeline-rail, since
   removed), but a single side can only run so wide before it crowds
   the page; alternating left/right around a CENTER line (same idea as
   the "Spine Timeline" option from the Career History layout
   comparison) leaves each photo a full half-width column, so the
   flip-box can run considerably bigger. Each photo is a real .flip-box
   (section 46): front is the photo, back is a one-line role descriptor
   plus a real "Jump to chapter" link — the actual navigation, since
   the flip-box itself isn't a plain link the way the old
   .timeline-nav__item was.

   Markup — year/label caption FIRST, photo after (not the other way
   round!): that order matters for the overlap math right below, so
   keep it if this ever gets touched again.
   <div class="timeline-spine">
     <div class="timeline-spine__row"> <!-- odd rows -> left column -->
       <div class="timeline-spine__content">
         <p class="timeline-rail__year">2026 – Present</p>
         <h3 class="timeline-rail__label">Real You Space</h3>
         <div class="flip-box" tabindex="0">…front photo / back role+link, as section 46…</div>
       </div>
       <span class="timeline-spine__dot" aria-hidden="true"></span>
     </div>
     … one row per era, alternating which side .timeline-spine__content
     is placed on via nth-child(odd)/(even) — see below …
   </div> */

.timeline-spine {
  position: relative;
  max-width: 68rem;
  margin: 0 auto;
}

.timeline-spine::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0.5rem;
  bottom: 0.5rem;
  width: 1px;
  background: var(--color-sepia-light);
  transform: translateX(-50%);
}

.timeline-spine__row {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 2.4rem 1fr;
  align-items: start;
}

/* The overlap: row N+1's CAPTION needs to start 60% of the way down
   row N's photo — not row N+1's photo itself. The caption is the
   FIRST thing in each row's column (see the markup note above), so it
   sits exactly at the row's own top edge; that means the row only
   needs to be pulled up by the uncovered 40% of the previous photo —
   the caption's own height doesn't factor in at all, since there's
   nothing above it in its own row to clear first:
     -(1 - 0.6) × photo height = -0.4 × photo height
   Photo height ≈ 23.3rem (.flip-box's 33rem cap × 2480/3508).
   0.4 × 23.3 ≈ 9.32rem. Recalculate if the flip-box cap or its
   aspect-ratio changes — this is hand-tuned to those exact numbers,
   not derived from them at render time. (Because the caption is
   shorter than the 40% gap it's landing in, the two PHOTOS still end
   up overlapping each other too, by roughly 40%-of-photo minus the
   caption's own height — that's expected, not a bug.) */
.timeline-spine__row + .timeline-spine__row {
  margin-top: -9.32rem;
}

.timeline-spine__dot {
  grid-column: 2;
  justify-self: center;
  margin-top: 0.5rem;
  width: 0.8rem;
  height: 0.8rem;
  border-radius: 50%;
  background: var(--color-accent);
  border: 2px solid var(--color-paper);
  box-shadow: 0 0 0 1px var(--color-accent);
}

.timeline-spine__row:nth-child(odd) .timeline-spine__content {
  grid-column: 1;
  text-align: right;
}

.timeline-spine__row:nth-child(even) .timeline-spine__content {
  grid-column: 3;
  text-align: left;
}

.timeline-rail__year {
  font-family: var(--font-body);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-accent-deep);
  margin: 0 0 0.2rem;
}

.timeline-rail__label {
  font-family: var(--font-secondary);
  font-weight: 700;
  font-size: 1.3rem;
  margin: 0 0 1rem;
}

/* +50% over the single-side rail's 22rem, since each side of the
   spine gets a full half-width column to work with instead of a
   narrow shared rail. */
.timeline-spine .flip-box {
  max-width: 33rem;
}

.timeline-spine__row:nth-child(odd) .flip-box {
  margin-left: auto;
  margin-right: 0;
}

.timeline-spine__row:nth-child(even) .flip-box {
  margin-left: 0;
  margin-right: auto;
}

@media (max-width: 720px) {
  .timeline-spine::before {
    left: 1.1rem;
  }
  .timeline-spine__row {
    grid-template-columns: 2.4rem 1fr;
  }
  .timeline-spine__row + .timeline-spine__row {
    margin-top: 2rem;
  }
  .timeline-spine__dot {
    grid-column: 1;
  }
  .timeline-spine__row:nth-child(odd) .timeline-spine__content,
  .timeline-spine__row:nth-child(even) .timeline-spine__content {
    grid-column: 2;
    text-align: left;
  }
  .timeline-spine__row:nth-child(odd) .flip-box,
  .timeline-spine__row:nth-child(even) .flip-box {
    margin-left: 0;
    margin-right: auto;
  }
}


/* --- 50. STAGGERED DRIFT GALLERY ---------------------------------------------------
   A scattered, hand-set row of four tiles — small alternating tilts
   and vertical offsets, like prints laid loosely on a table rather
   than filed into a grid. "Option III" from the Gallery layout
   comparison artifact — this is a direct port of that approved
   version (same four-tile composition, same uniform 4:5 frame, same
   tilt values via :nth-child(4n+…)), not a redesign, so it needs to
   keep matching that reference if it's ever adjusted again. First
   used for Performances → APAC.

   Every tile — video included — shares the same 4:5 portrait frame
   and object-fit:cover, same as the artifact's <video>. An <iframe>
   embed (e.g. a YouTube embed, which has no source file to crop
   losslessly) crops the same way under object-fit in current
   browsers, so it stays visually uniform with the photo tiles instead
   of standing out as a differently-shaped "feature" tile.

   Markup (exactly 4 tiles — the tilt pattern repeats every 4th):
   <div class="staggered-drift">
     <div class="staggered-drift__tile"><iframe …></iframe></div>  <!-- or <video>/<img> -->
     <div class="staggered-drift__tile"><img src="…" alt="…"></div>
     … 2 more …
   </div> */

.staggered-drift {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  padding: 0;
}

.staggered-drift__tile {
  position: relative;
  width: calc(37.5% - 1.5rem); /* 50% bigger than the original 25% — 2 tiles now fit a desktop row instead of 4 */
  aspect-ratio: 4 / 5;
  margin: 0.75rem;
  overflow: hidden;
  background: var(--color-paper-deep);
  border: 10px solid var(--color-paper);
  box-shadow: 0 2px 4px rgba(39, 38, 54, 0.12), 0 14px 26px rgba(39, 38, 54, 0.16);
  transition: transform 0.35s ease, box-shadow 0.35s ease;
}

.staggered-drift__tile img,
.staggered-drift__tile video,
.staggered-drift__tile iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
  display: block;
  filter: saturate(0.94);
}

.staggered-drift__tile iframe {
  filter: none; /* an embedded player's own chrome shouldn't be desaturated along with a photo */
}

.staggered-drift__tile:hover {
  transform: translateY(-6px) rotate(0deg) scale(1.3) !important;
  box-shadow: 0 10px 24px rgba(39, 38, 54, 0.22);
  z-index: 2;
}

.staggered-drift__tile:nth-child(4n+1) { transform: rotate(-2.2deg) translateY(6px); }
.staggered-drift__tile:nth-child(4n+2) { transform: rotate(1.6deg)  translateY(-10px); }
.staggered-drift__tile:nth-child(4n+3) { transform: rotate(-1deg)   translateY(18px); }
.staggered-drift__tile:nth-child(4n+4) { transform: rotate(2.4deg)  translateY(-4px); }

@media (max-width: 820px) {
  .staggered-drift__tile { width: calc(75% - 1.5rem); }
}

@media (max-width: 560px) {
  .staggered-drift__tile { width: calc(100% - 1.5rem); }
}

@media (max-width: 480px) {
  .staggered-drift__tile {
    width: calc(100% - 1.5rem);
    aspect-ratio: 4 / 3;
  }
}

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


/* --- 51. CASE STUDY LIST -----------------------------------------------------------
   A simple numbered stack for a handful of short case studies — no
   tabs, no accordion, everything visible at once. "Option B" from the
   Link Building layout comparison artifact: picked over sidebar tabs
   (too much interaction for 4 short entries) and an accordion (no
   real need to hide them). First used on The Art of Websites and
   SEO's Link Building tab.

   Markup:
   <div class="case-study-list">
     <div class="case-study-list__row">
       <p class="case-study-list__num">1.</p>
       <div>
         <h3 class="case-study-list__name">Client Name</h3>
         <p class="case-study-list__label">Campaign</p>
         <p class="case-study-list__text">Brief explanation…</p>
       </div>
     </div>
     … one row per case study …
   </div> */

.case-study-list {
  max-width: 38rem;
  margin: var(--space-lg) auto 0;
}

.case-study-list__row {
  display: grid;
  grid-template-columns: 3rem 7.5rem 1fr;
  gap: 1.1rem;
  align-items: center;
  padding: 1.2rem 0;
}

.case-study-list__row + .case-study-list__row {
  border-top: 1px solid var(--color-sepia-light);
}

/* "Thumbnail Beside the Number" — Option A from the Link Building
   image-placement artifact. A small square image slots between the
   number and the text; swap each .case-study-list__thumb's inner
   placeholder for a real <img> once campaign images exist. */
.case-study-list__thumb {
  width: 7.5rem;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed var(--color-border);
  background-color: var(--color-paper-deep);
  color: var(--color-ink-soft);
  font-family: var(--font-secondary);
  font-size: 0.55rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  text-align: center;
  overflow: hidden;
}

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

.case-study-list__num {
  font-family: var(--font-display);
  letter-spacing: 0.03em;
  font-size: 1.8rem;
  color: #3f5470; /* Writing and Editing's "ink blue" genre accent, used here deliberately per Kathryn's request rather than this page's own teal accent */
  line-height: 1;
  margin: 0;
}

.case-study-list__name {
  font-family: var(--font-secondary);
  font-weight: 700;
  font-size: 1.1rem;
  margin: 0 0 0.5rem;
  color: var(--color-ink);
}

.case-study-list__label {
  font-size: 0.7rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--color-ink);
  margin: 0 0 0.3rem;
}

.case-study-list__text {
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--color-ink-soft);
  font-style: italic;
  margin: 0;
}

/* All four Art of Websites and SEO tab panels run full-bleed, per
   Kathryn's request — every other .image-tabs__panel on the site stays
   inside the shared section's narrow .container (46rem), so each rule
   below breaks just that one panel out of that ancestor rather than
   widening the shared section (which would widen every panel on every
   page using this pattern). Standard centered-breakout technique: 100vw
   ignores the narrower ancestor entirely, and the negative margin
   re-centers it on the viewport; safe here because [hidden] is
   display:none, so this has zero effect on page layout/scroll while a
   different tab is active. Each panel's own content (.case-study-list,
   .client-dossier, .framed-client-grid, .dossier-row) is separately
   capped at var(--container-wide) and centered, so the panel fills the
   viewport but the content itself still reads at a normal measure. */
#panel-case-studies,
#panel-previous-clients,
#panel-link-building,
#panel-personal-projects {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding-left: var(--page-gutter);
  padding-right: var(--page-gutter);
  box-sizing: border-box;
}

#panel-link-building .case-study-list {
  max-width: var(--container-wide);
  margin-left: auto;
  margin-right: auto;
}


/* --- 52. FRAMED CLIENT GRID ---------------------------------------------------------
   "Option A" from the Previous Clients layout comparison — the same
   3-across grid as Edited Works, but each logo sits inside the ornate
   frame instead of a flat padded box. Originally the plain portrait
   frame (.ornate-photo-frame, section 47); switched to the horizontal
   variant (.ornate-photo-frame--h, section 47a) per Kathryn's request
   ("rotate the frames") since most of Agency Experience's real logos
   are wide wordmarks, which fit a landscape window far better than a
   portrait one. Reuses .ornate-photo-frame--h/__art/__inner verbatim,
   unmodified, so the logo's own background box is bounded EXACTLY by
   the frame art's real inner-opening — no extra padding/sizing layered
   on top, which is what made the first pass at this look off (the
   color box didn't match the frame's actual window). First used on The
   Art of Websites and SEO's Previous Clients (now Agency Experience)
   tab.

   Every logo also gets filter:brightness(0) on the <img> itself, per
   Kathryn's request that they all read as solid black rather than
   their original brand colors. Some source logos (Boa Concepts, BDA)
   had an OPAQUE badge/background rather than transparency around the
   mark — brightness(0) alone would have crushed badge and glyph to one
   flat black rectangle. images/boa-concepts-mono.png and bda-mono.png
   are duplicates with that background made transparent so the glyph
   still reads once flattened — same fix as the rolling bar's
   phrase-mono/surferseo-mono/seranking-mono/screamingfrog-mono
   (section 57). The originals are untouched in case they're needed in
   color elsewhere.

   Markup:
   <div class="framed-client-grid">
     <div class="framed-client">
       <div style="max-width:19rem; margin:0 auto 1.2rem;"> <!-- sizing wrapper, see .ornate-photo-frame's own note on why this can't take the aspect-ratio itself -->
         <div class="ornate-photo-frame--h">
           <div class="ornate-photo-frame--h__art" aria-hidden="true"></div>
           <div class="ornate-photo-frame--h__inner">
             <img src="…" alt="…" style="object-fit:contain; padding:1.5rem; background-color:var(--color-paper-deep); filter:brightness(0);">
           </div>
         </div>
       </div>
       <h3 class="framed-client__name">Client Name</h3>
       <p class="framed-client__label">The Company</p>
       <p class="framed-client__text">…</p>
       <p class="framed-client__label">SEO Work</p>
       <p class="framed-client__text">…</p>
     </div>
     … one per client …
   </div> */

.framed-client-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-xl);
  max-width: var(--container-wide);
  margin: var(--space-lg) auto 0;
}

.framed-client__name {
  font-family: var(--font-secondary);
  font-weight: 700;
  font-size: 1.1rem;
  text-align: center;
  margin: 0 0 0.7rem;
  color: var(--color-ink);
}

.framed-client__label {
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: center;
  color: var(--color-ink);
  margin: 0 0 0.25rem;
}

.framed-client__text {
  font-size: 0.85rem;
  line-height: 1.55;
  text-align: center;
  color: var(--color-ink-soft);
  margin: 0 auto 1rem;
  max-width: 20rem; /* base.css's global p{max-width:38em} otherwise lets a short line sit off-center under the centered name/label above it — capped and centered here to match */
}

@media (max-width: 820px) {
  .framed-client-grid {
    grid-template-columns: 1fr;
    max-width: 20rem;
    gap: var(--space-2xl);
  }
}


/* --- 53. FRAMED DOSSIER ROW ----------------------------------------------------------
   "Option C" from the Previous Clients layout comparison, reused for
   Personal Projects — a full editorial row per entry, framed logo on
   one side, alternating left/right down the page, instead of a
   repeated card grid. Same rule as the Framed Client Grid (section
   52): the framed image reuses .ornate-photo-frame/__inner verbatim,
   unmodified, so the logo's background box is bounded EXACTLY by the
   frame art's real inner-opening — don't add extra padding/sizing on
   top of that, which is what made the first pass at this look wrong.
   First used on The Art of Websites and SEO's Personal Projects tab.

   Markup:
   <div class="dossier-row">
     <div class="dossier-row__frame" style="max-width:13rem;">
       <div class="ornate-photo-frame">
         <div class="ornate-photo-frame__inner">
           <img src="…" alt="…" style="object-fit:contain; padding:1.5rem; background-color:var(--color-paper-deep);">
         </div>
       </div>
     </div>
     <div>
       <h3 class="dossier-row__name">Project Name</h3>
       <p class="dossier-row__label">The Site</p>
       <p class="dossier-row__text">…</p>
       <p class="dossier-row__label">What I Did</p>
       <p class="dossier-row__text">…</p>
     </div>
   </div>
   … one .dossier-row per project — :nth-child(even) automatically
   flips the frame to the right … */

.dossier-row {
  display: grid;
  grid-template-columns: 13rem 1fr;
  gap: var(--space-xl);
  align-items: center;
  max-width: var(--container-wide);
  margin: 0 auto;
}

.dossier-row + .dossier-row {
  margin-top: var(--space-xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--color-sepia-light);
}

.dossier-row:nth-child(even) {
  grid-template-columns: 1fr 13rem;
}

.dossier-row:nth-child(even) .dossier-row__frame {
  order: 2;
}

.dossier-row__name {
  font-family: var(--font-secondary);
  font-weight: 700;
  font-size: 1.4rem;
  margin: 0 0 0.9rem;
  color: var(--color-ink);
}

.dossier-row__label {
  font-size: 0.7rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--color-ink);
  margin: 0 0 0.3rem;
}

.dossier-row__text {
  font-size: 0.9rem;
  line-height: 1.65;
  color: var(--color-ink);
  margin: 0 0 1.1rem;
}

@media (max-width: 700px) {
  .dossier-row,
  .dossier-row:nth-child(even) {
    grid-template-columns: 1fr;
    text-align: center;
  }
  .dossier-row:nth-child(even) .dossier-row__frame {
    order: 0;
  }
  .dossier-row__frame {
    max-width: 11rem;
    margin: 0 auto;
  }
}


/* --- 54. TEXTURED CARD --------------------------------------------------------------
   A self-contained "paper card" that carries its own background image
   instead of the flat --color-paper — for content sitting on top of a
   page that has already replaced its own body background with a
   photographic texture (seo.html, systems.html, yoga.html's moodboard),
   where plain text with no card underneath reads as low-contrast
   against the busy photo. Same technique as .moodboard (section 48):
   images/systems background.jpg at background-size:cover, rounded
   corners, a two-layer shadow to read as a raised card. First used
   behind Link Building's list on The Art of Websites and SEO, where
   the list previously sat directly on this page's own seo background.jpg
   with nothing behind it.

   Markup: <div class="textured-card"> … any content … </div>

   .textured-card--solid swaps the texture for a flat --color-paper
   fill instead — first used on Link Building, per Kathryn's request
   for a plain cream block rather than the systems-background texture.
   Add the modifier alongside the base class:
   <div class="textured-card textured-card--solid"> … </div> */

.textured-card {
  background-image: url("../images/systems background.jpg");
  background-size: cover;
  background-position: center;
  border-radius: 20px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 16px 40px rgba(0, 0, 0, 0.12);
  padding: var(--space-xl) var(--page-gutter);
}

.textured-card--solid {
  background-image: none;
  background-color: var(--color-paper);
}


/* --- 55. CASE STUDY DOSSIER (SIDEBAR TABS) -------------------------------------------
   Case Studies reuses .client-dossier verbatim (section 31b, built for
   AI Editing's client list) — same sidebar-nav-plus-detail-panel
   pattern, just swapping the .link-list of blog posts for a single
   large screenshot placeholder + write-up per case study, since the
   real source material is a PDF of screenshots and explanations that
   hasn't been digitized into the site yet. Placeholder entries reuse
   the same real project names as Link Building (section 51) rather
   than invented ones, since these ARE those campaigns, just not yet
   written up in full.

   Markup, inside .client-dossier__detail's .image-tabs__panel — Brief /
   Duration / Notable Results per case study, screenshot last (once
   Kathryn has real ones to drop in):
   <h4 class="client-dossier__name">Project Name</h4>
   <p class="case-study-detail__label">Brief</p>
   <p class="case-study-detail__text">…</p>
   <p class="case-study-detail__label">Duration</p>
   <p class="case-study-detail__text">…</p>
   <p class="case-study-detail__label">Notable Results</p>
   <p class="case-study-detail__text">…</p>
   <div class="case-study-detail__shot">Screenshot placeholder</div> */

#panel-case-studies .client-dossier {
  max-width: var(--container-wide);
  margin-left: auto;
  margin-right: auto;
}

.case-study-detail__label {
  font-size: 0.85rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--color-ink);
  margin: 0 0 0.3rem;
}

/* Case Studies specifically wants extra breathing room under each
   project's h4 name, per Kathryn's request — scoped here rather than
   touching .client-dossier__name's shared margin, which AI Editing's
   client list (Writing and Editing) also relies on. */
#panel-case-studies .client-dossier__name {
  margin-bottom: 1.5rem;
}

.case-study-detail__text {
  font-size: 0.95rem;
  line-height: 1.7;
  color: var(--color-ink-soft);
  font-style: italic;
  max-width: 40rem;
  margin: 0 0 var(--space-md);
}

.case-study-detail__shot {
  width: 100%;
  aspect-ratio: 16 / 9;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--color-paper-deep);
  color: var(--color-ink-soft);
  font-family: var(--font-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-top: var(--space-md);
  border: 1px dashed var(--color-sepia-light);
  box-sizing: border-box;
}


/* --- 55b. CASE STUDY LEDGER ROW -------------------------------------------------------
   "Option B" (Ledger Row) from the SEO Stat Layouts comparison artifact —
   Kathryn's pick of the three. Sits inside a case study's
   .image-tabs__panel, between Notable Results and the screenshot: a single
   line of headline figures divided by hairline rules, no card around the
   whole thing, so it stays light under the prose above it. Flexes to
   however many figures a given case study has — two, four, whatever's
   real — since each stat is just a flex child with its own divider.

   Markup, straight after the Notable Results paragraph and before
   .case-study-detail__shot:
   <div class="ledger-row">
     <div class="ledger-row__stat">
       <p class="ledger-row__num">2.8K%</p>
       <p class="ledger-row__label">Total Clicks Increase</p>
     </div>
     … one .ledger-row__stat per figure …
   </div> */

.ledger-row {
  display: flex;
  flex-wrap: wrap;
  margin-top: var(--space-lg);
}

.ledger-row__stat {
  flex: 1 1 8rem;
  padding: 0 1.2rem;
  border-left: 1px solid var(--color-sepia-light);
  text-align: center;
}

.ledger-row__stat:first-child {
  border-left: none;
  padding-left: 0;
}

.ledger-row__num {
  font-family: var(--font-secondary);
  font-weight: 700;
  font-size: 1.85rem;
  color: var(--color-accent-deep);
  line-height: 1;
  margin: 0;
}

.ledger-row__label {
  margin: 0.5rem 0 0;
  font-size: 0.68rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
  line-height: 1.35;
}

@media (max-width: 460px) {
  .ledger-row__stat {
    flex: 1 1 45%;
    border-left: none;
    padding: 0.5rem 0;
    border-top: 1px solid var(--color-sepia-light);
  }
  .ledger-row__stat:first-child {
    border-top: none;
  }
}


/* --- 56. ICON BOX ROW, COMPACT MODIFIER ----------------------------------------------
   "Option A" from the Tools Bar comparison artifact — the same
   .icon-box-row/.icon-box (section 18) markup, just smaller: 72px
   icons down to 34px, tighter gaps, smaller uppercase labels. Add
   --compact alongside both base classes; no new markup shape needed.
   First used for The Art of Websites and SEO's Content Management
   Systems row.

   Markup:
   <div class="icon-box-row icon-box-row--compact">
     <div class="icon-box icon-box--compact">
       <img src="…" alt="" class="icon-box__icon">
       <p class="icon-box__label">Label</p>
     </div>
     …
   </div> */

.icon-box-row--compact {
  gap: 1.3rem 1.6rem;
  margin: var(--space-lg) 0;
}

.icon-box--compact {
  width: 4.6rem;
  gap: 0.35rem;
}

.icon-box--compact .icon-box__icon {
  width: 34px;
  height: 34px;
}

.icon-box--compact .icon-box__label {
  font-size: 0.62rem;
}


/* --- 57. ROLLING BAR ----------------------------------------------------------------
   "Option B" from the Tools Bar comparison artifact — a full-bleed
   strip of logos that scrolls continuously, like a "trusted by" logo
   marquee. The row of items is duplicated once in the markup so the
   loop can reset invisibly at the halfway point (translateX(-50%))
   instead of jumping. First used for The Art of Websites and SEO's
   Tools Used row — background went through two revisions per Kathryn's
   feedback: the artifact's own dark #3f4552 read as "the wrong grey"
   once live, and a first swap to --color-sepia wasn't right either, so
   it's now --color-sepia-light, an actual light grey already in the
   token set. Light background flips the item text/divider to dark
   (--color-ink / --color-border) instead of the artifact's cream-on-
   dark, and labels use --font-body (typewriter) instead of the
   artifact's original serif, also per Kathryn's request.

   Markup:
   <div class="rolling-bar">
     <div class="rolling-bar__track">
       <div class="rolling-bar__item">
         <img src="…" alt="" class="rolling-bar__icon">
         <p>Label</p>
       </div>
       … then the SAME set of items again, for the seamless loop …
     </div>
   </div> */

.rolling-bar {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  background: var(--color-sepia-light);
  padding: var(--space-md) 0;
  overflow: hidden;
}

.rolling-bar__track {
  display: flex;
  align-items: center;
  width: max-content;
  animation: rolling-bar-scroll 28s linear infinite;
}

@keyframes rolling-bar-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .rolling-bar__track {
    animation: none;
  }
}

.rolling-bar__item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0 var(--space-lg);
  border-right: 1px solid var(--color-border);
  flex: none;
}

.rolling-bar__icon {
  width: 28px;
  height: 28px;
  object-fit: contain;
  display: block;
  /* Forces every logo to solid black, per Kathryn's request — these are
     a mix of raster PNG favicons and multi-color SVGs, so a filter
     (rather than editing each source file's fill, as done for the CMS
     row's single-fill SVGs in section 56) is the one technique that
     works uniformly across all of them regardless of source format.
     Two icons (Phrase.io, SurferSEO) have an OPAQUE badge background
     rather than transparency around their mark — brightness(0) alone
     crushed badge and glyph to the same flat black square, erasing the
     shape. images/icons/phrase-mono.png and surferseo-mono.png are
     duplicates with that badge background made transparent so the
     glyph still reads once flattened to black; the originals are left
     untouched since Writing and Editing's Tools Used row still shows
     them in full color. */
  filter: brightness(0);
}

.rolling-bar__item p {
  margin: 0;
  font-family: var(--font-body);
  font-size: 0.8rem;
  color: var(--color-ink);
  white-space: nowrap;
}


/* --- 58. DOTTED LINE LIST -------------------------------------------------------------
   "Option C" from the Industries Bar comparison artifact — one
   centered small-caps line, items separated by middle dots, under a
   short rule. Same voice as .publication-line's "Vol. IV — End of
   Volume" (section 10a), just wrapped for an arbitrary list instead of
   two fixed spans. First used for The Art of Websites and SEO's
   industries-served list, under the SEO Experience paragraph.

   Markup:
   <div class="dotted-line-list">
     <div class="dotted-line-list__rule"></div>
     <p class="dotted-line-list__line">
       Item One<span class="dotted-line-list__sep">·</span>Item Two<span class="dotted-line-list__sep">·</span>…
     </p>
   </div> */

.dotted-line-list {
  max-width: 42rem;
  margin: var(--space-lg) auto 0;
  padding: 0 var(--page-gutter);
  text-align: center;
}

.dotted-line-list__rule {
  width: 3.5rem;
  height: 1px;
  background: var(--color-sepia-light);
  margin: 0 auto 1.1rem;
}

.dotted-line-list__line {
  font-family: var(--font-body);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
  line-height: 2.1;
  margin: 0;
}

.dotted-line-list__sep {
  color: var(--color-sepia-light);
  margin: 0 0.5em;
}

.dotted-line-list__item {
  white-space: nowrap; /* keeps a two-word item like "Pet Care" or "Co-working" from breaking mid-phrase when the line wraps */
}


/* --- 59. PILL CLOUD -------------------------------------------------------------------
   "Option A" from the Industries Bar comparison artifact — wrapped,
   rounded tags instead of the dotted-line-list's single sentence-like
   line (section 58, kept in case it's useful elsewhere; Kathryn felt
   the dotted line didn't read like the artifact once live, so this
   replaced it for the industries-served list under the SEO Experience
   paragraph).

   Markup:
   <div class="pill-cloud">
     <span class="pill">Item One</span>
     <span class="pill">Item Two</span>
     …
   </div> */

.pill-cloud {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.55rem;
  max-width: 42rem;
  margin: var(--space-lg) auto 0;
  padding: 0 var(--page-gutter);
}

.pill {
  font-family: var(--font-body);
  font-size: 0.72rem;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-accent-deep);
  background: var(--color-paper-deep);
  border: 1px solid var(--color-sepia-light);
  border-radius: 999px;
  padding: 0.4rem 0.9rem;
  white-space: nowrap;
}


/* --- 60. CONTACT ICON ROW --------------------------------------------------------------
   "Concept 02" from the Contact Me lightbox comparison artifact — a
   ringed icon per contact method (email, LinkedIn, Instagram) instead
   of the plain .link-list that was live before. Rings/icons/hover use
   var(--color-ink), matching the site's black ink rather than the
   maroon accent the artifact mocked it up in.

   Stacked vertically (icon left, label+value right) rather than side
   by side — a horizontal row left too little width for the full email
   address to sit on one line; full-width rows give it room.

   Markup:
   <div class="contact-icon-row">
     <a class="contact-icon" href="mailto:…">
       <span class="contact-icon__ring">…svg…</span>
       <span class="contact-icon__text">
         <span class="contact-icon__label">Email</span>
         <span class="contact-icon__value">…</span>
       </span>
     </a>
     …
   </div> */

.contact-icon-row {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  max-width: 22rem;
  margin: var(--space-lg) auto;
}

.contact-icon {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.9rem;
  width: 100%;
  text-decoration: none;
}

.contact-icon__ring {
  flex-shrink: 0;
  width: 48px; /* +20% over the original 40px, per Kathryn's request */
  height: 48px;
  border-radius: 50%;
  border: 1.5px solid var(--color-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-ink);
  transition: background-color 0.2s ease, color 0.2s ease;
}

.contact-icon__ring svg {
  width: 22px; /* +20% over the original 18px */
  height: 22px;
}

.contact-icon:hover .contact-icon__ring,
.contact-icon:focus-visible .contact-icon__ring {
  background-color: var(--color-ink);
  color: var(--color-paper);
}

.contact-icon__text {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  text-align: left;
  min-width: 0; /* lets __value wrap/break inside a flex child instead of forcing the row wider */
}

.contact-icon__label {
  margin: 0;
  font-family: var(--font-secondary);
  font-size: 0.84rem; /* +20% over the original 0.7rem, per Kathryn's request */
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-ink);
  text-align: left;
}

.contact-icon__value {
  margin: 0;
  font-family: var(--font-body);
  font-size: 0.86rem; /* +20% over the original 0.72rem */
  color: var(--color-ink-soft);
  text-align: left;
  word-break: break-word;
}
