:root {
  --ground: #f3f2ef;
  --ink: #16150f;
  --ink-soft: #6f6d64;
  --rule: #d8d6cf;

  --field-width: 62%;
  --gutter: 3.5rem;

  /* Swap to #14130f for a dark field behind the images. */
  --field-ground: #ffffff;
  --column-count: 3;
  --column-gap: 1.25rem;

  --type-ui: "Helvetica Neue", Helvetica, Arial, sans-serif;

  --type-heavy: "Archivo Black", "Arial Black", Impact, sans-serif;

  /* Archivo the text family, a variable font covering 300 to 600. Separate
     from Archivo Black, which is its own family with a single weight. */
  --type-text: "Archivo", "Helvetica Neue", Helvetica, Arial, sans-serif;

  /* The name band takes the bottom fifth of the field. Type is sized off the
     band so it always fits the three rows. */
  /* The band sits in the panel, so its type is sized off the panel's own
     width rather than the window height it used to span. */
  /*
    Proportional to the panel it sits in, with a second term that pulls it
    down hard on a short window, where the panel has no room to spare. Both
    terms are equal at a 1131px window, so the reference layout is unchanged
    and only shorter windows see the band give way.
  */
  --band-type: min(6.4cqi, max(0.875rem, calc(6.5vh - 35px)));
  --band-rule: rgba(22, 21, 15, 0.14);

  --pink: #ff2dab;

  /* Both foot icons, the house and the info button, so they stay the same
     size as each other. */
  --foot-icon: 1.1rem;

  /* Bullets are black. Set to var(--pink) to match the reference exactly. */
  --dot-colour: var(--ink);

  /* Kept proportional to the reference, which strokes 1.5px at 82px type. */
  --outline-width: 0.75px;
}

@font-face {
  font-family: "Archivo Black";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/archivo-black-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2122, U+2212, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Archivo Black";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/archivo-black-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+1E00-1E9F, U+2020, U+20A0-20AB, U+2113,
    U+2C60-2C7F, U+A720-A7FF;
}

