:root {
  --black-transparent: #111215cc;
  /* Denser than --black-transparent, for plates that have to carry white
     text over something bright rather than just dim what is behind them.
     Still short of opaque, so what it covers stays faintly readable. */
  --black-plate: #111215ed;
  --transparent: transparent;
  --white: #fff;
  --grey-100: #111215;
  --black: #000;
  --max-screen-width: 1280px;
  /* The heading rail beside Resume and Contact. Both sections share it so
     their rules sit on one vertical line whatever the word in the rail; used
     as the max of minmax(max-content, ...) so a longer word still never
     clips. */
  --section-rail-width: 112px;
  --flyux-gradient-light: #4ae8df;
  --vrt-gradient-blue: #5644e5;
  --vrtmax-dark: #001039;
  --vrtmax-red: #ff484c;
  --vrt-gradient-pink: #e544b8;
  --solidlab-dark: #322bff;
  --flanders-gradient-dark: #eca81e;
  --flanders-gradient-light: #f8e028;
  --solidlab-light: #3bc8ff;
  --flyux-gradient-dark: #4a67e8;
  --white-2: white;
  --black-2: black;
  --grid-line: #ffffff1a;
  --grid-pitch: 48px;
  /* Secondary text on the footer's dark ground: the bottom-bar tagline, and
     the dimmer column titles. */
  --footer-muted: #a9aaae;
  --footer-muted-2: #8b8d92;
  /* Same graph paper as the hero, inverted for the light sections that
     follow it - dark lines on white instead of white lines on dark. Set a
     touch stronger than --grid-line because a dark line at that alpha over
     white all but disappears. Pitch is shared, so the two grids line up
     column for column across the seam. */
  --grid-line-ink: #11121521;
  /* The dashed outline of a plate still waiting for its content (the
     project pages' empty sections) - a step darker than the ruling, so it
     reads as an outline of its own rather than as more graph paper. */
  --grid-line-dashed: #11121540;
  /* Ink, dimmed: small uppercase labels, and the chapters in the project
     pages' rail that aren't the one being read. */
  --ink-muted: #111215b3;
  --ink-quiet: #111215a1;
  /* Secondary white text laid straight on a colour, such as a project's
     gradient banner. */
  --on-color-muted: #ffffffc4;
  /* The ground of the homepage below the hero: white, like the plates laid
     on it - only the grain over it (homepage.css) tells the two apart. */
  --paper: #fff;
  /* That grain: inline SVG noise, tiled at 220px and kept faint - texture
     you notice only up close. Shared by the homepage and the project pages. */
  --paper-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.07 0 0 0 0 0.07 0 0 0 0 0.08 0.035 0 0 0 -0.0125'/%3E%3C/filter%3E%3Crect width='100%' height='100%' filter='url(%23g)'/%3E%3C/svg%3E");
  --ticker-line: 16px;
  /* The hero fills the screen, but never gets taller than it is wide. On a
     monitor turned on its side a full-height hero is enormously tall, and
     since the portrait stops growing at 860px the composition just drifts
     apart into empty space. Capping it keeps the same proportions there and
     simply lets the next section start earlier. The 1100px floor is what
     keeps phones and tablets - narrow but legitimately tall - untouched;
     without it "no taller than wide" would squash a phone to 375px. */
  --hero-height: min(100vh, max(100vw, 1100px));
  /* Measured against the hero rather than the viewport so it stays in
     proportion once that cap kicks in. Identical while it hasn't. */
  --portrait-height: min(calc(var(--hero-height) * 0.86), 860px);
  /* Width of the band the background sketches are laid out in - the same
     expression .hero-sketches uses, restated in viewport units so things
     inside it can be sized off it (a percentage there would resolve against
     the wrong thing, and font-size has no percentage-of-width at all). The
     only difference is that 100vw counts the scrollbar; at the scales this
     feeds that is well under a pixel. */
  --sketch-band: min(100vw, calc(var(--portrait-height) * 2.6));
}

.w-layout-blockcontainer {
  max-width: 940px;
  margin-left: auto;
  margin-right: auto;
  display: block;
}

.w-layout-layout {
  grid-row-gap: 20px;
  grid-column-gap: 20px;
  grid-auto-columns: 1fr;
  justify-content: center;
  padding: 20px;
}

.w-layout-cell {
  flex-direction: column;
  justify-content: flex-start;
  align-items: flex-start;
  display: flex;
}

@media screen and (max-width: 991px) {
  .w-layout-blockcontainer {
    max-width: 728px;
  }
}

@media screen and (max-width: 767px) {
  .w-layout-blockcontainer {
    max-width: none;
  }
}

/* The site's fonts, self-hosted from fonts/ rather than loaded from Google
   Fonts: no second and third server to reach before the first paint, files
   that can be preloaded (see each page's <head>), and no visitor's IP sent
   to Google on every page view.

   Raleway is one variable file per script, covering every weight from 100
   to 800. Latin carries the site's own text and is preloaded; Latin
   Extended (accented names and the like) is only fetched by a page that
   uses one of its characters. The ranges are Google Fonts' own.

   It goes by a name of its own, "Raleway Site", not "Raleway": the Botpress
   chat widget declares a "Raleway" of its own from Google Fonts once the
   page has loaded, and as the later rule it would take over the page's
   text - a slightly different build of the font, so the page reflowed
   under the reader seconds after loading. Under this name, the widget's
   rules only reach the widget. */
