/* ---------- RESET ---------- */
*, *::before, *::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* Paired with the same on body. A full-bleed section is sized in vw, which
     counts the scrollbar, so it overhangs the page by that much on each side.
     clip is the one overflow value that leaves the other axis visible, so
     vertical scrolling is untouched. */
  overflow-x: clip;
}

/* The one grey the page fills a surface with. The Get in touch button and the
   work previews are meant to read as the same material, so they take it from
   here rather than each carrying their own copy of the value. */
:root {
  --surface: #F5F5F5;
}

body {
  font-family: 'Helvetica Neue', 'HelveticaNeue', 'Helvetica', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  font-weight: 300;
  color: #000000;
  background: #FFFFFF;
  line-height: 1.5;
  letter-spacing: -0.015em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* A full-bleed section is sized in vw, and vw includes the scrollbar, so
     without this the page gains a horizontal scrollbar the width of it. Clip
     rather than hidden, so this does not become a scroll container and the
     fixed nav is left alone. */
  overflow-x: clip;
}

a {
  color: inherit;
  text-decoration: none;
  transition: opacity 0.2s ease;
}

a:hover {
  opacity: 0.6;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

h1, h2, h3, h4, h5, h6 {
  font-weight: 400;
  letter-spacing: -0.015em;
}

/* ---------- NAV ---------- */
/* A three column grid rather than space-between, so Work and Play sit on the
   true centre of the page instead of being pushed off it by the difference in
   width between the wordmark and About. */
.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 28px 56px;
  background: transparent;
  z-index: 100;
  font-size: 15px;
  font-weight: 300;
}

.nav-logo   { justify-self: start; }
.nav-center { justify-self: center; }
.nav-right  { justify-self: end; }

/* The wordmark and About are single pills. They carry no glass until nav-lit
   is set, which the header script does once there is something behind the bar
   worth frosting. */
.nav-logo,
.nav-right,
.nav-center {
  border-radius: 999px;
  border: 1px solid transparent;
  background: transparent;
  transition: background 0.35s ease, border-color 0.35s ease,
              -webkit-backdrop-filter 0.35s ease, backdrop-filter 0.35s ease;
}

.nav-logo,
.nav-right {
  padding: 6px 16px;
}

/* The Play page is lit from the start rather than on the script's say-so. That
   page is a full bleed gallery, so there is always artwork under the bar, and
   its works are drawn as facets rather than as anything the header counts as a
   picture: teaching it to recognise them would mean measuring a couple of
   hundred of them on every check, to answer a question whose answer never
   changes. */
.nav-lit .nav-logo,
.nav-lit .nav-right,
.nav-lit .nav-center,
.play-page .nav-logo,
.play-page .nav-right,
.play-page .nav-center {
  background: rgba(255, 255, 255, 0.55);
  border-color: rgba(255, 255, 255, 0.65);
  -webkit-backdrop-filter: blur(14px) saturate(170%);
  backdrop-filter: blur(14px) saturate(170%);
}

/* Work and Play sit in one glass pill; the current one carries its own mark. */
.nav-center {
  position: relative;
  padding: 4px;
  gap: 4px;
}

.nav-center a {
  position: relative;
  z-index: 1;
  padding: 6px 16px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: transparent;
  white-space: nowrap;
  transition: background 0.3s ease, border-color 0.3s ease;
}

/* Which section is current is written into the markup, so the pill is drawn
   from the very first frame with no measuring involved. */
.nav-center a.is-current {
  border-color: #000000;
}

/* Only once a picture is actually behind the pill does the stroke give way to a
   white fill. Scrolling over body copy keeps the stroke, since a white fill on
   a white page is invisible. */
.nav-art .nav-center a.is-current {
  background: rgba(255, 255, 255, 0.92);
  border-color: transparent;
}

/* Once the script is up it hands the pill over to a single element that can
   actually travel between the two links. Swapping a class from one link to the
   other can only switch one border off and another on, which is a jump; a lone
   element has a position to animate. The static pill above is what shows until
   this takes over, so nothing flashes on load. */
