/* ==========================================================================
   home.css — default landing hero (SVG logotype) + header logo mark +
   work (sticky/scroll) section. This design was built and tested as
   home-alt.html/css/js before being promoted to be the site's default;
   the previous 4-line-headline design was removed as part of the
   pre-launch cleanup.
   ========================================================================== */

/* Satoshi is now the site-wide font (@font-face + body rule live in
   style.css) — was a homepage-only preview scoped here, promoted per
   direct request. Nothing homepage-specific left to do here; the
   explicit "Satoshi" font-family below on .hero__based-line/.work__desc/
   .work__meta is redundant now that body itself is Satoshi, but left in
   place rather than stripped out, since it's harmless and documents
   intent locally. */

/* ---------- animated noise overlay (EXPERIMENTAL — homepage only) ----------
   Reverse-engineered from 2xa.studio/contact's own .noise div, per direct
   request — same general technique, not a copy of their actual asset
   (this uses its own generated tile, assets/noise.png): a tiny (100x100)
   tile of random static, repeated via the browser's normal background-
   image tiling, blended into the page with mix-blend-mode + low opacity
   so it reads as grain, "animated" for free with no per-frame image
   swapping — a steps(1) keyframe jumps background-position to a new
   random offset within the tile every ~180ms, re-tiling the same static
   image into a different-looking arrangement each jump.

   Two rounds of live-tested fixes on top of the first pass:

   1) The first version animated `transform: translate/rotate` on a
      div sized ~2048px (well past the viewport on every edge, matching
      2xa's own oversizing). That's unnecessary here — this tile repeats
      infinitely via background-repeat, so translating/rotating it can
      never reveal a bare edge regardless of element size, which was the
      only reason for the oversizing in the first place. Worse, animating
      a *transform* on that large an offscreen-overflowing fixed layer
      visibly glitched in testing (a hard vertical seam mid-animation —
      a compositor artifact from Chromium only partially recompositing
      such a large transformed+blended layer some frames). Animating
      `background-position` instead needs none of that: the element is
      simply inset:0 (viewport-sized), and only the tiny background
      image's offset changes each step — no transform, no oversized
      layer, no glitch.

   2) Opacity 0.3 (matching 2xa's own measured value) read far too
      strong here — turned down hard, see .noise below.

   3) The big "SAIM ALSHAFI" wordmark isn't page content — it's
      .site-header--logo's own SVG (see the "header logo mark" section
      below), position:fixed + mix-blend-mode:difference, per the
      site's existing "legible over anything" header treatment (see
      .site-header in style.css). A difference-blended element samples
      the actual rendered pixels behind it to invert against, regardless
      of z-index stacking order — so even though this noise layer is
      genuinely BEHIND the header in paint order, the header's own blend
      math still "reads" the grain through to itself, which is why it
      visibly picked up texture in testing while plain (non-blended)
      text elsewhere didn't. There's no stacking fix for that — it's
      inherent to how mix-blend-mode:difference works — so the practical
      answer is the same low-opacity tuning from point 2: quiet enough
      that what bleeds into the header logo is negligible.

   z-index: -1 is what gets this "above the background, below the text"
   for everything else — .page (style.css) is already position:relative
   + z-index:2, its own local stacking context; being this element's
   nearest positioned ancestor (and this div being placed as .page's own
   first child, see the markup comment in index.html) means -1 here is
   evaluated INSIDE that context, not against the page at large. A
   stacking-context root's own background always paints first, before
   ANY descendant — even a negative-z-index one — while normal,
   non-positioned in-flow content (the hero text, work titles, all of
   it) paints afterward, on top. */
.noise {
  position: fixed;
  z-index: -1;
  inset: 0;
  background-image: url("../assets/noise.png");
  opacity: 0.09; /* was 0.3, then 0.05 (too subtle) — bumped back up */
  mix-blend-mode: hard-light;
  pointer-events: none;
  animation: noise 1.8s steps(1) infinite;
}

/* Random offsets within the 100x100 tile (see assets/noise.png) — each
   step just re-tiles the same static image from a different starting
   point, which is what actually reads as "flickering" frame to frame. */
@keyframes noise {
  0% { background-position: 0px 0px; }
  10% { background-position: -37px 12px; }
  20% { background-position: 64px -28px; }
  30% { background-position: -12px -63px; }
  40% { background-position: 41px 55px; }
  50% { background-position: -78px 3px; }
  60% { background-position: 19px -47px; }
  70% { background-position: -55px 71px; }
  80% { background-position: 83px 26px; }
  90% { background-position: -29px -81px; }
  100% { background-position: 0px 0px; }
}