@font-face {
  font-family: "Raleway Site";
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url(../fonts/raleway-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Raleway Site";
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url(../fonts/raleway-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Material Symbols Sharp, cut down to the icons the site uses. build.js
   writes the file and keeps the ?v= on this URL in step with its icon set
   (see "Icon font subset" there). font-display: block, so an icon is never
   seen as its ligature name in fallback text while the font loads. */
@font-face {
  font-family: "Material Symbols Sharp";
  font-style: normal;
  font-weight: 100 700;
  font-display: block;
  src: url(../fonts/material-symbols-sharp.woff2?v=215379e2) format("woff2");
}

.hero-image {
  /* Flat dark ground with a drawn grid over it - no photographic backdrop.
     Grid pitch is one cell + a 1px line; both are tokens so the density
     and weight can be tuned in variables.css.
     Drawn as one cell tiled by background-size, not a repeating-linear-
     gradient: at fractional zoom or display scaling a repeating gradient
     samples each 1px line at a different sub-pixel offset and drops some of
     them, where a tile is rasterised once and every line comes out alike.
     Every ruling on the site is drawn this way. */
  background-color: var(--grey-100);
  background-image:
    linear-gradient(to right, var(--grid-line) 1px, #0000 1px),
    linear-gradient(to bottom, var(--grid-line) 1px, #0000 1px);
  background-size: var(--grid-pitch) var(--grid-pitch);
  -webkit-text-fill-color: inherit;
  background-clip: border-box;
  /* positioned so this section is the containing block for .hero-container -
     without it, overflow: hidden never clips the absolutely positioned
     portrait and an oversized crop leaks horizontal page scroll. */
  position: relative;
  width: 100%;
  height: var(--hero-height);
  overflow: hidden;
}

/* Mobile-only stand-in for the nav's name+ticker: .nav-identity (below)
   hides that at this width in favor of the "TT" logo, so this repeats the
   same content above the hero photo instead - purely decorative (the "TT"
   link already covers the home-link semantics), hence aria-hidden in the
   markup. Hidden here and switched on in responsive.css, where it also
   gets its background - a solid one, not the nav bar's fade, since here it
   needs to fully hide the grid pattern behind both lines, not blend into
   it. Name and ticker are stacked in normal flow (no absolute positioning
   like .logo-role uses) since nothing here needs to line up with sibling
   nav links - it's not inside the nav bar. */
.hero-identity {
  display: none;
  text-align: center;
}

.hero-identity-name {
  color: var(--white);
  text-transform: uppercase;
  font-family: "Raleway Site", sans-serif;
  font-size: 24px;
  font-weight: 800;
  line-height: 1;
}

.hero-role {
  display: block;
  margin-top: 4px;
  height: var(--ticker-line);
  overflow: hidden;
}

/* Decorative UX-sketch line art behind the portrait (traced hand sketches,
   white baked into the SVGs themselves). Everything is sized and placed in
   % of the hero, so the composition holds its proportions at any viewport
   size rather than drifting as the window changes. Widths deliberately let
   the outer two bleed off their edges, as in the design. DOM order (before
   .hero-container) puts these behind the portrait - both are absolutely
   positioned with no z-index, so plain DOM order decides the stacking.
   responsive.css re-lays these out for mobile. */
/* The sketches are positioned against this band rather than the full hero,
   so the composition stays wrapped around the portrait instead of spreading
   to the window's edges. The portrait is sized from viewport *height*, so on
   a wide-but-short window it stays narrow while edge-anchored sketches drift
   outwards, leaving a gap in the middle - capping the band at a multiple of
   the portrait's own height ties the two together, so the sketches stay
   touching the portrait at any window shape rather than drifting out to the
   edges. Full width (so nothing changes) until the window is wider than
   that. On a very wide window this does mean the right-hand sketch finishes
   on screen instead of running off the edge - that's the intended trade:
   sitting against the portrait matters more than bleeding off the side, and
   its right-hand edge is a closed frame line, so it reads as a finished
   drawing rather than a cut-off one. */
.hero-sketches {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  width: min(100%, calc(var(--portrait-height) * 2.6));
  margin-left: auto;
  margin-right: auto;
  pointer-events: none;
}

.hero-sketch {
  position: absolute;
  height: auto;
  /* Webflow's base stylesheet sets img { max-width: 100% }, which silently
     clamps any width over 100% - the mobile card is deliberately wider than
     the viewport so it can bleed off the edge, and was being shrunk to fit
     instead. Same override .hero-portrait needs, for the same reason. */
  max-width: none;
  opacity: 0.7;
  pointer-events: none;
}

/* The hero's tagline. It used to be a traced SVG of the same words; as real
   text it stays crisp at any size, can be restyled without going back to a
   drawing program, and is readable to search engines and screen readers -
   which is why .hero-sketches is no longer aria-hidden (the two remaining
   drawings are decorative and carry alt="" instead). Still inside the band
   so it stays wrapped around the portrait like the drawings do.

   The size expression is the one the SVG's width used, divided by the
   roughly 6.5 ems "PRODUCT &" occupies at this tracking, so the lettering
   lands at the width the drawing did. The second term caps its height on
   wide-and-short windows, which is what keeps it clear of the sketch below
   - without it the lettering keeps growing while that one stays put. */
.hero-lettering {
  position: absolute;
  top: 26%;
  left: 6%;
  transform: rotate(-6.1deg);
  transform-origin: 0 0;
  margin: 0;
  opacity: 1;
  color: var(--white);
  font-family: "Raleway Site", sans-serif;
  font-size: min(calc(var(--sketch-band) * 0.0446), calc(var(--hero-height) * 0.0784));
  line-height: 0.9;
  text-transform: uppercase;
}

/* One element per line, in place of a <br>. The intro types the two lines in
   turn (css/modules/interactions.css), which needs each one to be clippable
   on its own. fit-content keeps each box hugging its own text, so the clip
   percentages the script sets mean the same thing on both. */
.hero-lettering-line {
  display: block;
  width: fit-content;
}

/* Solid, and thin by weight rather than by outline - Raleway's hairline
   stems are what make the contrast against DESIGN, so nothing is drawn on
   the glyph edges. Colour comes from .hero-lettering. */
.hero-lettering-thin {
  font-weight: 100;
  letter-spacing: 0.05em;
}

/* Set a touch larger than the rest. Shares the block's single baseline
   angle - no turn of its own. */
.hero-lettering-bold {
  font-size: 1.12em;
  font-weight: 800;
  letter-spacing: 0.01em;
}

/* Anchored by its top so the drawing reads from the top down - the circle
   and first placeholder box sit on screen and the tall remainder runs off
   the hero's bottom edge, where overflow: hidden crops it. Width is capped
   against viewport height as well as width (it's sized in % of width, so
   its aspect-derived height would otherwise swing wildly with window
   shape); the vh term holds its height near 73vh, keeping roughly the same
   two-thirds of the drawing on screen whatever the window shape. */
.hero-sketch-frame {
  top: 48%;
  left: 8.7%;
  width: min(25%, calc(var(--hero-height) * 0.445));
  transform: rotate(-14deg);
}

/* Surfaces visitors can doodle on (js/sketchpad.js): the hero, the white
   graph paper, the footer, and a project page's banner and ruled body. Each is its
   own stacking context so the canvas the script puts in it can sit at
   z-index -1 - behind the surface's content, but in front of its own
   background rather than dropping behind it. */
[data-sketchpad] {
  isolation: isolate;
}

body[data-sketchpad] {
  position: relative;
}

/* The canvases never take pointer events - the script listens on the
   document and hit-tests what is under the pen itself. Height and top are
   set by the script, which keeps each canvas to a window around the
   viewport rather than the full length of a tall surface. */
.sketchpad {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  z-index: -1;
  pointer-events: none;
}

/* Same opacity as .hero-sketch on the dark surfaces, so a drawn line reads
   as the same material as the traced ones; graphite on the white paper. */
[data-sketchpad="dark"] > .sketchpad {
  opacity: 0.7;
}

[data-sketchpad="light"] > .sketchpad {
  opacity: 0.8;
}

/* The hero's canvas is in the markup, after the sketch images but before
   .hero-container, and stacks in plain DOM order rather than at -1: that
   way drawings land on top of the printed sketches but still behind the
   portrait and the hero text. */
.hero-sketchpad {
  z-index: auto;
}

/* Pen cursor, shown only while the pointer is over paper the pen can
   actually write on (the script toggles .is-sketchable), so content keeps
   its own cursor. Scoped to pointers that can hover and point precisely -
   touch screens, where a drag means scroll, not draw, are left completely
   alone. The script bails out on the same query. */
@media (hover: hover) and (pointer: fine) {
  [data-sketchpad="dark"].is-sketchable {
    cursor: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='28'%20height='28'%20viewBox='0%200%2028%2028'%3E%3Cpath%20d='M3%2025l1.6-5.2%2014-14a2.6%202.6%200%200%201%203.6%203.6l-14%2014L3%2025z'%20fill='%23fff'%20stroke='%23111215'%20stroke-width='1.2'%20stroke-linejoin='round'/%3E%3C/svg%3E") 3 25, crosshair;
  }

  [data-sketchpad="light"].is-sketchable {
    cursor: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='28'%20height='28'%20viewBox='0%200%2028%2028'%3E%3Cpath%20d='M3%2025l1.6-5.2%2014-14a2.6%202.6%200%200%201%203.6%203.6l-14%2014L3%2025z'%20fill='%232a2b2f'%20stroke='%23fff'%20stroke-width='1.2'%20stroke-linejoin='round'/%3E%3C/svg%3E") 3 25, crosshair;
  }
}

/* Same width cap, sized to keep the slight bottom-right bleed the design
   has rather than letting it run off entirely on very wide, short windows.
   The width/right numbers look oversized because this SVG's canvas extends
   well past what's meant to be on screen: they're scaled so the drawing
   renders at the intended size with its right-hand portion running off the
   edge, rather than shrinking the whole thing to fit the canvas on screen. */
.hero-sketch-card {
  top: 26%;
  right: -11.5%;
  width: min(48%, calc(var(--hero-height) * 0.84));
  transform: rotate(11deg);
}

/* Cut-out headshot. The source is already cropped at the chest, so it sits on
   the bottom edge at its natural proportions - height-driven, with a px ceiling
   so tall viewports don't blow it up. Scale lives in one token per breakpoint
   (see variables.css / responsive.css); .hero-image clips any side overhang.
   The source photo itself isn't symmetric - the subject sits a couple percent
   right of the frame's own center - so centering the frame (translateX(-50%))
   left more shoulder overhanging on the right than the left. Shifting the
   translate a touch past -50% moves the crop window left to compensate. */
.hero-portrait {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-52.5%);
  width: auto;
  height: var(--portrait-height);
  max-width: none;
  pointer-events: none;
}

.hero-container picture {
  display: contents;
}

.navbar-logo-center-container {
  z-index: 5;
  background-color: #0000;
  width: 1030px;
  max-width: 100%;
  margin-left: auto;
  margin-right: auto;
  padding: 20px;
}

.navbar-logo-center-container.shadow-three {
  width: 100%;
  max-width: 1140px;
}

.container {
  width: 100%;
  max-width: 940px;
  margin-left: auto;
  margin-right: auto;
}

.navbar-wrapper-three {
  justify-content: center;
  align-items: center;
  display: flex;
  position: relative;
}

.navbar-brand-three {
  z-index: 5;
  position: absolute;
}

.nav-menu-wrapper-three {
  width: 100%;
}

.nav-menu-three {
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0;
  display: flex;
}

.nav-menu-block {
  justify-content: center;
  align-items: center;
  margin-bottom: 0;
  display: flex;
}

.nav-link {
  color: #1a1b1f;
  letter-spacing: .25px;
  margin-left: 5px;
  margin-right: 5px;
  padding: 5px 10px;
  font-size: 14px;
  line-height: 20px;
  text-decoration: none;
}

.nav-link:hover {
  color: #1a1b1fbf;
}

.nav-link:focus-visible, .nav-link[data-wf-focus-visible] {
  outline-offset: 0px;
  color: #0050bd;
  border-radius: 4px;
  outline: 2px solid #0050bd;
}

.nav-dropdown {
  margin-left: 5px;
  margin-right: 5px;
}

.nav-dropdown-toggle {
  letter-spacing: .25px;
  padding: 5px 30px 5px 10px;
  font-size: 14px;
  line-height: 20px;
}

.nav-dropdown-toggle:hover {
  color: #1a1b1fbf;
}

.nav-dropdown-toggle:focus-visible, .nav-dropdown-toggle[data-wf-focus-visible] {
  outline-offset: 0px;
  color: #0050bd;
  border-radius: 5px;
  outline: 2px solid #0050bd;
}

.nav-dropdown-icon {
  margin-right: 10px;
}

.nav-dropdown-list {
  background-color: #fff;
  border-radius: 12px;
}

.nav-dropdown-list.w--open {
  padding-top: 10px;
  padding-bottom: 10px;
}

.nav-dropdown-link {
  padding-top: 5px;
  padding-bottom: 5px;
  font-size: 14px;
}

.nav-dropdown-link:focus-visible, .nav-dropdown-link[data-wf-focus-visible] {
  outline-offset: 0px;
  color: #0050bd;
  border-radius: 5px;
  outline: 2px solid #0050bd;
}

.nav-link-accent {
  color: #1a1b1f;
  letter-spacing: .25px;
  margin-left: 5px;
  margin-right: 20px;
  padding: 5px 10px;
  font-size: 14px;
  font-weight: 700;
  line-height: 20px;
  text-decoration: none;
}

.nav-link-accent:hover {
  color: #1a1b1fbf;
}

.button-primary {
  color: #fff;
  letter-spacing: 2px;
  text-transform: uppercase;
  background-color: #1a1b1f;
  padding: 12px 25px;
  font-size: 12px;
  line-height: 20px;
  transition: all .2s;
}

.button-primary:hover {
  color: #fff;
  background-color: #32343a;
}

.button-primary:active {
  background-color: #43464d;
}

.container-2 {
  width: 100vw;
  max-width: none;
  height: 20px;
}

.container-3 {
  position: sticky;
  top: 0;
}

.navbar {
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  display: flex;
  position: static;
}

.nav-link-2 {
  position: static;
}

.container-4 {
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  display: flex;
}

.container-5 {
  flex-direction: row;
  justify-content: space-between;
  align-items: flex-start;
  display: flex;
}

.navbar-2 {
  background-color: var(--transparent);
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 64px;
  display: flex;
}

.nav-menu {
  height: 64px;
}

.navigation-bar {
  background: linear-gradient(to bottom, var(--grey-100) 0%, var(--grey-100) 75%, transparent 100%);
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
  display: flex;
  /* Absolute, not fixed: the bar sits over the top of the hero exactly as it
     did, but it scrolls away with it instead of following the page down. That
     hands every section below the hero the full height of the window - which
     the projects shelf spends on its covers (see homepage.css's --cover-cap)
     rather than on a strip of chrome that is only useful at the top. */
  position: absolute;
  inset: 0% 0% auto;
  /* Content (the plain nav links' own padding) naturally sizes this to 60px,
     which reads as cramped now that the name+ticker block fills most of it.
     Desktop only - see responsive.css, which resets this back to auto so the
     mobile bar (TT logo + hamburger) keeps its original tighter height. */
  min-height: 76px;
}

/* Skip link (partials/nav.html): the first thing a keyboard user tabs to on
   every page, straight past the nav to the page's <main id="main">. Out of
   sight until it has focus. */
.skip-link {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 2000;
  padding: 10px 16px;
  background: var(--grey-100);
  color: var(--white);
  font-family: "Raleway Site", sans-serif;
  font-size: 16px;
  font-weight: 600;
  text-decoration: none;
  transform: translateY(calc(-100% - 24px));
}

.skip-link:focus {
  transform: none;
  outline: 3px solid var(--white);
  outline-offset: -6px;
}

/* The "TT" home link only has something to show at mobile widths, where
   .logo appears (responsive.css); on desktop the bar's own "Tim Theys" link
   is the way home. Hidden rather than removed, so it keeps its share of the
   bar's space-around spacing, but out of the tab order and the
   accessibility tree, where it would be an empty, unnamed stop. */
.nav-home {
  visibility: hidden;
}

/* The menu toggle is a native <button> (keyboard and screen readers get it
   for free); this undoes what a button brings by default so it looks like
   the plain box css/webflow.css's .w-nav-button draws, and gives it a focus
   ring, which webflow.css takes away. */
button.nav-menu-toggle {
  border: 0;
  border-radius: 0;
  margin: 0;
  color: inherit;
  font-family: inherit;
  line-height: inherit;
  text-align: inherit;
}

.nav-menu-toggle .material-icon {
  display: block;
}

.nav-menu-toggle:focus-visible {
  outline: 2px solid var(--white);
  outline-offset: -6px;
}

.nav-container {
  flex-direction: row;
  flex: 1;
  justify-content: space-around;
  align-items: center;
  max-width: 1000px;
  display: flex;
}

.link-block {
  flex-direction: column;
  justify-content: center;
  align-items: center;
  display: flex;
}

.navigation {
  width: 160px;
  color: var(--white);
  text-align: center;
  text-transform: uppercase;
  object-fit: fill;
  align-self: center;
  font-family: "Raleway Site", sans-serif;
  font-size: 16px;
  font-weight: 400;
  /* Overrides w-nav-link's 20px so this box (padding + border + 20px
     line-height) spans the full 76px bar (see .navigation-bar min-height)
     edge to edge - otherwise the box is shorter than the bar, gets centered
     inside it, and the :hover border-bottom below no longer lines up with
     the bar's actual bottom edge. Desktop only - responsive.css resets this
     back to 20px for the stacked mobile menu, which doesn't share the taller
     bar. padding-bottom is 1px less than padding-top to leave room for that
     border, which is reserved (transparent) here rather than only added on
     :hover - otherwise hovering one link changes its box height, which
     shifts the whole row down by a pixel while hovered. */
  padding-top: 28px;
  padding-bottom: 27px;
  border-bottom: 1px solid transparent;
}

.navigation:hover {
  border-bottom-color: var(--white);
}

.navigation.w--current {
  color: var(--white);
}

.navigation.logo {
  align-self: auto;
  width: auto;
  padding: 0;
  font-size: 24px;
  line-height: 1;
  font-weight: 800;
  white-space: nowrap;
  text-decoration: none;
  display: block;
}

.navigation.logo:hover {
  border-bottom-style: none;
  border-bottom-width: 0;
  transform: scale(1.1);
}

.navigation.logo.w--current {
  color: var(--white);
}

.navigation.logo.hidden {
  display: none;
}

/* Nav centre: name over a rotating role ticker, replacing the former "TT"
   monogram. The ticker sits outside the anchor so the home link's accessible
   name stays "Tim Theys" rather than absorbing all four rotating lines.
   The ticker is positioned absolutely below the name so it never affects the
   name's own box for layout purposes - but that also means align-self: center
   (which centers based on the name's box alone) would center the name, not
   the name+ticker unit, leaving the ticker hanging below the other nav links'
   vertical center. The translateY shifts the whole unit up by half of what
   the ticker adds (its own height plus the gap above it), so the combined
   block's centre - not just the name's - lines up with the other links. */
.nav-identity {
  position: relative;
  align-self: center;
  transform: translateY(calc((var(--ticker-line) + 4px) / -2));
  /* The plain links carry 25px of their own side margin, so without these
     the centre - the one item that wants room around it - ends up in the
     tightest gaps on the bar (25px against the others' 50px). These set it
     apart from About and Resume instead. */
  margin-left: 32px;
  margin-right: 32px;
  width: 160px;
  text-align: center;
}

.logo-role {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: 2px;
  height: var(--ticker-line);
  display: block;
  overflow: hidden;
}

.navigation.logo.foot {
  text-align: left;
  align-self: flex-start;
  margin-bottom: 8px;
  display: block;
}

.navigation.logo.foot:hover {
  transform: none;
}

.navigation.hidden {
  display: none;
}

.nav-logo {
  margin: 20px;
}

/* An icon is a ligature: its name, spelled exactly, in lowercase. The font
   is a subset of just the site's icons (see icon_names in each page's
   <head>), and that subset only knows the lowercase spellings - so an icon
   inside an uppercase or letter-spaced label must not inherit either, or it
   shows as its name in text. */
.icon {
  font-family: Material Symbols Sharp, sans-serif;
  font-weight: 200;
  text-transform: none;
  letter-spacing: normal;
}

/* Every Material Symbols glyph is exactly 1em wide. Until the font arrives
   the icon is its name in invisible fallback text ("handshake" is nine
   letters wide, and may wrap), and when the font lands the text around it
   reflowed - on the Diploma page enough to push the whole case study down,
   a layout shift past the 0.1 Core Web Vitals line. Held to its final size
   from the start, an icon has nothing to change when its font loads. Two
   icons sit centred in a box wider than themselves, where a 1em box would
   pull the glyph off centre, and are left out: the hero's "Discover my best
   work" arrow and the prototype's "Full screen" button. */
.icon:not(.hero-cta .icon, .case-proto-full .icon),
.material-icon {
  width: 1em;
  overflow: hidden;
  white-space: nowrap;
}

.icon.medium {
  font-size: 24px;
}

/* Scroll cue at the foot of the hero: the label sits above the arrow and
   the pair bounce as one, so .bounce-arrow goes on this wrapper rather than
   on the arrow itself. It also carries the positioning that used to live on
   .icon.medium.bottom, which is now just an in-flow line inside it. */
.hero-cta {
  position: absolute;
  inset: auto 0% 3%;
  /* As wide as its words rather than the hero, centred by the auto margins:
     a full-width link turned the whole band across the hero into something
     the sketch pen (js/sketchpad.js) has to stop at, and into a click target
     well off to either side of the label. */
  width: fit-content;
  margin-left: auto;
  margin-right: auto;
  text-align: center;
  text-decoration: none;
}

.hero-cta-label {
  display: block;
  /* Keeps the line clear of the chat widget parked in the bottom corner on
     narrow screens: reserving a margin either side (the label stays centred)
     makes the text wrap instead of running under it. Only bites once the
     line no longer fits, so it stays on one line on wider screens. */
  max-width: max(160px, calc(100vw - 180px));
  margin-left: auto;
  margin-right: auto;
  margin-bottom: 2px;
  text-wrap: balance;
  color: var(--white);
  text-transform: uppercase;
  font-family: "Raleway Site", sans-serif;
  font-size: 16px;
  font-weight: 600;
}

.icon.medium.bottom {
  color: var(--white);
  text-align: center;
  font-size: 32px;
}

.nav-links {
  flex: 1;
  display: flex;
}

.logo {
  color: var(--white);
  font-family: "Raleway Site", sans-serif;
  font-size: 24px;
  font-weight: 800;
  display: none;
}

.logo.hidden {
  display: none;
}

.section {
  height: 100vh;
}

.hero-container {
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  display: flex;
  position: absolute;
  inset: 0%;
}

.carrousel {
  display: block;
  color: var(--white);
  text-align: center;
  text-transform: uppercase;
  white-space: nowrap;
  font-family: "Raleway Site", sans-serif;
  font-size: 14px;
  font-weight: 300;
  letter-spacing: .6px;
  height: var(--ticker-line);
  line-height: var(--ticker-line);
}

.changer-overflow {
  float: right;
  height: 20px;
  overflow: hidden;
}

.changer-move {
  display: inline-block;
}

.changer {
  position: relative;
  top: 0;
}

.text-block-2 {
  position: absolute;
  inset: auto 0% 0%;
}

.section-title {
  color: var(--grey-100);
  text-align: right;
  text-transform: uppercase;
  align-self: flex-end;
  margin-top: 0;
  margin-bottom: 0;
  padding-left: 16px;
  padding-right: 16px;
  font-family: "Raleway Site", sans-serif;
  font-size: 20px;
  font-weight: 300;
  line-height: 100%;
  position: sticky;
  top: 70px;
}

.section-title.right {
  text-align: left;
  align-self: flex-start;
}

.section-title.center {
  text-align: center;
}

/* Projects: the shared section heading with a pair of arrow buttons beside
   it, above <cover-shelf> (js/cover-shelf.js), the custom element that draws the four
   project panels as a cover-flow shelf. Everything inside the shelf is styled
   in its own shadow DOM; only the frame around it lives here.

   The section itself is full-bleed - it lays no ground of its own, so the
   graph paper runs behind the shelf from edge to edge and the cards, which
   are white, read as paper laid on it. The cards get the whole column instead
   of the column minus a plate's padding; only the written parts are held to
   the site's max width. */
.projects {
  position: relative;
  width: 100%;
}

/* Heading and arrows, on one line: the arrows sit just right of the word
   rather than on a row of their own under it, which hands that row's height
   to the covers (see --cover-cap below). The 20px inset sits outside the max
   width, as it does in .intro-section, so these line up exactly with the
   intro content below. */
.projects-inner {
  display: flex;
  align-items: center;
  column-gap: 24px;
  margin-bottom: 20px;
  width: 100%;
  max-width: calc(var(--max-screen-width) + 40px);
  margin-left: auto;
  margin-right: auto;
  padding-left: 20px;
  padding-right: 20px;
}

/* Full-bleed, while the shelf it holds stays in the column. */
.shelf-wrap {
  position: relative;
  width: 100%;
}

/* What the shelf may spend on a cover, in height: the window less the heading
   and arrows above it and the card's own text below it. Nothing is set aside
   for the nav bar, which scrolls away with the hero (hero-and-nav.css). The
   component treats this as a ceiling, not a size - on a tall screen the covers
   are as wide as the column allows and this never binds; on a laptop it is
   what keeps the whole section on one screen. */
.shelf-wrap cover-shelf {
  --cover-cap: calc(100vh - 336px);
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  display: block;
  width: 100%;
  max-width: calc(var(--max-screen-width) + 40px);
  margin-left: auto;
  margin-right: auto;
  padding-left: 20px;
  padding-right: 20px;
}

/* From a tablet up, the text below the cover counts the focused project's
   subtitle too, which is several lines on its own. Not on a phone: the page
   scrolls past the shelf there anyway, and a cover pulled in for the subtitle
   would only narrow the card and wrap its text onto more lines. */
@media screen and (min-width: 768px) {
  .shelf-wrap cover-shelf {
    --cover-cap: calc(100vh - 416px);
  }
}

/* The shelf's projects as plain links, which build.js writes inside
   <cover-shelf> from the shelf's own list: what is shown where script never
   runs, and what an agent reading the raw HTML follows. Once the shelf runs
   its shadow root takes over - it has no <slot>, so these are never drawn -
   and until then .w-mod-js keeps them out of sight, so they never flash up
   first. */
.cover-shelf-fallback {
  display: grid;
  gap: 24px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: "Raleway Site", sans-serif;
  font-size: 16px;
  line-height: 1.5;
}

.cover-shelf-fallback a {
  color: inherit;
  font-size: 20px;
  font-weight: 700;
}

.cover-shelf-fallback p {
  margin: 4px 0 0;
}

.w-mod-js .cover-shelf-fallback {
  display: none;
}

/* .section-title minus what only suits a heading beside a ruled column: the
   right-hand alignment, the 16px stand-off from the rule, and the sticky
   offset. Two classes so this outranks responsive.css's per-breakpoint
   .section-title re-alignments as well. */
.projects .projects-title {
  position: static;
  align-self: auto;
  text-align: left;
  margin-bottom: 0;
  /* Straight on the graph paper, and with no padding of its own, so the word
     starts on the same line as the focused card below it. Anything around it
     reads as an edge that does not line up with the card. */
  padding: 0;
}

.shelf-controls {
  display: flex;
  grid-column-gap: 8px;
  grid-row-gap: 8px;
}

/* Same ink outline as .button, squared off for a lone icon; fills with ink
   on hover instead of scaling, so the pair stays put under the pointer. White
   rather than transparent, so the pair reads off paper like the heading above
   them instead of off the ruling. */
.shelf-arrow {
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--grey-100);
  background-color: var(--white);
  color: var(--grey-100);
  justify-content: center;
  align-items: center;
  display: inline-flex;
  cursor: pointer;
  transition: background-color 150ms ease, color 150ms ease;
}

.shelf-arrow .icon {
  font-size: 22px;
  line-height: 1;
}

.shelf-arrow:hover {
  background-color: var(--grey-100);
  color: var(--white);
}

.shelf-arrow:focus-visible {
  outline: 3px solid var(--grey-100);
  outline-offset: 3px;
}

/* At the ends of the shelf. Faded rather than hidden, so the pair does not
   move about as you step through the projects. */
.shelf-arrow:disabled {
  opacity: 0.3;
  cursor: default;
}

.shelf-arrow:disabled:hover {
  background-color: var(--white);
  color: var(--grey-100);
}

/* On a short window the gap between the heading row and the shelf tightens,
   buying back a little more height for the covers. The buttons keep their
   44px - that is the smallest a touch target should be, and a couple of
   lines of margin are cheaper than a control nobody can hit. */
@media screen and (max-height: 860px) {
  .projects-inner {
    margin-bottom: 12px;
  }
}

@media screen and (max-height: 760px) {
  .projects-inner {
    margin-bottom: 8px;
  }
}

.image {
  width: 100%;
  height: 100%;
}

.cell-3 {
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.title-homepage {
  color: var(--grey-100);
  margin-top: 0;
  margin-bottom: 10px;
  font-family: "Raleway Site", sans-serif;
  font-size: 17px;
  font-weight: 700;
  line-height: 1.3;
}

.button {
  grid-column-gap: 8px;
  grid-row-gap: 8px;
  background-color: var(--transparent);
  color: var(--grey-100);
  border: 1px solid var(--grey-100);
  flex-direction: row;
  justify-content: center;
  align-items: center;
  padding: 12px 36px;
  font-family: "Raleway Site", sans-serif;
  font-size: 16px;
  line-height: 120%;
  display: flex;
}

.button:hover {
  background-color: var(--transparent);
  transform: scale(1.05);
}

.button.white {
  border-color: var(--white);
  color: var(--white);
}

.page-body {
  flex-direction: column;
  align-items: center;
  display: flex;
}

/* Every page's <main id="main"> (the skip link's target), around everything
   between the nav and the footer. Full width and itself a centring flex
   column, so the sections inside lay out exactly as they did directly under
   .page-body. */
.page-main {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Sketching paper under everything below the hero: a white ground with a
   very fine grain over it, so the page reads as paper while the plates laid
   on it (the intro, the CV columns, the project cards) stay clean white.
   Set on the body, homepage only, so it runs unbroken from the hero's foot
   to the footer; the hero and the footer paint their own dark grounds over
   it. The grain is inline SVG noise, tiled, and kept faint - texture you
   notice only up close. */
.page-body.home {
  /* The one gap between sections on the homepage - every .spacer, and the
     intro's top padding that stands in for one - so they all move together.
     The project pages keep the .spacer default. */
  --section-gap: 18vh;
  background-color: var(--paper);
  background-image: var(--paper-grain);
  background-size: 220px 220px;
}

.image-2 {
  width: 100%;
}

.spacer {
  height: var(--section-gap, 12vh);
}

/* The sections the nav links to. One that fits the window is centred in it
   when a link glides there (js/interactions.js); one taller than the window
   - the resume - starts this far below its top edge instead, as it does on
   a plain jump to the hash. */
.projects,
.intro-section,
.cv,
.contact {
  scroll-margin-top: 24px;
}

/* Contact: the hero's lettering vocabulary brought down onto the paper -
   the same ruled heading column as Resume, then three lines of Raleway
   100/800, one lead line, and the two actions under the copy, so the block
   reads top-to-bottom like the hero does.

   Geometry copied from .cv, but mirrored: the heading rail sits on the right,
   so "Contact" and its rule end on the same right-hand line as the Resume
   plates, and the copy beside it is set flush right to meet it. No vertical
   padding: the .spacers above and below are the whole gap to the
   neighbouring sections. */
.contact {
  width: 100%;
  max-width: calc(var(--max-screen-width) + 40px);
  display: grid;
  /* Mirrored: the photo, then the copy, then the heading rail last, so the
     rail's outer edge is the column's right edge - the line the Resume plates
     end on. The copy takes only what it needs; the photo gets the rest. */
  grid-template-columns: minmax(0, 1fr) auto minmax(max-content, var(--section-rail-width));
  column-gap: 20px;
  padding: 0 20px;
  box-sizing: border-box;
}

.contact-heading-cell {
  order: 3;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  border-left: 1px solid var(--grey-100);
}

/* Mirror of .cv .section-title: right-aligned, with the 16px stand-off on the rule
   side only. Two classes so this outranks responsive.css's per-breakpoint
   .section-title re-alignments. */
.contact .section-title {
  text-align: right;
  align-self: flex-end;
  padding: 0 0 0 16px;
}

/* Set flush right, so the lettering, lead and actions end on the same line
   as the Resume plates above - the column shares .cv's geometry, so its
   right edge already is that line. */
/* A cut-out, cropped at the thighs, so it stands on the section's bottom
   edge - the line the rail and the buttons end on as well. Held to a size
   where it keeps company with the lettering rather than towering over it. */
.contact-photo {
  order: 1;
  align-self: end;
  justify-self: center;
  width: 100%;
  max-width: 440px;
}

.contact-photo img {
  display: block;
  width: 100%;
  height: auto;
}

.contact-body {
  order: 2;
  align-self: end;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: right;
  gap: 32px;
}

/* Same construction as .hero-lettering: thin by weight, not by outline, with
   the bold words set a touch larger on a shared 0.9 line-height (the line
   boxes overlap slightly - intended, as in the hero). No rotation here - this
   one sits square on the paper, since it heads a block of type rather than a
   drawing. Fluid so the three lines never outrun the column. */
.contact-lettering {
  margin: 0;
  color: var(--grey-100);
  font-family: "Raleway Site", sans-serif;
  font-size: clamp(38px, 4.6vw, 56px);
  font-weight: 100;
  letter-spacing: 0.05em;
  line-height: 0.9;
  text-transform: uppercase;
}

.contact-lettering-line {
  display: block;
}

.contact-lettering-bold {
  font-size: 1.12em;
  font-weight: 800;
  letter-spacing: 0.01em;
}

/* At .intro-text's size - the site's one step above body text - so it reads
   as the voice of the about section rather than as a caption under the
   lettering. */
.contact-lead {
  margin: 0;
  max-width: 44ch;
  color: var(--grey-100);
  font-family: "Raleway Site", sans-serif;
  font-size: clamp(16px, 2.5vw - 12.9px, 20px);
  font-weight: 400;
  line-height: 130%;
}

.contact-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 12px;
}

/* The filled button: the primary action of its block - "Send me an email",
   where two outlined buttons side by side would carry equal weight, and
   "Download my CV" under the Resume. Everything else - type, padding, the
   scale on hover - is .button's. */
.button.button-solid {
  background-color: var(--grey-100);
  border-color: var(--grey-100);
  color: var(--white);
}

.button.button-solid:hover {
  background-color: var(--grey-100);
  color: var(--white);
}

/* The heading drops above the copy and the rule goes under it, as the Resume
   title does at this width, and the copy goes back to flush left - the
   right-hand line only means something with the rail beside it. The photo
   stays beside the copy, now on the right, still standing on the line the
   buttons end on; the copy takes what it needs and the photo the rest. */
@media screen and (max-width: 991px) {
  .contact {
    grid-template-columns: auto minmax(0, 1fr);
    row-gap: 20px;
  }

  .contact-heading-cell {
    grid-column: 1 / -1;
    order: 0;
    align-items: flex-start;
    border-left: none;
    border-bottom: 1px solid var(--grey-100);
    padding-bottom: 8px;
  }

  .contact .section-title {
    text-align: left;
    align-self: flex-start;
    padding: 0;
  }

  .contact-body {
    align-items: flex-start;
    text-align: left;
  }

  .contact-actions {
    justify-content: flex-start;
  }

  .contact-body {
    order: 1;
  }

  .contact-photo {
    order: 2;
    justify-self: end;
    max-width: 320px;
  }
}

/* Phone: no room beside the copy, so the photo closes the section instead -
   smaller, centred over the full-width buttons, and standing on a rule of
   its own. The rule gives the crop at the thighs a floor, and echoes the one
   under the title, so the section reads as one framed block. */
@media screen and (max-width: 767px) {
  .contact {
    grid-template-columns: minmax(0, 1fr);
  }

  .contact-photo {
    display: flex;
    justify-content: center;
    justify-self: stretch;
    max-width: none;
    margin-top: 12px;
    border-bottom: 1px solid var(--grey-100);
  }

  .contact-photo img {
    width: min(300px, 80%);
  }

  .contact-actions {
    width: 100%;
  }

  .contact-actions .button,
  .contact-actions .link-button {
    flex: 1 1 100%;
    justify-content: center;
    padding-left: 20px;
    padding-right: 20px;
  }
}

.link-button {
  grid-column-gap: 8px;
  grid-row-gap: 8px;
  color: var(--grey-100);
  border: 1px solid var(--grey-100);
  align-items: center;
  padding: 12px 36px;
  font-family: "Raleway Site", sans-serif;
  font-size: 16px;
  line-height: 120%;
  text-decoration: none;
  display: flex;
}

.link-button:hover {
  transform: scale(1.05);
}

/* The light half of the page's graph paper: one band wrapping the intro, the
   projects and the spacers around them, so they all sit on a single continuous
   sheet - rows and columns run through unbroken rather than restarting at
   each section's top edge. It carries the hero's ruling on - same pitch, ink
   instead of white - so the two read as one sheet across the seam rather than
   as a dark band with a plain page under it. Full-bleed (the .page-body flex
   column centres its children, hence the explicit width) so the ruling runs
   edge to edge and its columns line up with the hero's; it is itself a
   centring flex column so the sections inside lay out exactly as they did
   directly under .page-body. */
.ruled-band {
  position: relative;
  /* Own stacking context, so the ruling below can sit at z-index -1 - behind
     the band's content but still in front of the page - rather than dropping
     behind the page background entirely. */
  isolation: isolate;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* The ruling itself, on a layer of its own so it can sit behind the band's
   content. It runs at full strength from the hero down to the footer, which
   carries the same ruling on in white. */
.ruled-band::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    linear-gradient(to right, var(--grid-line-ink) 1px, #0000 1px),
    linear-gradient(to bottom, var(--grid-line-ink) 1px, #0000 1px);
  background-size: var(--grid-pitch) var(--grid-pitch);
  pointer-events: none;
}

/* The section under the projects: a first-person intro and the logos of the
   organisations the work was done for, on one plate. Its top padding is the gap under
   the projects shelf, in place of a .spacer, and matches one. No bottom
   padding: the .spacer under it is the whole gap to the Resume heading, so
   the section sits the same distance from the blocks above and below it. */
.intro-section {
  width: 100%;
  padding: var(--section-gap, 12vh) 20px 0;
}

/* One heading, then one white plate holding everything else: the lettering
   across the top, then the copy and the logos side by side under it, split
   by a hairline (2a in the About me handoff). The tablet and phone layouts
   in responsive.css fold the logos down into a strip at the plate's foot. */
.intro-layout {
  width: 100%;
  max-width: var(--max-screen-width);
  margin-left: auto;
  margin-right: auto;
}

/* .section-title minus what only suits a heading beside a ruled column: the
   right-hand alignment, the 16px stand-off from the rule, and the sticky
   offset. Centred in a row one grid square tall, so the word sits on the
   ruling's rhythm above the plate. Two classes so this outranks
   responsive.css's per-breakpoint .section-title re-alignments as well. */
.intro-layout .intro-title {
  display: flex;
  align-items: center;
  min-height: var(--grid-pitch);
  margin-bottom: 0;
  padding: 0;
  position: static;
  text-align: left;
}

/* The plate: a white ground under the copy, so running text is read off
   paper rather than off the ruling, and one edge around the copy and the
   logos together so they read as one block.

   No bottom padding: the photo stands on the plate's bottom edge, so the
   inset under the copy and under the logos is each column's own instead.
   The inset is a custom property because the photo, the logo strip and
   those columns all reach back out through it at the smaller sizes. */
.intro-plate {
  --plate-inset: 32px;
  padding: var(--plate-inset) var(--plate-inset) 0;
  background-color: var(--white);
  /* Edged in the same line as the graph paper it lies on. */
  border: 1px solid var(--grid-line-ink);
}

/* "Hi, I'm Tim." - built like .contact-lettering (Raleway at its thinnest
   and heaviest, uppercase, the bold word 1.12 times the thin one) and sized
   to match it, but kept on one line rather than stacked. */
.intro-lettering {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 16px;
  margin: 0 0 28px;
  color: var(--grey-100);
  font-family: "Raleway Site", sans-serif;
  font-size: clamp(38px, 4.6vw, 56px);
  line-height: .9;
  text-transform: uppercase;
}

.intro-lettering-thin {
  font-weight: 100;
  letter-spacing: .05em;
}

.intro-lettering-bold {
  font-size: 1.12em;
  font-weight: 800;
  letter-spacing: .01em;
}

/* Copy, hairline, logos. Bottom-aligned, so the photo at the foot of the
   copy column stands on the plate's edge while the logos end on the same
   inset line as the call to action. The logo column gives way before the
   copy does as the window narrows. */
.intro-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 1px minmax(320px, 420px);
  column-gap: 32px;
  align-items: end;
}

.intro-copy-col {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* Three facts across the copy column, ruled top and bottom and between the
   cells in the plate's own edge line, so the rail reads as part of the paper
   rather than as three cards on it. */
.intro-facts {
  display: flex;
  margin: 0 0 24px;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--grid-line-ink);
  border-bottom: 1px solid var(--grid-line-ink);
}

.intro-fact {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  padding: 16px 18px;
  color: var(--grey-100);
  font-family: "Raleway Site", sans-serif;
}

.intro-fact:first-child {
  padding-left: 0;
}

.intro-fact:last-child {
  padding-right: 0;
}

.intro-fact + .intro-fact {
  border-left: 1px solid var(--grid-line-ink);
}

/* Shared by the facts' labels and the "Key collaborations" one, which is the
   same small label rather than a section title of its own. */
.intro-fact-label,
.collaborations-label {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 8px;
  color: var(--ink-muted);
  font-family: "Raleway Site", sans-serif;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .6px;
  line-height: 1.2;
  text-transform: uppercase;
}

/* Same thin Material Symbols Sharp line as every other icon on the page
   (.icon sets the family and weight), in full ink rather than the label's
   muted one. */
.intro-fact-icon {
  flex: none;
  color: var(--grey-100);
  font-size: 20px;
  line-height: 1;
  letter-spacing: 0;
  text-transform: none;
}

.intro-fact-value {
  font-size: 15px;
  font-weight: 700;
  line-height: 1.2;
}

.intro-fact-meta {
  font-size: 13.5px;
  font-weight: 400;
  line-height: 1.3;
}

/* The phone's one-line version of the meta, which sits inside the value line
   (responsive.css). A shorter wording than the meta for two of the three, so
   it is its own copy rather than the meta restyled. */
.intro-fact-short {
  display: none;
  font-weight: 400;
}

.intro-text {
  margin: 0;
  color: var(--grey-100);
  font-family: "Raleway Site", sans-serif;
  font-size: 17px;
  font-weight: 400;
  line-height: 1.4;
  text-wrap: pretty;
}

/* The first paragraph runs the copy column's full width; the rest share the
   foot with the photo. */
.intro-copy-col > .intro-text {
  margin-bottom: 12px;
}

/* The last two paragraphs and the call to action beside the photo. A plain
   two-column grid in place of a float: the photo is its own column, so the
   copy never has to wrap round it and nothing needs measuring after layout. */
.intro-foot {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 200px;
  column-gap: 18px;
}

/* The inset under the copy lives here rather than on the plate, so the photo
   beside it can go all the way down to the plate's edge. */
.intro-foot-copy {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-bottom: var(--plate-inset);
}

/* A cut-out with a real alpha channel, mirrored to face into the copy,
   standing on the plate's bottom edge. Square, from the source; wider than
   its column, so it reaches into the gap beside it rather than shrinking -
   which is also why Webflow's base max-width: 100% on img is lifted. */
.intro-photo {
  display: block;
  align-self: end;
  justify-self: end;
  width: 220px;
  height: 220px;
  max-width: none;
  object-fit: contain;
  transform: scaleX(-1);
}

/* The one call to action in this section, flush with the left edge of the
   running text. The white behind it is the plate's, which is why the
   transparent fill .button gives every other button on the site works here
   too. */
.intro-cta {
  box-sizing: border-box;
  height: 44px;
  width: fit-content;
  margin: 14px 0 0;
  white-space: nowrap;
  text-decoration: none;
}

/* "Let's connect" everywhere but the phone, where the button shares the
   plate's foot with the photo and is shortened to "Connect". */
.intro-cta-short {
  display: none;
}

/* The hairline between the copy and the logos: from under the lettering down
   to the same inset line the copy and the logos end on. */
.intro-rule {
  align-self: stretch;
  margin-bottom: var(--plate-inset);
  background: var(--grid-line-ink);
}

.collaborations {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding-bottom: var(--plate-inset);
}

.collaborations-label {
  margin-bottom: 14px;
}

.collaborations-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.collaboration {
  position: relative;
  margin: 0;
  aspect-ratio: 1;
  overflow: hidden;
  /* Lets the caption below size its own type off the tile it covers. */
  container-type: inline-size;
}

.collaboration-logo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Who they are, over the logo on hover. It is in the markup unconditionally
   and only ever hidden by opacity, so it stays in the accessibility tree -
   which is what lets the logos themselves carry alt="" (the caption already
   names them) and what gives screen-reader and keyboard users the context a
   hover can't. tabindex on the figure is what makes it reachable by keyboard.
   A touch screen has no hover at all, so there a tap is the only way in - and
   neither :focus-visible nor :focus can be relied on for that (Safari on iOS
   does not focus a non-form element on tap), which is why the tap path is a
   class toggled by js/interactions.js rather than a pseudo-class. */
.collaboration-caption {
  position: absolute;
  /* A pixel proud of the tile on every side, not flush with it: the tiles are
     squares of a fractional width (three of them across a fluid column), and
     flush edges let a hairline of un-covered logo survive the rounding along
     the top. The bleed is clipped by the figure's overflow: hidden, so it
     costs nothing. */
  inset: -1px;
  /* Both undo Webflow's base figcaption rule (css/webflow.css), which is
     written for a caption sitting under its figure: its margin-top offsets
     an absolutely positioned box even with inset: 0 set, leaving a strip of
     bare logo along the top of the plate, and its centring is at odds with
     the ranged-left text everywhere else on the page. */
  margin: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 6px;
  gap: clamp(3px, 3.5cqi, 6px);
  padding: 12px;
  padding: clamp(7px, 8cqi, 12px);
  /* --black-transparent is not enough here: the brightest marks (VRT MAX's
     red, Flanders' yellow) read straight through it. The logo still shows
     faintly under the text, which is what keeps the overlay attached to the
     tile it belongs to. */
  background-color: var(--black-plate);
  color: var(--white);
  text-align: left;
  opacity: 0;
  transition: opacity 200ms ease;
}

.collaboration:hover .collaboration-caption,
.collaboration:focus-visible .collaboration-caption,
.collaboration.is-open .collaboration-caption {
  opacity: 1;
}

/* Both sizes are given twice on purpose: the plain value is what a browser
   without container queries keeps, the cqi one is what everything since 2023
   uses to scale the caption with its tile - which is what lets the same
   overlay work on a 96px tile and a 300px one. */
.collaboration-name {
  font-family: "Raleway Site", sans-serif;
  font-size: 13px;
  font-size: clamp(10px, 11.5cqi, 14px);
  font-weight: 700;
  line-height: 120%;
  text-transform: uppercase;
  letter-spacing: .6px;
}

.collaboration-context {
  font-family: "Raleway Site", sans-serif;
  font-size: 11px;
  font-size: clamp(9px, 9.8cqi, 12px);
  /* "broadcaster," is wider than a phone tile's measure at the smallest
     sizes; breaking it is better than letting it run out of the plate. */
  overflow-wrap: break-word;
  font-weight: 400;
  line-height: 125%;
}

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

/* The shelf's arrows step through covers that only exist where script runs. */
html:not(.w-mod-js) .shelf-controls {
  display: none;
}

/* Digital CV: the Experience and Education lists under the projects shelf,
   and the "Download my CV" button that goes with them. Each entry is a native
   <details>, so the open/close behaviour needs no JavaScript; the shared
   name= attribute on each list makes it an exclusive accordion (one open at a
   time per column) in browsers that support it, and simply allows several
   open at once in those that don't.

   Layout mirrors the contact section: a ruled heading column on the left, the
   content beside it, held to the site's max width. The 20px inset sits
   outside that width, as it does in .projects-inner, so the section's content
   starts on the same left line as the projects title and the intro above it
   rather than 20px in from it. */
.cv {
  width: 100%;
  max-width: calc(var(--max-screen-width) + 40px);
  display: grid;
  /* The rail is just wider than the word in it - the shared
     --section-rail-width, so the rule lines up with Contact's below - and the
     two lists take the width that frees up. */
  grid-template-columns: minmax(max-content, var(--section-rail-width)) minmax(0, 1fr) minmax(0, 1fr);
  grid-column-gap: 20px;
  grid-row-gap: 20px;
  /* No vertical padding: the .spacers above and below are the gaps to the
     sections either side, as they are everywhere else on the page. */
  padding: 0 20px;
  box-sizing: border-box;
}

/* Spans all three rows - the lists, the band under them and the download
   button under that - so the rule runs the full height of the section and
   ends level with the button's bottom edge. Written as a span rather than
   1 / -1, which would resolve against the explicit grid only and stop after
   the first row. */
.cv-heading-cell {
  grid-row: 1 / span 3;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  border-right: 1px solid var(--grey-100);
}

/* Left-aligned and with no padding on that side, the same way .projects-title
   is, so the word starts exactly where "Projects" and "Key collaborations"
   start above it. The 16px on the right is the stand-off from the rule, which
   the max-content rail then sits directly behind. Two classes so this outranks
   responsive.css's per-breakpoint .section-title re-alignments. */
.cv .section-title {
  text-align: left;
  align-self: flex-start;
  padding: 0 16px 0 0;
}

/* Each column sits on a white plate, the same device .intro-text uses: this
   section lands inside .ruled-band, and a long run of body copy read straight
   off the graph paper is noticeably harder going than one read off paper. The
   heading column is left bare on purpose - a single word at 20px is exactly
   what the ruling is a good ground for, and it keeps the section reading as
   part of the same sheet as the projects above it. If you would rather have
   no ruling here at all, paste the block after .ruled-band's closing </div>
   instead and drop the background/padding below. */
.cv-column {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 28px 32px;
  background-color: var(--white);
  /* Edged in the same line as the graph paper it lies on. */
  border: 1px solid var(--grid-line-ink);
  box-sizing: border-box;
}

.cv-column-title {
  margin-bottom: 12px;
}

/* The list is ruled rather than boxed: a hairline above every row and one
   under the last, so it reads as the same drawn line work as the rest of the
   page instead of as a stack of cards. */
.cv-list {
  display: flex;
  flex-direction: column;
  border-bottom: 1px solid var(--grey-100);
}

.cv-item {
  border-top: 1px solid var(--grey-100);
}

.cv-item > summary {
  display: flex;
  align-items: center;
  grid-column-gap: 16px;
  padding: 18px 0;
  cursor: pointer;
  list-style: none;
  font-family: "Raleway Site", sans-serif;
  color: var(--grey-100);
}

/* Safari's default disclosure triangle; the +/- icon on the right replaces it. */
.cv-item > summary::-webkit-details-marker {
  display: none;
}

.cv-item > summary:hover {
  opacity: .65;
}

.cv-item > summary:focus-visible {
  outline: 3px solid var(--grey-100);
  outline-offset: 3px;
}

/* Sized to the full height of the two lines beside it, so the mark carries
   the row rather than sitting as a detail on the date line. */
.cv-logo {
  width: 48px;
  height: 48px;
  flex: none;
  object-fit: contain;
}

.cv-role {
  display: flex;
  flex-direction: column;
  grid-row-gap: 4px;
  min-width: 0;
  flex: 1;
}

.cv-role-title {
  font-size: 15px;
  font-weight: 500;
  line-height: 1.2;
  overflow-wrap: break-word;
}

.cv-role-meta {
  font-size: 13.5px;
  font-weight: 400;
  line-height: 1.3;
  overflow-wrap: break-word;
}

/* Material Symbols, same family as the rest of the site's icons. The glyph
   name is set here rather than in the markup so it can swap on open without
   any script. */
.cv-item .cv-item-icon {
  flex: none;
  font-size: 24px;
  line-height: 1;
}

.cv-item .cv-item-icon::before {
  content: "add";
}

.cv-item[open] .cv-item-icon::before {
  content: "remove";
}

.cv-detail {
  list-style: none;
  margin: 0 0 20px;
  padding: 0 32px 0 0;
  display: flex;
  flex-direction: column;
  grid-row-gap: 12px;
}

/* No markers: the page carries no bullet marks anywhere, and the gap between
   items reads the entries apart on its own. */
.cv-detail li {
  color: var(--grey-100);
  font-family: "Raleway Site", sans-serif;
  font-size: 16px;
  font-weight: 400;
  line-height: 145%;
  text-wrap: pretty;
}

/* Awards, certifications and languages: short factual entries with nothing
   to open, so they get the same ruled rows as the lists above but no toggle
   and no logo. They sit in one band under both columns, spanning them, which
   keeps the two accordions the same length and gives the three short lists a
   line of their own. */
.cv-band {
  grid-column: 2 / span 2;
}

.cv-band-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-column-gap: 32px;
  grid-row-gap: 32px;
}

.cv-band-column {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* A step under the columns' own .title-homepage headings, so the band reads
   as supporting material rather than as a third list of equal weight. */
.cv-band-title {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 12px;
  color: var(--grey-100);
  font-family: "Raleway Site", sans-serif;
  font-size: 17px;
  font-weight: 700;
  line-height: 1.3;
}

/* An icon ahead of each heading in the band, so the three can be told apart
   at a glance. The same thin Material Symbols Sharp line as every other icon
   on the page (the shelf arrows, the +/- on the entries above), in the
   heading's ink. */
.cv-band-icon {
  flex: none;
  font-size: 24px;
  line-height: 1;
}

.cv-fact-list {
  display: flex;
  flex-direction: column;
  border-bottom: 1px solid var(--grey-100);
}

.cv-fact {
  display: flex;
  flex-direction: column;
  grid-row-gap: 4px;
  padding: 14px 0;
  border-top: 1px solid var(--grey-100);
  font-family: "Raleway Site", sans-serif;
  color: var(--grey-100);
}

/* Languages are two short strings, so they read better as a pair on one line
   than stacked like an award and its issuer. */
.cv-fact-inline {
  flex-direction: row;
  align-items: baseline;
  justify-content: space-between;
  grid-column-gap: 16px;
}

.cv-fact-name {
  font-size: 15px;
  font-weight: 500;
  line-height: 1.2;
}

.cv-fact-meta {
  font-size: 13.5px;
  font-weight: 400;
  line-height: 1.3;
}

.cv-fact-inline .cv-fact-meta {
  text-align: right;
}

/* Inline link inside a detail bullet: the ink colour it sits in, underlined
   so it reads as a link without a colour the page uses nowhere else. */
.cv-link,
.cv-link:hover {
  color: var(--grey-100);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The download button sits straight on the graph paper in a row of its
   own under the band, right-aligned under the plates. It is the last thing
   in the grid, so its bottom edge is where the rule beside "Resume" ends. */
.cv-actions {
  grid-column: 2 / span 2;
  display: flex;
  justify-content: flex-end;
}

/* Between the max width and the tablet breakpoint the two columns are the
   part under pressure, so the plates give up padding first rather than the
   entries giving up width. The rail has nothing to give: it is already only
   as wide as the word in it. */
@media screen and (max-width: 1180px) {
  .cv-column {
    padding: 24px;
  }
}

/* The heading column folds away below the site's tablet breakpoint, exactly
   as the about/contact ones do, and the two lists stack. Stacked, the title
   no longer trails a tall column, so it does not need to stick either. */
@media screen and (max-width: 991px) {
  .cv {
    grid-template-columns: 1fr;
  }

  /* The rule moves under the title here, as Contact's does, so both stacked
     headings read the same way. */
  .cv-heading-cell {
    grid-row: auto;
    border-right: 0;
    border-bottom: 1px solid var(--grey-100);
    padding-bottom: 8px;
  }

  .cv .section-title {
    padding: 0;
    position: static;
  }

  .cv-column {
    padding: 28px 32px;
  }

  .cv-band,
  .cv-actions {
    grid-column: 1;
  }
}

/* Phone: the plates lose their outer inset (the page already has one), the
   detail column drops its right-hand stand-off, and the button goes full
   width - at this size a 36px-padded pill next to nothing reads as stranded. */
@media screen and (max-width: 767px) {
  .cv {
    padding: 0 12px;
    grid-column-gap: 12px;
    grid-row-gap: 12px;
  }

  .cv-column {
    padding: 20px;
  }

  .cv-detail {
    padding-right: 0;
  }

  .cv-band-grid {
    grid-template-columns: 1fr;
    grid-column-gap: 24px;
    grid-row-gap: 24px;
  }

  .cv-actions .button {
    flex: 1;
  }
}

/* Small phones: everything steps down one notch so a five-word job title
   still lands in two lines rather than five. */
@media screen and (max-width: 479px) {
  .cv-column {
    padding: 16px;
  }

  .cv-logo {
    width: 38px;
    height: 38px;
  }

  .cv-detail li {
    font-size: 15px;
  }

  .cv-actions .button {
    padding-left: 20px;
    padding-right: 20px;
  }
}

/* New footer — replaces the old .footer / .footer-content / .footer-meta /
   .footer-social-row / .footer-tagline / .footer-email-link rules. */

.footer {
  background-color: var(--grey-100);
  color: var(--white);
  width: 100%;
  /* Horizontal inset lives on .footer-content, as it does on the sections
     above, so the two share one left and right edge. */
  padding: 56px 0 24px;
  font-family: "Raleway Site", sans-serif;
  position: relative;
  overflow: hidden;
}

.footer::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
  background-size: var(--grid-pitch) var(--grid-pitch);
}

.footer-content {
  position: relative;
  box-sizing: border-box;
  width: 100%;
  max-width: calc(var(--max-screen-width) + 40px);
  margin: 0 auto;
  padding: 0 20px;
}

.footer-top {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: 64px;
  align-items: start;
}

.footer-cta {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.footer-heading {
  margin: 0;
  font-size: 56px;
  line-height: 0.9;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  font-weight: 100;
}

.footer-heading-accent {
  font-size: 1.12em;
  font-weight: 800;
  letter-spacing: 0.01em;
}

.footer-email-link {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  width: fit-content;
  color: var(--white);
  text-decoration: none;
  font-size: 30px;
  font-weight: 300;
  letter-spacing: 0.01em;
  border-bottom: 1px solid #ffffff59;
  padding-bottom: 6px;
  transition: border-color 0.2s ease;
}

.footer-email-link:hover {
  border-bottom-color: var(--white);
}

.footer-email-link .icon {
  font-size: 28px;
}

.footer-name {
  font-size: 14px;
  letter-spacing: .6px;
  text-transform: uppercase;
  color: #d6d7d9;
  font-weight: 600;
}

.footer-columns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 40px;
}

.footer-col {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.footer-col-title {
  font-size: 12px;
  letter-spacing: .6px;
  text-transform: uppercase;
  color: var(--footer-muted-2);
  font-weight: 600;
}

.footer-link {
  color: var(--white);
  text-decoration: none;
  font-size: 15px;
  display: flex;
  align-items: center;
  gap: 10px;
}

.footer-link--icon svg,
.footer-link--icon .icon {
  flex: 0 0 auto;
  font-size: 20px;
}

.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24px;
  margin-top: 40px;
  font-size: 13.5px;
  color: var(--footer-muted);
}

.footer-bottom-left {
  display: flex;
  gap: 20px;
  align-items: center;
}

.footer-wordmark {
  font-size: 20px;
  font-weight: 800;
  color: var(--white);
  letter-spacing: 0.01em;
}

.footer-back-to-top {
  /* Pushed right even when the bar wraps and it lands on a line of its own. */
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--white);
  text-decoration: none;
  text-transform: uppercase;
  letter-spacing: .6px;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
}

.footer-back-to-top .icon {
  font-size: 20px;
}

/* The top row stacks from the site's tablet breakpoint rather than only on
   phones: between 768 and 991px the left column is narrower than
   "scrolling?" and the email address, which would run into the link columns. */
@media screen and (max-width: 991px) {
  .footer-top {
    grid-template-columns: 1fr;
    gap: 40px;
  }
}

@media screen and (max-width: 767px) {
  .footer {
    padding: 40px 0 20px;
  }
  .footer-columns {
    grid-template-columns: 1fr 1fr;
    gap: 24px;
  }
  .footer-heading {
    font-size: 40px;
  }
  .footer-email-link {
    font-size: 22px;
  }
  .footer-bottom {
    flex-wrap: wrap;
  }
  /* Side by side, the tagline wraps to leave "Theys" on a line of its own. */
  .footer-bottom-left {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
  }
}

/* The chat button (js/chat.js), where Botpress drew its own launcher: same
   size, same corner, same soft shadow. Botpress loads on the first press,
   which takes a moment, so the robot dims under a turning ring until the
   chat opens. Ring and focus outline are white on ink, to show on the dark
   banners and the white paper alike. */
.chat-launcher {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 1000;
  width: 64px;
  height: 64px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  box-shadow: 0 4px 6px #0000001a;
  cursor: pointer;
  transition: transform .2s ease-out;
}

.chat-launcher[hidden] {
  display: none;
}

.chat-launcher:hover {
  transform: scale(1.05);
}

.chat-launcher:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--white), 0 0 0 5px var(--grey-100);
}

.chat-launcher img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
}

.chat-launcher.is-loading {
  cursor: progress;
}

.chat-launcher.is-loading::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--black-transparent);
}