.nav-js .nav-center a.is-current,
.nav-js.nav-art .nav-center a.is-current {
  border-color: transparent;
  background: transparent;
}

.nav-marker {
  position: absolute;
  top: 3px;
  left: 0;
  height: calc(100% - 6px);
  width: 0;
  border-radius: 999px;
  border: 1px solid #000000;
  background: transparent;
  opacity: 0;
  pointer-events: none;
  transition: transform 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
              width 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
              background 0.3s ease, border-color 0.3s ease, opacity 0.2s ease;
}

.nav-marker.is-set {
  opacity: 1;
}

/* Placing it for the first time, or after a resize, must not animate. */
.nav-marker.no-anim {
  transition: none;
}

.nav-art .nav-marker {
  background: rgba(255, 255, 255, 0.92);
  border-color: transparent;
}

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

@media (prefers-reduced-motion: reduce) {
  .nav-center a { transition: none; }
}

.nav-logo {
  font-weight: 300;
}

.nav-center {
  display: flex;
  gap: 16px;
  align-items: center;
}

.nav-divider {
  color: #6C848E;
  opacity: 0.5;
}

.nav-right {
  font-weight: 300;
}

/* ---------- HOME PAGE ---------- */

.hero {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 120px 24px 80px;
}

/* The dandelion field is off the home page for now, and js/dandelion.js is
   kept for later. Putting it back needs the canvas and the .hero-copy wrapper
   in the markup, the script loaded, and these four rules live again:

   .hero        { position: relative; overflow: hidden; background: #FFFFFF;
                  min-height: 100svh; }
   .dandelion-field { position: absolute; inset: 0; display: block;
                  width: 100%; height: 100%; pointer-events: none; }
   .hero-copy   { position: relative; z-index: 1; margin-bottom: 7vh; }

   The overflow and the relative position are what hold the canvas to the
   section; the margin on the copy is what lifts it clear of the heads. */


.hero-headline {
  font-size: 16px;
  font-weight: 300;
  /* wide enough to hold the sentence on a single line, as in the reference */
  max-width: 560px;
  line-height: 1.5;
  margin-bottom: 18px;
  letter-spacing: -0.01em;
}

.hero-sub {
  font-size: 13px;
  font-weight: 300;
  color: #000000;
  margin-bottom: 28px;
}

.pill-button {
  display: inline-block;
  padding: 14px 20px;
  background: var(--surface);
  border-radius: 999px;
  font-size: 16px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.01em;
  transition: background 0.2s ease;
}

.pill-button:hover {
  background: #EAEAEA;
  opacity: 1;
}

.divider {
  max-width: 1312px;
  margin: 0 auto;
  border-top: 1px solid #E5E5E5;
}

/* ---------- WORK SECTION (Home) ---------- */
.work-section {
  padding: 120px 64px;
  max-width: 1440px;
  margin: 0 auto;
}

.project {
  margin-bottom: 120px;
  /* clears the fixed nav when the Work link jumps here */
  scroll-margin-top: 110px;
}

.project-label {
  text-align: center;
  font-size: 16px;
  font-weight: 300;
  margin-bottom: 40px;
}

/* Each card comes in smaller and opens out to its full size as it rises up the
   screen. The scroll itself is the timeline rather than a clock, so the box
   tracks the wheel exactly and holds wherever the reader stops, and the
   browser runs it off the main thread.

   The scaling sits on this wrapper rather than on the card so that the two do
   not fight: the card's own transform is its hover lift, and an animation on a
   property beats a rule for it. The wrapper also keeps the full size reserved
   in the layout while the card is small, so nothing below it moves. */
.project-frame {
  transform-origin: 50% 50%;
}

@keyframes project-open {
  from { transform: scale(0.78); }
  to   { transform: scale(1); }
}

/* A browser without scroll-driven animations gets the card at full size, which
   is the end of this animation anyway, so nothing is lost but the movement. */
