/* ---------------------------------------------------------------- fonts -- */

/* Eurostile Next Pro Wide Semi Bold - Linotype, via a MyFonts webfont kit.
   A single static face: one weight, no italic, no variable axes. The weight is
   declared explicitly so the browser matches it to body{font-weight:600}
   instead of synthesising a fake bold on top of it. */
@font-face {
  font-family: 'Eurostile Next Pro Wide';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/eurostile_next_pro_wide_semibold.woff2') format('woff2');
}

/* --------------------------------------------------------------- tokens -- */

:root {
  --bg: #202020;                  /* Monochrome/100 */
  --fg: #ffffff;                  /* White/100 */
  --fg_dim: #949494;              /* Monochrome/48 */

  /* Two roles want the same step of the scale, so it is named once and they
     both point at it - one edit when the scale moves. */
  /* A link's rule at rest, and the ring around the close button: white at a
     twelfth, not a gray of its own. Over the page's own color the two are a
     level apart and indistinguishable - but the rule also crosses the modal's
     veil and the button stands over screenshots, and there an opaque gray
     would be a patch rather than a line. */
  --fg_line: rgba(255, 255, 255, 0.12);
  --fg_line: color-mix(in srgb, var(--fg) 12%, transparent);

  /* Ours, not the design's: nothing in the file draws an empty image, and this
     is only ever seen for the moment one takes to arrive. */
  --mono_88: #3b3b3b;             /* Monochrome/88 */
  --thumb_empty: var(--mono_88);

  /* Eurostile sets its caps hard against the top of its line box: a 0.75em
     ascent over a 0.6875em cap height, with the full 0.25em descent left empty
     below. At 32/36 that leaves 4px above the caps and 10px below the baseline,
     so a line entering and leaving a one-line window is clipped unevenly. This
     puts the glyphs in the middle of the box, which makes the two edges bite
     at the same point. */
  /* In whole pixels rather than as a ratio, and one for each size the shift
     is used at. As 0.094em it landed on 1.128px at body size, and a line that
     starts on a fraction of a pixel has its horizontal terminals split across
     two rows of them - at twelve pixels the foot of an I is one pixel tall, so
     half of it goes pale and the letter reads as cut off. Only at 1x: a phone
     has two or three device pixels to spend on that same foot and the fraction
     disappears into them, which is why it looked right there and wrong here.
     The values are what the ratio already came to, rounded: 1.128 and 3.008. */
  --optical_shift: 1px;
  --optical_shift_title: 3px;

  /* home-desktop. The tablet and mobile artboards keep the same type and the
     same 160x120 thumbnails - only the spacing scale changes. */
  --gutter: 48px;        /* page margin, and the gap between thumbnail groups */
  --gap: 48px;           /* between the two columns of a row */
  --gap_inline: 16px;    /* between items inside one column */

  /* Body / H1 - 13/20, 1px tracking */
  --body_size: 13px;
  --body_leading: 20px;
  --body_tracking: 1px;
  --para_gap: 12px;      /* the style's own paragraph spacing */

  /* Title / H2 - 32/36 */
  --title_size: 32px;
  --title_leading: 36px;
  --title_tracking: 0;

  /* How far any line of text travels, whichever way it is moving: a title
     swapping for the next, a link's label on hover, a line arriving with the
     page. One distance for all of them - they already share the two curves, and
     this is the last number that told them apart. Read by the stylesheet and by
     title_swap.js, intro.js and project_modal.js alike. */
  --swap_travel: 20px;
  /* The entrance drops a thumbnail this far. Read by intro.js, which starts
     the tween from the value the stylesheet already painted. */
  --intro_rise: 60px;

  /* The grain's whole strength, now that it lies over the page rather than
     modulating it. It is a different number from the one overlay wanted: at
     0.4 a flat gray would be laid over everything. This is the dial. */
  --grain_opacity: 0.04;

  /* Three columns, and both screens are built on them: a middle twice the
     width of either side, with the page's margin outside and a gap between.

     The tracks are shares, the contents are not. A share alone would let the
     middle grow past the measure the screenshots were drawn at on a wide
     display; a fixed width alone would make the middle the only thing that
     gives when the display is narrow, which is what it used to do - 800 at
     1920 but 208 at 1200, because the side column held its 400 and the middle
     paid for all of it. Capped shares hold the drawn measure above 1792 and
     share the squeeze below it.

     The about screen borrows the same system for one reason: so its words come
     out exactly as wide as the modal's screenshots. Its pictures keep the
     sizes they were drawn at. */
  --col_side: 400px;      /* the most either outer column's content takes */
  --col_middle: 800px;    /* and the most the middle's does */

  --col_span: calc(100vw - 2 * var(--gutter) - 2 * var(--gap));
  --col_side_w: min(var(--col_side), calc(var(--col_span) / 4));
  --col_middle_w: min(var(--col_middle), calc(var(--col_span) / 2));

  /* Whether the description stands beside the screenshots or above them. The
     stylesheet is where the breakpoint lives, so this is where the answer is
     written down; project_modal.js reads it back rather than carrying a width
     of its own. It carried 1024 once, and when the breakpoints moved to 1200
     the two disagreed for the 176 pixels between them - the images started at
     the top of the column, over a description that was never told to go. */
  --modal_beside: 1;
  --modal_field_gap: 32px;   /* between one labeled field and the next */
  --modal_intro_gap: 32px;   /* and between the title and the first of them */

  /* About. The three pieces of media are placed against the columns' own
     edges, at the sizes they were drawn at. */
  --about_block_gap: 32px;   /* between the paragraph and the invitation */

  /* The width each picture was drawn at, per view. Only the width: all three
     are 4:3 or 3:4 and the asset keeps its own ratio, so the height follows
     rather than being a second number to keep in step. */
  --about_boulder_w: 320px;
  --about_sirius_w: 304px;
  --about_me_w: 198px;

  /* How far forward the pictures stand. On the wide view they are at full
     strength and carry their filenames under them; on the narrow ones they
     step back behind the words and the labels go, because a picture placed
     under a paragraph rather than beside it is a ground rather than an
     exhibit. */
  --about_media_opacity: 1;
  /* White, and the pair carrying them is what is faint - not the lines. It
     matters where they cross something other than the page: an opaque gray that
     matches over the background would be wrong over the modal's veil. */
  --crosshair: #ffffff;
  --crosshair_opacity: 0.12;
  --crosshair_weight: 1px;
  --modal_veil: 88%;      /* how much of the page color covers what is behind */
  --modal_blur: 50px;

  /* The close button is a round target with a cross in it. Bigger on the
     tablet, where it leaves the corner and stands on its own at the foot of
     the screen with nothing beside it to give it scale. */
  --close_size: 48px;
  --close_arm: 13px;     /* one stroke of the cross, corner to corner */
  --close_inset: 42px;   /* from the top and right edges, on the wide view */

  /* The ring is there at rest and lights on hover, the way a link's rule does.
     White at a twelfth is the same faint line the crosshair is drawn in.

     On the wide view the button stands on the veil and needs nothing behind
     it. Below that it stands over the screenshots, so it carries a ground and
     a blur of its own - the veil's treatment in miniature. */
  --close_ring: color-mix(in srgb, var(--fg) 12%, transparent);
  --close_ground: color-mix(in srgb, var(--bg) 48%, transparent);
  --close_blur: 24px;

  --thumb_w: 160px;
  --thumb_h: 120px;
  --thumb_gap: 4px;
  --thumb_count: 6;      /* per project, and so the width of one group */

  /* One group of thumbnails across: what a tap opens a project from. */
  --project_span: calc(
    var(--thumb_count) * var(--thumb_w) +
    (var(--thumb_count) - 1) * var(--thumb_gap)
  );
}