.chat-launcher.is-loading::after {
  content: "";
  position: absolute;
  inset: 18px;
  border: 3px solid #ffffff40;
  border-top-color: var(--white);
  border-radius: 50%;
  animation: chat-launcher-spin .8s linear infinite;
}

@keyframes chat-launcher-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .chat-launcher {
    transition: none;
  }
  .chat-launcher.is-loading::after {
    animation: none;
    border-color: var(--white);
  }
}

/* Leftovers from the Webflow export, unrelated to the footer itself. */
.container-9 {
  width: 100%;
  max-width: var(--max-screen-width);
}

.text-block-4 {
  text-align: center;
  position: absolute;
  inset: auto 0% 0%;
}

.quick-stack {
  margin-top: 60px;
}

/* Project (case-study) pages. All four are one template -
   partials/project-page.html, filled in from projects/_data/<slug>.json by
   build.js - and every rule for them is in this file, so a change here
   shows up on all four after one build. The only thing a page sets for
   itself is its colour: --project-grad (and --project-accent) on
   .case-hero, from the project's JSON, the same gradient as its card on the
   homepage (js/cover-shelf.js). No radii and no shadows anywhere. */

/* The rail's links jump to their chapter; let them glide there. */
html:has(body.case-page) {
  scroll-behavior: smooth;
}