/* ---------- hero / landing ----------
   No in-flow content — the "headline" is now the header's SVG logo mark
   (see the "header logo mark" section below), which lives in .site-header
   and visually overlaps this section on scroll via a pure CSS transform,
   not by living inside .hero itself. .hero__bottom is absolutely
   positioned (see below), so this section has zero in-flow children;
   the padding/flex-centering rules below are inherited from the
   original design and are harmless no-ops here. */
.hero {
  position: relative;
  height: 100vh; /* fallback for browsers without dvh support */
  height: 100dvh; /* dynamic viewport height — shrinks/grows with the
    mobile browser's show/hide toolbar chrome instead of being sized off
    the largest possible viewport, which was clipping content on mobile */
  min-height: 100vh;
  min-height: 100dvh;
  width: 100%;
  padding-inline: var(--margin);
  padding-top: 6.3125rem;
  padding-bottom: 8.125rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.hero__bottom {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--margin);
  padding-inline: var(--margin);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  /* Was keeping this above the liquid-cursor canvas that used to mount
     into #hero as its last child on this page — removed per direct
     request, and the module file itself was later deleted in the
     pre-launch cleanup. Left in place since it's harmless. */
  z-index: 1;
}

/* Replaces the old 4-line headline: one bold paragraph sits where the
   headline used to read, left-aligned opposite .hero__based on the
   right. Both fade out together on scroll (see initHeroScrollExit in
   home.js) — the established "opacity is fine on scroll-EXIT, never on
   reveal-IN" exception, same one already used for hero__based's fade in
   the classic design. */
.hero__paragraph {
  font-size: 2.5rem; /* 40px — was 32px */
  font-weight: 500; /* medium — was 600 semibold (bold pass) */

  line-height: 1.25; /* was 1.15 — bumped to fix Satoshi descender clipping (g/y/p): measured Satoshi's own font-metrics box (ascent+descent) at ~1.25× font-size, vs PP Neue Montreal's ~1.11×, so anything tighter than 1.25 clips descenders inside the overflow:hidden reveal mask */
  letter-spacing: -0.01em;
  max-width: 46rem; /* was 34rem, sized for the 32px version — widened to
    keep the same 3-line break ("...has shipped / impactful products for
    over 6 years / across multiple countries.") at the larger 40px size,
    per direct reference screenshot; found by testing width values
    against a fresh page load per value (TextReveal splits lines into
    fixed-width DOM nodes at load time, so injecting a width change on an
    already-split page doesn't reflow it — each candidate needs its own
    load). */
}

/* Four manually-broken lines (Based in / each city / Düsseldorf) — each
   its own element rather than one paragraph with <br>s, since the
   line-reveal splitter groups words by offsetTop and <br>-forced breaks
   sit right at the edge of that detection, occasionally producing a
   spurious empty "line" (confirmed while testing this).
   18pt / semibold per spec (not the extrabold used for the big hero
   text). text-align:right (and the flex column's own natural
   stretch-to-widest-line width) keeps every line flush to a shared
   right edge — matching a block anchored to the right side of the row,
   rather than the ragged-right look left-aligned text would give it. */
.hero__based {
  display: flex;
  flex-direction: column;
  text-align: right;
}

.hero__based-line {
  font-size: 1.125rem; /* 18px, per direct request — was 1rem/16px (the site-wide body-text standardization pass had brought this down to 16px along with everything else; bumped back up to 18px specifically here per direct follow-up) */
  font-weight: 500; /* medium — was 600 semibold */
  line-height: 1.3; /* per direct request — was 1.25 */
  font-family: "Satoshi", sans-serif; /* Satoshi preview, per direct request — was "PP Neue Montreal" */
  letter-spacing: -0.01em;
}

.hero__based s {
  color: var(--black);
  text-decoration-thickness: 1px;
  transition: color 0.4s var(--ease-out);
}