/* Archivo variable, 300 to 600. One file covers Light through SemiBold, so
   font-weight can be set freely anywhere in that range. */
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url("fonts/archivo-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2122, U+2212, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url("fonts/archivo-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+1E00-1E9F, U+2020, U+20A0-20AB, U+2113,
    U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Archivo";
  font-style: italic;
  font-weight: 300 600;
  font-display: swap;
  src: url("fonts/archivo-italic-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2122, U+2212, U+FEFF, U+FFFD;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  overflow: hidden;
}

body {
  background: var(--ground);
  color: var(--ink);
  font-family: var(--type-ui);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

.layout {
  display: grid;
  grid-template-columns: var(--field-width) 1fr;
  height: 100%;
}

/* ---------- Left: image columns above the client name band ---------- */

.field {
  display: grid;
  /* One row: the name band lives in the info panel now, so the work fills
     the whole left side. */
  grid-template-rows: 1fr;
  overflow: hidden;
  background: var(--field-ground);
}

.field__work {
  position: relative;
  overflow: hidden;

  /* Centres the retouching page's single image. The homepage columns sit
     inside this too, but they are positioned absolutely and ignore it. */
  display: grid;
  place-items: center;
}

.columns {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(var(--column-count), 1fr);
  gap: var(--column-gap);
  padding: 0 var(--column-gap);
}

.column {
  position: relative;
  overflow: hidden;
  contain: paint;
}

/*
  The column animation is created in JavaScript via the Web Animations API, not
  here, so it can be paused on hover and scrubbed by dragging. It is still a
  compositor transform animation, same as a CSS one.

  No will-change on purpose. These tracks are over 7000px tall, and will-change
  forces the whole thing to stay promoted and eagerly rasterised, which showed
  up as an occasional stutter when the compositor re-rasterised tiles.
*/
.column__track { display: block; }

@media (hover: hover) {
  .column { cursor: grab; }
  .column.is-dragging { cursor: grabbing; }
}

.column__track img {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: var(--column-gap);
  background: #f7f6f4;

  /* Otherwise dragging an image starts a native file drag with a ghost image. */
  -webkit-user-drag: none;
  user-select: none;
}

/* ---------- Retouching page: horizontal row grid ----------
   Same idea as .columns, rotated 90 degrees: fixed row height instead of
   fixed column width, images sized by height with width auto instead of the
   other way round. */

/* ---------- Retouching page: one image at a time ----------

   Both images sit in the same grid cell so the outgoing one is still there to
   fade out from. The inset is padding rather than a smaller image, so a
   portrait and a landscape frame both sit on the same centre. */

/*
  Sized in JavaScript to the picture actually on screen, not to the field, so
  hovering and clicking only count over the image itself. The margin around it
  is dead space, which is the point.
*/
.slide {
  position: relative;
  padding: 0;
  appearance: none;
  border: 0;
  background: none;
  cursor: pointer;

  /* Horizontal swipe (prev/next) is handled in JS; vertical touch keeps
     scrolling the page natively. */
  touch-action: pan-y;
}

/* Homepage only: #slide sits alongside #columns, hidden until the narrow
   layout replaces the columns with the slideshow. Retouching.html's bare
   .slide has no preceding #columns sibling, so this does not touch it. */
#columns + .slide { display: none; }

.slide__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity 500ms linear;

  /* Otherwise dragging the image starts a native file drag. */
  -webkit-user-drag: none;
  user-select: none;
}

.slide__img.is-on { opacity: 1; }

.slide:focus-visible {
  outline: 2px solid var(--pink);
  outline-offset: -6px;
}

.rows {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: repeat(4, 1fr);
  gap: var(--column-gap);
  padding: var(--column-gap) 0;
}

.row {
  position: relative;
  overflow: hidden;
  contain: paint;
}

/* Animation is created in JavaScript via the Web Animations API, same as the
   columns and for the same reason: it needs to be paused on hover and
   scrubbed by dragging or the wheel. No will-change, same reasoning as the
   columns: these tracks are thousands of pixels wide. */
.row__track {
  display: flex;
  height: 100%;
  align-items: stretch;
}

@media (hover: hover) {
  .row { cursor: grab; }
  .row.is-dragging { cursor: grabbing; }
}

.row__track img {
  display: block;
  height: 100%;
  width: auto;
  margin-right: var(--column-gap);
  background: #f7f6f4;

  /* Otherwise dragging an image starts a native file drag with a ghost image. */
  -webkit-user-drag: none;
  user-select: none;
}

/* ---------- Client name band ---------- */

.marquee {
  display: grid;
  grid-template-rows: repeat(var(--band-rows, 3), auto);
  flex: none;
  overflow: hidden;
  border-top: 1px solid var(--band-rule);
  user-select: none;

  /* Full bleed across the panel: the names should run off both edges rather
     than stop at the text margin. */
  margin: 0 calc(var(--gutter) * -1);
}

.marquee__row {
  display: flex;
  align-items: center;
  overflow: hidden;
  contain: paint;
  padding: calc(var(--band-type) * 0.14) 0;
  border-top: 1px solid var(--band-rule);
}
.marquee__row:first-child { border-top: 0; }

@media (hover: hover) {
  .marquee__row { cursor: grab; }
  .marquee__row.is-dragging { cursor: grabbing; }
}

/* Same reasoning as the columns: these tracks are 7700px wide. */
.marquee__track {
  display: flex;
  align-items: center;
  width: max-content;
  /* Duration is set from JavaScript, from the measured width and a speed in
     pixels per second. The fallback only covers the moment before that runs. */
  animation: marquee-x var(--duration, 30s) linear infinite;
}

.marquee__row--reverse .marquee__track { animation-direction: reverse; }

/* The track holds the list twice, so -50% lands exactly on the repeat. */
@keyframes marquee-x {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

.marquee__name,
.marquee__dot {
  font-family: var(--type-heavy);
  font-size: var(--band-type);
  line-height: 1;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink);
}

/* Outlined row: transparent fill with a stroke, same as the reference. */
.marquee__row--outline .marquee__name {
  color: transparent;
  -webkit-text-stroke: var(--outline-width) var(--ink);
}

/* Bottom row, pink. */
.marquee__row--accent .marquee__name,
.marquee__row--accent .marquee__dot {
  color: var(--pink);
}

.marquee__dot {
  margin: 0 0.28em;
  font-size: calc(var(--band-type) * 0.36);
  color: var(--dot-colour);
}

/* ---------- Right: still info column ---------- */

/* Dark panel. Redefining the shared tokens here means every rule below picks
   up the inverted palette without needing its own dark variant. */
.info {
  --ink: #f4f3f0;
  --ink-soft: #948f84;
  --rule: #302e28;

  /* A few shades up from the panel's #0d0c0a, deliberately not white. */
  --scroll-thumb: #38352f;
  --scroll-thumb-live: #4c4840;

  /* The current page's own link, a few shades down from the full white. */
  --link-here: #948f84;

  /* The band's rules were drawn for black on white. Inverted here.
     The bullets need naming again as well: --dot-colour resolves its var()
     where it is declared, so the one in :root stayed the light-theme ink. */
  --band-rule: rgba(244, 243, 240, 0.16);
  --dot-colour: var(--ink);

  /* So the band's type can be sized off the panel width. */
  container-type: inline-size;

  background: #0d0c0a;
  color: var(--ink);
  overflow-y: auto;

  /* So the wordmark can slide out of the panel without the browser offering a
     horizontal scrollbar to chase it. */
  overflow-x: hidden;
  scrollbar-width: none;
}
.info::-webkit-scrollbar { display: none; }

/* Height, not min-height. The CV is long enough to outgrow the panel, and with
   min-height the inner would stretch to fit it and carry the foot down the
   page as the text typed in. Pinned to the panel, the foot never moves and the
   CV scrolls inside its own box instead. */
.info__inner {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: var(--gutter) var(--gutter) 2rem;
}

/* Sits in the corner the head's own padding already defines, so it lines up
   with the wordmark below it without repeating the gutter value. Absolute
   and out of flow, so it does not compete with .info__head for the flex
   space it occupies while hidden. */
.info__close {
  position: absolute;
  top: var(--gutter);
  right: var(--gutter);
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2em;
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  color: var(--ink);
  transition: opacity 160ms linear;
}
.info__close[hidden] { display: none; }
.info__close:hover { opacity: 0.7; }

.info__close:focus-visible {
  outline: 2px solid var(--pink);
  outline-offset: 3px;
}

.info__close-x {
  font-family: var(--type-heavy);
  font-size: 1.25rem;
  line-height: 1;
}

.info__close-label {
  font-family: var(--type-text);
  font-weight: 300;
  font-size: 0.6875rem;
  letter-spacing: 0.02em;
}

/* The block sits centred across the panel, its middle on the quarter line of
   the viewport height. vh rather than a percentage because percentage padding
   resolves against width, and the panel is full viewport height. */
.info__head {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* 134.2px at a 1131px window, the size this was drawn at, but falling away
     faster than that below it: the band and the contact line underneath now
     need the room. Both terms land on the old value at 1131 exactly, so the
     reference layout is unmoved and only shorter windows give way. */
  /* Still 134.2px at the 1131 reference, but giving way steeply below it.
     This empty space above the wordmark is the only slack big enough to let
     the retouching page's five copy blocks fit without touching the gaps
     between the paragraphs themselves, which is what must never be squeezed
     again. Floored, so it can never invert. */
  padding-top: max(1.5rem, calc(47.8vh - 406.4px));

  /*
    A real gap between the copy and the band, and it has to be padding here
    rather than anything above the band.

    This box is flex: 1, so the space under the copy was only ever the slack
    left after everything else had taken its share. On a tall window that came
    to about 78px and looked deliberate; at 1440x800 it came to nothing at all
    and the italic lines sat directly on top of the moving names. Padding is
    the floor the slack cannot eat.
  */
  padding-bottom: min(4.5rem, 7.5vh);

  /* So the kit line, which sits outside .mark, can be sized off the same
     width .mark is sized from and stay in step with the service lines. */
  container-type: inline-size;

  /* Accelerating out, so it reads as a swoosh rather than a slide. The
     duration is mirrored by SWOOSH_MS in info-panel.js. */
  transition: transform 260ms cubic-bezier(0.5, 0, 0.75, 0), opacity 180ms linear;
}

.info__head.is-out {
  transform: translateX(100%);
  opacity: 0;
}

/* The display above outranks the hidden attribute's own display: none, so
   without this the parked wordmark keeps its space and pushes the CV down. */
.info__head[hidden] { display: none; }

/* ---------- Wordmark block ----------

   Six lines, each set at whatever size makes it fill the stack width, so the
   block is flush on both edges. Sizes are in cqw, a percentage of .mark's own
   width, so the whole thing scales with the panel and needs no JavaScript.
   The numbers were measured in the browser against Archivo's real metrics;
   changing the font, the tracking or the copy means remeasuring them. */

/*
  Sized to the note, which is the widest thing in here, so nothing has to
  overhang the container.

  The 334px floor is the size measured off Bonny's screenshot, below which the
  type stops being comfortable to read. It holds until the panel itself is
  narrower than that, at which point the 100% cap takes over and the block
  tracks the panel rather than overflowing it.
*/
.mark {
  width: min(100%, max(61%, 334px));
  container-type: inline-size;
}

.mark__stack {
  width: 56.8%;
  margin: 0 auto;
  font-family: var(--type-text);
  font-weight: 600;
  line-height: 0.85;
  letter-spacing: -0.02em;
  text-transform: uppercase;
}

.mark__line {
  display: block;
  white-space: nowrap;
}

/* Archivo Black is a separate family with one weight, not Archivo at 900. */
.mark__line--heavy {
  font-family: var(--type-heavy);
  font-weight: 400;
}

.mark__line:nth-child(1) { font-size: 16.52cqw; }  /* Digi.B */
.mark__line:nth-child(2) { font-size: 8.87cqw; }   /* Bonny Sadr */
.mark__line:nth-child(3) { font-size: 14.88cqw; }  /* Digital */
.mark__line:nth-child(4) { font-size: 7.43cqw; }   /* Operator and */
.mark__line:nth-child(5) { font-size: 6.06cqw; }   /* Post-production */
.mark__line:nth-child(6) { font-size: 8.64cqw; }   /* London UK */

/* Retouching page. Five lines, measured the same way as the six above. */
.mark__stack--retouch .mark__line:nth-child(1) { font-size: 16.516cqw; } /* Digi.B */
.mark__stack--retouch .mark__line:nth-child(2) { font-size: 8.859cqw; }  /* Bonny Sadr */
.mark__stack--retouch .mark__line:nth-child(3) { font-size: 6.05cqw; }   /* Post-production */
.mark__stack--retouch .mark__line:nth-child(4) { font-size: 8.656cqw; }  /* Retouching */
.mark__stack--retouch .mark__line:nth-child(5) { font-size: 8.628cqw; }  /* London UK */

.is-pink { color: var(--pink); }

.mark__note {
  /* Proportional to the block, but capped against the window height so a
     short window loses the air rather than pushing the foot off the panel.
     The second term matches the flat 8vh it replaces at 1131 and falls away
     faster below, paying for the gap under the copy. */
  margin: max(1rem, min(25cqw, calc(14vh - 68px))) 0 0;
  font-family: var(--type-text);
  font-weight: 300;
  font-style: italic;
  font-size: 3.549cqw;
  line-height: 1.3;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;

  /* Deliberately allowed to wrap. Once the panel is narrower than the frozen
     block, two centred lines are better than clipping the ends off one. */
  text-wrap: balance;
}

/* The gap above belongs between the wordmark and the block, not between the
   lines of the block. Capped against window height as well, because the
   retouching page stacks five of these and em alone never gives way on a
   short window. The em term still wins at 1131, so the reference is unmoved. */
.mark__note + .mark__note { margin-top: min(1.7em, 2.4vh); }

/* Second beat of the block on the retouching page, so it reads as its own
   thought rather than a fourth item in the list above. */
.mark__note + .mark__note--break { margin-top: min(4.5em, 3vh); }

/*
  NOTHING here may use a bare min() around a calc() that can reach zero.

  An earlier attempt to line the two pages up wrote
  `min(1.7em, calc(4.82vh - 38px))` on the retouching copy. Below an 788px
  window that calc turns negative, min() picks the negative number, and the
  paragraphs are pulled up on top of one another into an unreadable stack. It
  passed a check at 1440x800 because the value there is +0.6px, which fits by
  arithmetic and is worthless to read. Every capped value in this file is
  wrapped in a max() floor for exactly that reason. Keep it that way.
*/

/*
  The kit line is longer than the service lines and holds one line on a wide
  panel, so it sits outside .mark and takes the full width of the head.

  The font size repeats .mark's own width expression rather than using cqw,
  because inside .mark cqw would measure .mark, and out here it measures the
  head. Written this way the two resolve to the same pixel size at every
  width, including below the 334px floor.
*/
.mark__kit {
  max-width: 100%;

  /* Measured off the window height, not the type, so the gap gives way on a
     short window instead of pushing the foot out of sight. 104px at 1131, the
     same as the flat 9.2vh it replaces, but falling away far faster below
     that: this is where the room for the gap under the copy comes from. */
  margin: max(1.5rem, calc(20.8vh - 131.4px)) 0 0;
  font-family: var(--type-text);
  font-weight: 300;
  font-style: italic;
  font-size: calc(min(100cqw, max(61cqw, 334px)) * 0.03549);
  line-height: 1.3;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  text-wrap: balance;
}

/* ---------- CV panel ----------
   One text node with the newlines and tabs kept, so info-panel.js can reveal
   it by slicing the string and let the wrapping fall out as it types. */

.cv {
  flex: 1;

  /* A flex item will not shrink below its content without this, which is what
     would push the foot down as the text arrives. */
  min-height: 0;
  overflow-y: auto;

  /*
    The scrollbar is left to the browser rather than hidden, so each platform
    does what its users expect: macOS fades an overlay bar in while scrolling
    and takes no space for it, Windows keeps one there. Detecting the device
    would not help, since a trackpad and a wheelless mouse look identical.

    The gutter is reserved up front so that the bar appearing part way through
    the typing does not narrow the column and reflow the text.
  */
  scrollbar-width: thin;
  scrollbar-color: var(--scroll-thumb) transparent;
  scrollbar-gutter: stable;

  padding: 2rem 1.5rem 1.5rem;
  font-family: var(--type-text);
  font-weight: 400;
  font-size: 0.9375rem;
  line-height: 1.3;
  color: var(--ink);
}
/* Safari and older Chrome ignore scrollbar-color, so it is spelled out again
   here. Same two colours. */
.cv::-webkit-scrollbar { width: 8px; }
.cv::-webkit-scrollbar-track { background: transparent; }
.cv::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb);
  border-radius: 4px;
}
.cv::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-live); }

