.app-content {
  max-width: 960px;
  margin: 0 auto;
  padding: 30px 32px 70px;
  display: flex;
  flex-direction: column;
  gap: 30px;
}

@media (max-width: 599px) {
  .app-content {
    padding: 18px 18px 50px;
    gap: 24px;
  }
}

.eyebrow {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: rgba(23, 21, 26, 0.45);
  margin: 0 0 4px;
}

.section-title {
  font-size: 22px;
  font-weight: 700;
  margin: 0;
  letter-spacing: -0.01em;
}

.section-sub {
  font-size: 14px;
  color: rgba(23, 21, 26, 0.6);
  margin: 4px 0 0;
  line-height: 1.5;
}

/* ===== Placeholder tiles (until real visuals are dropped in) ===== */
.ph-tile {
  --hue: 260;
  position: relative;
  display: flex;
  align-items: flex-end;
  border-radius: 16px;
  overflow: hidden;
  background: linear-gradient(
    135deg,
    hsl(var(--hue) 70% 58%),
    hsl(calc(var(--hue) + 46) 75% 38%)
  );
  color: #fff;
  min-height: 90px;
}

.ph-tile::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    circle at 30% 20%,
    rgba(255, 255, 255, 0.35),
    transparent 55%
  );
}

.ph-tile__label {
  position: relative;
  padding: 10px 12px;
  font-size: 12px;
  font-weight: 600;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

/* ===== Buttons / pills ===== */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 600;
  transition: transform 0.15s var(--ease-out-soft), background 0.15s;
}

.btn:active {
  transform: scale(0.96);
}

.btn--primary {
  background: #17151a;
  color: #fff;
}

.btn--primary:hover {
  background: #33303a;
}

.btn--ghost {
  background: rgba(23, 21, 26, 0.08);
  color: #17151a;
}

.btn--ghost:hover {
  background: rgba(23, 21, 26, 0.14);
}

.pill-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.pill {
  padding: 7px 14px;
  border-radius: 999px;
  background: rgba(23, 21, 26, 0.08);
  font-size: 12.5px;
  font-weight: 600;
  color: rgba(23, 21, 26, 0.75);
}

/* ===================== ABOUT ===================== */
/* "Ciavbot" fake chat — a fixed header, a scrollable thread of question/
   answer bubbles (yours on the right, the bot's on the left), and a row of
   topic pills pinned at the bottom that each append one exchange to the
   thread (see wireAppInteractions() in render.js). */
.about-app {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: #000;
}

/* Header styled like iOS Messages' own contact view: a centered circular
   avatar with the name below it, instead of a left-to-right app titlebar
   row. The language toggle floats in the corner instead, out of the way. */
.about-chatbot-header {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 8px 20px 6px;
  background: #000;
}

.about-chatbot-avatar {
  width: 78px;
  height: 78px;
  border-radius: 50%;
  object-fit: cover;
  background: transparent;
}

.about-chatbot-name {
  font-weight: 600;
  font-size: 13px;
  color: rgba(255, 255, 255, 0.75);
}

.about-chatbot-header .lang-toggle {
  position: absolute;
  top: 10px;
  right: 16px;
  padding: 6px 14px;
  font-size: 12.5px;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
}

.about-chatbot-header .lang-toggle:hover {
  background: rgba(255, 255, 255, 0.2);
}

.about-thread {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 20px;
  background: #000;
  scroll-behavior: smooth;
}

/* iMessage-style bubbles: blue for what you asked (sent, right), light grey
   for Ciavbot's answer (received, left) — each with the little tail that
   bubble style is known for, drawn with two overlapping circles (the
   bubble's own color, then a page-background-color circle that "erases"
   part of it into a curved notch). */
.about-bubble {
  position: relative;
  z-index: 0; /* scopes the tail's negative z-index to this bubble alone */
  max-width: 75%;
  padding: 10px 16px;
  border-radius: 18px;
  font-size: 15px;
  line-height: 1.4;
  white-space: pre-line;
}

.about-bubble--bot {
  align-self: flex-start;
  background: #8b0000;
  color: #fff;
}

.about-bubble--bot::before {
  content: "";
  position: absolute;
  z-index: -1;
  bottom: 0;
  left: -6px;
  width: 18px;
  height: 20px;
  background: #8b0000;
  border-bottom-right-radius: 15px;
}

.about-bubble--bot::after {
  content: "";
  position: absolute;
  z-index: -1;
  bottom: -2px;
  left: -10px;
  width: 12px;
  height: 22px;
  background: #000;
  border-bottom-right-radius: 10px;
}