/* ---------------------------------------------------------------- reset -- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: 'Eurostile Next Pro Wide', system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-weight: 600;
  font-size: var(--body_size);
  line-height: var(--body_leading);
  letter-spacing: var(--body_tracking);
  text-transform: uppercase;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

p {
  margin: 0;
}

.visually_hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------- stage -- */

/* The page is a single fixed view - the thumbnail strip is the only axis
   that scrolls, so the document itself must never move. */
.stage {
  position: fixed;
  inset: 0;
  overflow: hidden;

  /* The page color is on the body too, but it has to be here as well: a fixed
     element opens its own stacking context, and the grain's blend mode only
     sees what is painted inside that. Against the transparency it would
     otherwise find, the noise stops being a modulation of the background and
     turns into white specks dropped on top of it. */
  background-color: var(--bg);
}

/* Lies over the page rather than behind it - grain is on the print, not on
   the table under it - and never takes the pointer.

   Overlay, not plain alpha. On a backdrop this dark it scales the background
   rather than painting over it: every channel comes out at 0.6 to 1.4 times
   what was there, so the grain swings around the page's own color instead of
   dropping white specks onto it. That is what keeps it fine at an opacity this
   high - #1b1b1b lands between 16 and 38 rather than being speckled with
   white. */
/* Over everything: grain is on the print, not on the table under it, and that
   includes the words. The one thing lifted out of its way is the close button
   - see .modal_close.

   Laid over rather than blended in. Overlay left pure white and pure black
   alone and bit hardest in the midtones, which kept the type crisp; plain
   coverage takes everything by the same faint amount, the white type included.
   That is the trade, and the opacity above is what keeps it faint.

   It covers the screen and no more: the grain moves by being redrawn, not by
   being pushed about, so there is nothing hanging off the edges to hide. */
.grain {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: var(--grain_opacity);

  /* The canvas is smaller than the element it fills. Without this the browser
     would smooth the grain back into a gray haze on the way up. */
  image-rendering: pixelated;
}

/* Two hairlines crossing wherever the pointer is. Page furniture rather than
   the about page's own, so it lives here beside the grain: there is one set on
   the page and one inside the modal, and a single module drives both.

   Behind whatever is on the page and taking no clicks. Nothing painted at all
   until the pointer has been somewhere - without that the lines would sit
   across the top-left corner from the moment the page loaded, which is not a
   position anyone chose.

   White lines at a low opacity on the frame rather than a gray line at full
   strength: it is the frame that carries the value, the way the design sets
   it, so the two lines cannot drift apart from each other. */
.crosshair {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
}

.crosshair.is_live {
  opacity: var(--crosshair_opacity);
}

/* Arriving from another page of the site, the crossing was already on screen
   and must not blink out for the frames it takes a module to load. The head
   puts the last known position into these two variables before the first
   paint, and the lines stand there until the module writes its own transform
   over the top. */
.is_pointed .crosshair {
  opacity: var(--crosshair_opacity);
}

.is_pointed .crosshair_x {
  transform: translateX(calc(var(--crosshair_x) - var(--crosshair_weight) / 2));
}

.is_pointed .crosshair_y {
  transform: translateY(calc(var(--crosshair_y) - var(--crosshair_weight) / 2));
}

.crosshair_line {
  position: absolute;
  top: 0;
  left: 0;
  background-color: var(--crosshair);
  will-change: transform;
}

.crosshair_x {
  width: var(--crosshair_weight);
  height: 100%;
}

.crosshair_y {
  width: 100%;
  height: var(--crosshair_weight);
}

/* The desktop view only. Below that the screen is touched rather than pointed
   at: there is no pointer to follow, so the lines would either stand where a
   finger last was or sit in the corner the page loaded in, and neither is a
   position anyone chose. */
@media (max-width: 1199px), (max-height: 540px) {
  .crosshair {
    display: none;
  }
}


/* --------------------------------------------------------------- header -- */

.header {
  position: absolute;
  inset: 0 0 auto 0;
  z-index: 2;
  display: flex;
  align-items: flex-start;
  gap: var(--gap);
  padding: var(--gutter) var(--gutter) 0;
}

.header_col {
  display: flex;
  flex: 1 1 auto;
  gap: var(--gap_inline);
  min-width: 0;
}

/* Two groups - the menu, then the address - a full column gap apart, while the
   links inside the menu sit at the tighter inline spacing. */
.header_col_end {
  flex: 0 0 auto;
  gap: var(--gap);
  justify-content: flex-end;
}

.header_menu {
  display: flex;
  align-items: center;
  gap: var(--gap_inline);
}


/* The address and the line under it move as one: the hint is hovered through
   the link, never on its own, so it is taken out of the hit area entirely -
   it sits below the group's box and would otherwise steal the hover back off
   the link the moment it appeared. */
.email_copy {
  position: relative;
  flex: 0 0 auto;

  /* Only as wide as the address. In the header it is a flex item and hugs
     anyway; standing in a column of its own it would otherwise take the whole
     measure, and the hint under it would drift off to the far edge. */
  width: fit-content;
}

.copy_hint {
  position: absolute;

  /* Directly under the address, no gap - one line pitch down, the way a
     caption sits under a picture and a value under its label. */
  top: 100%;
  right: 0;
  display: grid;
  height: var(--body_leading);
  overflow: hidden;
  color: var(--fg_dim);
  text-align: right;
  pointer-events: none;
}

/* Both states are the year's move, run from the stylesheet for the same reason
   the link hover is: two fixed states with nothing in between. Neither line is
   on show at rest - each waits above the window and drops in. */
