/*
  Section paging layout.
  Mobile-first default: sections are normal stacked blocks, natural scroll.
  When JS detects a desktop/laptop pointer+viewport, it adds `fp-active` to
  <html>. Sections stay in REAL document flow (real native scrollbar stays
  usable) — JS just drives one-section-per-wheel-notch smooth scrolling, and
  CSS scroll-snap backs up drags/trackpad momentum so it still settles on a
  section boundary.

  Section color always comes from the fixed wave-bg layer (css/waves.css),
  at every viewport size, not just desktop paging mode — .section itself
  stays transparent so that layer shows through underneath.
*/

.sections-container {
  position: relative;
}

/* Two rows, and the top one is the TITLE BAND.

   The wave artwork leaves a strip of the previous section's colour along the
   top of every screen -- that is what the layer in `band` state is. Putting
   every section's title in that strip is what makes the deck read as one
   document rather than as a stack of unrelated screens: the title lands in
   the same place every time, and the thing that changes underneath it is the
   colour. It is also the only way to give the heading its own space without
   taking any from the content, since the band is there whether or not
   anything sits in it.

   --band-height is set in css/variables.css from the wave geometry, not
   guessed -- see the comment there. */
.section {
  position: relative;
  min-height: 100vh;
  width: 100%;
  display: grid;
  grid-template-rows: var(--band-height) minmax(0, 1fr);
  /* Small, because --band-height already carries the clearance: the band row
     ends at the wave's deepest point, so this gap is breathing room between
     the title and the content, not protection from the artwork. */
  row-gap: clamp(var(--space-2), 1.4vh, var(--space-3));
  align-items: center;
  justify-items: center;
  /* Vertical padding scales with the VIEWPORT, not the type scale. A flat
     4rem top and bottom costs 128px of a 720px-tall laptop screen, which is
     the difference between the "Who's Roman?" stack clearing the fold and
     being cut off by the overflow: hidden below. 5vh was still 14px too
     much at 1280x720 once the landing section grew a second button and a
     role line; 4vh is what makes that case land exactly on the fold. */
  /* No top padding: the band row owns that space now. */
  padding: 0 var(--space-4) clamp(1rem, 4vh, 4rem);
  background-color: transparent;
  overflow: hidden;
}

/* Grid rather than a plain block, so `align-content` can centre a short
   section's content in the row without the row itself collapsing. It fills
   the row's height because the landing section's portrait is sized by it;
   everything else centres inside that. */
.section__inner {
  position: relative;
  z-index: 2;
  display: grid;
  align-content: center;
  max-width: var(--content-max-width);
  width: 100%;
  height: 100%;
}

/* For a section whose content is MEANT to fill the row rather than sit in
   the middle of it -- the landing page, whose portrait runs floor to
   ceiling. */
.section__inner--fill {
  align-content: stretch;
  grid-template-rows: minmax(0, 1fr);
}

/* One measure for every band in the deck -- the WIDER of the two content
   measures, which is the one "Who's Roman?" uses.

   It used to follow the content below it, and that was wrong: the content
   measure changes from page to page, so the title and the Back/Next pair
   moved with it. On a 1740px screen the project pages' title started 120px
   further in than the landing page's, and their buttons ended 120px short of
   where the landing page's did. The band is chrome; chrome that shifts when
   you page through it reads as the page jumping, not as the layout breathing.
   A narrow column below a full-width band is fine -- that is a measure choice
   for prose. A title that moves is not.

   A row, because the band carries two things: the headings on the left and
   an actions slot on the right. That slot is where the deck's own controls
   go -- Back/Next on a project page, the landing page's two CTAs -- and
   putting them in the band rather than in the content is what keeps them in
   the same place on every screen, which is the same argument the title
   itself makes. */
.section__band {
  position: relative;
  z-index: 2;
  /* TOP of the row, not the middle of it. The row is as tall as the band's
     DEEPEST point, because the content below has to clear the band across
     the whole width; but the title only has to clear the band where the
     title sits, which is shallower. Centring in the deep row would push the
     headings down into the shallow part. Sitting at the top, they are
     measured against --band-floor-head instead, which is that shallow part.

     The offset is a fraction of the head depth rather than a fixed rem, so
     it scales with the band the way everything else in here does. */
  align-self: start;
  margin-top: calc(var(--band-head) * 0.2);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
  max-width: var(--content-max-width-wide);
  width: 100%;
}