.about-bubble--user {
  align-self: flex-end;
  background: #0b84ff;
  color: #fff;
}

.about-bubble--user::before {
  content: "";
  position: absolute;
  z-index: -1;
  bottom: 0;
  right: -6px;
  width: 18px;
  height: 20px;
  background: #0b84ff;
  border-bottom-left-radius: 15px;
}

.about-bubble--user::after {
  content: "";
  position: absolute;
  z-index: -1;
  bottom: -2px;
  right: -10px;
  width: 12px;
  height: 22px;
  background: #000;
  border-bottom-left-radius: 10px;
}

/* The 5 "quick reply" options read as their own stack of (unsent) bot
   bubbles — same maroon, same tail — rather than a row of pill buttons,
   per the PDF mockup (Ex visuel interface CIAVbot.pdf). */
.about-options {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  padding: 14px 20px 20px;
  background: #000;
}

.about-option {
  position: relative;
  z-index: 0;
  max-width: 75%;
  padding: 10px 16px;
  border-radius: 18px;
  background: #8b0000;
  color: #fff;
  font-size: 14px;
  line-height: 1.4;
  text-align: left;
  transition: background 0.15s, transform 0.15s var(--ease-out-soft);
}

.about-option::before {
  content: "";
  position: absolute;
  z-index: -1;
  bottom: 0;
  left: -6px;
  width: 18px;
  height: 20px;
  background: #8b0000;
  border-bottom-right-radius: 15px;
}

.about-option::after {
  content: "";
  position: absolute;
  z-index: -1;
  bottom: -2px;
  left: -10px;
  width: 12px;
  height: 22px;
  background: #000;
  border-bottom-right-radius: 10px;
}

.about-option:hover,
.about-option:hover::before {
  background: #a40000;
}

.about-option:active {
  transform: scale(0.97);
}

/* ===================== CONTACT ===================== */
/* Full-bleed, per the PDF: the yellow card art fills the entire window.
   Each field is a visible pill button over the art (copy / open Instagram
   / call) — a clear tap target, not just the invisible text underneath. */
.contact-app {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  background-color: #ffcf30;
  /* Two layers of the SAME source canvas (both cropped identically, so
     `cover` scales/positions them in lockstep): the halftone typography
     art on top, the cupids paper photo underneath — this is the client's
     own "type" file meant to sit registered on the angels photo, not
     CSS-rendered text. */
  background-image:
    url("../assets/images/contact/typo.webp"),
    url("../assets/images/contact/cupids.webp");
  background-size: cover, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-evenly;
  padding: 40px 24px;
  text-align: center;
}

/* Fully invisible hit target, not a visible pill — the real typography is
   already baked into the art underneath (see .contact-field__label/__value
   below), and a visible pill never quite lined up with it pixel-for-pixel
   across every window size/aspect ratio anyway. The generous width/height
   here is sized to comfortably cover each line without needing to match it
   exactly, since nothing about it is ever shown. */
.contact-field {
  background: transparent;
  border: none;
  border-radius: 999px;
  padding: 14px 28px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: min(86%, 420px);
  max-width: 100%;
  min-height: 15%;
  justify-content: center;
  transition: transform 0.15s var(--ease-out-soft);
}

/* Title bar matches the yellow card instead of the generic glass default. */
.app-window[data-app-id="contact"] .app-window__titlebar {
  background: rgba(255, 207, 48, 0.24);
}

.contact-field:active {
  transform: scale(0.97);
}

/* Mobile: a single pre-composed portrait image (cupids + typography already
   baked together, laid out for a phone screen) replaces the desktop
   two-layer cover crop — each field's position is set to match where that
   image's own baked-in text actually sits (measured from the source file),
   not evenly spaced like the desktop layout. `background-position: top`
   keeps the cupids clear of the floating mobile titlebar/close button
   regardless of device aspect ratio — top-anchoring is never cropped,
   whichever dimension ends up needing it. */
@media (max-width: 1023px) {
  .contact-app {
    background-image: url("../assets/images/contact/phone-bg.webp");
    background-size: cover;
    background-position: center top;
  }

  .contact-field {
    position: absolute;
    left: 50%;
    top: var(--contact-field-top);
    transform: translate(-50%, -50%);
    min-height: auto;
    /* Wider than the desktop pill (min(86%, 420px)) — the email line's
       baked-in text nearly touches the image's own edges, so the shorter
       desktop width clips its ends here. */
    width: min(94%, 420px);
  }

  .contact-field:nth-child(1) {
    --contact-field-top: 70.1%;
  }

  .contact-field:nth-child(2) {
    --contact-field-top: 78%;
  }

  .contact-field:nth-child(3) {
    --contact-field-top: 85.8%;
  }
}