.copy_hint_line {
  grid-area: 1 / 1;
  height: var(--body_leading);
  line-height: var(--body_leading);
  white-space: nowrap;
  will-change: transform, opacity;
  transform: translateY(calc(var(--swap_travel) * -1));
  opacity: 0;
  transition:
    transform 0.6s cubic-bezier(0.25, 0.2, 0, 1),
    opacity 0.35s cubic-bezier(0.7, 0.2, 0, 1);
}

/* Keyboard focus offers the same thing a hover does. It is read off the link
   rather than the group, because a mouse click focuses the link too and
   :focus-within would then keep the hint up after the pointer had gone.

   The focus half is unconditional; the hover half is not. See the link's own
   rules below for why. */
.header_email:focus-visible ~ .copy_hint .copy_hint_line:first-child {
  transform: translateY(0);
  opacity: 1;
}

@media (hover: hover) and (min-width: 1200px) and (min-height: 541px) {
  .email_copy:hover .copy_hint .copy_hint_line:first-child {
    transform: translateY(0);
    opacity: 1;
  }
}

/* After the copy the offer leaves downwards and the confirmation arrives from
   above - a forward step of the reel, in every value. The pointer has no say
   from here on: the confirmation answers the click, not the hover. */
.email_copy.is_copied .copy_hint .copy_hint_line:first-child {
  transform: translateY(var(--swap_travel));
  opacity: 0;
}

.email_copy.is_copied .copy_hint .copy_hint_line:last-child {
  transform: translateY(0);
  opacity: 1;
}

/* Its two seconds up, the confirmation leaves upwards - back over the top of
   the window, not on down past the offer - and nothing replaces it. That puts
   it exactly where a line waits at rest, so clearing the classes afterwards
   moves nothing.

   The offer stays out of sight below until the pointer has left and come back:
   an address that has just been copied should not be asking to be copied. */
.email_copy.is_done .copy_hint .copy_hint_line:first-child {
  transform: translateY(var(--swap_travel));
  opacity: 0;
}

.email_copy.is_done .copy_hint .copy_hint_line:last-child {
  transform: translateY(calc(var(--swap_travel) * -1));
  opacity: 0;
}


/* A way home, not a link: no rule under it, nothing to swap, and none of the
   hover a link carries - it behaves the way a logo would.

   display matters. A <p> was block and could be moved; an anchor is inline,
   and a transform does nothing to an inline box - so the entrance would have
   left it sitting where it was while everything beside it travelled. */
.header_name {
  display: block;
  color: var(--fg);
  text-decoration: none;
  white-space: nowrap;
}

/* On the home screen it leads where you already are. Out of reach rather than
   removed: the same thing the menu's current entry does. */
.header_name.is_here {
  pointer-events: none;
}

.header_role {
  color: var(--fg_dim);
  white-space: nowrap;
}

.link {
  position: relative;
  display: inline-flex;
  align-items: center;
  height: var(--body_leading);

  /* Its own window. What rolls in on arrival is the label inside; the rule sits
     at the bottom edge of this box and stays where it is. */
  overflow: hidden;
  color: var(--fg);
  text-decoration: none;
  white-space: nowrap;
}

/* The same link an octave up: the address on the about page is set in the
   title face, and its window has to be the taller line to match. */
.link_title {
  height: var(--title_leading);
  font-size: var(--title_size);
  letter-spacing: var(--title_tracking);
}

.link_title .link_swap {
  height: var(--title_leading);
}

.link_title .link_line {
  height: var(--title_leading);
  line-height: var(--title_leading);
}

/* The rule is painted, not a border: a border either eats a pixel out of the
   line or adds one to it, and the link has to measure exactly the line height
   so it sits level with the plain labels beside it. */
.link::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 1px;
  background-color: var(--fg_line);
  transition: background-color 0.4s ease;

  /* A pseudo-element cannot be tweened, so the entrance writes its own fade
     into this variable on the link and the rule reads it. At rest it is 1 -
     the rule is simply there. */
  opacity: var(--rule_opacity, 1);
}

/* The same move as the project reels, in the same direction as a forward step
   of theirs - the old line leaves downwards, the new one arrives from above -
   and at the same two speeds. A hover is a plain two-state toggle, so CSS does it. The curves go in verbatim here;
   it was the reels that needed a solver, because GSAP has no cubic-bezier. */
.link_swap {
  display: grid;
  height: var(--body_leading);
  overflow: hidden;
}

.link_line {
  grid-area: 1 / 1;
  display: block;
  height: var(--body_leading);
  line-height: var(--body_leading);
  white-space: nowrap;
  will-change: transform, opacity;
  transition:
    transform 0.6s cubic-bezier(0.25, 0.2, 0, 1),
    opacity 0.35s cubic-bezier(0.7, 0.2, 0, 1);
}

.link_line:first-child {
  transform: translateY(0);
  opacity: 1;
}

.link_line:last-child {
  transform: translateY(calc(var(--swap_travel) * -1));
  opacity: 0;
}

.link:focus-visible .link_line:first-child {
  transform: translateY(var(--swap_travel));
  opacity: 0;
}

.link:focus-visible .link_line:last-child {
  transform: translateY(0);
  opacity: 1;
}

.link:focus-visible::after {
  background-color: var(--fg);
}

/* Hover: the desktop view only, and only where there is something to hover
   with. Both halves are needed, and they answer different things.

   The pointer half is the device. A finger cannot hover, but a browser fakes
   one for it anyway: a tap raises hover on the way to the click and leaves it
   raised until something else is tapped. So on a phone the label of a link
   someone merely touched stayed rolled up and its rule stayed lit, on a page
   they had already moved on from. The state is not wrong on a touch screen so
   much as it is meaningless there, and it has no way to end. This half is what
   covers a phone held on its side and a laptop with a touch screen - neither
   of which is answered by a width.

   The view half is the design. The tablet and phone screens were not drawn
   with hover states, so they do not have them, and that has to hold when the
   view is reached by dragging a window narrow rather than by picking up a
   phone - which is how it is usually looked at.

   541 is one past the 540 the narrow views begin at, and 1200 is where the
   desktop view begins. Both are the breakpoints section's numbers, said the
   other way round; if those move, these move with them.

   Keyboard focus is deliberately not in here. It is the same state, it means
   the same thing, and it has to work on every device and in every view - a
   tablet with a keyboard case included. */
@media (hover: hover) and (min-width: 1200px) and (min-height: 541px) {
  .link:hover .link_line:first-child {
    transform: translateY(var(--swap_travel));
    opacity: 0;
  }

  .link:hover .link_line:last-child {
    transform: translateY(0);
    opacity: 1;
  }

  .link:hover::after {
    background-color: var(--fg);
  }
}