/* ---------- header logo mark ----------
   justgowiththeflow.com/services-style: one single SVG mark lives in the
   header at all times, authored at its real resting size (24px tall,
   docked into the header row exactly like the text logo it replaces),
   then visually blown up to hero scale with a pure CSS transform so it
   reads as a giant landing wordmark at the top of the page. Because
   transforms never affect layout, the header's flex row (nav pinned
   top-right via justify-content:space-between) never moves or reflows
   no matter how large the visual scale gets — only paint size changes.
   transform-origin: 0 0 anchors that scaling to the mark's own top-left
   corner (which sits at the header's normal var(--margin) inset), so
   growing/shrinking never shifts that corner — exactly the "same
   top-left anchor" requirement. */
.logo--mark {
  display: inline-flex;
  align-items: flex-start;
}

/* The scale target: GSAP scrubs this from 13 -> 1 as the hero scrolls
   out (see initHeroScrollExit in home.js). 13 is not arbitrary — the
   source logotype's own viewBox is 983x312 units, and 312 design-px /
   24px resting height = 13 exactly, so scaling this wrapper by 13 while
   the SVG itself is authored at a fixed 24px resting height reproduces
   the logotype at its native, full-detail hero size with no extra
   positioning math. transform is also the static rest-state/no-JS
   fallback here (same convention as .pg-card__image-inner's
   transform:scale(1.35) fallback elsewhere) — without JS the page still
   shows *something* correct, just without the scroll-driven shrink. */
.logo-scale {
  display: inline-block;
  transform-origin: 0% 0%;
  transform: scale(13);
}

/* Per-glyph reveal: each letter/shape in the SVG is its own
   <clipPath>-masked group (see the <defs>/<g> structure in
   index.html) — SVG has no overflow:hidden, so clipPath stands in
   for what .line-mask does for text elsewhere on the site. Each
   .logo-glyph inner group starts offset below its own clip window (a
   plain SVG transform="translate(0,N)" in the markup, no-JS fallback)
   and GSAP staggers them back to translate(0,0) left-to-right in
   home.js's initHeaderAndHeroIntro() — "element by element," rather
   than the whole mark sliding up as one block. */
.logo-svg {
  display: block;
  height: 1.5rem; /* 24px resting/docked height, per spec */
  width: auto;
}

/* ---------- work / scroll section ---------- */
.work {
  position: relative;
  width: 100%;
  align-items: start;
}

.work__sticky-col {
  grid-column: 1 / 5;
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  /* Back to space-between, per direct request: counter+title (top group,
     .work__content) pin to the top like the original layout, while desc+meta
     (bottom group, .work__bottom) pin to the bottom as their own unit —
     only their prior single-block bottom-alignment was wrong, not the
     top/bottom split itself. The flexible gap this creates sits between
     the title and the desc; the fixed 32px gap between desc and meta is
     controlled by .work__desc's margin-bottom below. */
  justify-content: space-between;
  padding-top: 6rem; /* header clearance — was 9.25rem/148px (tuned for an exact 40px gap below the header), set directly to 6rem/96px per direct request */
  padding-bottom: var(--margin);
}

.work__content {
  overflow: hidden; /* clips the swap-out/in transforms */
}

/* Bottom group: body copy + client/city/year meta line, moved down and
   kept together as a unit (32px gap between them via .work__desc's
   margin-bottom) — counter+title stay in .work__content above, pinned to
   the top by .work__sticky-col's space-between. */
.work__bottom {
  overflow: hidden; /* clips the swap-out/in transforms, same as .work__content */
}

.work__title {
  /* Matches .hero__paragraph's typographic settings exactly (2rem/32px,
     800, 1.15, -0.01em), per direct request — was 2.5rem/40pt/1.2
     line-height originally.
     max-width is exactly 4 grid columns, computed from the 12-col grid
     math, not an eyeballed value chosen to dodge a particular line break —
     text placed in the grid should fill the columns it's assigned to,
     per direct instruction. This happens to equal .work__sticky-col's
     own natural width (also 4 columns), so in practice it's the same
     as leaving max-width unconstrained here — set explicitly anyway so
     the intent (4 columns) is documented and stays correct even if the
     sticky column's own span ever changes. */
  font-size: 2.5rem; /* 40px — was 32px */
  font-weight: 500; /* medium — was 600 semibold (bold pass) */

  line-height: 1.25; /* was 1.15 — bumped to fix Satoshi descender clipping, same reasoning as .hero__paragraph above */
  letter-spacing: -0.01em;
  margin-bottom: 1.5rem;
  max-width: 33.3333rem; /* 4 columns: 4×7.2083rem col + 3×1.5rem gutter */
  /* Was text-wrap:balance — switched to normal (greedy) wrap per direct
     request: balance kept every line closer to equal length, but that
     meant a word like "new" could get pushed to the next line even with
     visible room left on the one above, which read as wasted column
     width. Normal wrap fills each line as full as it'll go before
     breaking instead. Trade-off to know about: balance was originally
     added because, without it, this exact title/width pair produced a
     4th line with a single orphaned word ("new") at some viewport
     widths in an 11-viewport sweep (1000-1728px) — that risk is back
     now that balance is off. If an orphan line like that turns up again
     at some width, this is the first place to look. */
  text-wrap: wrap;
}