@supports (animation-timeline: view()) {
  .project-frame {
    animation: project-open linear both;
    animation-timeline: view();
    /* From the moment its top edge appears until the whole card is in frame. */
    animation-range: entry 0% entry 90%;
    will-change: transform;
  }

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

.project-card {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--surface);
  border-radius: 48px;
  overflow: hidden;
  position: relative;
  transition: transform 0.3s ease;
}

.project-card:hover {
  transform: translateY(-4px);
  opacity: 1;
}

/* Render_2_phones.png is transparent. Its opaque phone bodies are centred at
   38.4% x / 47.9% y, not 50/50, and the soft shadow runs out to about 94% of
   the width. Translating past the 38.4% figure shifts the render left so that
   shadow stays in frame, which reads better than centring the bodies alone. */
.project-card img {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 89%;
  height: auto;
  max-width: none;
  transform: translate(-43%, -47.9%);
}

/* The percentages above are for a render that bleeds past the frame and is
   nudged off centre on purpose. A preview that is a whole composition instead
   has to sit inside the frame complete, so it is fitted to whichever side runs
   out first and centred on both axes rather than cropped by either. */
.project-card img.art-fit,
.case-hero-image img.art-fit {
  width: auto;
  height: auto;
  max-width: 96%;
  max-height: 96%;
  transform: translate(-50%, -50%);
}

/* ---------- ABOUT PAGE ---------- */
.about-page {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 120px 56px;
  gap: 80px;
}

.about-image {
  width: 320px;
  height: 320px;
  background: #D9D9D9;
  border-radius: 32px;
  overflow: hidden;
  flex-shrink: 0;
}

.about-text {
  max-width: 520px;
}

.about-text p {
  font-size: 15px;
  margin-bottom: 18px;
  line-height: 1.6;
}

.about-buttons {
  display: flex;
  gap: 12px;
  margin-top: 32px;
}

.about-pill {
  padding: 8px 20px;
  border: 1px solid #000000;
  border-radius: 999px;
  font-size: 14px;
  transition: background 0.2s ease;
}

.about-pill:hover {
  background: #000000;
  color: #FFFFFF;
  opacity: 1;
}

/* ---------- CASE STUDY PAGE ---------- */
.case-hero {
  padding: 120px 56px 40px;
}

.case-hero-image {
  max-width: 1400px;
  margin: 0 auto;
  aspect-ratio: 16 / 9;
  background: #FBFEFF;
  border-radius: 32px;
  overflow: hidden;
  position: relative;
}

.case-hero-image img {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 86%;
  height: auto;
  max-width: none;
  transform: translate(-43%, -47.9%);
}

.case-intro {
  max-width: 900px;
  margin: 0 auto;
  padding: 80px 24px 40px;
}

.case-title {
  font-size: 56px;
  font-weight: 400;
  letter-spacing: -0.02em;
  margin-bottom: 24px;
}

.case-description {
  font-size: 20px;
  color: #000000;
  line-height: 1.5;
  margin-bottom: 64px;
  max-width: 780px;
}

.case-meta {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 40px;
  padding-bottom: 40px;
  border-bottom: 1px solid #E5E5E5;
  margin-bottom: 40px;
}