/* Desktop: same idea as the mobile block above — `space-evenly` never
   actually lined the pills up with the two-layer typo/cupids art's own
   baked-in text, it just spaced them evenly top to bottom. Positions
   measured from typo.webp directly (2185×1197): the window's aspect ratio
   is always narrower than the art's own (1.83), so `cover` only ever crops
   left/right, never top/bottom — meaning these top % values need no
   per-viewport adjustment, unlike a horizontal measurement would. Widths
   are generous (not pixel-exact) since how much gets cropped off the sides
   does shift with the window's exact aspect ratio. */
@media (min-width: 1024px) {
  .contact-field {
    position: absolute;
    left: 50%;
    top: var(--contact-field-top-desktop);
    transform: translate(-50%, -50%);
    min-height: auto;
    width: var(--contact-field-width-desktop);
  }

  .contact-field:nth-child(1) {
    --contact-field-top-desktop: 23.85%;
    --contact-field-width-desktop: 98%;
  }

  .contact-field:nth-child(2) {
    --contact-field-top-desktop: 50.46%;
    --contact-field-width-desktop: 38%;
  }

  .contact-field:nth-child(3) {
    --contact-field-top-desktop: 76.82%;
    --contact-field-width-desktop: 58%;
  }
}

/* The label/value text stays in the DOM (screen readers, click target
   sizing) but is invisible — the halftone "typo" layer above shows the
   real typography, already baked at the right position/style. */