.page-body {
  flex-direction: column;
  align-items: center;
  display: flex;
}

/* The homepage's sketching paper - white ground, faint grain - under the
   whole page. The banner and the footer paint their own grounds over it. */
.page-body.case-page {
  background-color: var(--paper);
  background-image: var(--paper-grain);
  background-size: 220px 220px;
}

/* The shared nav bar, laid straight over the banner: no fade behind it, and
   none of the solid fill interactions.css gives it once the page scrolls -
   the bar scrolls away with the banner, which is colour all the way up. */
.navigation-bar.navigation-bar--overlay,
.navigation-bar.navigation-bar--overlay.nav-solid {
  background: none;
}

/* Small uppercase label: the eyebrow, "In collaboration with", fact and
   phase labels, quote credits. Dimmed ink on the paper, dimmed white on the
   banner. */
.case-label {
  color: var(--ink-muted);
  font-family: "Raleway Site", sans-serif;
  font-size: 10.5px;
  font-style: normal;
  font-weight: 600;
  letter-spacing: .6px;
  line-height: 1.2;
  text-transform: uppercase;
}

.case-hero .case-label {
  color: var(--on-color-muted);
}

/* ---- Banner ------------------------------------------------------------ */

/* The hero's white ruling, over the project's gradient instead of the flat
   dark ground. The accent underneath only shows if the gradient can't. */
