/* ==========================================================================
   Rotating circle masks
   --------------------------------------------------------------------------
   The page scrolls, but nothing on screen moves with it. A fixed "stage"
   holds one panel per project. Each panel is a fixed circle (clip-path)
   containing a second, identical circle (the mask) that rotates around an
   off-screen pivot; the content counter-rotates around the same pivot, so
   it stays put while the mask sweeps across it. Content is visible only
   where both circles overlap.

   Scroll position -> --t (0..1) -> --p (0..count-1, current page)
   Panel i rotates by (i - p) * step:
     p < i  : positive angle, parked clockwise (below, rises in from the bottom)
     p = i  : 0, in place
     p > i  : negative angle, swept counter-clockwise (out to the upper left)

   The dot background turns the other way: clockwise, around its own
   centre, by --dots-turn per page.

   All geometry is in mockup pixels (450 x 800 design), scaled by --u.
   ========================================================================== */

@property --t {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

:root {
  /* Number of pages. The script keeps this in sync with the markup;
     update it here too so the CSS-only path is correct without JS. */
  --count: 4;

  /* Design canvas, in mockup px */
  --design-w: 450;
  --design-h: 800;

  /* Mask circle: center and radius (design px) */
  --cx: 283;
  --cy: 445;
  --r: 345;

  /* Rotation pivot (design px). Off screen, just past the bottom-left corner. */
  --px: -40;
  --py: 790;

  /* Gap between neighbouring circles at mid-transition (design px).
     0 = circles touch; larger = more dot background shows between them. */
  --gap: 16;

  /* Angle between neighbouring circles, derived so they sit --gap apart.
     Replace with a fixed angle (e.g. 96deg) to tune by eye. */
  --R: hypot(calc(var(--cx) - var(--px)), calc(var(--cy) - var(--py)));
  --step: calc(2 * asin(min(1, (var(--r) + var(--gap) / 2) / var(--R))));

  /* Dot background (images/bg-dots.svg): diameter and centre of rotation,
     in design px relative to the canvas. Wide screens override these below
     (centre on the bottom-left corner of the window). */
  --dots-size: 1730;
  --dots-x: -392;
  --dots-y: 832;
  /* Clockwise turn per page of scroll */
  --dots-turn: -5deg;

  /* Text overlay: centre of its close button (design px, outside the
     circle's upper left) and the duration of the sweep */
  --more-x: 70;
  --more-y: 129;
  --overlay-time: 0.8s;

  /* Full-size gallery: top of the blue band under the images (design y) */
  --gallery-band: 695;

  /* Overlay disc pivot: the page pivot mirrored across the circle's
     vertical centre line, so the disc sweeps in from the upper right
     (the mirror image of the page masks' exit to the upper left) */
  --opx: calc(2 * var(--cx) - var(--px));
  --opy: var(--py);

  /* One design px. Mobile fits width, desktop fits height. */
  --u: min(calc(100vw / var(--design-w)), calc(100svh / var(--design-h)));

  /* Current page position, fractional while scrolling */
  --p: calc(var(--t) * (var(--count) - 1));

  /* Colours */
  --ink: #1d3b73;
  --ink-soft: #3c5a88;
  --rule: #b9c3d3;
  --paper: #fff;
  --overlay: #224b8e;
  --gallery: #f2f2f2;
}

/* CSS-only path: scroll position drives --t directly.
   Browsers without scroll-driven animations get --t from rotate-mask.js. */
@supports (animation-timeline: scroll()) {
  :root {
    animation-name: page-progress;
    animation-duration: auto;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: scroll(root block);
  }
}

@keyframes page-progress {
  from { --t: 0; }
  to   { --t: 1; }
}

/* --------------------------------------------------------------------------
   Scroll track: empty full-height sections that give the page its scroll
   length and snap points. One per panel.
   -------------------------------------------------------------------------- */

html {
  scroll-snap-type: y mandatory;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  color: var(--ink);
}

.page {
  height: 100svh;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* The last page is exactly one viewport tall, so the total scroll range is
   (count - 1) page heights and every snap point lands on a whole --p,
   even while the mobile URL bar shows and hides. */
.page:last-child {
  height: 100dvh;
}

/* --------------------------------------------------------------------------
   Stage: fixed and full screen. Holds the background, the masthead and
   the panels.
   -------------------------------------------------------------------------- */

.stage {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background-color: var(--paper);
}

/* Dot background: a disc of Fibonacci-spiral dots that turns clockwise
   around its own centre as the page scrolls. Positioned like the canvas
   (centred horizontally, anchored to the top) so it lines up with the
   circle on phones. */
.stage::before {
  content: "";
  position: absolute;
  left: calc(50% + (var(--dots-x) - var(--design-w) / 2) * var(--u));
  top: calc(var(--dots-y) * var(--u));
  width: calc(var(--dots-size) * var(--u));
  aspect-ratio: 1;
  translate: -50% -50%;
  rotate: calc(var(--p) * var(--dots-turn));
  background: url("img/2026/bg-dots.svg") center / contain no-repeat;
  will-change: transform;
  pointer-events: none;
}

/* Wide screens: the centre sits on the window's bottom-left corner */
@media (min-aspect-ratio: 1/1) {
  :root {
    --dots-size: 1880;
    --dots-y: 800;
  }

  .stage::before {
    left: 0;
  }
}

/* The white band sits below the panels (circles sweep over it);
   the logo itself is raised above them. */
.masthead {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: calc(80 * var(--u));
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding-top: calc(18 * var(--u));
  box-sizing: border-box;
  /* background: var(--paper); */
}

/* Placeholder logo: replace with the supplied graphic */
.logo {
  position: relative;
  z-index: 100;
  display: block;
  padding: calc(2 * var(--u)) calc(6 * var(--u)) calc(4 * var(--u));
  /* border: calc(1.5 * var(--u)) solid var(--rule); */
  font-size: calc(32 * var(--u));
  font-weight: 300;
  line-height: 0.82;
  text-align: center;
  text-decoration: none;
  color: var(--ink);
}

/* Image logo (draft.html): about the text logo's height */
.logo:has(> img) {
  padding: 0;
}

.logo img {
  display: block;
  width: auto;
  height: calc(60 * var(--u));
}

/* The 450 x 800 design canvas, scaled and centred horizontally. Anchored to
   the top so it does not jump when the mobile URL bar collapses. */
.frame {
  position: absolute;
  top: 0;
  left: 50%;
  width: calc(var(--design-w) * var(--u));
  height: calc(var(--design-h) * var(--u));
  translate: -50% 0;
  /* Own stacking context, so the close buttons' z-index: -1 keeps them
     above the dot background even where translate is none (landscape) */
  isolation: isolate;
}

/* --------------------------------------------------------------------------
   Panel = a fixed circle in the home position (clip-path). It never moves;
   nothing of the panel shows outside it.
   -------------------------------------------------------------------------- */

.panel {
  /* Distance from this panel's page, clamped to one step either way */
  --d: clamp(-1, calc(var(--p) - var(--i)), 1);
  --abs-d: max(var(--d), -1 * var(--d));

  position: absolute;
  z-index: var(--i);
  left: calc((var(--cx) - var(--r)) * var(--u));
  top: calc((var(--cy) - var(--r)) * var(--u));
  width: calc(2 * var(--r) * var(--u));
  height: calc(2 * var(--r) * var(--u));
  clip-path: circle(50%);
  /* All panels share this circle; only the masks take clicks, so the
     topmost panel does not block the ones beneath it */
  pointer-events: none;
}

/* Mask = the same circle, rotating around the pivot. Content shows only
   where the mask and the panel's fixed circle overlap. */
.panel__mask {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: 50%;
  background: var(--paper);

  /* Pivot, relative to this element's box */
  transform-origin:
    calc((var(--px) - var(--cx) + var(--r)) * var(--u))
    calc((var(--py) - var(--cy) + var(--r)) * var(--u));
  rotate: calc(-1 * var(--d) * var(--step));

  /* Parked masks (a full step away) collapse to nothing, so they can never
     catch clicks, even if --gap is set below 0 and circles overlap. */
  scale: clamp(0, (1 - var(--abs-d)) * 1000, 1);
  will-change: transform;
  pointer-events: auto;
}

/* Edge line of the mask, drawn over the content so photos cannot cover
   it. It rotates with the mask, so the moving edge stays visible
   mid-transition. */
.panel__mask::after {
  content: "";
  position: absolute;
  z-index: 1;
  inset: 0;
  border: calc(1 * var(--u)) solid var(--rule);
  border-radius: 50%;
  pointer-events: none;
}

/* Content counter-rotates around the same pivot, so it never moves.
   Its box is the full design canvas: position content in mockup px. */
.panel__content {
  position: absolute;
  left: calc((var(--r) - var(--cx)) * var(--u));
  top: calc((var(--r) - var(--cy)) * var(--u));
  width: calc(var(--design-w) * var(--u));
  height: calc(var(--design-h) * var(--u));
  transform-origin: calc(var(--px) * var(--u)) calc(var(--py) * var(--u));
  rotate: calc(var(--d) * var(--step));
  /* 1em = 16 design px; size type in em so it scales with the layout */
  font-size: calc(16 * var(--u));
}

/* Edge line of the panel's fixed circle, drawn over the content so photos
   cannot cover it. It sits in the counter-rotated canvas, so it stays put
   and only shows where the mask overlaps the fixed circle. */
.panel__content::after {
  content: "";
  position: absolute;
  z-index: 3;
  left: calc((var(--cx) - var(--r)) * var(--u));
  top: calc((var(--cy) - var(--r)) * var(--u));
  width: calc(2 * var(--r) * var(--u));
  height: calc(2 * var(--r) * var(--u));
  box-sizing: border-box;
  border: calc(1 * var(--u)) solid var(--rule);
  border-radius: 50%;
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   Panel content layout (positions are design px / 16 = em)
   -------------------------------------------------------------------------- */

/* Text above the photo, one of three arrangements:
   - .panel__text: shared by every slide
   - .slide__text: changes with each image
   - .panel__heading (shared title) + .slide__caption (per image): the title
     stays put while only the caption changes
   - .panel__heading + .panel__caption: title and blurb both shared, in the
     same places as the arrangement above */
.panel__text,
.slide__text {
  position: absolute;
  left: 8.3125em;          /* 133px */
  bottom: 25.375em;        /* bottom edge at y = 394px */
  width: 18.75em;          /* 300px */
}

.panel__heading {
  position: absolute;
  left: 8.3125em;
  bottom: 37.125em;        /* bottom edge at y = 206px; grows upward */
  width: 18.75em;
}

.panel__heading .panel__title {
  margin: 0;
}

.panel__note {
  margin: 0.4em 0 0;
  font-size: 0.875em;
  font-weight: 300;
  color: var(--ink-soft);
}

.panel__note a {
  color: inherit;
}

/* "Opens in a new window" arrow: an SVG mask over currentColor, so it is
   the same in every browser (the ↗ character is not) */
.icon-external {
  display: inline-block;
  width: 0.75em;
  height: 0.75em;
  /* A word space; no actual space, so it never wraps away from the text */
  margin-left: 0.25em;
  vertical-align: -0.05em;
  background: currentColor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 18 18 6M8 6h10v10'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* Screen-reader-only text */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.panel__caption,
.slide__caption {
  position: absolute;
  left: 8.3125em;
  top: 13.625em;           /* y = 218px; grows downward */
  width: 18.75em;
}

.panel__title {
  margin: 0 0 0.5em;
  font-size: 1.75em;       /* 28px */
  font-weight: 300;
  line-height: 1.05;
  color: var(--ink);
}

.panel__body {
  margin: 0;
  font-size: 1.03em;       /* 16.5px */
  font-weight: 300;
  line-height: 1.35;
  color: var(--ink-soft);
}

.panel__services {
  position: absolute;
  /* In this element's own 14px ems */
  left: 1.4286em;          /* 20px */
  bottom: 29em;            /* bottom edge at y = 394px; grows upward */
  /* The column left of the caption (x 20-128); longer labels wrap, with
     their lines balanced */
  width: 7.7143em;         /* 108px */
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.875em;      /* 14px */
  font-weight: 600;
  line-height: 1.1;
  color: var(--ink);
  text-wrap: balance;
}

/* Wrapped labels read as one item: a little space between items */
.panel__services li + li {
  margin-top: 0.25em;
}

/* --------------------------------------------------------------------------
   Text-only circles: the intro (first) and contact (last). No images, just
   a .panel__lead block centred on the circle, over the dot pattern sized
   to the circle (the overlay disc's, shared above with it). The intro is
   white with the dots at full strength, like the page background; the
   contact circle matches the overlay disc.
   -------------------------------------------------------------------------- */

/* .panel doubled: outranks the shared dot rule, which comes later */
.panel.panel--intro .panel__content::before {
  opacity: 1;
}

.panel--contact .panel__mask {
  background: var(--overlay);
}

/* Same columns as the overlay text (x 29-421), vertically centred on the
   circle (y = 445) */
.panel__lead {
  position: absolute;
  left: 1.8125em;          /* 29px */
  top: 27.8125em;          /* 445px */
  width: 24.5em;           /* 392px */
  translate: 0 -50%;
}

.panel__lead .panel__body {
  /* 19.2 design px, but never below 16 or above 22 screen px */
  font-size: clamp(1rem, 1.2em, 1.375rem);
  line-height: 1.4;
}

/* The intro's opening sentence, set larger above the rest */
.panel__intro {
  margin: 0 0 0.6em;
  /* 26 design px, but never below 21 or above 30 screen px */
  font-size: clamp(1.3125rem, 1.625em, 1.875rem);
  font-weight: 300;
  line-height: 1.2;
  color: var(--ink);
  text-wrap: balance;
}

.panel--contact .panel__lead .panel__body {
  margin: 0 0 1em;
  font-size: clamp(1.125rem, 1.4em, 1.5rem);
  color: var(--paper);
}

.panel--contact .panel__lead a {
  color: inherit;
  text-underline-offset: 0.15em;
}

.panel--contact .panel__lead a:focus-visible {
  outline: 0.1em solid var(--paper);
  outline-offset: 0.15em;
}

/* Wide screens show the whole circle: centre the block in it (x 87-479) */
@media (min-aspect-ratio: 1/1) {
  .panel__lead {
    left: 5.4375em;
  }
}

/* --------------------------------------------------------------------------
   Carousel: one .slide per image (up to 12). Each slide's radio button
   is its dot, so tapping a dot or using the arrow keys switches slides
   without script. Slides use display: contents, which lines every slide's
   radio up in the carousel's single flex row, right-aligned.
   -------------------------------------------------------------------------- */

.carousel {
  position: absolute;
  inset: 0;                /* the full design canvas */
  display: flex;
  justify-content: flex-end;
  align-items: flex-start;
  /* Dot size: the hit area (box) and the visible circle (size). Smaller
     when there are many slides, so up to 12 dots fit across the circle. */
  --dot-box: 2.75em;       /* 44px */
  --dot-size: 2em;         /* 32px */
  gap: 0.0625em;
  /* Dot row centred on y = 445px, last dot's right edge at x = 425px */
  padding:
    calc(27.8125em - var(--dot-box) / 2)
    calc(1.5625em - (var(--dot-box) - var(--dot-size)) / 2)
    0 0;
  /* It covers the canvas; let clicks through to anything beneath */
  pointer-events: none;
}

.carousel:has(> .slide:nth-child(9)) {
  --dot-box: 2.5em;        /* 9-10 slides: 40px box, 28px circle */
  --dot-size: 1.75em;
}

.carousel:has(> .slide:nth-child(11)) {
  --dot-box: 2.0625em;     /* 11-12 slides: 33px box, 24px circle */
  --dot-size: 1.5em;
}

.slide {
  display: contents;
}

.slide__dot,
.slide__text,
.slide__caption,
.slide__link,
.carousel__expand {
  pointer-events: auto;
}

.slide__dot,
.carousel__expand {
  --dot-fill: var(--paper);
  --dot-ring: #9aa3b0;
  position: relative;
  z-index: 2;
  flex: none;
  appearance: none;
  box-sizing: border-box;
  width: var(--dot-box);
  height: var(--dot-box);
  margin: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  /* Inputs default to a fixed 13.33px font; the em sizes need the
     carousel's 16 design px */
  font-size: inherit;
  cursor: pointer;
}

/* The visible dot, smaller than the input: the rest of the box is extra
   hit area. The ring is a real border, which browsers snap to whole device
   pixels, so it stays crisp at any scale. */
.slide__dot::before,
.carousel__expand::before {
  content: "";
  position: absolute;
  inset: calc((var(--dot-box) - var(--dot-size)) / 2);
  box-sizing: border-box;
  /* At least 2 screen px: 2 design px alone render as 1px on phones,
     where the design is scaled down and borders round down */
  border: max(2px, 0.125em) solid var(--dot-ring);
  border-radius: 50%;
  background: var(--dot-fill);
}

.slide__dot:hover,
.carousel__expand:hover {
  --dot-ring: var(--ink);
}

.slide__dot:checked {
  --dot-fill: var(--ink);
  --dot-ring: var(--paper);
}

.slide__dot:focus-visible,
.carousel__expand:focus-visible {
  outline: 0.125em solid var(--ink);
  outline-offset: calc(0.125em - (var(--dot-box) - var(--dot-size)) / 2);
}

/* Expand dot: the last dot in the row (rotate-mask.js adds it), the same
   size as the others. It opens the project's full-size gallery. The icon
   (two diagonal arrows) is a mask over currentColor. */
.carousel__expand {
  --dot-fill: var(--gallery);
  padding: 0;
  color: var(--ink);
}

.carousel__expand::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--dot-size) * 0.5);
  aspect-ratio: 1;
  translate: -50% -50%;
  background: currentColor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 3h7v7M21 3l-7 7M10 21H3v-7M3 21l7-7'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* Link button on a slide (e.g. "View Interactive"): a small pill centred
   near the bottom of the circle, over the image. It sits in its .slide, so
   it fades with that slide's image. */
.slide__link {
  position: absolute;
  z-index: 2;
  left: calc(var(--cx) * var(--u));
  top: calc(750 * var(--u));     /* centre; bottom edge near y = 765 */
  translate: -50% -50%;
  padding: 0.55em 1.1em;
  border: max(1px, 0.09em) solid var(--paper);
  border-radius: 2em;
  background: var(--ink);
  color: var(--paper);
  font-size: 0.8125em;           /* 13px */
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.02em;
  text-decoration: none;
  white-space: nowrap;
}

/* Taller hit area than the pill */
.slide__link::before {
  content: "";
  position: absolute;
  inset: -0.5em -0.25em;
}

.slide__link:hover {
  background: var(--overlay);
}

.slide__link:focus-visible {
  outline: 0.15em solid var(--ink);
  outline-offset: 0.15em;
}

.slide__media {
  position: absolute;
  /* Wider than the canvas: the circle extends past both edges, and on
     wide screens more of it is visible on the right */
  left: -5em;              /* -80px */
  width: 45.625em;         /* 730px */
  top: 27.8125em;          /* y = 445px */
  height: 23.125em;        /* 370px, past the bottom of the circle */
  object-fit: cover;
  object-position: 30% 50%;
  box-shadow: 0 -0.25em 0.6em rgb(0 0 0 / 0.15);
  background: var(--paper);
}

/* Whole image, no cropping: for layouts, logos and artwork. The element
   still fills the same band (opaque white), so crossfades between any two
   slides stay clean; padding shrinks the drawing area to the largest
   3:2 rectangle that fits inside the visible part of the circle. */
.slide__media--contain {
  box-sizing: border-box;
  padding: 0.625em 15.40625em 7.625em 7.90625em;  /* image at x 46-404, y 455-693 */
  /* background: var(--paper); */
  object-fit: contain;
  object-position: 50% 50%;
  box-shadow: none;
}

/* Wide screens show the whole circle: centre a larger rectangle in it */
@media (min-aspect-ratio: 1/1) {
  .slide__media--contain {
    padding: 0.625em 10.375em 5.75em 10.125em;    /* image at x 82-484, y 455-723 */
  }
}

/* Narrow screens clip the circle's left side (window narrower than 574
   design px: the circle's left edge at x = -62, mirrored about the
   centred canvas). The band is centred on the circle, right of the screen
   centre, so move edge-to-edge photos 40px (about 9% of the screen) to
   the left. Whole images are already centred on the screen. */
@media (max-aspect-ratio: 287/400) {
  .slide__media:not(.slide__media--contain) {
    left: -7.5em;          /* -120px */
  }
}

/* Slide change: the new image fades in on top of the old one (no dip to
   white); old text fades out, then the new text fades in. Inactive slides
   are visibility: hidden, so screen readers and the tab order only see the
   current slide's content. */
.slide__media,
.slide__text,
.slide__caption,
.slide__link {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0s linear 0.45s, visibility 0s linear 0.45s;
}

.slide__text,
.slide__caption,
.slide__link {
  transition-duration: 0.2s, 0s;
  transition-delay: 0s, 0.2s;
}

.slide:has(> .slide__dot:checked) > .slide__media,
.slide:has(> .slide__dot:checked) > .slide__text,
.slide:has(> .slide__dot:checked) > .slide__caption {
  z-index: 1;
  opacity: 1;
  visibility: visible;
  transition: opacity 0.45s ease;
}

/* The link stays above the current image (z-index 2) */
.slide:has(> .slide__dot:checked) > .slide__link {
  opacity: 1;
  visibility: visible;
}

.slide:has(> .slide__dot:checked) > .slide__text,
.slide:has(> .slide__dot:checked) > .slide__caption,
.slide:has(> .slide__dot:checked) > .slide__link {
  transition: opacity 0.3s ease 0.2s;
}

/* --------------------------------------------------------------------------
   Text overlay: a blue disc that covers the whole circle, for more text
   than fits beside the images. It lives inside .panel__content, so it is
   part of the project: the page masks sweep it away and back with
   everything else, and it stays open while other projects are in view.

   Opening sweeps it in from the upper right: a disc parked one step
   clockwise around the mirrored pivot (--opx, --opy), outside the mask,
   rotates counter-clockwise into place. Closing reverses it. Its text
   counter-rotates, so, as with the page masks, only the edge moves.
   rotate-mask.js toggles .is-open on the panel, and adds .has-toggled the
   first time: the sweep transitions only exist from then on, so nothing
   can animate while the page loads.
   The full-size gallery (.panel__gallery, toggled by .is-expanded) is a
   second disc that sweeps exactly the same way; its rules follow.
   -------------------------------------------------------------------------- */

.panel__overlay,
.panel__gallery {
  position: absolute;
  z-index: 3;
  left: calc((var(--cx) - var(--r)) * var(--u));
  top: calc((var(--cy) - var(--r)) * var(--u));
  width: calc(2 * var(--r) * var(--u));
  height: calc(2 * var(--r) * var(--u));
  overflow: hidden;
  border-radius: 50%;
  background: var(--overlay);
  transform-origin:
    calc((var(--opx) - var(--cx) + var(--r)) * var(--u))
    calc((var(--opy) - var(--cy) + var(--r)) * var(--u));
  rotate: var(--step);
  /* Hidden once parked, so closed text is not read out or focusable */
  visibility: hidden;
}

.panel.has-toggled :is(.panel__overlay, .panel__gallery) {
  transition:
    rotate var(--overlay-time) ease-in-out,
    visibility 0s linear var(--overlay-time);
}

/* Counter-rotating layer: the full design canvas, like .panel__content */
.panel__overlay-body,
.panel__gallery-body {
  position: absolute;
  left: calc((var(--r) - var(--cx)) * var(--u));
  top: calc((var(--r) - var(--cy)) * var(--u));
  width: calc(var(--design-w) * var(--u));
  height: calc(var(--design-h) * var(--u));
  transform-origin: calc(var(--opx) * var(--u)) calc(var(--opy) * var(--u));
  rotate: calc(-1 * var(--step));
  color: var(--paper);
}

.panel.has-toggled :is(.panel__overlay-body, .panel__gallery-body) {
  transition: rotate var(--overlay-time) ease-in-out;
}

.panel.is-open .panel__overlay,
.panel.is-expanded .panel__gallery {
  rotate: 0deg;
  visibility: visible;
}

.panel.has-toggled.is-open .panel__overlay,
.panel.has-toggled.is-expanded .panel__gallery {
  transition: rotate var(--overlay-time) ease-in-out, visibility 0s;
}

.panel.is-open .panel__overlay-body,
.panel.is-expanded .panel__gallery-body {
  rotate: 0deg;
}

/* The dot pattern, sized to the circle, at 5%. The text-only circles
   share it (the intro overrides the opacity with its other rules). */
.panel__overlay-body::before,
.panel--intro .panel__content::before,
.panel--contact .panel__content::before {
  content: "";
  position: absolute;
  left: calc((var(--cx) - var(--r)) * var(--u));
  top: calc((var(--cy) - var(--r)) * var(--u));
  width: calc(2 * var(--r) * var(--u));
  height: calc(2 * var(--r) * var(--u));
  background: url("img/2026/bg-dots.svg") center / contain no-repeat;
  opacity: 0.05;
}

/* Text block: x 29-421, from y = 258 down to y = 670 at most (its corners
   stay inside the circle); longer text scrolls within it */
.panel__overlay-text {
  position: absolute;
  left: 1.8125em;          /* 29px */
  top: 16.125em;           /* 258px */
  width: 24.5em;           /* 392px */
  max-height: 25.75em;     /* 412px */
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-color: rgb(255 255 255 / 0.4) transparent;
}

.panel__overlay-text p {
  margin: 0 0 1.38em;
  /* 14.4 design px, but never below 15 or above 18 screen px */
  font-size: clamp(0.9375rem, 0.9em, 1.125rem);
  font-weight: 300;
  line-height: 1.38;
}

.panel__overlay-text p:last-child {
  margin-bottom: 0;
}

/* Subheadings (the intro's list of services) */
.panel__overlay-text h3 {
  margin: 0 0 0.3em;
  font-size: clamp(1rem, 1em, 1.25rem);
  font-weight: 600;
  line-height: 1.2;
}

/* Wide screens show the whole circle: centre the block in it (x 87-479) */
@media (min-aspect-ratio: 1/1) {
  .panel__overlay-text {
    left: 5.4375em;
  }
}

/* Full-size gallery: rotate-mask.js copies the project's carousel into
   the disc (.carousel--full, its radios renamed), so the dots and the
   crossfade work exactly as in the small one. Each image covers the
   visible part of the circle down to a blue band (from y = 695, the
   --gallery-band line) that holds the dots (y = 721) and any slide link
   (y = 762), both centred on the circle. */
.panel__gallery {
  background: var(--gallery);
}

/* The band. It is in the counter-rotating canvas, so it sweeps in with
   the disc, which clips it to the circle. z-index 2 puts it over the
   images (1); the dots and link (also 2) come later, so they stay on top. */
.panel__gallery-body::before {
  content: "";
  position: absolute;
  z-index: 2;
  left: calc((var(--cx) - var(--r)) * var(--u));
  width: calc(2 * var(--r) * var(--u));
  top: calc(var(--gallery-band) * var(--u));
  height: 100dvh;          /* past the circle's bottom, and the screen's */
  background: var(--overlay);
}

.carousel--full {
  justify-content: center;
  /* Dot row centred on y = 721; padding-left moves the row's centre from
     x = 225 to the circle's centre, x = 283 */
  padding: calc(45.0625em - var(--dot-box) / 2) 0 0 7.25em;
}

/* The image spans the circle's full width, so the crop stays centred on
   the circle even where the screen clips its sides (phones; unlike the
   main view's photos, it is not moved left) */
.carousel--full .slide__media {
  --top: max(0px, calc((var(--cy) - var(--r)) * var(--u)));
  box-sizing: border-box;
  left: calc((var(--cx) - var(--r)) * var(--u));
  width: calc(2 * var(--r) * var(--u));
  top: var(--top);
  height: calc(var(--gallery-band) * var(--u) - var(--top));
  padding: 0;
  object-fit: cover;
  object-position: 50% 50%;
  box-shadow: none;
  background: var(--gallery);
}

.carousel--full .slide__link {
  left: calc(var(--cx) * var(--u));
  top: calc(762 * var(--u));
}

/* Ink focus rings would vanish against the band */
.carousel--full :is(.slide__dot, .slide__link):focus-visible {
  outline-color: var(--paper);
}

/* Round icon buttons: the open "+" (.panel__open) and the close "x"
   (.panel__more). ::before draws the ring (a real border, so it is crisp)
   and the cross; the button's own box is the hit area. */
.panel__open,
.panel__more {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--ink);
  cursor: pointer;
}

.panel__open::before,
.panel__more::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--ring);
  height: var(--ring);
  translate: -50% -50%;
  box-sizing: border-box;
  border: var(--stroke) solid currentColor;
  border-radius: 50%;
  /* The cross: two bars, --bar long */
  background:
    linear-gradient(currentColor, currentColor) center / var(--bar) var(--stroke) no-repeat,
    linear-gradient(currentColor, currentColor) center / var(--stroke) var(--bar) no-repeat,
    var(--paper);
}