.work__desc {
  font-family: "Satoshi", sans-serif; /* Satoshi preview, per direct request — was "PP Neue Montreal" */
  font-size: 1rem; /* 16px, per direct request — was 1.125rem/18pt */
  font-weight: 500;
  line-height: 1.3; /* per direct request — was 1.22 (measured off the design's wrapped paragraph lines) */
  color: #BABABA; /* per direct request */
  /* 3 grid columns wide, computed from the same formula as .work__title
     above — was 26rem, a rough guess rather than a true column width. */
  max-width: 24.625rem; /* 3 columns: 3×7.2083rem col + 2×1.5rem gutter */
  margin-bottom: 2rem; /* 32px — exact gap above .work__meta, per direct request (was 1.75rem) */
  /* Switched to normal (greedy) wrap, per direct request — same reasoning
     as .work__title above: balance could leave visible slack on a line
     while pushing a word down. Same orphan-line trade-off applies here
     too — if a single word ends up alone on its own line at some
     viewport width, this is the first place to look. */
  text-wrap: wrap;
}

/* Now a single line — client name / city / year — sitting where the
   counter used to (bottom of .work__sticky-col, pinned there by its
   parent's justify-content:space-between), swapped with the counter's
   old position per direct request. Uppercase + the same 1.5rem/24px gap
   .work__tags used to use for its pill row, now spacing these three
   plain-text fields instead. */
.work__meta {
  font-family: "Satoshi", sans-serif; /* Satoshi preview, per direct request — was "PP Neue Montreal" */
  font-size: 0.75rem; /* 12px, per direct request — was 0.875rem/14px */
  font-weight: 500;
  line-height: 1.5;
  color: var(--gray);
  text-transform: uppercase;
}

/* White only in dark mode, per direct request — the work section sits on
   the page's normal background (light cream in light mode, black in dark
   mode via html.dark-mode body), so literal white here would be
   near-invisible in light mode. Uses the site's own --white token rather
   than a literal #ffffff, per direct request. */
html.dark-mode .work__meta {
  color: var(--white);
}

/* Layout has to live on .line-inner (the actual parent of the three
   field spans) rather than on .work__meta itself — same reasoning as
   the old .work__tags .line-inner rule this replaces. */
.work__meta .line-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* No flex gap — spacing between fields now comes entirely from the
     dot separator's own 8px margins below, so the two never stack. */
  gap: 0;
}

/* Small 4px dot between each field (not after the last one), 8px of
   space on either side of it, per direct request — replaces the old
   plain 24px flex gap. Lives on the field span itself (not a separate
   flex item) so it travels with clientName/clientCity but not year. */
.work__meta .line-inner span[data-field]:not(:last-child)::after {
  content: "";
  display: inline-block;
  width: 0.25rem;  /* 4px */
  height: 0.25rem; /* 4px */
  margin: 0 0.5rem; /* 8px on each side */
  border-radius: 50%;
  background: currentColor; /* same grey as the text — inherits .work__meta's color */
  vertical-align: middle;
}

.work__counter {
  /* Now sits where .work__tags used to (top of .work__content, first
     element before the title) — swapped with .work__meta per direct
     request. Went small (14px, --gray) then back up to a bold 40px
     display number in a near-black — no longer reads as a small in-line
     label, back to being a standalone headline number again. The "/05"
     total suffix was dropped from the markup per direct request — only
     the index digits show now (see index.html, .work__total span
     removed). 800 (Extrabold) used for "bold" since the site has no
     700 file. */
  font-size: 2.5rem; /* 40px */
  font-weight: 800;
  line-height: 1.15;
  color: #1e1e1e;
  letter-spacing: -0.01em;
  margin-bottom: 1.25rem; /* was .work__tags's own margin-bottom, now inherited by whichever element leads .work__content */
  overflow: hidden;
}