/* The two heading lines are one flex item, not two. */
.section__headings {
  min-width: 0;
  /* The positioning context for the split layer below. */
  position: relative;
}

/* ---- The split layer ----
   A second copy of the heading, laid exactly over the first and clipped to
   whatever part of it is sitting on a LIGHT wave colour. The copy underneath
   is white and clipped to the COMPLEMENT, so between them every part of the
   word wears the face that suits the colour actually behind it and the
   boundary between the two is the wave's own edge.

   MUTUALLY EXCLUSIVE, and that word is the whole lesson of the first attempt
   at this. Stacking them left the white copy's ring and halo underneath the
   dark copy's glyphs -- a dark halo around near-black type on a pale band,
   doubled because both layers carried one. It was unreadable. Only one
   treatment may render in any one place.

   Which is why THIS layer has no ring and no halo at all. It is shown only
   where the wave is light enough to prefer near-black, and near-black on such
   a colour is 4.64:1 at the very worst and 16:1 on the pale pink the landing
   page sits on. It has nothing to be protected from. The ring exists for
   white type on a colour that will not carry it; where the face is dark, the
   ring would be a dark edge on dark type and the halo a dark cloud around it.

   Built and clipped by js/waves.js (_prepareSplitLayers, _splitBoundary,
   _splitClip). Cloned at startup rather than written into all 23 section
   files. `aria-hidden` and `user-select: none` keep the same words out of the
   accessibility tree and out of a copied selection.

   Clipped to NOTHING by default, so the layer is inert until the JS has
   measured: with no script, reduced motion, or a heading whose geometry
   cannot be resolved, what shows is the white copy with its ring -- which is
   the treatment this improves on rather than replaces. */
.section__headings-split {
  position: absolute;
  inset: 0;
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
  clip-path: inset(100% 0 0 0);
}

/* ---- ...and it is plain near-black ----
   It carried a palette colour and a shadow for a while, and both are gone.
   Not because they failed a measurement -- they passed -- but because the
   rule that chose them could only choose by CONTRAST, and contrast is not
   taste. "The lightest palette entry that still clears the bar" put #80382B,
   a warm rust, on alt-3's cold pale blue #DDEDFF: 7.00:1, and wrong. A ladder
   that spans warm and cool will keep producing that, and no threshold fixes
   it, because the thing being got wrong is not a number.

   If the dark face should be coloured, the colour has to be CHOSEN per
   palette by someone looking at it, not derived. That is a one-line addition
   to wave-config.json when it is wanted -- see the note in plan.md. */
/* While a selection touches the heading (js/waves.js sets `is-selecting`):
   the white, selectable copy is shown whole and the dark copy is hidden, so
   the selection highlight can be seen. `!important` because the clips are
   inline styles the split pass writes. */
.section__headings.is-selecting > .section__title,
.section__headings.is-selecting > .section__subtitle {
  clip-path: none !important;
}

.section__headings.is-selecting > .section__headings-split {
  visibility: hidden;
}

.section__headings-split .section__title,
.section__headings-split .section__subtitle {
  color: var(--band-ink);
  -webkit-text-stroke-width: 0;
  text-shadow: none;
}

/* `margin-left: auto` is not redundant beside the band's `space-between`.
   Space-between only distributes along a line that HAS free space: once the
   title grows enough to push the actions onto a second line, that line holds
   one item and space-between puts it at the start, so Back/Next dropped to
   the left edge under the title on a narrow desktop window. The auto margin
   pushes it right on a line of its own, and on a shared line it absorbs the
   same free space space-between would have, so the wide layout is unchanged.

   `justify-content: flex-end` for the same reason one level down: when the
   buttons themselves wrap, the short last row stays on the right. */
.section__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: clamp(var(--space-2), 1vw, 1.125rem);
  margin: 0 0 0 auto;
  flex-shrink: 0;
}