.panel__open:focus-visible,
.panel__more:focus-visible {
  outline: 0.125em solid var(--ink);
  outline-offset: calc(0.125em - (var(--hit) - var(--ring)) / 2);
}

/* Open button: inline after the last word of each caption (rotate-mask.js
   adds it), sized to the text. The hit area reaches past the ring, over
   the surrounding text. */
.panel__open {
  --ring: 1em;
  --hit: 2.2em;
  --stroke: 0.0625em;
  --bar: 0.55em;
  position: relative;
  width: var(--hit);
  height: var(--hit);
  /* Lay out at ring size, so the line height is unaffected */
  margin: calc((var(--ring) - var(--hit)) / 2);
  margin-left: calc(0.3em + (var(--ring) - var(--hit)) / 2);
  vertical-align: middle;
  font-size: inherit;
  color: inherit;
}

.panel__open:hover {
  color: var(--ink);
}

/* Keeps the last word and the button on one line */
.panel__open-wrap {
  white-space: nowrap;
}

/* Close button. It sits outside the circle, so it cannot be inside the
   clipped panel: it is a sibling of its panel, carries the same --i, and
   turns around the pivot exactly like that panel's mask, so it rides in
   and out with its circle. It sits beneath every panel, so while rotating
   in it passes behind the outgoing circle.
   It is only there while the overlay is open: it sweeps in from the left
   (parked one step counter-clockwise around the page pivot) as the disc
   opens, and back out as it closes. That sweep uses transform, which adds
   to the page rotation (rotate) around the same pivot, so only it is
   transitioned. */