.case-hero {
  position: relative;
  isolation: isolate;
  width: 100%;
  overflow: hidden;
  color: var(--white);
  font-family: "Raleway Site", sans-serif;
  background-color: var(--project-accent, var(--grey-100));
  background-image:
    linear-gradient(to right, var(--grid-line) 1px, #0000 1px),
    linear-gradient(to bottom, var(--grid-line) 1px, #0000 1px),
    var(--project-grad, none);
  background-size: var(--grid-pitch) var(--grid-pitch), var(--grid-pitch) var(--grid-pitch), auto;
}

/* Text left, art right; the two stack once there's no room for both at
   340px. The min() keeps a single column from ever being wider than a
   phone's screen. Top padding clears the nav bar sitting over it. */
.case-hero-inner {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
  gap: clamp(32px, 5vw, 64px);
  align-items: center;
  width: 100%;
  max-width: 1320px;
  margin: 0 auto;
  padding: clamp(120px, 14vw, 168px) 20px clamp(56px, 7vw, 88px);
}

.case-hero-text,
.case-hero-art {
  min-width: 0;
}

.case-eyebrow {
  margin-bottom: 20px;
}

/* The homepage's lettering pair: thin words, then the project's name set
   larger and heavy. Balanced, so it wraps into even lines and never leaves
   a word on its own - no line breaks written into the copy. */
.case-title {
  max-width: 20ch;
  margin: 0 0 24px;
  color: var(--white);
  font-family: "Raleway Site", sans-serif;
  font-size: clamp(28px, 3.4vw, 44px);
  font-weight: 100;
  line-height: 1;
  letter-spacing: .05em;
  text-transform: uppercase;
  text-wrap: balance;
}

.case-title-bold {
  font-size: 1.12em;
  font-weight: 800;
  letter-spacing: .01em;
}

.case-subtitle {
  max-width: 44ch;
  margin: 0 0 32px;
  color: var(--white);
  font-size: clamp(16px, 2.5vw - 12.9px, 20px);
  font-weight: 400;
  line-height: 130%;
}

.case-partners {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.case-partner-logos {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

/* A banner light enough that white lettering all but disappears on it - the
   Flemish yellow - sets its type in ink instead (heroInk in the project's
   JSON). The ruling over the gradient goes with it, white lines being just
   as faint on a bright ground as white words are; --grid-line is redefined
   here rather than in .case-hero's own background-image, which reads it
   from whichever of the two classes on the element last set it. */
.case-hero--ink {
  --grid-line: var(--grid-line-ink);
  color: var(--grey-100);
}

.case-hero--ink .case-title,
.case-hero--ink .case-subtitle {
  color: var(--grey-100);
}

.case-hero--ink .case-label {
  color: var(--ink-muted);
}

/* The nav bar is laid straight over the banner with nothing behind it, so
   it takes the banner's ink too. Scoped to the page through the hero rather
   than through a class on the nav itself: the bar is the shared partial,
   the same on all five pages. */
body:has(.case-hero--ink) .navigation-bar--overlay :is(.navigation, .carrousel, .logo, .material-icon) {
  color: var(--grey-100);
}

body:has(.case-hero--ink) .navigation-bar--overlay .navigation:hover {
  border-bottom-color: var(--grey-100);
}

/* Square tiles, as under "Key collaborations" on the homepage. */
.case-partner {
  display: block;
  width: 64px;
  height: 64px;
  object-fit: cover;
}

.case-hero-art {
  display: block;
}

.case-hero-img {
  display: block;
  width: 100%;
  height: auto;
}

/* ---- Body: rail + chapters on ruled paper ------------------------------ */

/* Clip, never hidden: hidden would make this a scroll container, and the
   rail's sticky nav would stick to it instead of to the window. */
.case-body {
  position: relative;
  isolation: isolate;
  width: 100%;
  overflow-x: clip;
  color: var(--grey-100);
  font-family: "Raleway Site", sans-serif;
}

/* The ink ruling, on its own layer behind the content - the homepage's
   .ruled-band, carried on down the page. */
.case-body::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(to right, var(--grid-line-ink) 1px, #0000 1px),
    linear-gradient(to bottom, var(--grid-line-ink) 1px, #0000 1px);
  background-size: var(--grid-pitch) var(--grid-pitch);
}

.case-layout {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  column-gap: clamp(32px, 4vw, 64px);
  width: 100%;
  max-width: 1320px;
  margin: 0 auto;
  padding: clamp(48px, 8vh, 96px) 20px clamp(56px, 10vh, 110px);
}

/* A page with no chapters to list (privacy.html): no rail, and one column
   kept to a readable width in the middle. Two classes, to outweigh the
   narrower screens' .case-layout rules below. */
.case-layout.case-layout--single {
  grid-template-columns: minmax(0, 860px);
  justify-content: center;
}

/* Stretched to the height of all the chapters as a grid item, so its rule
   runs the whole column. */
.case-rail {
  min-width: 0;
  padding-right: 24px;
  border-right: 1px solid var(--grey-100);
}

.case-rail-nav {
  position: sticky;
  top: 40px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: right;
}

/* The chapter being read is set in full ink and bold by js/interactions.js. */
.case-rail-link {
  padding: 10px 0;
  color: var(--ink-quiet);
  font-size: 16px;
  font-weight: 300;
  line-height: 1.2;
  text-decoration: none;
  text-transform: uppercase;
}

.case-rail-link:hover {
  color: var(--grey-100);
}

.case-rail-link.is-active {
  color: var(--grey-100);
  font-weight: 700;
}

.case-main {
  display: flex;
  flex-direction: column;
  gap: clamp(48px, 8vh, 96px);
  min-width: 0;
}

.case-section {
  scroll-margin-top: 32px;
}

/* A section takes focus when a link glides to it (js/interactions.js), on
   any page, so Tab carries on from there; the section itself needs no ring
   for that. */
[tabindex="-1"]:focus {
  outline: none;
}

/* The site's .section-title voice. */
.case-heading {
  margin: 0 0 16px;
  color: var(--grey-100);
  font-family: "Raleway Site", sans-serif;
  font-size: 20px;
  font-weight: 300;
  line-height: 100%;
  text-transform: uppercase;
}

.case-body .icon {
  font-size: 20px;
  line-height: 1;
}

/* A white sheet laid on the paper. */
.case-plate {
  padding: 32px;
  background-color: var(--paper);
  border: 1px solid var(--grid-line-ink);
}

/* Running text at the homepage's .intro-text size. */
.case-copy {
  font-size: 17px;
  font-weight: 400;
  line-height: 1.4;
}

.case-copy p {
  margin: 0;
}

.case-copy p + p {
  margin-top: 14px;
}

.case-copy a,
.case-fact-meta a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* ---- The challenge ----------------------------------------------------- */

.case-split {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: clamp(24px, 4vw, 48px);
  align-items: start;
}

.case-facts {
  display: flex;
  flex-direction: column;
  margin: 0;
  border-top: 1px solid var(--grid-line-ink);
}

.case-fact {
  padding: 16px 0;
  border-bottom: 1px solid var(--grid-line-ink);
}

.case-fact .case-label {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.case-fact .icon {
  color: var(--grey-100);
  letter-spacing: 0;
}

.case-fact-value {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.2;
}

.case-fact-meta {
  margin: 0;
  font-size: 13.5px;
  font-weight: 400;
  line-height: 1.3;
}

/* ---- The process ------------------------------------------------------- */

/* Each card its own plate with real gaps between - not a 1px gap over a
   grey container, which leaves a grey slab wherever the last row is short. */
.case-phases {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: clamp(10px, 1.2vw, 16px);
}

.case-phase {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px;
  background-color: var(--paper);
  border: 1px solid var(--grid-line-ink);
  color: var(--grey-100);
  text-decoration: none;
  transition: transform .15s ease;
}

.case-phase:hover {
  color: var(--grey-100);
  transform: scale(1.02);
}

.case-phase-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.case-phase-title,
.case-card-title {
  font-size: 15px;
  font-weight: 700;
  line-height: 1.2;
}

.case-phase-tools,
.case-card-body {
  font-size: 13.5px;
  font-weight: 400;
  line-height: 1.3;
}

/* ---- Steps and results: content blocks --------------------------------- */

.case-flow {
  display: flex;
  flex-direction: column;
  gap: 28px;
}

/* Copy that carries straight on from the copy above it, with no heading of
   its own (build.js marks it), sits a paragraph's distance under it. */
.case-flow > .case-copy--cont {
  margin-top: -14px;
}

/* Long lines of 17px text are hard to follow across a full-width plate. */
.case-flow .case-copy {
  max-width: 68ch;
}

.case-subhead {
  margin: 0 0 10px;
  color: var(--grey-100);
  font-family: "Raleway Site", sans-serif;
  font-size: 17px;
  font-weight: 700;
  line-height: 1.3;
}

.case-list {
  margin: 0;
  padding-left: 20px;
  list-style: square;
}

.case-list li + li {
  margin-top: 8px;
}

.case-figure {
  margin: 0;
}

/* Never stretched past its own width - a small diagram stays sharp - and
   scaled down to the plate when it's wider. */
.case-figure img {
  display: block;
  max-width: 100%;
  height: auto;
}

/* For screenshots with a white edge of their own, which would otherwise
   run into the plate. */
.case-figure--border img {
  border: 1px solid var(--grid-line-ink);
}

.case-figure-link {
  display: block;
  width: fit-content;
  max-width: 100%;
}

/* text-align undoes the centring Webflow gives every figcaption. */
.case-caption {
  margin-top: 10px;
  text-align: left;
  color: var(--ink-muted);
  font-size: 13.5px;
  line-height: 1.3;
}

.case-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: 24px clamp(16px, 2vw, 24px);
  align-items: end;
}

/* ---- Staged images ----------------------------------------------------- */

/* An image set on a ground of the project's own colour instead of laid
   straight on the white plate - the session still in the middle of a case,
   the product shot at the end of it. Same ruling as the page carries, so
   the ground reads as a piece of the same paper rather than a panel from
   somewhere else. --project-accent and --project-grad come off .case-body,
   which the template gives them alongside the banner. */
.case-showcase {
  margin: 0;
}

.case-stage {
  padding: clamp(20px, 3.4vw, 40px);
  background-color: color-mix(in srgb, var(--project-accent, var(--grey-100)) var(--stage-tint, 12%), var(--paper));
  background-image:
    linear-gradient(to right, var(--grid-line-ink) 1px, #0000 1px),
    linear-gradient(to bottom, var(--grid-line-ink) 1px, #0000 1px);
  background-size: var(--grid-pitch) var(--grid-pitch);
  border: 1px solid var(--grid-line-ink);
}

/* The project's gradient at full strength, exactly as the banner wears it -
   for the one shot that should read as the shipped product rather than as a
   step of the work. */
.case-showcase--bright .case-stage {
  background-color: var(--project-accent);
  background-image:
    linear-gradient(to right, var(--grid-line) 1px, #0000 1px),
    linear-gradient(to bottom, var(--grid-line) 1px, #0000 1px),
    var(--project-grad);
  background-size: var(--grid-pitch) var(--grid-pitch), var(--grid-pitch) var(--grid-pitch), auto;
  border-color: #0000;
}

/* The banner's gradient mirrored top to bottom, for a product shot that
   would otherwise sit on the same diagonal as the banner it repeats. A
   project gives it as --project-grad-flipped (gradFlipped in its JSON). */
.case-showcase--flipped .case-stage {
  background-color: var(--project-accent);
  background-image:
    linear-gradient(to right, var(--grid-line) 1px, #0000 1px),
    linear-gradient(to bottom, var(--grid-line) 1px, #0000 1px),
    var(--project-grad-flipped, var(--project-grad));
  background-size: var(--grid-pitch) var(--grid-pitch), var(--grid-pitch) var(--grid-pitch), auto;
  border-color: #0000;
}

.case-stage-badge {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: clamp(16px, 2.2vw, 24px);
}

.case-stage-mark {
  display: block;
  width: 44px;
  height: 44px;
}

.case-stage-art {
  display: flex;
  justify-content: center;
}

/* An artefact wider than the plate - a flow laid out in a row - reads as
   cropped by accident when it is letterboxed with air on both sides. This
   runs it off the right edge of its ground instead, where it reads as
   continuing past it. */
.case-stage--bleed-right {
  padding-right: 0;
  overflow: hidden;
}

/* The same, off the left edge: for an artefact whose own ground is
   transparent (a canvas export), which then reads as the stage continuing
   rather than as a picture laid on it. */
.case-stage--bleed-left {
  padding-left: 0;
  overflow: hidden;
}

.case-stage-item {
  display: block;
  max-width: 100%;
  height: auto;
}

/* Two device shots as one picture: the laptop set out across the ground and
   the phone standing over its bottom-right corner, which is how the two
   ends of the product actually meet. Every measure is a percentage of the
   pair's own box, so the composition holds at any width the plate takes. */
.case-stage-art--duo {
  display: block;
  position: relative;
}

.case-stage-art--duo .case-stage-item--main {
  width: 86%;
}

.case-stage-art--duo .case-stage-item--inset {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 26%;
}

/* Two laptops rather than a laptop and a phone: the second is a device of
   the same size, so it stands over the first at half the ground instead of
   a quarter. */
.case-stage-art--duo.case-stage-art--laptops .case-stage-item--main {
  width: 76%;
}

.case-stage-art--duo.case-stage-art--laptops .case-stage-item--inset {
  width: 54%;
}

/* ---- Copy beside its figure -------------------------------------------- */

/* Two equal columns that fold to one as soon as either would be under
   300px - which is the auto-fit's job, not a breakpoint's. Used for copy
   beside a staged screenshot, and for copy beside a clip. */
.case-duo {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: clamp(24px, 3vw, 40px);
  align-items: center;
}

.case-duo > * {
  min-width: 0;
}

/* A stage in half a plate takes the padding of half a plate. */
.case-duo .case-stage {
  padding: clamp(16px, 2.4vw, 28px);
}

/* ---- Findings ---------------------------------------------------------- */

/* Peer findings, each on its own plate, the same box as an impact card:
   three findings read as three findings, where the same three set as list
   items read as one paragraph chopped up. */
.case-findings {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: clamp(10px, 1.2vw, 16px);
}

.case-finding {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px;
  background-color: var(--paper);
  border: 1px solid var(--grid-line-ink);
}

.case-finding-body {
  font-size: 13.5px;
  line-height: 1.35;
}

/* ---- Collage ----------------------------------------------------------- */

/* A loose pile of session frames instead of a tidy strip. The box keeps its
   own aspect through padding-bottom, so the frames placed absolutely inside
   it can never escape it; every frame is placed in percentages of that box,
   so the composition holds at whatever width the plate takes. Back to front
   in source order. */
.case-collage {
  position: relative;
  width: 100%;
  padding-bottom: var(--collage-ratio, 76%);
}

/* top / right / bottom / left / width come from the item's own style, which
   its block writes: where a frame sits is composition, and composition
   belongs to the page's content, not to the stylesheet. */
.case-collage-item {
  position: absolute;
}

/* A white mount and a hairline: what makes a pile of screenshots read as
   prints rather than as a grid that has come apart. */
.case-collage-item img {
  display: block;
  width: 100%;
  height: auto;
  background-color: var(--paper);
  border: 6px solid var(--paper);
  outline: 1px solid var(--grid-line-ink);
}

/* ---- A clip with a playhead -------------------------------------------- */

/* The bar is the whole point: it is what tells a muted, looping clip with
   no controls apart from a still. It sits flush under the video, one box
   with it. */
.case-videoblock video {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--grid-line-ink);
  border-bottom: 0;
}

.case-playbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  background-color: var(--paper);
  border: 1px solid var(--grid-line-ink);
}

.case-playbar-track {
  position: relative;
  flex: 1;
  height: 3px;
  background-color: var(--grid-line-ink);
}

.case-playbar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  background-color: var(--grey-100);
}

.case-playbar-label {
  color: var(--ink-muted);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .6px;
  text-transform: uppercase;
}

.case-video {
  aspect-ratio: 16 / 9;
}

.case-video iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* The clip's stand-in until it's played (see build.js, BLOCKS.video): its
   still, filling the frame, under a round play button. */
.case-video-facade {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  cursor: pointer;
}

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

.case-video-play {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 72px;
  height: 72px;
  border-radius: 50%;
  background: rgba(17, 18, 21, 0.78);
  color: var(--white);
  transform: translate(-50%, -50%);
  transition: transform 0.2s ease, background-color 0.2s ease;
}

.case-video-play .icon {
  font-size: 40px;
  font-weight: 300;
}

.case-video-facade:hover .case-video-play,
.case-video-facade:focus-visible .case-video-play {
  background: var(--grey-100);
  transform: translate(-50%, -50%) scale(1.08);
}

.case-video-facade:focus-visible {
  outline: 3px solid var(--grey-100);
  outline-offset: 3px;
}

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

.case-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: clamp(10px, 1.2vw, 16px);
}

.case-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px;
  background-color: var(--paper);
  border: 1px solid var(--grid-line-ink);
}

.case-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr));
  gap: clamp(16px, 2vw, 24px);
}

.case-metric {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 16px;
  border-top: 1px solid var(--grid-line-ink);
}

/* The heavy half of the title's lettering pair. Lining figures: Raleway's
   default old-style ones bob up and down, which a figure set this large
   makes obvious. */
.case-metric-figure {
  font-size: clamp(32px, 3.4vw, 44px);
  font-weight: 800;
  font-variant-numeric: lining-nums;
  line-height: 1;
  letter-spacing: .01em;
}

.case-metric-text {
  font-size: 13.5px;
  line-height: 1.3;
}

.case-quotes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: clamp(16px, 2vw, 24px);
}

/* Undoes Webflow's grey 5px blockquote bar (css/webflow.css) for a single
   ink rule. */
.case-quote {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 0;
  padding: 2px 0 2px 20px;
  border-left: 1px solid var(--grey-100);
  font-size: clamp(17px, 1.6vw, 20px);
  font-weight: 300;
  line-height: 1.35;
}

.case-quote p {
  margin: 0;
}

/* One participant's words, set large under the findings they bear out: an
   accent rule instead of the ink one, so it reads as a voice from the
   sessions rather than as another finding, and a measure short enough that
   it reads at a glance. */
.case-quotes--pull .case-quote {
  gap: 14px;
  max-width: 44ch;
  padding: 4px 0 4px clamp(20px, 2.4vw, 28px);
  border-left: 3px solid var(--project-accent, var(--grey-100));
  font-size: clamp(20px, 2.2vw, 26px);
  line-height: 1.3;
  text-wrap: pretty;
}

.case-button {
  align-self: flex-start;
}

/* A section with no blocks yet. */
.case-empty {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  min-height: 260px;
  padding: 32px;
  background-color: var(--paper);
  border: 1px dashed var(--grid-line-dashed);
  color: var(--ink-muted);
  text-align: center;
}

.case-body .case-empty .icon {
  font-size: 28px;
}

.case-empty-title {
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: .6px;
  text-transform: uppercase;
}

.case-empty-hint {
  font-size: 13.5px;
  line-height: 1.3;
}

/* ---- Closing actions --------------------------------------------------- */

/* The homepage's .button and .button.button-solid, as in its contact
   section. */
.case-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 12px;
}

.case-actions .button,
.case-button {
  white-space: nowrap;
  transition: transform .15s ease;
}

/* A badge laid on a dark gradient takes the banner's white labels. */
.case-showcase--flipped .case-stage-badge .case-label {
  color: var(--on-color-muted);
}

/* ---- Data scope and scatter ------------------------------------------- */

/* Two content blocks, shaped by what the content is rather than by the
   project: "sources feeding one product" (datascope) and "items rated on
   two axes" (scatter). The scatter's chips are placed by js/interactions.js
   from the measured plot. */

/* =========================================================================
   1. DATA SCOPE - sources flowing into the product
   ========================================================================= */

/* Square-ish stage that keeps its own aspect so the absolutely positioned
   nodes never escape it. Positions are in % of this box, so the composition
   holds as it narrows - but the nodes themselves are sized in px, so the box
   never goes shorter than they need to stay clear of one another, and below
   560px of stage (the @container rule further down) the ring gives way to a
   grid. The stage is the container, so it is the room the diagram actually
   has that decides, not the window. */
.case-stage:has(> .case-datascope) {
  container-type: inline-size;
}

.case-datascope {
  position: relative;
  width: 100%;
  padding-bottom: max(var(--datascope-ratio, 66%), 460px);
  /* one em step drives every chip label in the diagram */
  font-size: clamp(9px, 0.82vw, 11.5px);
}

/* ---- Arrows: one per source, pointing at the core ---------------------- */
/* Each arrow is a bar rotated around the centre of the box. Its own length,
   angle and inner offset come from the item's inline style (composition,
   set per node). transform-origin is the centre point, so "left" in local
   coordinates always means "towards the core" - that is where the head is. */
.case-datascope-arrow {
  position: absolute;
  left: 50%;
  top: 50%;
  display: flex;
  align-items: center;
  box-sizing: border-box;
  height: 12px;
  transform-origin: left center;
}

.case-datascope-arrow::before { /* head */
  content: "";
  width: 0;
  height: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-right: 8px solid var(--grey-100);
}

.case-datascope-line {
  position: relative;
  flex: 1;
  height: 1px;
  background-image: linear-gradient(to left, var(--grid-line-ink), var(--grey-100));
}

/* The travelling dot is the whole point of the arrow: it says "data moves",
   which a static line does not. Accent-coloured, small, slow. */
.case-datascope-pulse {
  position: absolute;
  top: -2px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background-color: var(--project-accent);
  animation: case-datascope-flow 3.4s linear infinite;
}

@keyframes case-datascope-flow {
  0%   { left: 98%; opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { left: 0%;  opacity: 0; }
}

/* ---- Core and source nodes -------------------------------------------- */
.case-datascope-core {
  position: absolute;
  left: 50%;
  top: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  transform: translate(-50%, -50%);
}

.case-datascope-core-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: clamp(48px, 5.6vw, 72px);
  height: clamp(48px, 5.6vw, 72px);
  border-radius: 50%;
  background-color: var(--project-ink, #001039);
}

.case-datascope-core-mark img {
  display: block;
  width: 62%;
  height: auto;
  border-radius: 22%;
}

.case-datascope-core-label {
  padding: 4px 8px;
  background-color: var(--grey-100);
  color: var(--paper);
  font-size: 1em;
  font-weight: 600;
  letter-spacing: .6px;
  text-transform: uppercase;
  white-space: nowrap;
}

/* left/top/width and the animation-delay come from the item's inline style. */
.case-datascope-node {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  animation: case-datascope-drift 8s ease-in-out infinite;
}

@keyframes case-datascope-drift {
  0%, 100% { transform: translate(-50%, -50%); }
  50%      { transform: translate(-50%, calc(-50% - 7px)); }
}

/* App icons, not wordmarks: at this size a square mark is legible and a
   wordmark is not. Same tile for the two non-branded sources, with a
   Material glyph instead of an image. */
.case-datascope-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: clamp(44px, 5vw, 60px);
  height: clamp(44px, 5vw, 60px);
  overflow: hidden;
  background-color: var(--paper);
  border-radius: 22%;
  outline: 1px solid var(--grid-line-ink);
}

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

.case-datascope-mark .icon {
  font-size: 26px;
  color: var(--grey-100);
}

.case-datascope-node-title {
  font-size: 1em;
  font-weight: 700;
  letter-spacing: .6px;
  text-transform: uppercase;
}

.case-datascope-chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 4px;
}