.contact-field__label,
.contact-field__value {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

.contact-copied-toast {
  position: absolute;
  left: 50%;
  bottom: 28px;
  transform: translate(-50%, 8px);
  padding: 8px 18px;
  border-radius: 999px;
  background: #201700;
  color: #ffe14d;
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.01em;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s var(--ease-out-soft), transform 0.2s var(--ease-out-soft);
}

.contact-copied-toast.is-visible {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* ===================== VIDEO ===================== */
/* Per the PDF: the big mockup is the desktop view (video fills the whole
   window, slide to the next one) and the separate small vertical mockup
   next to the menu icons is the mobile view (a plain scrollable list, each
   clip ~1/3 of the screen tall) — two different layouts, not one responsive
   scale of the other. */
.video-app {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* Shared "frame": one video slot, whichever layout it's in — the poster,
   iframe, play button and meta overlay all fill it edge to edge. */
.video-scroll-slide,
.video-mobile-item {
  position: relative;
  width: 100%;
  height: 100%;
  padding: 0;
  gap: 0;
  display: block;
  overflow: hidden;
  background: #0b0b0d;
}

.video-stage__poster,
.video-scroll-slide iframe,
.video-scroll-slide video,
.video-mobile-item iframe,
.video-mobile-item video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
}

.video-stage__play {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.15);
  border: none;
  padding: 0;
  cursor: pointer;
}

/* Desktop is silent/automatic (see wireVideoScroll()) — no play button. */
@media (min-width: 1024px) {
  .video-scroll-slide .video-stage__play {
    display: none;
  }
}

.video-stage__play-btn {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #17151a;
}

.video-mute-toggle {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 1;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
}

.video-mute-toggle:hover {
  background: rgba(0, 0, 0, 0.7);
}

/* Covers the whole stage so tapping/clicking anywhere on the clip opens its
   YouTube page — sits under the mute button (which has its own, higher
   stacking position) so that control stays independently clickable. */
.video-external-link {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.video-stage__meta {
  position: absolute;
  left: 18px;
  bottom: 19px;
  right: 18px;
  pointer-events: none;
}

.video-stage__meta strong {
  display: block;
  font-size: 23px;
  line-height: 1.3;
  margin-bottom: 5px;
  color: #fff;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}

.video-stage__meta span {
  font-size: 13px;
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}

/* ===== Desktop (≥1024px): division.global-style silent reel — plain native
   vertical scroll (no drag engine, so nothing to stutter), one full-bleed
   clip per snapped section. Whichever slide is in view silently autoplays
   muted (see wireVideoScroll() in render.js); scrolling past it swaps it
   back to a static poster. ===== */
.video-scroll {
  display: none;
}

@media (min-width: 1024px) {
  .video-scroll {
    display: block;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    scroll-snap-type: y mandatory;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
  }

  .video-scroll-slide {
    scroll-snap-align: start;
    scroll-snap-stop: always;
  }
}

/* ===== Phone/tablet (<1024px): plain scrollable list ===== */
.video-mobile-list {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

@media (min-width: 1024px) {
  .video-mobile-list {
    display: none;
  }
}

.video-mobile-item {
  flex: 0 0 33.333%;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.scroll-hint {
  font-size: 12px;
  color: rgba(23, 21, 26, 0.45);
}

.photo-tile {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: 0;
  background: rgba(23, 21, 26, 0.06);
  transition: transform 0.15s var(--ease-out-soft);
  -webkit-user-drag: none;
  user-select: none;
  scroll-snap-align: start;
}

/* Fashion reuses .photo-tile (for its photoGrid()/shop-link handling) inside
   the same full-width page-stack Storyboard/Editorial use — the fixed 4:5
   crop above was for the old carousel grid and doesn't apply here; each
   photo just stacks at its own natural height like any other page. */
.storyboard-pages-stack .photo-tile {
  aspect-ratio: auto;
  object-fit: initial;
  height: auto;
}

.photo-tile-link {
  display: block;
  scroll-snap-align: start;
}

.photo-tile-link:hover .photo-tile,
.photo-tile-link:focus-visible .photo-tile {
  transform: scale(0.97);
}

/* ===================== SWITCHER SHELL (Fashion, Storyboard, Editorial) ===================== */
/* A "deck of cards" the user can drag through — see js/render.js wireSwitcher(). */
.switcher-app {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.project-switcher {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex: 0 0 auto;
  padding: 20px 32px 14px;
}

@media (max-width: 599px) {
  .project-switcher {
    padding: 14px 18px 10px;
  }
}

/* Storyboard: title + credit, at arrow level instead of inside each card —
   frees up the whole card for pages. */
.project-switcher__info .section-title {
  font-size: 18px;
}

.project-switcher__info .section-sub {
  margin-top: 2px;
}

.project-switcher__nav {
  display: flex;
  gap: 8px;
  flex: 0 0 auto;
}

.icon-btn {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: rgba(23, 21, 26, 0.08);
  display: flex;
  align-items: center;
  justify-content: center;
}

.icon-btn:hover {
  background: rgba(23, 21, 26, 0.14);
}

/* The switcher's outer deck owns the vertical axis entirely (drag, swipe, AND
   mouse wheel — see wireSwitcher()), so it never falls back to native touch
   scrolling. */
.switcher-viewport {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  position: relative;
  touch-action: none;
}

/* Storyboard only: the outer deck switches projects horizontally, but
   vertical touch movement must still reach the current card's native
   page-scroll (.storyboard-pages-stack) instead of being swallowed here —
   pan-y lets the browser handle vertical panning while horizontal stays
   free for this deck's own drag engine (see wireSwitcher() in render.js). */
.switcher-viewport--pan-y {
  touch-action: pan-y;
}

.switcher-track {
  display: flex;
  flex-direction: row;
  width: 100%;
  height: 100%;
  cursor: grab;
}

@media (max-width: 1023px) {
  .switcher-track {
    flex-direction: column;
  }
}

/* Fashion: always vertical (project switching), regardless of breakpoint —
   unlike Video's desktop deck, which stays horizontal. Declared after the
   mobile media query above so it wins at every width. */
.switcher-track--vertical {
  flex-direction: column;
}

/* Storyboard: always horizontal (project switching) at every breakpoint —
   vertical is reserved for the page-scroll inside each card. */
.switcher-track--horizontal {
  flex-direction: row;
}

/* No app puts raw title/description text as a direct child of a card
   anymore (Fashion/Editorial overlay it on the cover photo, Storyboard
   dropped it) — every card is edge-to-edge, no padding/gap. The one
   survivor is Fashion's "tap a photo to shop" hint, which still wants its
   own inset. */
.switcher-slide {
  flex: 0 0 100%;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.switcher-slide > .scroll-hint {
  flex: 0 0 auto;
  padding: 10px 32px 14px;
}

@media (max-width: 599px) {
  .switcher-slide > .scroll-hint {
    padding: 8px 18px 12px;
  }
}

/* A coachmark overlaid ON the project itself (not a caption pushing it
   down) — see dismissHint()/wireSwitcher() in render.js: fades on its own
   after a few seconds, or immediately once the visitor actually switches a
   project, whichever comes first. */
.switcher-app {
  position: relative;
}

.swipe-hint {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 3;
  max-width: calc(100% - 48px);
  padding: 10px 20px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #fff;
  font-size: 12px;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  opacity: 1;
  transition: opacity 0.5s ease;
}

.swipe-hint.is-hidden {
  opacity: 0;
}

@media (max-width: 599px) {
  .swipe-hint {
    font-size: 11px;
    padding: 8px 16px;
  }
}

/* ===== Storyboard: current project's pages, stacked full-width, one
   continuous native scroll — no swipe/re-render between pages (that's what
   caused the old "stutter"). Like a manga-reader chapter: page 2 starts
   right where page 1 ends, no gap, no snap. ===== */
.storyboard-pages-stack {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* A persistent, always-visible scroll indicator — the native scrollbar is
   hidden outright, even with the usual ::-webkit-scrollbar override to opt
   out of overlay auto-hide, some browsers (Safari in particular) still only
   reveal it while actively scrolling. A custom track+thumb drawn in JS (see
   wireCustomScrollbar() in render.js) replaces it instead, rebuilt fresh
   after every project switch since that markup gets replaced wholesale.
   Shared by Storyboard/Fashion/Editorial (.storyboard-pages-stack) and
   Video (.video-scroll). Positioned unconditionally (so the JS-created
   elements never disrupt layout at any breakpoint) but only shown on
   desktop — mobile already has its own native touch-scroll feedback. */
.storyboard-pages-stack,
.video-scroll {
  scrollbar-width: none;
}

.storyboard-pages-stack::-webkit-scrollbar,
.video-scroll::-webkit-scrollbar {
  display: none;
}

.custom-scrollbar {
  position: absolute;
  top: 8px;
  bottom: 8px;
  right: 4px;
  width: 5px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.08);
  z-index: 2;
  pointer-events: none;
  display: none;
}

.custom-scrollbar__thumb {
  position: absolute;
  left: 0;
  right: 0;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.3);
}

@media (min-width: 1024px) {
  .custom-scrollbar {
    display: block;
  }

  .custom-scrollbar[hidden] {
    display: none;
  }
}

.storyboard-pages-stack img {
  width: 100%;
  height: auto;
  display: block;
  -webkit-user-drag: none;
  user-select: none;
}

.storyboard-pages-peek img {
  width: 100%;
  height: auto;
  display: block;
  -webkit-user-drag: none;
  user-select: none;
}

/* Editorial only: title + credit as a sticky bar pinned to the BOTTOM of the
   page-stack's own scroll area — stays visible in the same spot the whole
   time you scroll through a project. White text with a shadow (same as
   Video's .video-stage__meta), no solid background — it reads over
   whichever photo currently sits behind it. Sticks relative to
   .storyboard-pages-stack (its nearest scrolling ancestor), so no extra
   positioning context needed there. */
.editorial-sticky-meta {
  position: sticky;
  bottom: 0;
  z-index: 2;
  padding: 24px 20px 16px;
  pointer-events: none;
}

.editorial-sticky-meta strong {
  display: block;
  font-size: 18px;
  color: #fff;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}

.editorial-sticky-meta span {
  display: block;
  font-size: 13px;
  margin-top: 2px;
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}

/* Test: the rest of a project's (portrait) photos, 2 per row instead of
   one full-width image at a time. */
.editorial-grid-2up {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3px;
}

/* A trailing photo with no pair spans the full row instead of leaving the
   other half blank next to it. */
.editorial-grid-2up__solo {
  grid-column: 1 / -1;
}

.editorial-grid-2up img {
  width: 100%;
  height: auto;
  display: block;
  -webkit-user-drag: none;
  user-select: none;
}

.storyboard-pages-peek {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  opacity: 0.9;
}

/* ===================== ILLUSTRATION ===================== */
/* No title, no hero — every real piece "en vrac", edge to edge, hierarchized
   by the client's own grande/moyenne/petite classification (see
   illustration.gallery in content.js): the tier sets each tile's WIDTH
   honestly (large = a full row, medium and small narrower under it) — never
   its height, which always comes from that image's own natural aspect
   ratio, so nothing is ever cropped. Chosen over a zero-gap justified-row
   layout specifically so a tile's rendered size stays truthful to its
   declared tier; the trade-off is that real gaps can appear where a short
   column has no legal neighbor to close it. All the placement math is in
   layoutIllustrationMasonry() in render.js. */
.illustration-masonry {
  position: relative;
}

.illustration-tile {
  position: absolute;
  display: block;
  /* No-op once real dimensions are loaded (the box is already sized to the
     image's own aspect ratio) — only matters for the brief pre-decode
     instant that uses the 1:1 fallback in layoutIllustrationMasonry(). */
  object-fit: cover;
}