.work__counter .counter-index {
  display: inline-block;
}

.work__images {
  grid-column: 6 / 13; /* 7 columns — column 5 is left as a spacer gap after the 4-col sticky text */
  display: flex;
  flex-direction: column;
  gap: var(--gutter);
  padding-top: 6rem; /* matches .work__sticky-col's own padding-top exactly, so the first image's top edge lines up with the "02" counter's top edge — was 9.125rem, a 50px mismatch (confirmed via getBoundingClientRect() diff), per direct request */
  padding-bottom: var(--margin);
}

/* No reveal animation on the default homepage's work images (removed
   per direct request — the AI Playground cards keep the
   justgowiththeflow.com/work-style curtain+zoom reveal; this page
   alone opts out). The mask/inner/cover markup structure is left in
   index.html unchanged (still has data-image-inner/data-image-cover)
   so the reveal can be dropped back in later just by reverting these
   two rules and re-adding Common.initImageReveals(".work__image-item")
   to home.js — but as shipped, __inner sits at its true rest scale (no
   zoomed-in start state) and __cover never renders at all, so the
   placeholder just shows plainly with nothing to animate. */
.work__image-item {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 951 / 698;
  overflow: hidden;
}

.work__image-item__inner {
  position: absolute;
  /* 1px overscan on every edge, instead of a flush inset:0 — during the
     first item's reveal (see .work__image-item__cover below) a hairline
     of background showed past the inner layer's true edge (subpixel
     rounding on the scale-transform tween). Same fix as
     .case-block__image-inner/.case-hero__image-inner in case-study.css. */
  inset: -1px;
  width: calc(100% + 2px);
  height: calc(100% + 2px);
  background: var(--black); /* fallback/loading backdrop behind lazy-loaded img/video */
  transform: scale(1);
}

.work__image-item__inner img,
.work__image-item__inner video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---------- slideshow items (DEWA website, Innovation Centre) ----------
   Several images stacked absolutely inside the same inner box (instead
   of the single plain img/video every other item uses), hard-cutting
   between each other on a timer — see initWorkSlideshows in home.js,
   which toggles .is-active every data-slideshow-interval ms (1.5s, per
   direct spec). No transition on opacity (was a 0.6s cross-fade,
   removed per direct request — instant swap instead). Needs its own
   position:relative-equivalent stacking context, which
   .work__image-item__inner already provides via its own
   position:absolute — each img here just layers on top via its own
   position:absolute;inset:0 rather than sizing itself in normal flow. */
.work__image-item__inner--slideshow img {
  position: absolute;
  inset: 0;
  opacity: 0;
}

.work__image-item__inner--slideshow img.is-active {
  opacity: 1;
}

.work__image-item__cover {
  display: none;
}

/* Reveal re-enabled for the FIRST project image only, per direct
   request — every other work image stays exactly as the note above
   .work__image-item describes (no reveal, __cover never renders). Same
   curtain+zoom visual language as ai-playground.css's
   .pg-card__image-cover: a solid panel slides up and away (yPercent)
   while the image settles from a slight zoom (scale) — see
   Common.initImageReveal, called just for this one mask in home.js.
   :first-of-type keeps this scoped without touching the HTML. */
.work__images .work__image-item:first-of-type .work__image-item__cover {
  display: block;
  position: absolute;
  inset: -1px; /* see the overscan comment on .work__image-item__inner above */
  width: calc(100% + 2px);
  height: calc(100% + 2px);
  z-index: 2;
  background: var(--white);
  will-change: transform;
}
html.dark-mode .work__images .work__image-item:first-of-type .work__image-item__cover {
  background: var(--black);
}
/* Deliberately NO scale/zoom on __inner — checked ai-playground.html's
   actual rendered .pg-card__image-inner live and its transform is a
   plain identity matrix, no scale at all. This reveal is a pure
   cover-slide, matching that exactly rather than the zoom-settle effect
   an earlier pass added here, which was never part of the reference
   behavior in the first place. */

.work__image-item::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Below the reveal cover's z-index:2 — was 3 (above it), which let this
     sheen's white tint outline the still-covered first image as a visible
     lighter box in dark mode (imperceptible in light mode, where the tint
     sits on a near-white cover instead of a black one). Sitting under the
     cover means it stays fully hidden until the cover slides away, same
     as the actual image underneath. */
  z-index: 1;
  background: radial-gradient(120% 120% at 20% 15%, rgba(255,255,255,0.08), rgba(0,0,0,0) 55%);
  pointer-events: none;
}