/* The page you are already on: underlined for good, and neither hoverable nor
   clickable - pointer-events also keeps the swap above from ever firing. */
.link_current {
  pointer-events: none;
}

.link_current::after {
  background-color: var(--fg);
}

/* On the way to another page this one gives the rule up, over the same time a
   hover takes to light one. The link being clicked is already white under the
   pointer, so what is left to see is this one going out. */
.is_leaving .link_current::after {
  background-color: var(--fg_line);
}

/* A column the width of one group of thumbnails, standing where the active
   group stands, the full height of the screen. Transparent: it is a target,
   not a surface.

   Under the header on purpose. On the tablet a group is wider than the screen,
   so this would otherwise lie over the menu and take its clicks; the header
   sits a layer above and keeps them.

   It does not take the strip's drags away either - those are watched on the
   window, so they arrive whatever is on top. What separates a tap from a drag
   is in main.js, where the strip's own state can be asked. */
.project_hit {
  position: absolute;
  top: 50%;
  left: var(--gutter);
  z-index: 1;
  width: var(--project_span);
  height: var(--thumb_h);
  translate: 0 -50%;
  padding: 0;
  border: 0;
  background: none;

  /* The keyboard's route in, and only the keyboard's: a pointer is answered by
     the group actually under it, which is not always the active one while the
     strip is on the move. Which is also why it is no taller than the strip -
     nothing is aimed at it, so it need only be somewhere sensible to land. */
  pointer-events: none;
  outline: none;
}


/* ----------------------------------------------------------- thumbnails -- */

/* The full height of the screen, not just the height of the strip. The
   thumbnails still sit on the middle line - the track below centers them - but
   each group now reaches from the top of the page to the bottom, because each
   group is what a tap opens a project from. */
.thumbs {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  cursor: grab;
  touch-action: none;
}

.thumbs.is_dragging {
  cursor: grabbing;
}

/* The track is translated as ONE element. Items are laid out by flexbox and
   never transformed individually - that keeps the per-frame work to a single
   transform write no matter how many thumbnails there are. */
.thumbs_track {
  display: flex;
  height: 100%;
  gap: var(--gap);
  padding-inline-start: var(--gutter);
  width: max-content;
  will-change: transform;
}

/* A column of its own: as tall as the page, with its thumbnails on the middle
   line. Tapping anywhere down it opens that project - not whichever one
   happens to be active, which is the point of giving every group its own
   column rather than standing one target at the gutter. */
.thumbs_list {
  display: flex;
  align-items: center;
  height: 100%;
  gap: var(--thumb_gap);
  flex: none;
  cursor: pointer;
}

.thumb {
  flex: none;
  width: var(--thumb_w);
  height: var(--thumb_h);
  margin: 0;
  overflow: hidden;
  background-color: var(--thumb_empty);
}

/* A picture is the browser's choosing made visible in the markup, not a box.
   Taken out of the layout entirely, so every rule written for the image still
   lands on the image and nothing had to be rewritten around the wrapper. */
picture {
  display: contents;
}

.thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
  -webkit-user-drag: none;
  user-select: none;
}

/* --------------------------------------------------------- project info -- */

/* Starts where the strip ends and runs to the bottom; the strip being centered
   is what leaves an equal band above. */
.project {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  top: 50%;
  margin-top: calc(var(--thumb_h) / 2);
  z-index: 2;
  display: flex;
  flex-direction: column;
  /* Both rows sit at the foot of the screen, a line's breath apart - the
     title used to stand at the top of this block and the rest at the bottom,
     and the design has since brought them together. */
  justify-content: flex-end;
  gap: var(--gap_inline);
  padding: var(--gutter);
  pointer-events: none;
}

/* The title has the row to itself now, and the width of the page with it. */
.project_info_top {
  display: flex;
  align-items: flex-end;
}

.project_info_top .line {
  flex: 1 1 0;
  min-width: 0;
}

/* The roles on the left, the years on the right - both in the dim color, and
   both reels of their own, so they swap with the project the way the title
   does. */
.project_info_bottom {
  display: flex;
  gap: var(--gap);
  align-items: flex-end;
}

/* the links inside have to be usable again */
.project_info_bottom .link {
  pointer-events: auto;
}

/* Not two halves of the row. The year is as wide as the year is - it stands at
   the margin and takes what it needs - and the roles have everything else,
   which is the whole row less the year and one gap. The artboards are drawn
   from exactly that: 1824 across the desktop leaves 1715 beside a 61-wide
   year and a 48 gap; the tablet's 746 leaves 653; the phone's 382 leaves 307.

   It matters most on the phone, where halves would have given the roles 179
   pixels to say 'UX/UI Design, Design System' in - and the reel they sit in is
   a clipping window, so what did not fit would simply have been cut. */
.project_info_col {
  display: flex;
  flex: 1 1 0;
  align-items: center;
  min-width: 0;
}

.project_info_col_end {
  flex: 0 0 auto;
  justify-content: flex-end;
}

/* One line per project, all in the same grid cell so they overlap while one
   swaps for another. Only the active line is displayed, so the box sizes to
   whichever title is showing. */
/* Lines that sit in a clipping window need the shift, and so does anything
   level with them - otherwise a shifted link and an unshifted label beside it
   sit a pixel apart. */
.roller_line,
.link_line,
.copy_hint_line,
.modal_year,
.modal_agency,
.modal_label,
.about_bio,
.about_call,
.about_caption,
.about_links_label,
.about_copyright_value,
.line_text,
.modal_progress_value,
.header_name,
.header_role {
  position: relative;
  top: var(--optical_shift);
}

/* The five of those that are set at title size rather than body size. They
   are listed here rather than given their own token in place because the
   shift belongs to the size of the type, and these are the only places the
   two disagree. */
.roller_title .roller_line,
.modal_title .line_text,
.link_title .link_line,
.about_bio,
.about_call {
  top: var(--optical_shift_title);
}

.roller {
  display: grid;
  height: var(--title_leading);

  /* A tight window: a line entering or leaving slides in and out behind these
     edges rather than moving in clear space. */
  overflow: hidden;
}

/* Hidden from the first paint, not from the first script: the lines are
   siblings in one grid cell, so leaving that to JS shows the whole stack
   piled on top of itself until the module runs. */
.roller_line {
  grid-area: 1 / 1;
  display: none;
  height: var(--title_leading);
  line-height: var(--title_leading);
  white-space: nowrap;
  will-change: transform, opacity, filter;

  /* A title too wide for its column ends in an ellipsis rather than being cut
     mid-letter at the edge. */
  overflow: hidden;
  text-overflow: ellipsis;
}

.roller_line:first-child {
  display: block;
}