/* One paragraph per element rather than one text node for the lot, so the
   bullets can hang their wrapped lines under the text instead of back at the
   margin. A blank line's worth of space between paragraphs, except between
   consecutive bullets, which run on. */
.cv p { margin: 0; white-space: pre-wrap; }
.cv p + p { margin-top: 1.3em; }
.cv .cv__bullet + .cv__bullet { margin-top: 0; }

/* The bullet is drawn rather than typed, so every line of the paragraph,
   first and wrapped alike, starts at the same indent. */
.cv__bullet {
  position: relative;
  padding-left: 2.1em;
}

.cv__bullet::before {
  content: "•";
  position: absolute;
  left: 0.55em;
}

/* No orphan bullet sitting there before its paragraph has been typed. */
.cv__bullet:empty::before { content: none; }

.cv.is-typing .is-caret::after {
  content: "";
  display: inline-block;
  width: 0.45em;
  height: 0.95em;
  margin-left: 0.08em;
  vertical-align: -0.1em;
  background: var(--ink);
  animation: caret 700ms steps(1) infinite;
}

@keyframes caret { 50% { opacity: 0; } }

/* ---------- Contact line ----------

   Email and phone sit on the panel under the band, not behind a button. This
   replaced a Contact section that swapped in over the wordmark the way the CV
   does: the site is sent to people who already need to reach her, so putting
   the two things they came for behind a click was backwards.

   One line on the panel. On mobile the band is not in the panel at all, and
   these stack under the copy instead. */