.panel__more {
  --d: clamp(-1, calc(var(--p) - var(--i)), 1);
  --abs-d: max(var(--d), -1 * var(--d));
  --box: 44;               /* hit area, design px; the ring is 40 */
  --hit: calc(var(--box) * 1em / 16);
  --ring: 2.5em;
  --stroke: 0.09375em;     /* 1.5px */
  --bar: 1.375em;          /* 22px */

  position: absolute;
  z-index: -1;
  left: calc((var(--more-x) - var(--box) / 2) * var(--u));
  top: calc((var(--more-y) - var(--box) / 2) * var(--u));
  width: calc(var(--box) * var(--u));
  height: calc(var(--box) * var(--u));
  font-size: calc(16 * var(--u));
  transform-origin:
    calc((var(--px) - var(--more-x) + var(--box) / 2) * var(--u))
    calc((var(--py) - var(--more-y) + var(--box) / 2) * var(--u));
  rotate: calc(-1 * var(--d) * var(--step));
  /* Parked buttons collapse, so only the current project's takes clicks */
  scale: clamp(0, (1 - var(--abs-d)) * 1000, 1);
  transform: rotate(calc(-1 * var(--step)));
  visibility: hidden;
  will-change: transform;
}

.panel.has-toggled + .panel__more {
  transition:
    transform var(--overlay-time) ease-in-out,
    visibility 0s linear var(--overlay-time);
}