.roller_title {
  font-size: var(--title_size);
  letter-spacing: var(--title_tracking);
}

.roller_role,
.roller_year {
  height: var(--body_leading);
  color: var(--fg_dim);
}

.roller_role .roller_line,
.roller_year .roller_line {
  height: var(--body_leading);
  line-height: var(--body_leading);
}

/* The year sits against the right gutter, so its lines have to as well.

   Every line of a reel shares one grid cell, and during a swap two of them
   are shown at once, so the cell is as wide as the wider. Left-aligned, a
   short year - 2021 against 2022-2025 - entered at the left of that wide cell
   and only reached the gutter when the outgoing line stopped being displayed
   and the cell collapsed: a jump of about fifty pixels, after the movement
   had finished. Aligned right, it enters where it ends up, and the cell edge
   that moves has nothing drawn on it. */
.roller_year .roller_line {
  text-align: right;
}



/* --------------------------------------------------------------- about -- */

/* One screen, nothing to scroll: everything is placed against the page's own
   edges or across it. Laid out for the wide view only so far - narrower than
   the modal's breakpoint it falls back to a plain column, which is a holding
   position rather than a design. */
/* The same three columns the modal is built on, and for one reason: so the
   words in the middle come out exactly as wide as the screenshots do there.
   The pictures keep the sizes they were drawn at - only the tracks they are
   placed against are shares.

   Everything inside a column is placed against that column's own edges, which
   is how the design builds it: the portrait is at the right of the middle
   column, not at some distance from the page. */
.about {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr);
  column-gap: var(--gap);
  padding-inline: var(--gutter);
}

.about_col {
  position: relative;
}

/* The words are centered on the page top to bottom, and centered in their own
   column across - which is where the 64 either side of them comes from. */
.about_text {
  position: absolute;
  z-index: 2;
  inset-inline: 0;
  top: 50%;
  max-width: var(--col_middle);
  margin-inline: auto;
  translate: 0 -50%;
}

.about_bio,
.about_call {
  margin: 0;
  font-size: var(--title_size);
  line-height: var(--title_leading);
  letter-spacing: var(--title_tracking);
}

/* Cut into one window per line so each can roll out from under its own edge.
   The shared rule is written for a line of body text; these are the title's
   measure. */
.about_bio .line_text,
.about_call .line_text {
  height: var(--title_leading);
  line-height: var(--title_leading);

  /* No optical shift, unlike every other line of its kind. The address sits
     directly under these two and is a link, and a link's label carries none -
     shifting one of a stack and not the other is the misalignment this is
     meant to prevent. */
  top: 0;
}

/* Wider than a paragraph space and narrower than a whole line: the design
   sets its own distance here, so it gets its own token rather than borrowing
   the leading and landing four pixels out. */
.about_call {
  margin-top: var(--about_block_gap);
}

/* Words over pictures, inside the about layer. Without saying so they paint in
   the order they are written, and the portrait is written after the paragraph
   it stands beside - so on any screen narrow enough for the two to meet, the
   picture covered the text. Both numbers are read only against each other:
   the layer they are in sits under the grain either way. */
.about_media {
  position: absolute;
  z-index: 1;
  margin: 0;
  opacity: var(--about_media_opacity);
}

/* What comes in the way a card does. The figure itself is placed against the
   column's edges, and one of them is centered with a translate of its own - a
   transform written on the figure would be overwritten the moment the entrance
   touched it, so the picture travels inside its box rather than the box
   traveling. The label below is not in here: it is text, and it arrives the
   way the rest of the text does. */
.about_rise {
  display: block;
}

.about_asset {
  display: block;
  width: 100%;
  height: auto;
  background-color: var(--thumb_empty);
}

/* Directly under its picture, one line pitch down - no gap, the way a label
   sits under a field in the modal. Body in every other respect: it takes the
   page's own size, leading, tracking and case.

   It is also its own one-line window, so the label rolls out from under its
   own edge on arrival like every other line of text here, rather than riding
   in on the picture. */
.about_caption {
  margin: 0;
  color: var(--fg_dim);
}

/* An inline box takes a transform and does nothing with it. */
.about_caption_text {
  display: block;
  height: var(--body_leading);
  line-height: var(--body_leading);
  white-space: nowrap;
}

/* Centered on the picture, not on the figure. The caption hangs below it and
   would otherwise pull the whole thing up by half its own height, so the frame
   carries a matching space above - one line, the caption's own. */
.about_media_sirius {
  left: 0;
  top: 50%;
  width: var(--about_sirius_w);
  padding-top: var(--body_leading);
  translate: 0 -50%;
}

/* Held against the right edge of the middle column and dropped past the
   page's own margin: the caption hangs into the bottom band, half a margin
   clear of the edge, rather than sitting on the line the links and the
   copyright share. */
.about_media_me {
  right: 0;
  bottom: calc(var(--gutter) / 2);
  width: var(--about_me_w);
}

.about_media_boulder {
  right: 0;
  top: calc(var(--gutter) + var(--body_leading) + var(--gap));
  width: var(--about_boulder_w);
}

.about_links {
  position: absolute;
  z-index: 2;
  left: 0;
  bottom: var(--gutter);
}

.about_links_label {
  margin: 0;
  color: var(--fg_dim);
}

.about_links_list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.about_links_list li {
  display: block;
}

/* The window is what is anchored; the shared optical shift sits on the line
   inside it, where 'top' is an offset rather than a second edge. */
.about_copyright {
  position: absolute;
  z-index: 2;
  right: 0;
  bottom: var(--gutter);
  height: var(--body_leading);
  text-align: right;
}

.about_copyright_value {
  margin: 0;
  color: var(--fg_dim);
}

/* The narrow views. The three columns go: the artboards hide the outer two and
   let the middle one run the width of the page, so the grid collapses to a
   single track and all three columns are laid into that one cell, stacked.

   They keep their boxes rather than being unwound into a flow, which is the
   point - every piece on this screen is placed against its column's own edges,
   and those edges are now the page's. So most of the page needs no saying
   again: the words stay centered on the screen, the boulder stays under the
   header band at the right, the links and the year stay in the bottom
   corners. Only the two pieces below actually move, and the sizes, which are
   tokens. */