/* ---------- inactive-image dim overlay ----------
   koto.com reference: whichever project is currently "active" (the one
   driving the sticky text column — see requestActive/the per-item
   ScrollTrigger in home.js) stays at full brightness; every other image
   in the scrolling column gets a flat black tint over it. Driven by JS
   toggling .is-dimmed rather than dimming-by-default, so there's no
   flash of every image dimmed before the section's own onEnter
   ScrollTrigger assigns the first active item (see that comment in
   home.js) — nothing is ever dimmed until JS has actually decided
   something else is active. z-index:3 sits above both the sheen
   (::after, z-index:1) and the first item's own load-in reveal cover
   (z-index:2), so it's always the topmost darkening layer once toggled
   on, regardless of which item it's on. */
.work__image-item::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  background: #0B0A0B;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s var(--ease-out);
}

.work__image-item.is-dimmed::before {
  opacity: 0.6;
}

/* ---------- work-item cursor-follow label ----------
   Same solid-color chip recipe as .pg-cursor-label (ai-playground.css),
   .case-next-cursor-label (case-study.css), and .footer-cursor-label
   (style.css) — duplicated rather than shared since each lives in its
   own page-specific stylesheet; keep the four in sync if this recipe
   ever changes. See initWorkCaseStudyLabel in home.js for the JS half
   (desktop-only, matchMedia-gated same as the others; color randomized
   there too via Common.pickCursorLabelColor, on every mouseenter).
   Scoped to just the one work item that has a real case-study page to
   link to (see [data-case-study] in index.html) — every other item is
   still a href="#" placeholder. */
.work-cursor-label {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 950;
  pointer-events: none;
  display: flex;
  flex-direction: row-reverse; /* DOM is text-then-icon; flips it to icon-first visually, matching the other cursor labels */
  align-items: center;
  gap: 0.25rem; /* 4px, per direct spec */
  color: #0B0A0B; /* black — per direct request, reads against every chip color */
  font-family: "Satoshi", sans-serif;
  font-size: 0.75rem; /* 12px, per direct spec */
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0;
  line-height: 1;
  white-space: nowrap;
  padding: 0.5rem 0.375rem 0.5rem 0.25rem; /* 8px top/bottom, 6px right, 4px left — per direct spec */
  border-radius: 0; /* sharp corners, per direct spec */
  background: #ff6831; /* JS overwrites this per zone entry — see pickCursorLabelColor in common.js; this is just the pre-JS fallback */
  opacity: 0;
  will-change: transform, opacity;
}

.work-cursor-label__arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.work-cursor-label__arrow img {
  width: 0.75rem; /* 12px, matches text size */
  height: 0.75rem;
  display: block;
}

/* Hidden outside the mobile breakpoint — desktop keeps using the sticky
   column instead, this element only ever gets shown by the media query
   below. Declared here, BEFORE that query, on purpose: a plain rule and
   a media-query rule for the same selector have equal specificity, so
   whichever comes later in the file wins regardless of which one is
   inside a query — this has to sit above the mobile block below it, or
   it would win at every viewport (including mobile) and this element
   would never actually show. */
.work__mobile-caption {
  display: none;
}

/* ==========================================================================
   mobile breakpoint (<=768px) — see style.css's "mobile breakpoint"
   comment for the shared root-font-freeze this all builds on. Homepage
   is the first page to get a real mobile layout; every rule below is
   scoped to this file/page on purpose so other pages are untouched
   until they get their own pass.
   ========================================================================== */