.panel__more::before {
  rotate: 45deg;
}

.panel:is(.is-open, .is-expanded) + .panel__more {
  transform: rotate(0deg);
  visibility: visible;
}

.panel.has-toggled:is(.is-open, .is-expanded) + .panel__more {
  transition: transform var(--overlay-time) ease-in-out, visibility 0s;
}

/* --------------------------------------------------------------------------
   Phones held sideways: text on the left, image on the right, dots stacked
   on the divider between them. The circle is sized from the screen height
   and runs off the top and bottom of the screen.

   Design canvas: 690 x 406, the circle's width by the visible height. The
   circle (r = 345, unchanged) is centred in it, so everything inside the
   circle has fixed positions on every phone; only the circle's horizontal
   position follows the screen width. Sized from 100svh (Safari's toolbar
   showing) and anchored to the top, so nothing resizes or jumps when the
   toolbar hides; the extra height then shows more of the circle below.
   (Positions are only set for the .panel__heading + .panel__caption /
   .slide__caption arrangements used in the drafts.)
   -------------------------------------------------------------------------- */

@media (orientation: landscape) and (max-height: 500px) {
  :root {
    /* Circle diameter as a multiple of the screen height, and the circle
       centre's distance from the left edge, as a share of the width */
    --circle-h: 1.7;
    --circle-x: 55%;

    --design-w: 690;
    --design-h: 406;       /* 690 / 1.7: the visible height */

    --cx: 345;
    --cy: 203;

    /* Pivot: same offset from the circle centre as in portrait, so the
       masks sweep the same way (in from the lower right, out to the upper
       left) */
    --px: 22;
    --py: 548;

    --u: calc(var(--circle-h) * 100svh / (2 * var(--r)));

    /* Dot background: centre on the bottom-left corner, 2.35 x the screen
       height, as on wide screens */
    --dots-size: 955;
    --dots-y: 406;

    /* Overlay button: outside the circle's lower left, clear of the logo */
    --more-x: 10;
    --more-y: 374;

    --gallery-band: 315;
  }

  /* Logo in the top-left corner, clear of the notch */
  .masthead {
    justify-content: flex-start;
    height: auto;
    padding: calc(14 * var(--u)) 0 0 max(calc(20 * var(--u)), env(safe-area-inset-left));
  }

  .logo img {
    height: calc(44 * var(--u));
  }

  .frame {
    left: calc(var(--circle-x) - var(--cx) * var(--u));
    translate: none;
  }

  /* Text column: x 66-264 (the circle's edge at its top and bottom, to
     just left of the divider at x = 293) */
  .panel__heading {
    left: 4.125em;         /* 66px */
    bottom: 19.375em;      /* bottom edge at y = 96px; grows upward */
    width: 12.375em;       /* 198px */
  }

  .panel__caption,
  .slide__caption {
    left: 4.125em;
    top: 6.75em;           /* y = 108px; grows downward */
    width: 12.375em;
  }

  /* No room beside the text */
  .panel__services {
    display: none;
  }

  /* Text-only circles: the overlay text's columns (x 100-590), centred on
     the visible height (y = 203) */
  .panel__lead {
    left: 6.25em;          /* 100px */
    top: 12.6875em;        /* 203px */
    width: 30.625em;       /* 490px */
  }

  /* Dots: a column centred on the divider (x = 293) and on the circle */
  .carousel {
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
    padding: 0 0 0 calc(18.3125em - var(--dot-box) / 2);
  }

  /* Image: from the divider to the circle's right edge, full screen height
     (dvh, so it reaches the bottom when the toolbar hides) */
  .slide__media {
    left: 18.3125em;       /* x = 293px */
    width: 24.8125em;      /* 397px, to the circle's right edge */
    top: 0;
    height: 100dvh;
    object-position: 50% 50%;
    box-shadow: -0.25em 0 0.6em rgb(0 0 0 / 0.15);
  }

  /* Largest 3:2 rectangle right of the divider: x 324-654, y 93-313 */
  .slide__media--contain {
    padding: 5.8125em 2.25em calc(100dvh - 19.5625em) 1.9375em;
    box-shadow: none;
  }

  /* Overlay text: x 100-590, y 40-370 */
  .panel__overlay-text {
    left: 6.25em;          /* 100px */
    top: 2.5em;            /* 40px */
    width: 30.625em;       /* 490px */
    max-height: 20.625em;  /* 330px */
  }

  /* Slide link: centred under the image half (x 293-654) */
  .slide__link {
    left: calc(474 * var(--u));
    top: calc(370 * var(--u));
  }

  /* Gallery: the image covers the circle's full width (the canvas) from
     the screen top down to the band at y = 315; in the band, the dot row
     at y = 341 and the link at y = 377 */
  .carousel--full {
    flex-direction: row;
    justify-content: center;
    align-items: flex-start;
    padding: calc(21.3125em - var(--dot-box) / 2) 0 0;
  }

  .carousel--full .slide__link {
    left: calc(var(--cx) * var(--u));
    top: calc(377 * var(--u));
  }
}