@media (max-width: 1199px), (max-height: 540px) {
  .about {
    grid-template-columns: minmax(0, 1fr);
  }

  .about_col {
    grid-area: 1 / 1;
  }

  /* The measure goes with the modal's. This is the width at which the modal
     stops standing its description beside its screenshots and stacks them, and
     stacked they both run the full page - so the cap that held these words to
     800 has nothing left to agree with. Keeping it would have put the one
     thing the two screens share, the width of the middle, out by everything
     between 800 and the page. */
  .about_text {
    max-width: none;
  }

  /* The labels go. They are the one piece of this screen that names a file
     rather than saying anything, and they were readable because the pictures
     were the subject - which below this width they are not. Gone rather than
     faded: there is no size at which an unreadable caption is better than no
     caption. Nothing moves for it, because the two landscape pictures hang
     from their top edge and the portrait stands on its bottom one.

     Named through the figure to outweigh '.line', which every caption also is
     and which sets a display of its own further down the sheet. Two classes
     of the same weight are settled by which comes last, and that one does. */
  .about_media .about_caption {
    display: none;
  }

  /* Down from the line the boulder hangs on by two margins, rather than centered
     on the screen: with one column the two landscape pictures share a band,
     and the design steps the near one down instead of letting them sit level.
     The space above the caption goes with the centering that wanted it. */
  .about_media_sirius {
    top: calc(
      var(--gutter) + var(--body_leading) + var(--gap) + var(--gutter) * 2
    );
    padding-top: 0;
    translate: none;
  }

  /* One margin in from the edge the links and the year stand on, and lifted
     off the foot of the screen by exactly what the boulder hangs from the top
     by - the portrait closes the page the way the boulder opens it. */
  .about_media_me {
    right: var(--gutter);
    bottom: calc(var(--gutter) + var(--body_leading) + var(--gap));
  }
}


/* --------------------------------------------------------------- modal -- */

/* Over the page, not instead of it: what is behind stays where it was and
   shows through, so closing puts the visitor back on the thumbnail they came
   from rather than on a page that has been rebuilt underneath them. */
.modal {
  position: absolute;
  inset: 0;
  z-index: 3;
  touch-action: none;

  /* Comes up from nothing on the same curve the words use, so the page is not
     covered before it has been seen going. Written out here rather than shared
     with the module because this is a two-state toggle, and CSS does those -
     the same reason the link hover is not a tween either. */
  background-color: transparent;
  -webkit-backdrop-filter: blur(0);
  backdrop-filter: blur(0);
  transition:
    background-color 0.35s cubic-bezier(0.7, 0.2, 0, 1),
    -webkit-backdrop-filter 0.35s cubic-bezier(0.7, 0.2, 0, 1),
    backdrop-filter 0.35s cubic-bezier(0.7, 0.2, 0, 1);
}

/* The flat color is the fallback for a browser without color-mix; it loses the
   show-through, not the modal. */
/* It is given the focus when it opens so that what follows starts inside the
   dialog, but it is a layer rather than a control and has nothing to show for
   it. */
.modal:focus {
  outline: none;
}

.modal.is_veiled {
  background-color: var(--bg);
  background-color: color-mix(in srgb, var(--bg) var(--modal_veil), transparent);
  -webkit-backdrop-filter: blur(var(--modal_blur));
  backdrop-filter: blur(var(--modal_blur));
}

.modal_close[hidden] {
  display: none;
}

.modal[hidden] {
  display: none;
}

/* Emptied and on its way out: the veil is still painted, but the page under it
   is already answering again. */
.modal.is_leaving {
  pointer-events: none;
}

/* Pinned, not scrolled: the title stays at the top of the screen and the role
   at the bottom of it however far the screenshots have run. */
.modal_info {
  position: absolute;
  top: var(--gutter);
  bottom: var(--gutter);
  left: var(--gutter);
  width: var(--col_side_w);
  display: flex;
  flex-direction: column;

  /* Description under the title, the rest against the bottom edge. Narrower
     than this the column runs top to bottom instead - see the breakpoint. */
  justify-content: space-between;
}

.modal_intro,
.modal_meta {
  display: flex;
  flex-direction: column;
  gap: var(--modal_field_gap);
}

/* The title stands further off its first field than the fields do from one
   another. */
.modal_intro {
  gap: var(--modal_intro_gap);
}

/* One line's worth of window per piece of text - the same tight edge a link's
   label rolls out from under. What moves is the child; the window stays. Used
   by the modal and by the home screen alike. */
.line {
  display: block;
  overflow: hidden;
}

.modal_title {
  margin: 0;
  font-size: var(--title_size);
  line-height: var(--title_leading);
  letter-spacing: var(--title_tracking);
  font-weight: inherit;
}

/* Its lines are the title's measure, not the body's - the shared rule is
   written for a line of body text. The optical shift is left where that shared
   rule puts it: this title stands on its own, with nothing unshifted beside it
   to disagree with. */
.modal_title .line_text {
  height: var(--title_leading);
  line-height: var(--title_leading);
}

/* A field is a dim label with its value directly beneath it - one line pitch
   apart, no gap, the way the design sets them. */
.modal_field {
  display: flex;
  flex-direction: column;
}

.modal_label {
  margin: 0;
  color: var(--fg_dim);
}

.modal_year,
.modal_agency {
  margin: 0;
}

.modal_blurb {
  display: flex;
  flex-direction: column;
  gap: var(--para_gap);
}

.modal_para {
  margin: 0;
}

/* One line of a paragraph, held to exactly one line so its window cannot show
   the one below it, and stopped from re-wrapping now that it stands alone. */
.line_text {
  display: block;
  height: var(--body_leading);
  line-height: var(--body_leading);
  white-space: nowrap;
}

.modal_role_list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.modal_role_list li {
  display: block;
}

/* The window the screenshots run behind. It does not scroll itself - the track
   inside it is translated, the way the home strip is - so there is no scrollbar
   to hide, and the percentage in the corner is the only readout. */
.modal_media {
  position: absolute;
  inset: 0;

  /* The middle column: half of what the three share, and never wider than the
     measure the screenshots were drawn at. */
  width: var(--col_middle_w);
  margin-inline: auto;
  overflow: hidden;
}

/* Sheared as one element, the way the strip is - never image by image. */
.modal_shots {
  display: flex;
  flex-direction: column;
  gap: var(--thumb_gap);
  padding-block: var(--gutter);
  will-change: transform;
}

.modal_shot {
  margin: 0;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background-color: var(--thumb_empty);
}

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

/* Inset the same distance from both edges it sits against - a little inside
   the page's margin rather than on it, which is what a round target wants
   next to a square one.

   It sits in the stage rather than in the modal, and above the grain. It has
   to: the modal carries a backdrop-filter, which opens a stacking context of
   its own whatever its z-index, so nothing inside it can be lifted past a
   layer outside it. The screenshots keep their grain because the modal stays
   under it; only this comes out. */