.case-datascope-chip {
  padding: 3px 7px;
  background-color: var(--paper);
  outline: 1px solid var(--grid-line-ink);
  white-space: nowrap;
}

/* The drift is what centres a node on its point, so a node that is not to
   drift is centred here instead. */
@media (prefers-reduced-motion: reduce) {
  .case-datascope-node,
  .case-datascope-pulse { animation: none; }
  .case-datascope-node { transform: translate(-50%, -50%); }
  .case-datascope-pulse { left: 40%; opacity: 1; }
}

/* Too narrow for the ring: the edge nodes would run off the stage and pile
   onto the core. The sources go into a grid instead, the product under
   them with an arrow into it - still many kinds of data, still flowing into
   one thing. The ring's placement is written on each node's own style, so
   it is overridden here rather than removed from the data. */
@container (max-width: 559px) {
  .case-datascope {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 20px 12px;
    padding-bottom: 0;
    font-size: 10.5px;
  }

  .case-datascope-arrow {
    display: none;
  }

  .case-datascope-node {
    position: static;
    left: auto !important;
    top: auto !important;
    width: auto !important;
    justify-content: flex-start;
    transform: none;
    animation: none;
  }

  .case-datascope-chip {
    white-space: normal;
    text-align: center;
  }

  .case-datascope-core {
    grid-column: 1 / -1;
    order: 1;
    position: static;
    transform: none;
  }

  .case-datascope-core::before {
    content: "arrow_downward";
    /* the ligature's name is not for a screen reader to read out */
    content: "arrow_downward" / "";
    font-family: "Material Symbols Sharp";
    font-size: 24px;
    font-weight: 300;
    line-height: 1;
    text-transform: none;
    letter-spacing: normal;
    font-feature-settings: "liga";
    color: var(--grey-100);
  }
}


/* =========================================================================
   2. SCATTER - items rated on two axes, every point labelled
   ========================================================================= */

/* Grid so the axis callouts line up with the LEFT EDGE OF THE PLOT, not
   with the block: column 1 is the rotated axis title, column 2 is the plot
   and everything that labels it. */
.case-scatter {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 10px;
  align-items: center;
  container-type: inline-size;
}

.case-scatter > .case-scatter-axis-note,
.case-scatter > .case-scatter-axis-ends,
.case-scatter > .case-scatter-plot { grid-column: 2; }

.case-scatter-axis-note,
.case-scatter-axis-y,
.case-scatter-axis-x {
  color: var(--ink-muted);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .6px;
  text-transform: uppercase;
}

.case-scatter-axis-y {
  grid-column: 1;
  grid-row: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: var(--grey-100);
  writing-mode: vertical-rl;
  transform: rotate(180deg);
}

/* The x axis's title sits in the middle of the row under the plot, between
   its two ends - the counterpart of the y axis's title beside it. Where the
   three would not fit on one line, the title takes a line of its own above
   the ends. */
.case-scatter-axis-ends {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-areas: "low title high";
  align-items: baseline;
  gap: 6px 16px;
}

.case-scatter-axis-ends > :first-child { grid-area: low; }
.case-scatter-axis-ends > :last-child { grid-area: high; justify-self: end; text-align: right; }

.case-scatter-axis-x {
  grid-area: title;
  color: var(--grey-100);
  text-align: center;
}

@container (max-width: 620px) {
  .case-scatter-axis-ends {
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "title title"
      "low high";
  }
}

/* The plot IS the tinted ground - no outer stage around it, so the chart
   gets the full width of the plate. Height is set by the script from the
   measured width. */
.case-scatter-plot {
  grid-row: 2;
  position: relative;
  min-width: 0;
  background-color: color-mix(in srgb, var(--project-accent) 9%, var(--paper));
  background-image:
    linear-gradient(to right,  var(--grid-line-ink) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-line-ink) 1px, transparent 1px);
  background-size: 12.5% 12.5%;
  border: 1px solid var(--grid-line-ink);
}

.case-scatter-axis-line {
  position: absolute;
  background-color: var(--grey-100);
}
.case-scatter-axis-line--x { left: 0; right: 0; top: 50%; height: 1px; }
.case-scatter-axis-line--y { top: 0; bottom: 0; left: 50%; width: 1px; }

/* One point = wrapper (full-bleed, so the leader can start anywhere) +
   anchor dot at the true value + leader + the chip itself. */
.case-scatter-point { position: absolute; inset: 0; pointer-events: none; }
.case-scatter-point.is-active { z-index: 9; }

.case-scatter-anchor {
  position: absolute;
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: 50%;
  background-color: color-mix(in srgb, var(--grey-100) 45%, transparent);
}

.case-scatter-leader {
  position: absolute;
  height: 1px;
  transform-origin: left center;
  background-color: color-mix(in srgb, var(--grey-100) 20%, transparent);
}

/* The source's mark over the name of the data, so the mark can be read at
   a size where it tells its source apart at a glance. The mark stands on
   the plot as it is - an app icon is a tile of its own - and only the name
   gets a white label behind it. Focus is shown by the active state below
   (the script sets it on focus as on hover), so the chip draws no outline
   of its own. */
.case-scatter-chip {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: auto;
  cursor: pointer;
  outline: none;
  transform: translate(-50%, -50%);
  transition: opacity .18s ease;
}

.case-scatter-chip-icon {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: 22%;
  transition: outline-color .18s ease;
  outline: 2px solid transparent;
  outline-offset: 2px;
}
.case-scatter-chip-icon img { display: block; width: 100%; height: 100%; object-fit: cover; }
.case-scatter-chip-icon .icon { color: var(--grey-100); }

.case-scatter-chip-label {
  background-color: var(--paper);
  outline: 1px solid color-mix(in srgb, var(--grey-100) 20%, transparent);
  box-shadow: 0 1px 0 var(--grid-line-ink);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: .2px;
  white-space: nowrap;
  color: var(--grey-100);
  transition: outline-color .18s ease;
}

/* On a narrow plot the names are hidden, and a picked point's name opens
   under its mark instead (over it where there is no room below - the
   script decides, and keeps it inside the plot at the sides). */
.case-scatter:not(.is-labelled) .case-scatter-chip-label {
  display: none;
}

.case-scatter:not(.is-labelled) .case-scatter-point.is-active .case-scatter-chip-label {
  display: block;
  position: absolute;
  top: 100%;
  left: 50%;
  margin-top: 6px;
  transform: translateX(-50%);
  font-size: 12px !important;
}

.case-scatter:not(.is-labelled) .case-scatter-point.is-active .case-scatter-chip-label.is-above {
  top: auto;
  bottom: 100%;
  margin-top: 0;
  margin-bottom: 6px;
}

/* Hover/focus/click: the picked point goes ink, the rest recede slightly. With
   its label hidden on a narrow plot, the mark itself takes the ring. */
.case-scatter.is-picking .case-scatter-chip { opacity: .62; }
.case-scatter .case-scatter-point.is-active .case-scatter-chip { opacity: 1; }
.case-scatter .case-scatter-point.is-active .case-scatter-chip-label {
  outline-color: var(--grey-100);
  box-shadow: 0 8px 22px color-mix(in srgb, var(--grey-100) 15%, transparent);
}
.case-scatter:not(.is-labelled) .case-scatter-point.is-active .case-scatter-chip-icon {
  outline-color: var(--grey-100);
}
.case-scatter .case-scatter-point.is-active .case-scatter-anchor,
.case-scatter .case-scatter-point.is-active .case-scatter-leader {
  background-color: var(--project-accent);
}

/* ---- Breakpoints ------------------------------------------------------- */

@media screen and (max-width: 1100px) {
  .case-layout {
    grid-template-columns: 170px minmax(0, 1fr);
  }
}