/* --------------------------------------------------------------------------
   Reduced motion: crossfade in place instead of sweeping.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .stage::before,
  .panel__mask,
  .panel__content,
  .panel__more {
    rotate: none;
  }

  .panel,
  .panel__more {
    opacity: clamp(0, 1 - var(--abs-d), 1);
  }

  /* Overlay and gallery fade instead of sweeping */
  .panel__overlay,
  .panel__gallery,
  .panel.is-open .panel__overlay,
  .panel.is-expanded .panel__gallery {
    rotate: none;
    opacity: 0;
  }

  .panel.is-open .panel__overlay,
  .panel.is-expanded .panel__gallery {
    opacity: 1;
  }

  .panel.has-toggled :is(.panel__overlay, .panel__gallery) {
    transition: opacity 0.3s ease, visibility 0s linear 0.3s;
  }

  .panel.has-toggled.is-open .panel__overlay,
  .panel.has-toggled.is-expanded .panel__gallery {
    transition: opacity 0.3s ease, visibility 0s;
  }

  .panel__overlay-body,
  .panel__gallery-body,
  .panel.is-open .panel__overlay-body,
  .panel.is-expanded .panel__gallery-body {
    rotate: none;
  }

  /* Close button fades with the disc instead of sweeping (filter, so it
     does not fight the page fade on opacity) */
  .panel__more,
  .panel:is(.is-open, .is-expanded) + .panel__more {
    transform: none;
    filter: opacity(0);
  }

  .panel:is(.is-open, .is-expanded) + .panel__more {
    filter: opacity(1);
  }

  .panel.has-toggled + .panel__more {
    transition: filter 0.3s ease, visibility 0s linear 0.3s;
  }

  .panel.has-toggled:is(.is-open, .is-expanded) + .panel__more {
    transition: filter 0.3s ease, visibility 0s;
  }

  .slide__media,
  .slide__text,
  .slide__caption,
  .slide__link,
  .slide:has(> .slide__dot:checked) > .slide__media,
  .slide:has(> .slide__dot:checked) > .slide__text,
  .slide:has(> .slide__dot:checked) > .slide__caption,
  .slide:has(> .slide__dot:checked) > .slide__link {
    transition: none;
  }
}