/* Controls in the band are sized against the band, like the title is, and
   for the same reason: a button set off the viewport's WIDTH is a button
   sized for a screen that may be much taller than this one, and it has to
   share a fixed strip with the heading. Padding in em so it follows.

   The shared `.btn` size is for a button standing in a content column with
   room around it; this is a chrome control. */
.section__actions .btn {
  font-size: clamp(0.8125rem, calc(var(--band-floor) * 0.11), 1.0625rem);
  padding: 0.8em 1.7em;
}


/* ---- A download under Back / Next ----
   Back and Next are the deck's controls and they stay side by side. A
   download leaves the deck for a file, so it does not sit between them: the
   Resume screens put their Download PDF on a line of its own directly UNDER
   them, right-aligned, in the same button style.

   Taken out of flow with `position: absolute`, so the band keeps the height
   and the title keeps the room it had -- js/band-fit.js measures the title
   against the slot's width, and the slot must not grow a second row. It
   hangs into the stretch between the band's buttons and the content row,
   which is empty on every Resume screen: measured at 1536x864, the buttons
   end at y 81 and the timeline's first card starts at 167. The slot is the
   positioning context, and its row gap is the gap above the download too. */
.section__actions:has(> .section__download) {
  position: relative;
}

/* `left: 0; right: 0` makes it exactly as wide as Back + gap + Next above
   it, so the three read as one block of controls. The slot's width is
   theirs alone: the download is out of flow and adds nothing to it. */
.section__actions > .section__download {
  position: absolute;
  top: calc(100% + clamp(var(--space-2), 1vw, 1.125rem));
  left: 0;
  right: 0;
  white-space: nowrap;
}


/* The title is the ONE piece of copy on this site that sits on the artwork
   rather than on a card, and it can only do that because at this size it is
   WCAG large text: 3:1 rather than 4.5:1. js/waves.js measures the colour of
   the band specifically -- not the section's main field, which is a
   different colour -- and sets --band-text to whichever of ink and cream
   wins against it. See the `data-band-contrast` block in css/waves.css. */
/* Sized against the BAND, not the viewport -- and specifically against
   --band-floor-head, the band's depth under the HEADINGS rather than its
   shallowest point anywhere. The title never reaches the far right, where
   the band is thinnest; sizing it off there made it small for no reason.

   The band's depth is a fraction of the viewport height that also moves with
   its aspect (see _publishBandFloor in js/waves.js), so a title set off vw
   fitted the strip on one screen shape and pushed its subtitle through the
   wave on another -- which is the bug this replaced. The rem clamps are the
   floor and ceiling of readability. */
.section__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.5rem, calc(var(--band-head) * 0.34), 3.75rem);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--band-text);
  text-wrap: balance;
}

/* A project's title names the project in two weights -- "Project 1:" heavy,
   the project's own name light -- which is the user's reference. `:has()`
   rather than a modifier class, because the two weights ARE the structure:
   a title with a <strong> in it is a split title, and one without is not. */
.section__title:has(strong) {
  font-weight: 400;
}

.section__title strong {
  font-weight: 700;
}

/* The colon belongs to the ONE-LINE form only, so it is punctuation the
   stylesheet adds rather than text the section carries. Stacked, the line
   break does the separating and a trailing colon would be left dangling. */
.section__title strong::after {
  content: ':';
}

/* ---- The heading's outline, and its halo ----
   The section title is the one piece of copy on this site that sits on the
   artwork rather than on a card. The measured ink flip carried that for a
   while; it no longer has to, because the title is now always white (see the
   .section__band block in css/waves.css), and what makes THAT safe is the
   ring.

   TWO MECHANISMS, DOING TWO JOBS, and that split is the whole design. Every
   earlier attempt failed because one thing was asked to be both.

   The RING is opaque and immediately adjacent, so it is what the contrast
   pair is measured against -- and because it is opaque, that pair is a
   CONSTANT rather than a function of the artwork: 12.96:1 at worst across the
   four palettes, against the 4.5:1 owed. A blur cannot take this job. On the
   pale band white is 1.20:1 bare; a blurred halo delivers about 35% coverage
   at the stroke, which reaches 2.77:1 and fails, and it would need 52% to
   pass -- by which point it is an opaque blob, which is what it looked like
   when it was tried.

   The HALO takes the job it is actually good at, once it is not being asked
   to be the ring: softening. A bare ring is a hard edge, and a hard edge at
   this weight reads as a sticker laid on the artwork. Three stacked blurred
   copies past the ring take that edge off and separate the WORD from the
   field, where the ring separates each letter from its own outline. It owes
   nothing to the contrast maths, so it is free to be as quiet as it likes.

   BOTH IN THE WAVE'S OWN PALETTE, set per band by js/waves.js -- see
   _outlineFor. A neutral near-black ring read as something imported and laid
   on top; the stack's own backdrop reads as part of the drawing.

   `paint-order: stroke fill` is not optional. Without it the stroke is
   centred on the glyph outline and half its width is taken out of the
   letterform, which at this size is the difference between an outline and a
   thinner typeface. Where it is unsupported the type simply looks slightly
   light; the ring is still there and the contrast pair still holds. */