.meta-block {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.meta-label {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #6C848E;
}

.meta-value {
  font-size: 16px;
  font-weight: 400;
  line-height: 1.4;
}

.case-toc {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  font-size: 14px;
  color: #6C848E;
}

.case-toc a {
  transition: color 0.2s ease;
}

.case-toc a:hover {
  color: #000000;
  opacity: 1;
}

/* Case sections */
.case-section {
  max-width: 900px;
  margin: 0 auto;
  padding: 100px 24px;
}

.section-label {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #6C848E;
  margin-bottom: 20px;
}

.section-title {
  font-size: 40px;
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin-bottom: 32px;
  margin-top: 48px;
}

.section-title:first-of-type {
  margin-top: 0;
}

.section-body {
  font-size: 18px;
  line-height: 1.6;
  color: #000000;
  margin-bottom: 24px;
  max-width: 780px;
}

.section-body em {
  font-style: italic;
  color: #6C848E;
}

/* Pain points / insights / principles — 3 col grids */
.pain-points,
.insights,
.principles {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
  margin-top: 48px;
  padding-top: 48px;
  border-top: 1px solid #E5E5E5;
}

.pain-point h3,
.principle h3 {
  font-size: 20px;
  font-weight: 400;
  margin-bottom: 12px;
}

.pain-point p,
.principle p {
  font-size: 15px;
  color: #6C848E;
  line-height: 1.5;
}

.insight-number {
  font-size: 12px;
  color: #6C848E;
  margin-bottom: 16px;
  letter-spacing: 0.08em;
}

.insight h3 {
  font-size: 20px;
  font-weight: 400;
  margin-bottom: 12px;
}

.insight p {
  font-size: 15px;
  color: #6C848E;
  line-height: 1.5;
}

/* Before/After comparison */
.before-after {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 32px;
  margin-top: 48px;
}

.ba-column {
  padding: 40px;
  background: #FBFEFF;
  border-radius: 24px;
}

.ba-tag {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #6C848E;
  margin-bottom: 16px;
}

.ba-headline {
  font-size: 24px;
  font-weight: 400;
  margin-bottom: 8px;
  letter-spacing: -0.01em;
}

.ba-sub {
  font-size: 14px;
  color: #6C848E;
}

/* Full-width image blocks */
.full-image {
  max-width: 1400px;
  margin: 48px auto;
  padding: 0 24px;
}

.full-image img {
  width: auto;
  max-width: 100%;
  max-height: 82vh;
  margin-inline: auto;
  border-radius: 24px;
  background: #FBFEFF;
}

/* Phone screenshot grids */
.phone-grid {
  display: grid;
  gap: 24px;
  max-width: 1200px;
  margin: 48px auto;
  padding: 0 24px;
}

.phone-grid.two-col {
  grid-template-columns: 1fr 1fr;
}

.phone-grid.three-col {
  grid-template-columns: repeat(3, 1fr);
}

/* A single screen on its own, narrower than a grid of one would be so it reads
   as one phone held up rather than a column that lost its neighbours. */
.phone-single {
  max-width: 400px;
  margin: 40px auto;
  padding: 0 24px;
}

.phone-single img {
  width: 100%;
  border-radius: 24px;
}

.phone-grid img {
  /* Sized by height so a whole phone fits on screen at once rather than
     overflowing the viewport: these renders are 1312x2656, ratio 0.494. */
  height: auto;
  max-height: 76vh;
  width: auto;
  max-width: 100%;
  margin-inline: auto;
  border-radius: 24px;
  background: transparent;
}

/* Next / back link */
.case-next {
  max-width: 900px;
  margin: 0 auto;
  padding: 80px 24px 120px;
  border-top: 1px solid #E5E5E5;
}

.next-link {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-size: 18px;
  padding-top: 40px;
}

.next-label {
  font-weight: 400;
}

.next-arrow {
  transition: transform 0.2s ease;
}

.next-link:hover .next-arrow {
  transform: translateX(-4px);
}

/* Footer */
.footer {
  padding: 40px 56px;
  text-align: center;
  font-size: 12px;
  color: #6C848E;
  border-top: 1px solid #E5E5E5;
}

/* ---------- RESPONSIVE ---------- */
@media (max-width: 900px) {
  .nav {
    padding: 20px 24px;
    font-size: 14px;
  }

  .hero-headline {
    font-size: 15px;
    max-width: 340px;
  }

  .hero-sub {
    font-size: 12px;
  }

  .pill-button {
    font-size: 15px;
    padding: 13px 18px;
  }

  .project-label {
    font-size: 15px;
    margin-bottom: 28px;
  }

  .project-card {
    border-radius: 28px;
  }

  .case-title {
    font-size: 40px;
  }

  .section-title {
    font-size: 28px;
  }

  .case-meta {
    grid-template-columns: repeat(2, 1fr);
    gap: 24px;
  }

  .pain-points,
  .insights,
  .principles {
    grid-template-columns: 1fr;
    gap: 24px;
  }

  .before-after {
    grid-template-columns: 1fr;
  }

  .phone-grid.two-col,
  .phone-grid.three-col {
    grid-template-columns: 1fr;
  }

  .about-page {
    flex-direction: column;
    padding: 100px 24px;
    gap: 40px;
    text-align: center;
  }

  .about-image {
    width: 240px;
    height: 240px;
  }

  .about-buttons {
    justify-content: center;
  }

  .work-section {
    padding: 80px 24px;
  }

  .case-hero {
    padding: 100px 24px 20px;
  }

  .case-section {
    padding: 60px 24px;
  }
}

/* ---------- PLAY PAGE: the room ----------
   The page is locked to the viewport and the gallery is the whole page, so the
   room stays put however far the wheel is spun.

   The camera in the reference sits 2000px from the origin with the field of
   view set so that one world unit equals one pixel at that distance. A CSS
   perspective of 2000px on the viewport reproduces exactly that, which is why
   the room can be laid out in plain pixels. */
.play-page,
.play-page body {
  height: 100%;
  overflow: hidden;
}

.gallery-stage {
  position: relative;
  height: 100vh;
  overflow: hidden;
  touch-action: none;
}

.room-viewport {
  position: absolute;
  inset: 0;
  perspective: 2000px;
  perspective-origin: 50% 50%;
}

/* Origin of the room sits at the centre of the viewport; every facet is then
   placed at its own coordinates in the room. */
.room {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
  transform-style: preserve-3d;
}

.work {
  position: absolute;
  left: 0;
  top: 0;
  margin: 0;
  transform-style: preserve-3d;
}

.work-art {
  position: absolute;
  left: 0;
  top: 0;
  transform-style: preserve-3d;
}

/* Each facet is one narrow upright slice of a picture, sat on the wall at the
   angle the path has reached. */
.facet {
  position: absolute;
  left: 0;
  top: 0;
  background-repeat: no-repeat;
  backface-visibility: hidden;
  will-change: transform;
}

/* A piece is clickable, so say so. */
.work-art {
  cursor: zoom-in;
}

/* ---------- PLAY: A CLOSER LOOK ---------- */

/* One piece on its own, over the room. It is sized to the frame rather than to
   the file, so the whole of it is visible at once however tall or wide it is:
   that is the point of opening it. */
.viewer {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 72px 24px 24px;
  background: rgba(255, 255, 255, 0.86);
  -webkit-backdrop-filter: blur(18px) saturate(120%);
  backdrop-filter: blur(18px) saturate(120%);
  cursor: zoom-out;
  outline: none;
  animation: viewer-in 0.34s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

/* Closing is its own animation rather than the element simply disappearing.
   The script holds the overlay on screen for the length of it and only then
   sets hidden, so the way out is as considered as the way in. */
.viewer.is-closing {
  animation: viewer-out 0.26s cubic-bezier(0.4, 0, 0.7, 1) both;
  pointer-events: none;
}

@keyframes viewer-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

.viewer.is-closing .viewer-frame {
  animation: viewer-sink 0.26s cubic-bezier(0.4, 0, 0.7, 1) both;
}

@keyframes viewer-sink {
  from { opacity: 1; transform: scale(1) translateY(0); }
  to   { opacity: 0; transform: scale(0.975) translateY(4px); }
}

/* This has to be here, and it has to be more specific than the rule above.
   The hidden attribute hides an element through a user agent rule, and any
   author rule setting display beats it, so .viewer { display: flex } alone
   leaves the overlay on screen permanently whatever the attribute says. */
.viewer[hidden] {
  display: none;
}

@keyframes viewer-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.viewer-frame {
  margin: 0;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  animation: viewer-rise 0.44s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

/* It settles rather than snapping: it comes up a little, grows the last of the
   way, and fades in over a slightly longer run than the ground behind it, so
   the picture arrives after the room has softened rather than with it. */
@keyframes viewer-rise {
  from { opacity: 0; transform: scale(0.94) translateY(10px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

/* Held at its fitted size rather than left to flex: as a shrinkable item it
   gave up height it did not need to, and opened smaller than the frame
   allowed. The subtraction covers the padding above and the caption below. */
.viewer-frame img {
  flex: 0 0 auto;
  max-width: 100%;
  max-height: calc(100vh - 170px);
  width: auto;
  height: auto;
  border-radius: 8px;
  cursor: default;
}

.viewer-cap {
  font-size: 13px;
  font-weight: 300;
  color: #6C848E;
}

/* Two strokes and nothing else. No disc behind it: the page draws its other
   controls as hairlines on the paper, and a filled circle read as a different
   kit. The 44px box is hit area rather than anything you can see, and it dims
   on hover the way every other link on the site does.

   The outline is turned off because the script moves focus into the overlay
   when it opens, and the browser's own focus ring was drawing a box around the
   X the moment it appeared. focus-visible puts the ring back for anyone
   actually navigating by keyboard, which is who it is for. */
.viewer-close {
  position: absolute;
  top: 22px;
  right: 26px;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  outline: none;
  background: transparent;
  color: #000000;
  cursor: pointer;
  transition: opacity 0.2s ease;
}

.viewer-close:hover {
  opacity: 0.6;
}

.viewer-close:focus-visible {
  outline: 1px solid #000000;
  outline-offset: 2px;
  border-radius: 999px;
}

.viewer-close svg {
  display: block;
  width: 20px;
  height: 20px;
}

.viewer-close path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.25;
  stroke-linecap: round;
}

@media (prefers-reduced-motion: reduce) {
  .viewer,
  .viewer-frame,
  .viewer.is-closing,
  .viewer.is-closing .viewer-frame {
    animation: none;
  }
}

.work-art img {
  display: none;
}

.work-cap {
  position: absolute;
  left: 0;
  top: 0;
  white-space: nowrap;
  font-size: 13px;
  font-weight: 300;
  color: #6C848E;
  transform-origin: 0 0;
}

.gallery-hint {
  position: absolute;
  bottom: 48px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 11px;
  font-weight: 300;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #6C848E;
  transition: opacity 0.4s ease;
  z-index: 2;
}

/* Page transitions.

   The browser runs these itself, which is the whole point: a hand-rolled fade
   has to empty the old page before the new one has loaded, and that gap is the
   dead moment where nothing is on screen. A view transition holds a picture of
   the outgoing page until the incoming one is ready to paint, so there is no
   gap at all, and it is quick.

   The marker's own travel is animated in script rather than here, so that it
   is the same movement whether or not the browser supports this. */
@view-transition {
  navigation: auto;
}

::view-transition-group(root) {
  animation-duration: 160ms;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 160ms;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* ---------- WIDGET CAROUSEL (Gemini case study) ---------- */
/* The two suggestion columns from the redesigned home screen, lifted off the
   phone so the cycling can actually be tried. It keeps the work previews'
   16/9 and corner radius, but takes the page's own white rather than the grey
   surface, so the cards read as sitting on the page instead of on a panel.

   The slides are Edward's exports dropped in untouched, byte for byte, which
   is how he wants them. They are portrait and carry their own heading and page
   dots, so the column supplies no heading of its own and the slide is held to
   a width that lets the whole export fit the 16/9 box. */
.widget-demo {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 330px));
  justify-content: center;
  gap: 30px;
  /* Full bleed, so the glow has somewhere to end. Confined to the text column
     it would have to stop somewhere a reader can see, which is the boundary
     Edward did not want. */
  width: 100vw;
  max-width: none;
  position: relative;
  margin: 22px 0 12px;
  margin-inline: calc(50% - 50vw);
  padding: 150px 24px 132px;
  /* Gemini's light mode, matched to the reference: a pale blue bloom sitting
     low and a little left of centre on a near-white ground, with a second,
     fainter one up and to the right, and a cool off-white filling the corners
     rather than pure page white. Low saturation throughout. Still stepped down
     in small increments, because the shoulder is what makes one of these read
     as an object rather than as light. */
  /* Every one of these has to finish at nothing inside its own box, which means
     a vertical radius no greater than the distance from its centre to the
     nearer edge. The previous set did not, which is why it ended in a straight
     line top and bottom. The box is also taller now, so the wash has somewhere
     to end, and the main one is wide enough to carry the labels rather than
     stopping below them. */
  background:
    radial-gradient(ellipse 30% 26% at 45% 58%,
      rgba(148, 196, 240, 0.62) 0%,
      rgba(160, 203, 242, 0.40) 40%,
      rgba(182, 216, 245, 0.16) 72%,
      rgba(255, 255, 255, 0) 100%),
    radial-gradient(ellipse 48% 50% at 48% 50%,
      rgba(164, 205, 242, 0.78) 0%,
      rgba(168, 208, 243, 0.72) 20%,
      rgba(176, 213, 244, 0.62) 34%,
      rgba(188, 219, 246, 0.48) 48%,
      rgba(202, 227, 248, 0.33) 62%,
      rgba(216, 234, 250, 0.20) 74%,
      rgba(229, 241, 251, 0.10) 85%,
      rgba(242, 248, 253, 0.03) 94%,
      rgba(255, 255, 255, 0) 100%),
    radial-gradient(ellipse 40% 34% at 74% 26%,
      rgba(198, 224, 247, 0.42) 0%,
      rgba(212, 232, 249, 0.26) 38%,
      rgba(228, 240, 251, 0.11) 68%,
      rgba(255, 255, 255, 0) 100%),
    radial-gradient(ellipse 50% 50% at 50% 50%,
      rgba(246, 249, 252, 0.85) 0%,
      rgba(246, 249, 252, 0.55) 50%,
      rgba(248, 250, 252, 0.20) 78%,
      rgba(255, 255, 255, 0) 100%);
  /* Gemini's own blue, sampled off the renders rather than guessed: the core of
     the glow behind the input bar reads #14296B, the body of it #0D1940 down
     into #091331, and it is black well before the edge of the screen. Two
     ellipses rather than one, a broad navy cloud with the brighter core sitting
     low inside it the way it sits low on the phone. Every stop finishes fully
     transparent, so there is no edge to find. */
}


.widget-col {
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* So the indicator below can be sized in cqi and stay in proportion to the
     card at every width, the way it is in the export. */
  container-type: inline-size;
}

/* The label, cut from the export rather than re-typeset, so it keeps the size,
   the face and the weight the original has instead of my approximation of it.
   It is the same 2796 wide as the card strip and is shown at the same width,
   which means the label sits at exactly the scale it does in the file. The gap
   beneath it is carried inside the image for the same reason. It lives outside
   the draggable area, so it does not travel when a card is swiped. */
.widget-head {
  display: block;
  width: 84%;
  height: auto;
  margin: 0 auto;
  background: transparent;
  -webkit-user-drag: none;
}

/* A card travels a full stack width on a change, which is what makes it read
   as paging rather than a nudge, and that means it has to leave the frame.
   Clipping it with overflow puts a hard edge across the card's soft shadow,
   which is the thing Edward objected to. A mask does the same job with a
   gradient instead of a cut: the card thins out as it goes rather than being
   sliced, and it can never stray into the column next door. mask-size and
   no-repeat matter, because without them the gradient tiles and everything
   outside the box stays visible. */
.widget-view {
  position: relative;
  width: 100%;
  margin: 0 auto;
  touch-action: pan-y;
  cursor: grab;
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 7%, #000 93%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0%, #000 7%, #000 93%, transparent 100%);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  /* The mask hides a departed card but does not stop it counting towards the
     page's scrollable width, and a card parked a full width to the right would
     otherwise put a horizontal scrollbar on the document. Clipping at the same
     boundary costs nothing visually, because the mask has already taken those
     pixels to zero before the clip edge is reached. */
  overflow: hidden;
  overflow: clip;
}

.widget-view.is-dragging {
  cursor: grabbing;
}

/* The cards cross-fade in place rather than sliding through a window. A track
   that slides needs an overflow clip to hide the card coming next, and that
   clip is a hard vertical edge cutting through the card's soft shadow every
   time one moves. Nothing is clipped here, so there is no edge to see: each
   card fades and drifts a little, and the one arriving fades up underneath it.
   The ratio is the exports' own, which gives the stack its height since the
   cards are positioned out of flow. */
/* Narrower than the view it sits in, so the mask's soft edge falls on empty
   space either side of the resting card rather than eating into it. */
.widget-track {
  position: relative;
  width: 84%;
  margin: 0 auto;
  /* The card band of the export, once the heading and page dots are off it. */
  aspect-ratio: 2796 / 2906;
}

.widget-track img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  border-radius: 0;
  background: transparent;
  will-change: opacity, transform;
  /* iOS-ish: off the mark quickly, then a long settle. The script overrides
     this per card so the two halves of a swipe can be timed separately. */
  transition: opacity 0.40s ease, transform 0.62s cubic-bezier(0.32, 0.72, 0, 1);
  -webkit-user-drag: none;
  user-select: none;
  pointer-events: none;
}

/* Edward's own indicator rather than an invented one. Measured off the export:
   four dots 40px across on 112px centres, sitting 60px below the card, all in
   the file's 2796px space. Expressed against the column so the whole thing
   holds its proportions to the card at any width. The active dot does not grow,
   it only comes up to full strength while the others sit at half, which is
   what the exports do as the card changes. */
.widget-dots {
  display: flex;
  justify-content: center;
  gap: 2.163cqi;
  margin-top: 1.803cqi;
}

.widget-dot {
  width: 1.202cqi;
  height: 1.202cqi;
  padding: 0;
  border: 0;
  border-radius: 999px;
  /* The export's own tone is a light grey, which was right on a black screen
     and disappears on this one. The size, spacing and the half-to-full
     relationship are his; only the hue is pitched for a light ground. */
  background: rgba(95, 99, 104, 0.45);
  cursor: pointer;
  transition: background 0.3s ease;
}

.widget-dot.is-on {
  background: rgba(95, 99, 104, 1);
}

.widget-dot[aria-disabled='true'] {
  cursor: default;
}

.widget-dot:focus-visible {
  outline: 1px solid rgba(24, 35, 54, 0.7);
  outline-offset: 3px;
}

.widget-cap {
  max-width: 980px;
  margin: 0 auto 48px;
  padding: 0 24px;
  font-size: 12px;
  letter-spacing: 0.02em;
  color: #6C848E;
  text-align: center;
}

@media (max-width: 720px) {
  .widget-demo {
    grid-template-columns: minmax(0, 320px);
    gap: 46px;
    padding: 76px 24px 64px;
  }
}

/* ---------- FLOW STRIP ---------- */
/* The recreated screens, read left to right in the order a session happens.
   They are supporting evidence rather than the argument, so they sit in one
   scrolling line instead of taking a full screen height each. */
.flow-strip {
  display: flex;
  gap: 20px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  max-width: 1400px;
  margin: 40px auto 0;
  padding: 4px 24px 24px;
  cursor: grab;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: #D6D6D6 transparent;
}

.flow-strip.is-dragging {
  cursor: grabbing;
  scroll-snap-type: none;
}

.flow-strip::-webkit-scrollbar {
  height: 6px;
}

.flow-strip::-webkit-scrollbar-thumb {
  background: #D6D6D6;
  border-radius: 999px;
}

.flow-strip::-webkit-scrollbar-track {
  background: transparent;
}

.flow-item {
  flex: 0 0 auto;
  width: 246px;
  scroll-snap-align: center;
}

.flow-item img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: 20px;
  background: transparent;
  -webkit-user-drag: none;
}

.flow-cap {
  display: flex;
  gap: 10px;
  margin-top: 14px;
  font-size: 12px;
  line-height: 1.45;
  color: #6C848E;
}

.flow-num {
  flex: 0 0 auto;
  color: #000000;
}

.flow-hint {
  max-width: 1400px;
  margin: 0 auto 48px;
  padding: 0 24px;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #6C848E;
}

@media (max-width: 720px) {
  .flow-item {
    width: 200px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .widget-track img {
    transition: none;
  }
}