@media (max-width: 768px) {

  /* ---------- hero ----------
     Full viewport height, per direct spec — the landing page IS this
     screen: intro paragraph pinned near the top, Based-in line pinned
     to the bottom, like the desktop hero's own top/bottom relationship
     just without the giant scroll-driven logo (see
     initHeaderAndHeroIntro/initHeroScrollExit's isMobile branches in
     home.js — the logo just sits at its small 24px docked size in the
     header the whole time, like every other page).
     .hero itself becomes the flex column (was only .hero__bottom
     before); padding-top is a flat 80px from the top of the viewport,
     per direct spec (was computed off the header's own bottom edge —
     simplified to a flat value instead), padding-bottom reserves the
     same var(--margin) inset the based-in line ends flush with —
     .hero__bottom (flex:1, space-between) fills everything between
     those two and pins its two children to the top/bottom of that
     remaining space. */
  .hero {
    height: 100vh; /* fallback for browsers without dvh support */
    height: 100dvh; /* see desktop .hero rule above for why */
    min-height: 100vh;
    min-height: 100dvh;
    padding-top: 7.5rem; /* 120px, per direct spec — was 80px */
    padding-bottom: var(--margin);
    padding-inline: 1.5rem; /* 24px, per direct spec — was var(--margin)/40px */
    display: flex;
    flex-direction: column;
  }

  /* Was absolutely positioned + bottom-anchored for the desktop
     giant-hero layout; now the flexible middle section between .hero's
     own top/bottom padding — grows to fill it (flex:1) and pins the
     paragraph to its own top edge, Based-in to its own bottom edge. */
  .hero__bottom {
    position: static;
    display: flex;
    flex-direction: column;
    flex: 1;
    justify-content: space-between;
    align-items: stretch;
    padding-inline: 0; /* .hero already carries var(--margin) via its own padding-inline, inherited unchanged */
  }

  .hero__paragraph {
    font-size: 2.5rem; /* 40px, per direct spec — was 32px */
    max-width: none;
  }

  /* Pinned to the bottom of the viewport by .hero__bottom's own
     justify-content:space-between above — no positioning needed here.
     Still right-aligned per its own existing (unmodified) rule. */

  /* ---------- work / scroll section ----------
     Sticky text column + scroll-driven image swap is desktop-only (see
     the isMobile early-return in initWorkSection, home.js) — mobile
     shows every project as its own static block instead: image, then
     title, then body text, one after another, per direct spec. */
  .work {
    display: block; /* was the shared grid-12 grid */
  }

  .work__sticky-col {
    display: none;
  }

  .work__images {
    grid-column: unset;
    padding-top: 0;
    padding-bottom: 0;
    /* No flex gap here — .work__mobile-caption's own margin-top/
       margin-bottom below do both jobs instead (small gap image->caption,
       larger gap caption->next image), since a flat gap on this
       container would apply the same space to both relationships. */
    gap: 0;
  }

  .work__mobile-caption {
    display: block;
    margin-top: 1.25rem; /* image -> its own title/text */
    margin-bottom: 5rem; /* 80px, per direct spec — this project's text -> next project's image */
  }

  .work__mobile-caption__title {
    font-size: 1.5rem; /* 24px, per direct spec */
    font-weight: 500;
    line-height: 1.25;
    letter-spacing: -0.01em;
    margin-bottom: 0.75rem;
  }

  .work__mobile-caption__desc {
    font-size: 1rem; /* 16px, per direct spec */
    font-weight: 500;
    line-height: 1.3;
    color: #BABABA;
    margin-bottom: 1.5rem; /* 24px, per direct spec — gap down to the meta line below */
  }

  /* Client name / city / year — the mobile equivalent of the desktop
     .work__meta line, which mobile otherwise has no counterpart for at
     all (per direct request: "missing the small text at the end"). Same
     12px/uppercase/grey treatment and dot-separator trick as .work__meta
     itself (see style.css), just its own class since this isn't part of
     the line-mask/line-inner reveal system .work__meta's real markup
     needs on desktop. */
  .work__mobile-caption__meta {
    /* margin-top no longer needed for the desc->meta gap — that's set by
       .work__mobile-caption__desc's own margin-bottom above (adjacent
       margins collapse to the larger of the two, so this doesn't stack
       on top of it). margin-bottom is the "after the meta text" 24px,
       per direct spec — collapses with .work__mobile-caption's own 80px
       project-to-project gap below (max wins), so it's a no-op visually
       when both are present but keeps this element correct on its own. */
    margin-bottom: 1.5rem;
    font-size: 0.75rem; /* 12px */
    font-weight: 500;
    line-height: 1.5;
    color: var(--gray);
    text-transform: uppercase;
  }

  html.dark-mode .work__mobile-caption__meta {
    color: var(--white);
  }

  .work__mobile-caption__meta span:empty {
    display: none;
  }

  .work__mobile-caption__meta span:not(:last-child):not(:empty)::after {
    content: "";
    display: inline-block;
    width: 0.25rem; /* 4px */
    height: 0.25rem;
    margin: 0 0.5rem; /* 8px each side */
    border-radius: 50%;
    background: currentColor;
    vertical-align: middle;
  }
}