.section__title,
.section__subtitle {
  /* A pixel floor, because the ring is the letter's BACKGROUND and one
     thinner than the device pixel it is drawn on antialiases to a smudge
     rather than an edge. Above ~25px the em is past the floor and it costs
     nothing. */
  -webkit-text-stroke: max(1px, 0.04em) var(--band-outline);
  paint-order: stroke fill;
  /* In em so the halo scales with the title, from 28px on a phone to 60px on
     a desktop. No pixel floor here -- unlike the ring, a soft edge losing a
     fraction of a pixel loses nothing that matters. */
  text-shadow:
    0 0 0.07em color-mix(in srgb, var(--band-outline) 85%, transparent),
    0 0 0.18em color-mix(in srgb, var(--band-outline) 75%, transparent),
    0 0 0.36em color-mix(in srgb, var(--band-outline) 60%, transparent);
}

/* ---- ...and MORE ring on the subtitle, not less ----
   An em-proportional ring is the wrong rule at the bottom of the size range,
   and the landing page is where that shows: its subtitle is the smallest type
   on the artwork, in the lightest weight, on the palest band in the deck.
   0.04em of 16px is 0.64px of ring around a stem barely more than a pixel
   wide -- proportionally right and practically nothing.

   Legibility does not scale linearly with size. A large letter is carried by
   its own shape and the ring only has to separate it; a small one has little
   shape left to be carried by, so the ring is doing more of the work and
   needs more of the letter's edge to do it with. So the subtitle gets a
   fatter ring in RELATIVE terms -- 1.4px against the title's 1.12px at the
   phone size, where proportion alone would have given it 0.64px.

   Its halo is pulled in at the same time. The same em radii that read as a
   soft separation around a 28px title read as fog around 16px type, because
   the blur is spreading over a distance comparable to the letter itself. */
.section__subtitle {
  -webkit-text-stroke-width: max(1.4px, 0.055em);
  text-shadow:
    0 0 0.05em color-mix(in srgb, var(--band-outline) 90%, transparent),
    0 0 0.12em color-mix(in srgb, var(--band-outline) 80%, transparent),
    0 0 0.24em color-mix(in srgb, var(--band-outline) 65%, transparent);
}

/* The landing section carries one, and so do Project 3's seven Screen
   Samples screens ("Project 3: Scoring System" over "Screen Samples (n of
   7)"), where "Scoring System: Screen Samples (n of 7)" on one line ran into
   the curve. It rides in the same band
   because it IS part of the title, and a pill above the heading -- which is
   what this used to be -- would have had to repeat on every section once the
   band became the deck's title slot. */
.section__subtitle {
  /* Tight, and deliberately not a vh clamp. The two lines have to clear the
     wave together, and on a wide screen the leading was the difference: the
     title fitted and the subtitle's descenders sat in the water. */
  margin: 0.25rem 0 0;
  font-size: clamp(0.875rem, calc(var(--band-head) * 0.145), 1.5rem);
  line-height: 1.3;
  color: var(--band-text);
  text-wrap: balance;
}