.modal_close {
  position: absolute;
  z-index: 5;
  top: var(--close_inset);
  right: var(--close_inset);
  width: var(--close_size);
  height: var(--close_size);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  /* The ring is there from the start, in the color a link's rule rests in,
     and lights the same way on hover - the button is a link in everything but
     where it goes. A shadow rather than a border: a border would take its
     width out of the box, and the box is the target size. */
  /* A shadow rather than a border: a border would take its width out of the
     box, and the box is the target size. */
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
  box-shadow: inset 0 0 0 1px var(--close_ring);
  cursor: pointer;
  transition: box-shadow 0.4s ease;
}

.modal_close:focus-visible {
  box-shadow: inset 0 0 0 1px var(--fg);
}

@media (hover: hover) and (min-width: 1200px) and (min-height: 541px) {
  .modal_close:hover {
    box-shadow: inset 0 0 0 1px var(--fg);
  }
}

/* Two hairlines crossing, drawn on the button itself.

   They hang off the button rather than off a box of their own on purpose. A
   box sized to the cross is the stroke turned a quarter turn - the arm over
   root two - which is never a whole number: at a 13px arm it is 9.19, and
   centering that in the button leaves it starting on a fraction of a pixel.
   Every hairline rotated out of that box then rasterises off-center by
   whatever the fraction was. Half of the button is 24px exactly, so measuring
   from there is the one place in the chain where nothing is left over. */
.modal_close::before,
.modal_close::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--close_arm);
  height: 1px;
  background-color: var(--fg);

  /* The label's timing, not the rule's: this is the button's content moving,
     and on a link it is the label that travels on the longer of the two. */
  transition: transform 0.6s cubic-bezier(0.25, 0.2, 0, 1);
}

.modal_close::before {
  transform: translate(-50%, -50%) rotate(45deg);
}

.modal_close::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

/* A quarter turn each, in opposite directions - each arm ending where the
   other began. The cross it lands on is the one it started from, since a cross
   turned a quarter is itself, so what there is to see is the journey: halfway
   through both arms lie flat, the mark is a single horizontal stroke, and then
   it opens again. */
.modal_close:focus-visible::before {
  transform: translate(-50%, -50%) rotate(-45deg);
}

.modal_close:focus-visible::after {
  transform: translate(-50%, -50%) rotate(45deg);
}

@media (hover: hover) and (min-width: 1200px) and (min-height: 541px) {
  .modal_close:hover::before {
    transform: translate(-50%, -50%) rotate(-45deg);
  }

  .modal_close:hover::after {
    transform: translate(-50%, -50%) rotate(45deg);
  }
}

/* The window is what is anchored here; the shared optical shift sits on the
   number inside it, where 'top' is an offset rather than a second edge. */
.modal_progress {
  position: absolute;
  right: var(--gutter);
  bottom: var(--gutter);
  height: var(--body_leading);
}

.modal_progress_value {
  margin: 0;
}


/* Off the corner and onto the middle of the foot, where it stands over the
   images rather than beside the words. It carries the veil's own blur in a
   circle of its own, so it stays legible over a screenshot of any color
   without painting anything on top of one. */
@media (max-width: 1199px), (max-height: 540px) {
  .modal_close {
    top: auto;
    right: auto;
    bottom: var(--gutter);
    left: 50%;
    translate: -50% 0;

    /* Ground and blur of its own: over a screenshot of any color the ring
       alone would be lost, so the button carries the veil's treatment in
       miniature. The flat color is the fallback for a browser without
       color-mix. */
    background-color: rgba(27, 27, 27, 0.48);
    background-color: var(--close_ground);
    -webkit-backdrop-filter: blur(var(--close_blur));
    backdrop-filter: blur(var(--close_blur));
  }
}

@media (max-width: 719px), (max-height: 540px) {
  .modal_close {
    --close_arm: 9px;
  }
}

/* Below the widest breakpoint the modal is one column: the description still
   pinned, but above the images rather than beside them, and the screenshots
   running the full width beneath it. The percentage goes with the two-column
   layout it belonged to.

   The description starts at the page's own margin. It used to clear a line
   before it - the close button was in the corner then and the words had to
   come below it - and now that the button has gone to the foot of the screen
   there is nothing up there to clear. */
@media (max-width: 1199px), (max-height: 540px) {
  :root {
    --modal_beside: 0;
    --close_size: 64px;
    --modal_field_gap: 12px;
    --modal_intro_gap: 24px;
  }

  .modal_info {
    top: var(--gutter);
    bottom: auto;
    left: var(--gutter);
    right: var(--gutter);
    width: auto;
    gap: var(--modal_field_gap);
  }

  .modal_media {
    width: auto;
    inset-inline: var(--gutter);
  }

  .modal_progress {
    display: none;
  }

  /* Room under the last screenshot, and it is exactly what it takes to clear
     the button that floats there: the page's margin, the button, and the
     margin again. Which is the design's own number both ways round - 128 on
     the tablet, 96 on the phone. */
  .modal_shots {
    padding-bottom: calc(var(--gutter) * 2 + var(--close_size));
  }
}

@media (max-width: 719px), (max-height: 540px) {
  :root {
    --close_size: 48px;
    --modal_field_gap: 8px;
    --modal_intro_gap: 16px;
  }
}


/* --------------------------------------------------------------- intro -- */

/* Against the top right gutter, level with the header's own line - the name is
   at one end of that line and this is at the other. Out of the page entirely
   unless the entrance is actually running: with scripting off or motion turned
   down there is nothing to take it away again, and it would sit over the page
   for good. */
.splash {
  position: absolute;
  inset: var(--gutter) var(--gutter) auto auto;
  z-index: 2;
  display: none;
  pointer-events: none;
}

/* The same box a link's label sits in, down to the clip: the name rolls out
   from under this edge on the way in and back under it on the way out. */
.splash_line {
  display: block;
  height: var(--body_leading);
  overflow: hidden;
}

.splash_text {
  display: block;
  height: var(--body_leading);
  line-height: var(--body_leading);
  white-space: nowrap;
  will-change: transform, opacity, filter;
}

/* The same white the name carries, and the same measure - the two are the ends
   of one line. Tabular figures so the number does not shuffle sideways on its
   way to 100: proportional ones make a 1 narrower than a 7, which on a counter
   reads as the text twitching rather than counting. */
.splash_percent {
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}


/* Where the entrance starts. The module reads these back and tweens away from
   them, so the number lives in one place. Under a reduced-motion setting the
   block never applies and the page is simply there. */