@media screen and (max-width: 991px) {
  .case-plate,
  .case-empty {
    padding: 24px;
  }

  .case-split {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The nav links move into a panel of their own with a dark ground of its
     own (responsive.css), so on an ink banner they stay white - it is only
     the bar's own two marks, the monogram and the hamburger, that are still
     laid on the banner. Once the panel is open the bar turns dark too
     (responsive.css), so both marks go white with it, and back to ink when
     it closes. */
  body:has(.case-hero--ink) .navigation-bar--overlay .nav-links :is(.navigation, .carrousel),
  body:has(.case-hero--ink) .navigation-bar--overlay:has(.nav-menu-toggle.w--open) :is(.logo, .material-icon) {
    color: var(--white);
  }
}

/* No room left to keep a phone beside a laptop at a quarter of the width:
   the laptop takes the whole ground and the phone stands over it. */
@media screen and (max-width: 700px) {
  .case-stage-art--duo .case-stage-item--main {
    width: 100%;
  }

  .case-stage-art--duo .case-stage-item--inset {
    width: 34%;
  }
}

/* No room left for the rail beside the chapters: it goes, and they take the
   full width. */
@media screen and (max-width: 820px) {
  .case-layout {
    grid-template-columns: minmax(0, 1fr);
  }

  .case-rail {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  html:has(body.case-page) {
    scroll-behavior: auto;
  }

  .case-phase,
  .case-actions .button,
  .case-button {
    transition: none;
  }

  .case-phase:hover,
  .case-actions .button:hover,
  .case-button:hover {
    transform: none;
  }
}

/* ---- A title too long for the banner's usual size ---------------------- */

/* titleLong in the project's JSON: a step smaller and a little wider, so a
   title of a sentence still sets in four or five even lines beside the art. */
.case-title--long {
  max-width: 22ch;
  font-size: clamp(26px, 3vw, 40px);
  line-height: 1.05;
}

/* ---- Hero art: a device mockup with a clip playing on its screen ------- */

/* The card is laid out at its own pixel size (--device-w x --device-h, from
   the markup) and scaled to the column by js/interactions.js
   (--device-scale), so the clip's mapping onto the screen - worked out by
   build.js in the card's own pixels - holds at any width. */
.case-hero-device {
  position: relative;
  width: 100%;
  aspect-ratio: var(--device-w) / var(--device-h);
  overflow: hidden;
}

.case-hero-device-stage {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(var(--device-w) * 1px);
  height: calc(var(--device-h) * 1px);
  transform-origin: 0 0;
  transform: scale(var(--device-scale, 1));
}

.case-hero-device-stage img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* Masked by the card's own alpha, so the clip can never spill past the
   device's rounded silhouette. */
.case-hero-device-screen {
  position: absolute;
  inset: 0;
  -webkit-mask-image: var(--device-mask);
  mask-image: var(--device-mask);
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
}

/* A 16:9 box warped onto the screen by the matrix3d build.js writes into
   the markup. The radius is in the clip's own pixels and is warped with
   it. The still card shows until the clip actually plays, then the clip
   fades in over it - the card "starts playing". */
.case-hero-device-screen video {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  width: 960px;
  height: 540px;
  object-fit: fill;
  border-radius: 22px;
  transform-origin: 0 0;
  opacity: 0;
  transition: opacity .6s ease;
}

.case-hero-device-screen video.is-playing {
  opacity: 1;
}

/* ---- Logo plates ------------------------------------------------------- */

.case-logos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: clamp(10px, 1.2vw, 16px);
}

.case-logo {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 20px;
  background-color: var(--paper);
  border: 1px solid var(--grid-line-ink);
}

/* A row of fixed height, so every label starts on the same line; each
   logo's own height (from the data) balances the wordmarks by eye. */
.case-logo-mark {
  display: flex;
  align-items: center;
  height: 40px;
}

.case-logo-mark img {
  display: block;
  width: auto;
  max-width: 100%;
}

/* ---- Zigzag: copy beside media, swapping sides every other row --------- */

.case-zigzags {
  display: flex;
  flex-direction: column;
  gap: 28px;
}

/* Flex and wrap rather than a grid: on a wide plate the even rows reverse
   (media left, copy right), and once a row wraps each item takes a line of
   its own with the copy always first - row-reverse only reverses the items
   within a line, never the order of the lines. No breakpoint involved. */
.case-zigzag {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 24px clamp(24px, 3vw, 40px);
  padding-top: 28px;
  border-top: 1px solid var(--grid-line-ink);
}

.case-zigzag--reverse {
  flex-direction: row-reverse;
}

.case-zigzag > * {
  flex: 1 1 300px;
  min-width: 0;
}

.case-zigzag .case-copy {
  max-width: 46ch;
}

.case-zigzag .case-stage {
  padding: clamp(16px, 2.4vw, 28px);
}

/* The bleed still wins on the padding it zeroes. */
.case-zigzag .case-stage--bleed-right {
  padding-right: 0;
}

.case-step-label {
  display: block;
  margin-bottom: 10px;
}

/* ---- A product's name as its badge ------------------------------------- */

.case-stage-wordmark {
  color: var(--white);
  font-family: Georgia, "Times New Roman", serif;
  font-size: 22px;
  letter-spacing: .02em;
  line-height: 1;
}

.case-stage-wordmark + .case-label {
  color: var(--on-color-muted);
}

/* ---- Clips full screen, pictures in a lightbox ------------------------- */

.case-figure [data-zoom],
.case-showcase [data-zoom],
.case-videoblock video {
  cursor: zoom-in;
}

.case-playbar-expand {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin: -6px -6px -6px 0;
  padding: 0;
  background: none;
  border: 0;
  color: var(--grey-100);
  cursor: pointer;
}

.case-playbar-expand:hover {
  background-color: var(--grid-line-ink);
}

.case-body .case-playbar-expand .icon {
  font-size: 18px;
}

.case-lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(16px, 4vw, 56px);
  background-color: color-mix(in srgb, var(--grey-100) 92%, transparent);
  cursor: zoom-out;
}

.case-lightbox[hidden] {
  display: none;
}

.case-lightbox img {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: 100%;
  object-fit: contain;
}

.case-lightbox-close {
  position: absolute;
  top: 16px;
  right: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  background: none;
  border: 1px solid color-mix(in srgb, var(--white) 35%, transparent);
  color: var(--white);
  cursor: pointer;
}

.case-lightbox-close:hover {
  border-color: var(--white);
}

.case-lightbox-close .icon {
  font-size: 24px;
}

html:has(.case-lightbox:not([hidden])) {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .case-hero-device-screen video {
    transition: none;
  }
}

/* ---- A zigzag row standing on its own ---------------------------------- */

/* One row, not one of a sequence: no rule over it. */
.case-zigzag--plain {
  padding-top: 0;
  border-top: 0;
}

/* ---- Staged ground bled into its bottom-right corner ------------------- */

/* For a board cropped at its right and bottom edges: it runs off both, and
   reads as continuing past the corner of the stage. Same in a zigzag, whose
   own stage padding would otherwise win. */
.case-stage--bleed-br,
.case-zigzag .case-stage--bleed-br {
  padding-right: 0;
  padding-bottom: 0;
  overflow: hidden;
}

/* ---- A service's app icon on a card ------------------------------------ */

.case-card-logo {
  display: block;
  width: 36px;
  height: 36px;
  border-radius: 22%;
  outline: 1px solid var(--grid-line-ink);
}

/* ---- Data scope, outward: one store feeding many services -------------- */

/* The arrows point away from the core and the dots travel outward along
   them. The core is the store itself: a glyph on the project's gradient
   rather than a product's mark. */
.case-datascope--out .case-datascope-arrow::before {
  display: none;
}

.case-datascope--out .case-datascope-arrow::after { /* head, at the node */
  content: "";
  width: 0;
  height: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-left: 8px solid var(--grey-100);
}

.case-datascope--out .case-datascope-line {
  background-image: linear-gradient(to right, var(--grid-line-ink), var(--grey-100));
}

.case-datascope--out .case-datascope-pulse {
  animation-name: case-datascope-flow-out;
}

@keyframes case-datascope-flow-out {
  0%   { left: 0%;  opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { left: 98%; opacity: 0; }
}

.case-datascope--out .case-datascope-core-mark {
  width: clamp(52px, 6vw, 80px);
  height: clamp(52px, 6vw, 80px);
  background-image: var(--project-grad);
  color: var(--white);
}

.case-datascope--out .case-datascope-core-mark .icon {
  font-size: clamp(26px, 3vw, 40px);
}

.case-datascope--out .case-datascope-mark {
  width: clamp(40px, 4.6vw, 56px);
  height: clamp(40px, 4.6vw, 56px);
}

/* In the grid a narrow stage falls back to, the store comes first with the
   arrow under it, flowing down into the services. */
@container (max-width: 559px) {
  .case-datascope--out .case-datascope-core {
    order: -1;
  }

  .case-datascope--out .case-datascope-core::before {
    order: 1;
  }
}

/* ---- Charts drawn by the build ----------------------------------------- */

/* Colours are per element (--uc, from the chart's data); heights and widths
   too (--h, --w). js/interactions.js only marks the bar being read. */
.case-chart {
  padding: clamp(20px, 3vw, 32px);
  background-color: var(--paper);
  border: 1px solid var(--grid-line-ink);
}

.case-chart-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px 24px;
  margin-bottom: 20px;
  font-size: 15px;
  line-height: 1.3;
}

.case-chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  font-size: 13.5px;
}

.case-chart-legend span {
  display: flex;
  align-items: center;
  gap: 6px;
}

.case-chart-legend i {
  width: 10px;
  height: 10px;
  background-color: var(--uc);
}

/* Grouped vertical bars. The scale's ticks are laid over the plot, the 0
   line in ink and the rest faint. */
.case-bars {
  position: relative;
  height: clamp(200px, 24vw, 280px);
  margin-left: 24px;
}

.case-bars-tick {
  position: absolute;
  left: 0;
  right: 0;
  height: 0;
  border-top: 1px solid color-mix(in srgb, var(--grey-100) 12%, transparent);
}

.case-bars-tick:first-child {
  border-top-color: var(--grey-100);
}

.case-bars-tick span {
  position: absolute;
  top: -8px;
  left: -24px;
  width: 16px;
  color: var(--ink-muted);
  font-size: 10.5px;
  text-align: right;
}

.case-bars-groups,
.case-bars-axis {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(8px, 2vw, 28px);
}

.case-bars-groups {
  position: absolute;
  inset: 0;
}

.case-bars-axis {
  margin-left: 24px;
  padding-top: 6px;
}

.case-bars-group {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 100%;
  min-width: 0;
}

/* --i is the step within the group; each step 11% lighter towards white. */
.case-bar {
  position: relative;
  flex: 1;
  min-width: 0;
  height: var(--h);
  background-color: color-mix(in srgb, var(--uc) calc(100% - var(--i) * 11%), #fff);
  cursor: pointer;
  transition: opacity .15s ease;
}

.case-bars.is-picking .case-bar:not(.is-active) {
  opacity: .4;
}

.case-bar-tip {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  z-index: 2;
  display: none;
  flex-direction: column;
  align-items: center;
  padding: 6px 8px;
  background-color: var(--grey-100);
  color: var(--white);
  white-space: nowrap;
  pointer-events: none;
  transform: translateX(-50%);
}

.case-bar.is-active .case-bar-tip {
  display: flex;
}

/* The bars at either end of the plot keep their tip over the plot. */
.case-bars-group:first-child .case-bar:first-child .case-bar-tip {
  left: 0;
  transform: none;
}

.case-bars-group:last-child .case-bar:last-child .case-bar-tip {
  right: 0;
  left: auto;
  transform: none;
}

.case-bar-tip b {
  font-size: 15px;
  line-height: 1.1;
}

.case-bar-tip small {
  color: var(--on-color-muted);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .6px;
  text-transform: uppercase;
}

.case-bars-ages {
  display: flex;
  gap: 2px;
}

.case-bars-ages span {
  flex: 1;
  min-width: 0;
  color: var(--ink-muted);
  font-size: 10.5px;
  line-height: 1.1;
  text-align: center;
}

.case-bars-name {
  display: block;
  margin-top: 6px;
  font-size: 13.5px;
  font-weight: 700;
  text-align: center;
}

@media (max-width: 640px) {
  .case-bars-ages {
    display: none;
  }
}

/* Horizontal bars: label, then the bar on a track ruled every 25%. */
.case-hbars {
  display: flex;
  flex-direction: column;
}

.case-hbar {
  display: grid;
  grid-template-columns: minmax(96px, 172px) minmax(0, 1fr);
  align-items: center;
  gap: 12px;
  margin-top: 2px;
  cursor: pointer;
  transition: opacity .15s ease;
}

/* The first row of each new category stands a little apart. */
.case-hbar--first {
  margin-top: 10px;
}

.case-hbar:first-child {
  margin-top: 0;
}

.case-hbars.is-picking .case-hbar:not(.is-active) {
  opacity: .4;
}

.case-hbar-label {
  font-size: 13.5px;
  line-height: 1.15;
  text-align: right;
}

.case-hbar.is-active .case-hbar-label {
  font-weight: 700;
}

.case-hbar-track {
  display: flex;
  align-items: center;
  height: 22px;
  background-image: linear-gradient(to right, color-mix(in srgb, var(--grey-100) 12%, transparent) 1px, transparent 1px);
  background-size: 25% 100%;
}

.case-hbar-fill {
  display: block;
  width: var(--w);
  height: 16px;
  background-color: var(--uc);
}

.case-hbar-value {
  display: none;
  margin-left: 8px;
  padding: 2px 6px;
  background-color: var(--grey-100);
  color: var(--white);
  font-size: 13.5px;
  font-weight: 700;
  line-height: 1.2;
}

.case-hbar.is-active .case-hbar-value {
  display: inline-block;
}

.case-hbars-axis {
  display: grid;
  grid-template-columns: minmax(96px, 172px) minmax(0, 1fr);
  gap: 12px;
  margin-top: 6px;
}

.case-hbars-axis > span:last-child {
  position: relative;
  height: 16px;
  border-top: 1px solid var(--grey-100);
}

.case-hbars-axis b {
  position: absolute;
  top: 4px;
  color: var(--ink-muted);
  font-size: 10.5px;
  font-weight: 400;
  transform: translateX(-50%);
}

.case-hbars-axis b:first-child {
  transform: none;
}

.case-hbars-axis b:last-child {
  right: 0;
  left: auto !important;
  transform: none;
}

/* ---- Screen picker ----------------------------------------------------- */

/* One screen large, on a ground of its own (--bg, per screen), and a row of
   tabs to swap it. A screen cropped at an edge sits flush against that edge
   of the view (data-align). */
.case-picker {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.case-picker-view {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 4 / 3;
  padding: clamp(16px, 3vw, 40px);
  overflow: hidden;
  background-image: var(--bg);
  border: 1px solid var(--grid-line-ink);
}

.case-picker-view[data-align="left"] {
  justify-content: flex-start;
  padding-left: 0;
}

.case-picker-view[data-align="right"] {
  justify-content: flex-end;
  padding-right: 0;
}

.case-picker-view[data-align="both"] {
  padding-right: 0;
  padding-left: 0;
}

.case-picker-view > img {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: 100%;
  object-fit: contain;
}

.case-picker-view[data-align="both"] > img {
  width: 100%;
}

.case-picker-badge {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 8px;
}

.case-picker-badge img {
  display: block;
  width: 32px;
  height: 32px;
  border-radius: 22%;
  outline: 1px solid color-mix(in srgb, var(--white) 35%, transparent);
}

.case-picker-badge img[hidden] {
  display: none;
}

.case-picker-badge .case-label {
  padding: 4px 8px;
  background-color: var(--grey-100);
  color: var(--white);
}

.case-picker-tabs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(110px, 100%), 1fr));
  gap: clamp(8px, 1vw, 12px);
}

.case-picker-tab {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px;
  background-color: var(--paper);
  border: 1px solid var(--grid-line-ink);
  color: var(--grey-100);
  font-family: "Raleway Site", sans-serif;
  text-align: left;
  cursor: pointer;
  transition: transform .15s ease;
}

.case-picker-tab:hover {
  transform: scale(1.03);
}

.case-picker-tab[aria-selected="true"] {
  border-color: var(--grey-100);
  outline: 1px solid var(--grey-100);
}

.case-picker-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 4 / 3;
  background-image: var(--bg);
}

.case-picker-thumb img {
  display: block;
  max-width: 92%;
  max-height: 92%;
}

/* ---- A live prototype -------------------------------------------------- */

/* No frame round it: the prototype gets the full width of the plate, on the
   project's gradient - which is also what fills the screen around it in
   full screen, the wrapper being what goes full screen. */
.case-proto-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 6px 16px;
  margin-bottom: 10px;
}

.case-proto-head .case-subhead {
  margin: 0;
}

.case-proto-tools {
  display: flex;
  align-items: center;
  gap: 16px;
}

.case-proto-hint {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--ink-muted);
  font-size: 13.5px;
}

.case-body .case-proto-hint .icon {
  font-size: 18px;
}

.case-proto-full[hidden] {
  display: none;
}

.case-proto {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 10;
  min-height: 420px;
  background-color: var(--project-accent);
  background-image: var(--project-grad);
}