/* In paging mode the section is not merely AT LEAST a viewport tall, it is
   EXACTLY one. That distinction is invisible until something inside is sized
   off the row's height: with `min-height`, the grid container's height is
   still auto, so a `1fr` row is sized to its content first and the extra is
   handed out afterwards -- which let the landing page's portrait resolve at
   its intrinsic 836px and push the section 22px past the fold, the very
   thing the row was supposed to be constraining.

   Scoped to the query js/scroll-engine.js pages on, not to a bare width:
   below it the page scrolls normally and a section is allowed to be as tall
   as its content. */
@media (min-width: 1024px) and (pointer: fine) {
  .section {
    height: 100vh;
  }
}


/* ---- FLOW MODE: below a landscape tablet ----
   Everything under 1024px. The paging engine is off here (js/scroll-engine.js
   gates it on the same width plus a fine pointer), so this is an ordinary
   scrolling document and the section stops being a slide.

   Three things have to come off together, and taking any one without the
   others is what produced the broken states this block replaces:

     `overflow: hidden`   only makes sense on a box that is exactly one
                          screen tall. On a section that may run longer than
                          the viewport it does not clip a little -- it makes
                          the bottom of the page unreachable, because there
                          is no inner scroll to reach it with.
     the fixed band row   --band-height is measured off the wave artwork,
                          which is painted on a FIXED, full-viewport layer.
                          In flow mode a section's top is wherever the reader
                          scrolled it to, so that strip has nothing to do
                          with this section's heading any more. The row goes
                          content-sized and the title gets ordinary padding
                          above it instead.
     the 100vh floor      kept, but minus the top bar (css/nav.css), so a
                          section still fills the screen it is read on. That
                          is what keeps one section reading as one field of
                          colour, and it is also what the wave background's
                          IntersectionObserver needs -- at 0.55 visibility,
                          several short sections on screen at once would flip
                          the artwork back and forth as the reader scrolled.

   The section may of course grow past that floor, and on a phone the landing
   page does. That is the point of the mode. */