@media (prefers-reduced-motion: no-preference) {
/* Only ever in the layout for the site's own opening, and only while the
     entrance is running. Both halves matter: every other arrival has no splash
     to play, and this one has to stop applying when the entrance ends, or
     clearing what the timeline wrote would put the splash straight back on the
     screen. */
  .is_intro.is_opening .splash {
    display: block;
  }

  .is_intro .splash_text {
    opacity: 0;
    transform: translateY(calc(var(--swap_travel) * -1));
  }

  /* Closed against its own top edge, and it opens downwards - the same arrival
     the about screen's pictures make. */
  .is_intro .thumb {
    clip-path: inset(0% 0% 100% 0%);
  }

  .is_intro .project {
    opacity: 0;
  }

  /* The header arrives with the site and never again. Between two pages it
     stood through the exit on the one before and it stands through the arrival
     here: fading it in would be the one thing the change is meant not to do.

     Which is why the opening is named here and not just the entrance: the
     about screen runs an entrance of its own, and its header is one that is
     already on the visitor's screen. The entrance is named too, and has to be
     - it is what ends this, the same way it ends every other start state
     below. Without it the rule outlives the timeline that was tweening away
     from it, and the header goes out the moment the entrance lets go. */
  .is_intro.is_opening .header {
    opacity: 0;
  }

  /* The about screen enters the same way, on the same two distances: its
     pictures come the length a card comes, its words the length a line does.
     The header is not in here - it stood through the exit on the page before
     and it stands through the arrival. */
  .is_intro .about {
    opacity: 0;
  }

  /* Nothing still on its way in answers the pointer.

     Opacity hides a thing without moving it out of the way: the email address
     in the header is invisible for the length of the opening and hoverable
     the whole time, and the hint that belongs to it is not inside the span
     being faded - so "Click to copy" appeared beside nothing at all. The
     about screen's socials and its own email are the same case.

     The header only while the site is opening. Between two pages it never
     left, and taking its menu away for the length of an arrival would be the
     one thing a page change is meant not to do. */
  .is_intro.is_opening .header,
  .is_intro .about {
    pointer-events: none;
  }

  /* Closed against its own top edge, and it opens downwards. No opacity here:
     a picture nothing has uncovered yet cannot be seen, and fading one that is
     already being uncovered only makes the uncovering harder to read. */
  .is_intro .about_rise {
    clip-path: inset(0% 0% 100% 0%);
  }

}


/* ---------------------------------------------------------- breakpoints -- */

/* The two narrow views: desktop from 1200 up, tablet 720 to 1199, phone below
   720 - or below 540 tall, whichever comes first. None of the three is an
   artboard size; they are where the devices fall, which is not the same
   question as where the design was drawn.

   1200 up top, because every iPad lands under it in landscape: 1080, 1133,
   1180, 1194. Only the 13-inch goes past, at 1366, and a screen that size with
   a keyboard in front of it has earned the wide view. Below it the smallest
   laptops - 1280, 1366 - stay where they belong.

   720 at the bottom, and not the artboard's own 810, which is what it was.
   810 is the iPad 10.2 in portrait, and that is the widest of the common
   tablets rather than the narrowest: the iPad mini is 744 across and a great
   many Android tablets are 800, so both of them were being handed the phone.
   720 takes them and still clears the widest phone in portrait - around 480 -
   by a quarter of a screen, so nothing held in one hand can reach it. The
   tablet view stretches 90px below the width it was drawn at to do it, which
   its two landscape pictures have the room for: 468 of fixed width inside 656
   of page.

   540 is the third number, and it is a height rather than a width, because a
   phone lying on its side is not a narrow screen: it is 932 across and 430
   tall, so on width alone it took the tablet view and then had half the
   height either view was drawn for. Turning the phone over should not change
   which view it gets.

   The gap it sits in is a wide one. A phone on its side is as tall as it is
   wide standing up, which is 375 to about 448 at the largest; a tablet on its
   side is 744 at the smallest, and the shortest laptop worth worrying about -
   a 1366 by 768 screen, less the browser's own furniture - is around 600. So
   540 clears the tallest phone by ninety and stays sixty short of the lowest
   laptop, and nothing has to be guessed about the ones in between, because
   there are none.

   It is on both narrow queries rather than only the phone's. The phone view is
   written as a set of differences from the tablet's - the opacity of the about
   screen's pictures is only ever set there, and so is the whole of its layout
   - so a short screen has to enter the cascade at the top of it. Which also
   means a desktop window dragged shorter than 540 gets the phone view whole
   rather than half of it. That is the honest answer for a layout that is one
   fixed screen with no scroll: the small scale is the one that fits. */

/* home-tablet (810 artboard) */
@media (max-width: 1199px), (max-height: 540px) {
  :root {
    --gutter: 32px;
    --gap: 32px;
    --gap_inline: 12px;
    --para_gap: 8px;

    /* about-tablet. Three quarters of the drawn desktop sizes, to the pixel. */
    --about_block_gap: 24px;
    --about_media_opacity: 0.8;
    --about_boulder_w: 240px;
    --about_sirius_w: 228px;
    --about_me_w: 148px;
  }
}

/* home-mobile (430 artboard) */
@media (max-width: 719px), (max-height: 540px) {
  :root {
    --gutter: 24px;
    --gap: 24px;
    --gap_inline: 8px;

    /* Its own type scale: 'Title Mobile' 20/22 and 'Body Mobile' 10/14. */
    --body_size: 10px;
    --body_leading: 14px;
    --para_gap: 4px;
    --title_size: 20px;
    --title_leading: 22px;

    /* 10px x 0.094 = 0.94, and 20px x 0.094 = 1.88. */
    --optical_shift: 1px;
    --optical_shift_title: 2px;

    /* Pulled back, for a grain that is not drawn any differently here. It is a
       size in css pixels and the canvas is not multiplied by the pixel ratio -
       on purpose, grain is not meant to get finer because the screen did - so
       on a 3x phone one grain covers three device pixels where it covers one
       on a desktop. Set against type that is smaller here too, and held closer
       to the eye, it comes out about twice the size it reads at on a desktop,
       and a grain that size reads as more noise than it did.

       Which argued for half. Half took too much out and three quarters put
       too much back; this sits between them, and it is the screen's answer
       rather than the arithmetic's. */
    --grain_opacity: 0.025;

    --thumb_w: 128px;
    --thumb_h: 96px;
    --thumb_gap: 2px;
    --intro_rise: 48px;
    --swap_travel: 18px;

    /* about-mobile. Not a share of the tablet's the way the tablet is of the
       desktop's - each one is drawn for the screen it is on. */
    --about_block_gap: 16px;
    --about_boulder_w: 136px;
    --about_sirius_w: 128px;
    --about_me_w: 90px;
  }

  /* The artboard drops the address and keeps the menu, */
  .header .email_copy {
    display: none;
  }

}

/* --------------------------------------------------- motion preferences -- */

@media (prefers-reduced-motion: reduce) {
  .link::after,
  .link_line,
  .copy_hint_line,
  .modal {
    transition: none;
  }
}