.reach {
  display: flex;
  flex: none;
  justify-content: center;
  gap: 2.25rem;

  /* Capped against window height for the same reason as everything else in
     here: on a short window the air gives way rather than the footer. */
  margin: min(2rem, 3vh) 0 0;
  font-family: var(--type-text);
  font-weight: 300;
  font-style: italic;
  font-size: 0.875rem;
  letter-spacing: 0.02em;
  text-align: center;
  white-space: nowrap;
}

.reach p { margin: 0; }

/* Undecorated until hovered: two plain lines read as information, where two
   underlined ones would read as another row of navigation next to the footer
   links just below. */
.reach a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.reach a:hover { border-bottom-color: var(--ink); }

/* ---------- Panel foot ----------
   Two rows: the links and the info button as one centred group, with the
   copyright smaller underneath on the right. Nothing here rearranges with the
   window: the same two rows hold at every width, so the links and the button
   never move relative to each other. */

.muted { color: var(--ink-soft); }
.small { font-size: 0.8125rem; }

.info__foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  row-gap: 0.75rem;

  /* 48px at 1131, same as the flat min(3rem, 5vh) it replaces, but giving way
     sooner below that so the contact line above it does not push the footer
     off the panel. */
  margin-top: max(0.75rem, min(3rem, calc(6.47vh - 25.2px)));
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
  font-family: var(--type-text);
  font-weight: 300;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Right corner, and smaller than the links so it reads as a footnote. */