@media (max-width: 1023.98px) {
  .section {
    min-height: calc(100vh - var(--topbar-height));
    height: auto;
    overflow: visible;
    grid-template-rows: auto minmax(0, 1fr);
    /* The band row no longer carries the top clearance, because it is no
       longer sized by the artwork. */
    /* Top and bottom are no longer the same figure. The top is only
       clearance under the fixed bar, and at 5vh it read as a gap the page
       had failed to fill; the bottom still ends a screen of content. */
    padding: clamp(var(--space-3), 2.5vh, var(--space-4)) var(--space-4)
             clamp(var(--space-4), 5vh, var(--space-6));
    row-gap: var(--space-4);
  }

  /* Sized off the artwork's depth on a desktop, which is meaningless here --
     and it made the heading grow and shrink with the WINDOW HEIGHT while the
     reader scrolled a page of a fixed width. Ordinary width-based type. */
  .section__band {
    align-self: auto;
    margin-top: 0;
    /* Wider than the desktop's 16px, because down here the row gap is not a
       gap between two things sharing a line -- it is the space between the
       headings and the control that has wrapped underneath them, and at 16px
       the button sat closer to the subtitle than the subtitle sat to its
       own title. */
    gap: var(--space-4);
  }

  /* 1.75rem rather than 1.5, and the vw term raised to meet it. On a 375px
     phone 5.5vw is 20.6px, so the OLD floor was what every common phone
     actually got -- the fluid middle never engaged below a 436px window. 24px
     is small for the page's own title, and it is small in a way that matters
     here: the ring and step are a fixed proportion of the type, so the
     counters they have to stay out of shrink with it. 28px buys about 17%
     more room inside a `d` for nothing but a size that was already modest. */
  .section__title {
    font-size: clamp(1.75rem, 6.6vw, 2.5rem);
  }


  .section__subtitle {
    font-size: clamp(1rem, 2.9vw, 1.125rem);
  }

  /* Auto, not 100% -- there is no definite row height to be 100% OF any
     more, and a percentage of an auto-height parent resolves to auto anyway.
     Stating it stops `--fill`'s `grid-template-rows: minmax(0, 1fr)` from
     trying to squeeze the landing page's stack into a row it no longer has.

     `align-content` is deliberately left alone. The section keeps a
     one-screen floor down here, and the short sections -- the phase
     placeholders -- have a card a fraction of that tall: pinned to the top
     of the row it left most of a blank screen under it, which reads as the
     page having failed to load rather than as a short page. Centred in the
     screen it is a short page. Sections whose content EXCEEDS the floor grow
     instead, and centring a box that fills its container does nothing. */
  .section__inner,
  .section__inner--fill {
    height: auto;
  }

  /* Back and Next are gone down here (below), so the download is the slot's
     only button and simply takes its place in flow. */
  .section__actions > .section__download {
    position: static;
  }

  /* ---- Back / Next come out ----
     They are the paging deck's controls, and there is no paging deck down
     here: the reader gets to the next page by scrolling, which is the
     gesture the whole mode is built on, or by the nav drawer. A "Next"
     button that scrolls you somewhere you were already on your way to is
     one more thing on a screen that has less room for it.

     Matched by what the buttons DO rather than by a modifier class, so a
     step control is hidden wherever anyone puts one. The landing page's
     "View Portfolio" carries `data-target`, not `data-step`, and stays: it
     goes somewhere the scroll would not take you next. So do the Resume
     screens' download buttons, which are plain links to a file.

     The STEP CONTROLS come out, not the slot around them. This rule used to
     be `.section__actions:has(> [data-step])`, which hid the whole slot --
     correct while Back and Next were the only things ever in it, and wrong
     the moment the Resume screens put a Download PDF beside them: on a phone
     the download simply vanished. The slot itself is hidden only when
     nothing but step controls is left in it, which is the case on every
     project page and keeps the original behaviour exactly. */
  .section__actions > [data-step] {
    display: none;
  }

  .section__actions:not(:has(> :not([data-step]))) {
    display: none;
  }

  /* ---- The landing page's two CTAs, which DO stay ----
     `flex-shrink: 0` is right in the band on a desktop, where the actions
     slot must not be squeezed by a long title beside it. On a phone it is
     what pushed "View Portfolio" off the right edge of the screen: the pair
     is ~390px of button in a 342px column, and a slot that refuses to shrink
     never gets the chance to wrap.

     Given the full width they wrap under the heading, and each button grows
     to share whatever row it lands on -- so they read as one pair on a
     tablet and as two stacked full-width targets on a phone, rather than
     as two ragged pills. */
  .section__actions {
    flex-shrink: 1;
    width: 100%;
    /* Full width, so it wraps under the headings -- but its contents start
       at the LEFT edge, under the title, rather than at the right edge of a
       column with nothing beside them. */
    justify-content: flex-start;
    /* The row is full-width down here, so there is no free space for the
       desktop band's auto margin to push against -- and left at `auto` it
       would fight `width: 100%` for the same pixels. */
    margin-left: 0;
  }

  /* Off the band's depth on a desktop, which down here is a strip that no
     longer exists. Ordinary type, and a minimum tap target. */
  .section__actions .btn {
    /* Not `1 1 auto`. Growing to fill the row was right when the landing
       page had TWO CTAs to divide it between; with one left it made a
       single 840px button out of a control that needs about 200, which
       reads as a banner rather than as something to press. Natural width,
       and they still wrap when the row cannot hold them. */
    flex: 0 1 auto;
    min-width: 0;
    min-height: 44px;
    font-size: clamp(0.8125rem, 2.4vw, 1rem);
    padding: 0.85em 1.5em;
  }
}


/* ---- ...and on a phone the landing page's CTA comes out too ----
   The same argument that took Back and Next out above, one breakpoint
   further down. On a desktop "View Portfolio" sits at the right end of the
   band, opposite the title, and is the one control on the screen. Below
   1024 the band stacks and it lands directly under the subheading, ahead of
   everything the page is actually about -- a button between the reader and
   the portrait, pointing at a section the drawer already lists and the
   scroll already reaches.

   The hero only, by id, not `.section__actions .btn`: the Resume screens'
   Download PDF is also a non-step control in this slot, and it is the only
   way to that file. This one has two others. */
@media (max-width: 560px) {
  #hero .section__actions {
    display: none;
  }
}