.case-proto iframe {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

@media (prefers-reduced-motion: reduce) {
  .case-picker-tab,
  .case-bar,
  .case-hbar {
    transition: none;
  }

  .case-picker-tab:hover {
    transform: none;
  }
}

@media screen and (min-width: 1280px) {
  .container-9 {
    padding-left: 20px;
    padding-right: 0;
  }
}

@media screen and (min-width: 1920px) {
  .page-body {
    align-items: center;
  }
}

/* Tablet (2b in the About me handoff): one column on the plate. The facts go
   back to the plate's full width, the hairline between copy and logos goes,
   and the logos fold down into a strip of nine at the plate's foot, behind a
   hairline that runs edge to edge - the photo then stands on that line
   rather than on the plate's edge. */
@media screen and (max-width: 1199px) {
  .intro-plate {
    --plate-inset: 28px;
  }

  .intro-lettering {
    column-gap: 14px;
    margin-bottom: 24px;
    font-size: 38px;
  }

  .intro-body {
    grid-template-columns: minmax(0, 1fr);
  }

  .intro-rule {
    display: none;
  }

  .intro-fact {
    padding: 14px 18px;
  }

  /* Out to the plate's right edge, so the cropped torso meets it. */
  .intro-photo {
    width: 210px;
    height: 210px;
    margin-right: calc(-1 * var(--plate-inset));
  }

  /* Out through the plate's inset on both sides, so the hairline on top runs
     from edge to edge, with the inset put back as padding. */
  .collaborations {
    margin: 0 calc(-1 * var(--plate-inset));
    padding: 20px var(--plate-inset) var(--plate-inset);
    border-top: 1px solid var(--grid-line-ink);
  }

  /* One row of nine at full width - the logos read as a single line of
     proof under the copy. */
  .collaborations-grid {
    grid-template-columns: repeat(9, minmax(0, 1fr));
    gap: 8px;
  }
}

@media screen and (max-width: 991px) {
  .container {
    max-width: 728px;
  }

  .navbar-wrapper-three {
    justify-content: space-between;
  }

  .navbar-brand-three {
    position: relative;
  }

  .nav-menu-wrapper-three {
    background-color: #0000;
    top: 70px;
  }

  .nav-menu-three {
    background-color: #fff;
    border-radius: 50px;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    margin-top: 10px;
    padding: 20px;
    display: flex;
    box-shadow: 0 8px 50px #0000000d;
  }

  .nav-link {
    padding-left: 5px;
    padding-right: 5px;
  }

  .nav-dropdown-list.shadow-three.w--open {
    position: absolute;
  }

  .menu-button {
    padding: 12px;
  }

  .menu-button.w--open {
    color: #fff;
    background-color: #a6b1bf;
  }

  .navigation-bar {
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    /* Desktop's min-height (hero-and-nav.css) is sized for the name+ticker
       block, which is hidden here in favor of the "TT" logo - keep this bar
       at its original, content-driven height instead of inheriting that. */
    min-height: auto;
  }

  .nav-container {
    grid-column-gap: 0px;
    grid-row-gap: 0px;
    flex-flow: row;
    flex: 1;
    justify-content: space-between;
    align-self: center;
    align-items: center;
    margin-left: auto;
    margin-right: auto;
    display: block;
  }

  .navigation {
    /* Undoes hero-and-nav.css's 28px/27px - that's sized for the taller
       desktop bar, which this breakpoint doesn't use (see .navigation-bar
       above). Still 1px less on the bottom than the top, matching the base
       rule, to leave room for the always-reserved hover border. */
    padding-top: 20px;
    padding-bottom: 19px;
  }

  .navigation.logo {
    margin-left: 20px;
    margin-right: 20px;
    display: none;
  }

  .nav-identity {
    display: none;
  }

  .hero-identity {
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    z-index: 2;
    /* Centers this block in the gap between the (fixed, 60px) mobile nav
       bar and the top of the hero photo, rather than pinning it to either
       edge - so it stays centered as that gap's size changes with viewport
       height. The photo's top sits (100vh - portrait height) down from
       .hero-image's own top; the gap runs from the nav's bottom (60px) to
       there, so its midpoint is 60px + half of (that minus 60px), i.e.
       30px + half of (100vh - portrait height). translateY(-50%) then
       centers this block's own (variable, content-driven) height on that
       point, the same way it would for any size box. */
    top: calc(30px + (var(--hero-height) - var(--portrait-height)) / 2);
    transform: translateY(-50%);
    /* Background hugs the text instead of spanning the full width, so it
       reads as a plate behind the name rather than a band cutting the
       sketch artwork in half. fit-content + auto side margins shrink the
       box to its widest line while keeping it centred between left/right:0.
       That width is stable as the ticker cycles: .ticker-track stacks all
       four lines, so its width is always the widest of them, not the
       currently visible one. */
    width: fit-content;
    margin-left: auto;
    margin-right: auto;
    padding: 8px 14px 10px;
  }

  /* The plate itself, as a backdrop rather than a background on the element.
     It has to be its own layer so it can fade out at the edges without the
     name fading with it - masking .hero-identity would take the text too.
     Sitting on a pseudo-element also lets the fade spread wider than the
     text without padding the box out, so the plate still hugs the name.
     Solid across the middle, then out to nothing by the edge, so the grid
     comes back gradually instead of at a hard rectangle - the same idea as
     the nav bar's fade, in every direction rather than just downwards.
     The end colour is grey-100 at zero alpha, not `transparent`: that
     keyword is transparent *black*, which greys the fade out on the way. */
  .hero-identity::before {
    content: "";
    position: absolute;
    inset: -20px -30px;
    z-index: -1;
    background: radial-gradient(ellipse farthest-side at center,
      var(--grey-100) 50%, #11121500 100%);
  }

  /* Denser grid on small screens: the desktop pitch leaves only a handful of
     cells across a phone, which reads as a few stray lines rather than as
     graph paper. */
  :root {
    --grid-pitch: 40px;
  }

  /* Mobile re-lays out the sketch composition rather than scaling it down -
     the portrait is wider than the screen here, so the drawings work around
     it: the card bleeds off the left at the top, the frame off the right
     below it. Both keep the tilt they have on desktop. */
  /* No lettering here - there is no room for it once the name plate takes
     the middle of the band above his head, and the ticker under his name
     already reads "Product Designer". */
  .hero-lettering {
    display: none;
  }

  .hero-sketch-card {
    top: 3%;
    left: -28%;
    right: auto;
    width: 92%;
  }

  /* Barely any bleed on this one: tilting it swings the header bar and its
     circle - the part that makes it read as a wireframe rather than as a few
     stray diagonals - a long way right, and at -22% they landed off screen
     entirely. */
  .hero-sketch-frame {
    display: block;
    top: 40%;
    bottom: auto;
    left: auto;
    right: -6%;
    width: min(58%, calc(var(--hero-height) * 0.46));
  }

  .navigation.logo.foot {
    margin-left: 0;
    margin-right: 0;
    padding-top: 0;
    padding-bottom: 0;
    padding-left: 0;
  }

  .nav-logo {
    justify-content: center;
  }

  .nav-links {
    background-color: var(--grey-100);
  }

  .nav-menu-toggle {
    background-color: var(--transparent);
  }

  .nav-home {
    visibility: visible;
  }

  .nav-menu-toggle.w--open {
    background-color: var(--black-transparent);
  }

  /* With the menu open, the bar takes a solid ground: the panel opens right
     under the bar, and without this the homepage bar's see-through fade-out
     (hero-and-nav.css) leaves a strip of grid between the two, and a case
     study's overlay bar (no ground at all, project-page.css) leaves a strip of
     banner. Bar and panel read as one dark sheet instead. Four classes deep
     so it outranks the overlay bar's background: none, .nav-solid variant
     included, wherever the two land in the bundle. */
  .navigation-bar.w-nav:has(.nav-menu-toggle.w--open) {
    background: var(--grey-100);
  }

  .logo {
    padding: 20px;
    display: inline-block;
  }

  .text-block, .material-icon {
    color: var(--white);
    font-family: Material Symbols Sharp, sans-serif;
    font-weight: 100;
    text-transform: none;
    letter-spacing: normal;
  }

  .section-title {
    align-self: flex-start;
    position: static;
  }

  .page-body.home {
    --section-gap: 10vh;
  }

  .spacer {
    height: var(--section-gap, 6vh);
  }

  /* From here down the shelf shows one card at a time with the next ones
     running off the edge, so it drops the column and takes the whole page:
     the cards that are not in focus reach the screen edges instead of
     stopping at the margin, which is width the focused card gets to keep.
     --lead then holds that card itself on the margin, in line with the
     heading and arrows, and the card before it fills the strip to its left.
     Its value is the page inset, so the two always agree. */
  .shelf-wrap cover-shelf {
    --lead: 20px;
    max-width: none;
    padding-left: 0;
    padding-right: 0;
  }

  /* Tracks .spacer's own drop - the top padding stands in for a spacer
     above this section. */
  .intro-section {
    padding: var(--section-gap, 6vh) 20px 0;
  }

  .container-8 {
    max-width: 728px;
  }
}

@media screen and (max-width: 767px) {
  /* :root, not .hero-portrait, so .hero-identity's centering calc (further
     down this file) sees the same updated value at this breakpoint too -
     a custom property set on one element isn't visible to its siblings. */
  :root {
    --portrait-height: min(calc(var(--hero-height) * 0.76), 700px);
  }

  .navbar-brand-three {
    padding-left: 0;
  }

  .nav-menu-three {
    border-radius: 20px;
    flex-direction: column;
    padding-bottom: 30px;
  }

  .nav-menu-block {
    flex-direction: column;
  }

  .nav-link {
    padding-top: 10px;
    padding-bottom: 10px;
    display: inline-block;
  }

  .nav-dropdown {
    flex-direction: column;
    align-items: center;
    display: flex;
  }

  .nav-dropdown-toggle {
    padding-top: 10px;
    padding-bottom: 10px;
  }

  .nav-dropdown-list.shadow-three {
    box-shadow: 0 8px 50px #0000000d;
  }

  .nav-dropdown-list.shadow-three.w--open {
    position: relative;
  }

  .nav-dropdown-list.shadow-three.mobile-shadow-hide {
    box-shadow: none;
  }

  .nav-link-accent {
    margin-right: 5px;
    padding-top: 10px;
    padding-bottom: 10px;
    display: inline-block;
  }

  .mobile-margin-top-10 {
    margin-top: 10px;
  }

  .section-title {
    text-align: left;
  }

  .section-title.right {
    order: -1;
  }

  /* Phone (2c in the About me handoff): a tighter plate, the facts stacked as
     one-liners, all the copy full width, and the photo and a shortened call
     to action side by side above the logos. */
  .intro-section {
    padding-left: 16px;
    padding-right: 16px;
  }

  .intro-plate {
    --plate-inset: 20px;
  }

  .intro-lettering {
    column-gap: 12px;
    margin-bottom: 20px;
  }

  /* The facts stacked, one per row: the label (icon and word) as on desktop,
     then the value with the shorter wording of its meta run on after it on a
     single line - the meta's own line goes. The list's top rule is each
     cell's instead, so every row is ruled above. */
  .intro-facts {
    flex-direction: column;
    margin-bottom: 20px;
    border-top: 0;
  }

  .intro-fact,
  .intro-fact:first-child,
  .intro-fact:last-child {
    padding: 10px 0;
    border-top: 1px solid var(--grid-line-ink);
  }

  .intro-fact + .intro-fact {
    border-left: 0;
  }

  .intro-fact-label {
    margin-bottom: 4px;
  }

  .intro-fact-meta {
    display: none;
  }

  .intro-fact-value {
    font-size: 14px;
    line-height: 1.3;
  }

  .intro-fact-short {
    display: inline;
  }

  .intro-text {
    font-size: 16px;
  }

  /* The foot's copy wrapper steps aside, so its paragraphs and button become
     the foot's own grid items: the paragraphs span both columns, full width
     like the first, and the button shares the last row with the photo,
     centred on it. */
  .intro-foot {
    grid-template-columns: minmax(0, 1fr) 150px;
    column-gap: 8px;
    align-items: center;
  }

  .intro-foot-copy {
    display: contents;
  }

  .intro-foot .intro-text {
    grid-column: 1 / -1;
  }

  .intro-foot .intro-text + .intro-text {
    margin-top: 12px;
  }

  .intro-foot .intro-text:last-of-type {
    margin-bottom: 8px;
  }

  .intro-cta {
    width: auto;
    margin: 0;
    justify-content: center;
    padding: 12px 20px;
  }

  .intro-cta-long {
    display: none;
  }

  .intro-cta-short {
    display: inline;
  }

  .intro-photo {
    width: 160px;
    height: 160px;
  }

  .collaborations {
    padding-top: 16px;
  }

  .collaborations-label {
    margin-bottom: 12px;
  }

  /* Back to 3x3: nine across a phone is tiles too small to tap. */
  .collaborations-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  /* The tracking is what tips the longer names onto a second line at phone
     tile sizes; nothing else about the caption changes here - the type
     scales with the tile on its own (see homepage.css). */
  .collaboration-name {
    letter-spacing: 0;
  }
}

@media screen and (max-width: 479px) {
  .container {
    max-width: none;
  }

  .nav-menu-three {
    flex-direction: column;
  }

  .section-title {
    padding-right: 0;
  }

  .button {
    justify-content: center;
  }

  .container-8 {
    max-width: none;
  }
}

/* Replaces the Webflow IX2 interactions that used to run on jQuery + js/webflow.js
   via data-w-id + inline transform styles. See js/interactions.js for the two
   behaviors here (.reveal, .nav-solid) that need scroll-position logic; the rest
   (.ticker-track, .bounce-arrow) are pure CSS. */

/* Nav bar: transparent over the hero, solid once the hero scrolls out of view. */
.navigation-bar {
  transition: background-color 200ms ease;
}

.navigation-bar.nav-solid {
  background: var(--grey-100);
}

/* Nav ticker: cycles through the 3 unique .carrousel lines, then a 4th line
   that duplicates the 1st (see partials/nav.html) so the loop can restart
   without an animated slide back up - see the keyframes below for why that
   4th line gets no hold of its own. The step is driven by --ticker-line so
   the line height and the travel can never drift apart. */
.ticker-track {
  display: block;
  animation: ticker-cycle 8s ease-in-out infinite;
}

@keyframes ticker-cycle {
  /* The 4th line is a duplicate of the 1st purely so this can restart
     invisibly: it's the endpoint of the last slide, held for 0% width, then
     the loop's 100% -> 0% restart is an instant, uninterpolated snap - and
     since the 4th line's text is identical to the 1st, that snap is
     imperceptible (no animated slide back, no visible change at all).
     Do NOT give this 4th line its own hold (e.g. "75%, 95%, 100%"): unlike
     the other three lines, its content reappears again immediately after at
     0% of the next iteration, so any hold here doubles - not adds to -  how
     long line 1's text visibly lingers versus the other two lines. */
  0%, 28.33% {
    transform: translateY(0);
  }
  33.33%, 61.66% {
    transform: translateY(calc(var(--ticker-line) * -1));
  }
  66.66%, 95% {
    transform: translateY(calc(var(--ticker-line) * -2));
  }
  100% {
    transform: translateY(calc(var(--ticker-line) * -3));
  }
}

/* Scroll-down arrow on the homepage hero. */
.bounce-arrow {
  animation: bounce-arrow 2s ease-in-out infinite;
}

@keyframes bounce-arrow {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-12px);
  }
}

/* Fade-up-on-scroll used by the homepage "Projects" heading and project cards.
   Only where script runs (.w-mod-js, set on <html> by the inline script in
   each page's head): without it nothing would ever reveal them, and a visitor
   or agent without script would get an empty Projects section. */
.w-mod-js .reveal {
  opacity: 0;
  transform: translateY(75px);
  transition: opacity 1000ms cubic-bezier(0.215, 0.61, 0.355, 1), transform 1000ms cubic-bezier(0.215, 0.61, 0.355, 1);
}

.w-mod-js .reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* Page-load intro: the background sketches draw themselves one traced stroke
   at a time and the tagline types out. Driven by js/hero-intro.js.

   The .hero-intro class is put on <html> by an inline script in the head, so
   the hero is hidden before the first paint rather than flashing complete and
   then restarting. That script also removes it on a timer, and the intro
   script removes it as soon as it takes over, so nothing here can leave the
   hero permanently blank if the JS never arrives. It is never added at all
   under prefers-reduced-motion, which is why none of this needs disabling in
   the media query below. */
.hero-intro .hero-sketch,
.hero-intro .hero-lettering {
  visibility: hidden;
}

/* Handed back once the <img> has been swapped for an inline <svg>, at which
   point the individual <path> elements - one per traced pen stroke - carry
   the reveal instead of the whole picture. */
.hero-sketch.is-drawing {
  visibility: visible;
}

.hero-sketch.is-drawing path {
  opacity: 0;
  transition: opacity 140ms linear;
}

.hero-sketch.is-drawing path.is-drawn {
  opacity: 1;
}

/* Typing is a clip that walks left to right. Clipping leaves the text's own
   layout completely alone - no width animation, no character wrapping, and
   the text node stays one piece for screen readers and crawlers. The script
   sets the right inset per character; the vertical bleed is here because
   line-height 0.9 lets glyphs stand outside their line box, and a clip flush
   with that box would shave their tops and tails off. */
.hero-lettering.is-typing {
  visibility: visible;
}

.hero-lettering.is-typing .hero-lettering-line {
  clip-path: inset(-40% 100% -40% -4%);
}

/* Respect a reduced-motion preference: hold the ticker on its first line and
   drop the decorative loops, but never leave revealed content stuck hidden. */
@media (prefers-reduced-motion: reduce) {
  .ticker-track,
  .bounce-arrow {
    animation: none;
  }

  .w-mod-js .reveal {
    transition: none;
  }

  .w-mod-js .reveal {
    opacity: 1;
    transform: none;
  }
}

/* Read by screen readers and agents, never drawn: the homepage's one <h1>
   (who the page is about), which the hero says in pictures instead. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