.info__foot p {
  align-self: flex-end;
  margin: 0;
  font-size: 0.6875rem;
  white-space: nowrap;
}

/* The button takes the same gap as the links, so it sits in the row as the
   last item rather than as a separate control. */
.info__nav {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.links {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 0;
  font-size: 0.8125rem;
}

.links a {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid rgba(244, 243, 240, 0.5);
}
.links a:hover { border-bottom-color: var(--ink); }

/* No underline on the house, since the rule would sit under it and read as
   part of the drawing. */
.links a.links__home {
  display: flex;
  border-bottom: 0;
  transition: opacity 160ms linear;
}
/* Same rem size as the info button, so the two icons match. */
.links__home svg {
  display: block;
  width: var(--foot-icon);
  height: var(--foot-icon);
}
.links__home:hover { opacity: 0.7; }

/* The link to the page you are already on is dimmed, so the row shows where
   you are without adding a word. The house takes the colour through
   currentColor. */
.links a[aria-current="page"] {
  color: var(--link-here);
  border-bottom-color: rgba(148, 143, 132, 0.45);
}

.links__home:focus-visible {
  outline: 2px solid var(--pink);
  outline-offset: 3px;
}

.info__mark {
  display: grid;
  place-items: center;
  width: var(--foot-icon);
  height: var(--foot-icon);
  padding: 0;
  border: 2px solid var(--ink);
  border-radius: 50%;
  appearance: none;
  background: none;
  cursor: pointer;
  font-family: inherit;
  font-size: 0.75rem;
  font-style: italic;
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1;
  text-transform: none;
  color: var(--ink);
  transition: background-color 160ms linear, color 160ms linear;
}

/* Filled while the CV is showing, so the button reads as a state and not just
   a way in. */
.info__mark[aria-expanded="true"] {
  background: var(--ink);
  color: #0d0c0a;
}

.info__mark:focus-visible {
  outline: 2px solid var(--pink);
  outline-offset: 3px;
}

/* ---------- Between pages ----------

   The same swoosh the info button uses, applied to navigation between the two
   pages: the panel leaves to the right and the next one arrives from the
   right, while the field behind it crosses over.

   This is the browser's own cross-document transition, so there is no
   JavaScript in the click path. A browser without it navigates normally and
   loses only the animation, which is why nothing here is load-bearing.

   The out curve and the 260ms match SWOOSH_MS in info-panel.js. Change one
   and change the other, or the two swooshes stop feeling like one gesture. */

@view-transition { navigation: auto; }

.field { view-transition-name: field; }
.info { view-transition-name: panel; }

::view-transition-old(panel) {
  animation: 260ms cubic-bezier(0.5, 0, 0.75, 0) both panel-out;
}

/* Decelerating on the way in, so it arrives rather than slams. */
::view-transition-new(panel) {
  animation: 300ms cubic-bezier(0.16, 1, 0.3, 1) both panel-in;
}

@keyframes panel-out { to { transform: translateX(100%); opacity: 0; } }
@keyframes panel-in { from { transform: translateX(100%); opacity: 0; } }

/* The field holds still and crosses over, so the columns do not appear to
   jump sideways as well. */
::view-transition-old(field),
::view-transition-new(field) { animation-duration: 260ms; }

/* ---------- Reduced motion ----------
   The whole design is motion, so this needs a real alternative rather than a
   frozen frame: static columns of work, client list underneath, page scrolls. */

@media (prefers-reduced-motion: reduce) {
  html, body { overflow: auto; }

  /* Pages change on a hard cut here, same as the CV does. */
  @view-transition { navigation: none; }

  /* The CV swap still happens, it just happens at once. info-panel.js skips
     the swoosh and the typing to match. */
  .info__head { transition: none; }
  .cv.is-typing::after { display: none; }

  /* Here the page scrolls rather than the panel, so the panel is not viewport
     height and the inner has to grow with its content again. */
  .info__inner { height: auto; min-height: 100%; }
  .cv { overflow-y: visible; }

  .layout { height: auto; min-height: 100%; }

  .field {
    grid-template-rows: auto auto;
    overflow: visible;
  }

  .field__work { overflow: visible; }

  .columns {
    position: static;
    align-items: start;
    padding: var(--column-gap);
  }

  .column { overflow: visible; cursor: auto; }

  /* Retouching page: the slideshow holds one frame and the click still
     changes it, so nothing moves on its own. */
  .slide__img { transition: none; }

  .rows {
    position: static;
    display: block;
  }

  .row { overflow: visible; cursor: auto; }

  .row__track {
    height: auto;
    flex-wrap: wrap;
  }

  .row__track img {
    height: auto;
    width: 9rem;
    margin: 0 var(--column-gap) var(--column-gap) 0;
  }

  /* One static row that wraps, rather than three frozen strips. */
  .marquee { grid-template-rows: auto; }

  .marquee__row { overflow: visible; }

  .marquee__track {
    animation: none;
    width: auto;
    flex-wrap: wrap;
    padding: 1.5rem var(--column-gap);
  }

  .marquee__name,
  .marquee__dot { font-size: 1.75rem; }
  .marquee__dot { font-size: 0.7rem; }
}

/* The wordmark block stops shrinking at its floor, so the panel gutters give
   way instead. This is what keeps the block at full size down to roughly a
   1050px window rather than about 1740. */
@media (max-width: 1400px) { :root { --gutter: 2rem; } }
@media (max-width: 1050px) { :root { --gutter: 1.5rem; } }

/* ---------- Narrow screens ----------

   Two screens, not a stacked page. .layout becomes the scroll container and
   its two children each take one viewport, snapping hard so the page can only
   ever rest on one of them: the black panel, then the work.

   svh, not vh: on iOS vh is the height with the browser chrome retracted, so
   a section sized in vh is taller than what you can actually see and its
   bottom (the arrow, the footer) sits under the toolbar. svh is the smallest
   the viewport gets, which is the only value that guarantees a whole section
   fits. The vh line before it is the fallback for browsers without svh.

   Everything below 861px is its own design. The desktop rules above are
   untouched, which is why so little here is a modification rather than an
   override. */

/* Mobile-only chrome. Declared ahead of the media query so source order does
   not fight the override that turns them back on. .reach is not in here: it
   shows at both widths, it just changes shape. */
.menu-toggle,
.mobile-menu,
.pager__arrow,
.work-foot { display: none; }

@media (max-width: 860px) {
  :root { --gutter: 1.5rem; }

  /* The scroller is .layout, not the body. The body scrolling was what left
     the top of the panel visible under the work: two children of a scrolling
     body cannot snap to it. */
  html, body { overflow: hidden; }

  .layout {
    grid-template-columns: 1fr;
    height: 100vh;
    height: 100svh;
    overflow-y: auto;
    overflow-x: hidden;
    scroll-snap-type: y mandatory;
    scroll-behavior: smooth;
    scrollbar-width: none;
  }
  .layout::-webkit-scrollbar { display: none; }

  /* A mouse cannot scroll these by dragging, so info-panel.js does it by
     hand. While it is, nothing should be selecting text under the cursor. */
  .layout.is-dragging {
    user-select: none;
    cursor: grabbing;
  }

  /* The panel is the first screen, the work the second. */
  .info { order: -1; }

  .info,
  .field {
    height: 100vh;
    height: 100svh;
    scroll-snap-align: start;

    /* A flick must not carry past a screen: with only two, skipping one means
       skipping half the site. */
    scroll-snap-stop: always;
  }

  /* Was a scroller in its own right on desktop. Nested inside the snapping
     .layout it would swallow the vertical drag before the page ever saw it.
     Relative so the hamburger and the down arrow hang off this screen rather
     than off the viewport, which would carry them over the work as well.

     container-type has to come off, and it is not cosmetic. It applies
     containment, which makes .info the containing block for any fixed
     descendant, so the info overlay inside it would pin to this screen
     instead of the viewport and sit off the top of the work. The band is the
     only thing that needed the container and it lives in .field here. */
  .info {
    position: relative;
    overflow: visible;
    container-type: normal;
  }

  /* ---------- First screen: the panel ---------- */

  .info__inner {
    height: 100%;
    justify-content: center;

    /* Top clears the hamburger, bottom clears the arrow. Both sit in the
       corners this padding defines. */
    padding: 4.5rem var(--gutter) 5.5rem;
  }

  /* Sized off the window on desktop so the foot never got pushed out of
     sight. There is no foot on this screen, so the block simply centres. */
  .info__head {
    flex: 0 0 auto;
    padding-top: 0;
  }

  /* The desktop gaps are measured against window height and are far too
     generous once the whole screen is one block. */
  .mark__note { margin-top: 1.9rem; }
  .mark__note + .mark__note { margin-top: 1.15rem; }
  .mark__note + .mark__note--break { margin-top: 1.6rem; }
  .mark__kit { margin-top: 1.9rem; }

  /* Replaced by the hamburger and by the links on the work screen. */
  .info__foot { display: none; }

  /* Stacked here rather than side by side: the panel is a whole screen with
     room to spare, and two lines sit better under the centred copy. */
  .reach {
    display: block;
    margin: 1.9rem 0 0;
    font-size: 0.9375rem;
  }

  .reach p { margin: 0.7rem 0 0; }

  .work-foot a {
    color: inherit;
    text-decoration: none;
  }

  /* ---------- Second screen: the work ---------- */

  .field {
    display: flex;
    flex-direction: column;
    position: relative;

    /* Clears the up arrow, which is out of flow in the corner above. */
    padding-top: 4.5rem;
  }

  /* Holds the swipe track. The desktop centring grid would fight the track's
     own full-bleed sizing. */
  .field__work {
    flex: 1;
    min-height: 0;
    display: block;
    overflow: hidden;
  }

  /* Desktop's two-layer cross-fade. The track below replaces it here. */
  #columns,
  .slide { display: none; }

  /*
    The swipe is a real scroll container, not pointer arithmetic on a
    transform. That is what makes it follow the thumb, carry momentum and
    settle where a native carousel would, and it means a vertical drag is
    still handed to the page rather than being swallowed by a handler that
    only understood x. Do not add touch-action here: the browser already
    separates the axes, and pinning one would break the other.
  */
  .swipe {
    display: flex;
    height: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }
  .swipe::-webkit-scrollbar { display: none; }

  /* Only says "you can pull this" where there is a cursor to say it to. */
  @media (hover: hover) {
    .swipe { cursor: grab; }
    .layout.is-dragging .swipe { cursor: grabbing; }
  }

  /* No scroll-snap-stop here on purpose. Forcing one frame per gesture is
     what made the gallery feel stiff; without it a flick carries across
     several and settles, the way the name band does. */
  .swipe__slide {
    flex: 0 0 100%;
    height: 100%;
    scroll-snap-align: center;
  }

  /*
    object-fit: contain against an explicit width and height, not max-width
    and max-height.

    The max-* pair silently did nothing: a percentage max-height has to
    resolve against a definite containing block, and inside a centred grid
    whose row is auto there was not one, so the image kept its natural
    proportions and ran 12px past the bottom of the frame, where
    .field__work's overflow clipped it. Same family as the inset trap noted
    further down: a replaced element needs a real width and height.

    contain also means the whole photograph is always visible, whatever the
    window. These are Bonny's pictures; nothing here may crop one to make the
    box tidy, so a frame that does not match the window letterboxes instead.
  */
  .swipe__slide img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    -webkit-user-drag: none;
    user-select: none;
  }

  /* Moved out of the panel and into the work by the page script, so it is on
     white here and takes the light palette from :root. Full width, so the
     panel's negative gutters have to come off. */
  .marquee {
    --band-type: 1.05rem;
    flex: none;
    margin: 0;
  }

  .work-foot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.9rem;
    padding: 1.1rem var(--gutter) 1.4rem;
    font-family: var(--type-text);
    font-weight: 300;
  }

  .work-foot__reach {
    display: flex;
    gap: 1.6rem;
    margin: 0;
    font-style: italic;
    font-size: 0.875rem;
    letter-spacing: 0.02em;
  }

  .work-foot__nav {
    display: flex;
    align-items: center;
    gap: 1.1rem;
    font-size: 0.8125rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }

  .work-foot__nav a { border-bottom: 1px solid var(--ink); }

  /* Solid, where the panel's is an outline: on white a hairline ring reads as
     a smudge at this size. */
  .work-foot__nav .info-open {
    display: grid;
    place-items: center;
    width: 1.4rem;
    height: 1.4rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    appearance: none;
    background: var(--ink);
    color: var(--field-ground);
    font-family: var(--type-text);
    font-size: 0.8125rem;
    font-style: italic;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
  }

  /* ---------- The two arrows ---------- */

  .pager__arrow {
    display: grid;
    place-items: center;
    position: absolute;
    z-index: 5;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    appearance: none;
    background: none;
    cursor: pointer;
  }

  .pager__arrow svg {
    width: 26px;
    height: 26px;
    display: block;
  }

  /* Bottom centre of the panel: it is the invitation to go down, so it sits
     under the copy rather than off in a corner. */
  .pager__arrow--down {
    left: 50%;
    bottom: 1.6rem;
    transform: translateX(-50%);
    color: var(--ink);
    animation: nudge-down 1.8s ease-in-out infinite;
  }

  /* Top right of the work, taking over the corner the hamburger held on the
     screen above, so one corner is always where the control is. */
  .pager__arrow--up {
    top: 1rem;
    right: var(--gutter);
    color: var(--ink);
    animation: nudge-up 1.8s ease-in-out infinite;
  }

  /* ---------- Menu ---------- */

  .menu-toggle {
    display: flex;
    position: absolute;
    top: 1rem;
    right: var(--gutter);
    z-index: 20;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    appearance: none;
    background: none;
    cursor: pointer;
  }

  .menu-toggle__bar {
    display: block;
    width: 26px;
    height: 3px;
    background: var(--ink);
  }

  .mobile-menu {
    display: flex;
    flex-direction: column;
    position: absolute;
    top: calc(1rem + 44px + 0.4rem);
    right: var(--gutter);
    z-index: 20;
    gap: 0.85rem;
    padding: 1rem 1.35rem;
    background: #1c1a16;
    border-radius: 6px;
    font-family: var(--type-text);
    font-size: 0.875rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #f4f3f0;
  }
  .mobile-menu[hidden] { display: none; }

  .mobile-menu a,
  .mobile-menu button {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    color: inherit;
    font: inherit;
    text-align: right;
    text-decoration: none;
    cursor: pointer;

    /* Not covered by the font shorthand, and form controls get
       text-transform: none from the UA stylesheet. Without this the anchor
       read RETOUCH and the button next to it read Info. */
    text-transform: inherit;
    letter-spacing: inherit;
  }

  /* ---------- Info overlay ----------

     A fixed sheet over the whole viewport, rather than the desktop swap that
     replaces the wordmark inside the panel. That swap was invisible from the
     work screen: it changed the screen above the one you were looking at. On
     top of everything, it cannot be missed from either. */

  .cv {
    position: fixed;
    inset: 0;
    z-index: 60;
    flex: none;
    min-height: 0;
    padding: 5rem var(--gutter) 3rem;
    background: #0d0c0a;
    color: #f4f3f0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
  }
  .cv::-webkit-scrollbar { display: none; }
  .cv[hidden] { display: none; }

  /* Fixed over a scrolling column of text, so it needs to sit on something:
     without the panel behind it the CV runs straight under the X. */
  .info__close {
    position: fixed;
    top: 1.15rem;
    right: var(--gutter);
    z-index: 61;
    color: #f4f3f0;
    background: #0d0c0a;
    padding: 0.4rem 0.55rem;
    border-radius: 4px;
  }
}

/* Both arrows travel the way they point, so the movement says which way the
   page is about to go. */
@keyframes nudge-down {
  0%, 100% { transform: translate(-50%, 0); }
  50%      { transform: translate(-50%, 7px); }
}

@keyframes nudge-up {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-7px); }
}

/* The arrows still work, they just stop asking for attention, and the page
   stops taking the scroll out of the reader's hands. */
@media (prefers-reduced-motion: reduce) {
  .pager__arrow--down,
  .pager__arrow--up { animation: none; }

  @media (max-width: 860px) {
    .layout {
      scroll-snap-type: none;
      scroll-behavior: auto;
    }
    .swipe { scroll-snap-type: none; }
  }
}