/* ---- The split title stacks when the one-line form stops fitting ----
   The stacked form is the same title broken where its meaning already
   divides: the project on the first line, the page on the second, set down a
   step so the pair reads as a title and its subtitle rather than as two
   headings. The colon goes with it -- it is punctuation the one-line form
   needs and the line break replaces, see `.section__title strong::after`
   above.

   WHEN it stacks is measured, not written down. This used to be a
   `max-width: 1279.98px` media query and the threshold was never honest,
   because the title's size is not a function of the window's WIDTH: it is
   sized off the band's depth, which js/waves.js measures from the artwork
   and which grows with window HEIGHT. The same 1335px-wide window fits
   "Medical Software: Phase 1-A" on one line at 900px tall, where the type is
   44px, and cannot at 1300px tall, where it is 60px and Back/Next drop to a
   line of their own under it. No single width divides those two cases, and
   picking one either stacks titles that fit or leaves titles that do not.

   js/band-fit.js measures the one-line width against the room actually left
   beside the actions and sets `is-stacked` on the bands that need it. There
   is no CSS-only fallback here on purpose: the sections themselves are
   fetched by js/main.js, so a page with no JS has no title to stack. */
.section__band.is-stacked .section__title:has(strong) {
  text-wrap: initial;
}

.section__band.is-stacked .section__title strong {
  display: block;
}

.section__band.is-stacked .section__title strong::after {
  content: none;
}

.section__band.is-stacked .section__title-tail {
  display: block;
  font-size: 0.72em;
  font-weight: 500;
  line-height: 1.15;
}

/* The state js/band-fit.js measures IN: the one-line form, held on one line.
   `nowrap` is what makes `scrollWidth` mean "the width this title would want
   if nothing constrained it" -- wrapped, it reports the width of the box it
   has already been squeezed into, which is the question, not the answer.

   It is set for a single frame between a write and a read and never paints.
   The stacked class is removed before this goes on, so the two never have to
   out-specify each other. */
.section__band.is-measuring .section__title {
  white-space: nowrap;
  text-wrap: initial;
}


/* ---- Desktop/laptop paging mode ----
   Deliberately NO `scroll-snap-type: mandatory` here. Mandatory snapping
   fights an in-flight programmatic smooth scroll: the browser re-snaps
   while js/scroll-engine.js's scrollIntoView() is still animating, which
   dragged the page BACKWARD past its target and left it stranded
   mid-section (measured: a single wheel gesture ending at scrollY 455 of
   a 1090px section, showing two sections at once). Since fp-active mode
   preventDefault()s every wheel event, the engine is the only thing that
   scrolls the page, so snapping has nothing left to correct.

   `proximity` keeps a gentle assist for the one path the engine does NOT
   drive — dragging the visible custom scrollbar — without contesting the
   engine's own animation. */
html.fp-active {
  scroll-snap-type: y proximity;
}

html.fp-active .section {
  scroll-snap-align: start;
}

/* Even `proximity` cannot coexist with a LONG programmatic scroll. A one-
   notch move is short enough that the nearest snap point is the target
   almost immediately; a nav click that jumps the whole deck is not. For most
   of that animation the nearest snap point is still the section it started
   from, so the browser hauls it back there -- measured: a hero -> contact
   click finished at scrollY 9 instead of 3600, leaving the page on the hero
   while the wave background had already advanced to the contact section.

   js/scroll-engine.js adds `.is-paging` for exactly as long as its own
   scroll is in flight, so snapping is off only while the engine is driving
   and returns for the drags it is there to assist. */
html.fp-active.is-paging {
  scroll-snap-type: none;
}

/* ---- Sidebar offset ----
   The sidebar (css/nav.css) is `position: fixed`, so it is out of flow and
   would sit on top of the content. `main` is inset by the same width to give
   the content its own column.

   1024px is not an arbitrary breakpoint: it is the width floor in
   js/scroll-engine.js's DESKTOP_QUERY, so the rail appears exactly when
   wheel-paging does. Below it the sidebar is an off-canvas drawer behind a
   fixed top bar (css/nav.css), so the content reclaims the full width and is
   inset from the TOP instead. */
@media (min-width: 1024px) {
  #main {
    padding-left: var(--nav-width);
  }
}

@media (max-width: 1023.98px) {
  #main {
    padding-top: var(--topbar-height);
  }
}
