/* ============================================================================
   SHELL
   The page furniture the blocks do not carry: the nav, the site footer, the
   page rhythm, and the case-study body. Everything else on this site is a
   block from /blocks, pasted whole and left alone.

   Written to design-system/SKILL.md. The five non-negotiables apply here too:

     1. hard relief, never a blur:  box-shadow: Npx Npx 0 0 var(--c)
     2. everything at 23 degrees (67deg for a gradient, which is 90 minus 23)
     3. square corners, hairline rules
     4. nothing hovers on a phone: every hover inside @media (hover:hover)
     5. dark is a token swap and nothing else

   ONE DEPARTURE FROM THE SKILL, AND WHY.
   The skill says tokens live on the block's own root class and never on :root.
   That rule exists because of Webflow: two embeds on one page would fight, and
   anything on :root would leak into the client stylesheet. Off Webflow there is
   no client stylesheet and no second owner, so the shell declares tokens on
   :root and the blocks keep their own scoped copies. That keeps every block
   still pasteable anywhere, unchanged, while the shell stops repeating the
   palette in six places.
   ========================================================================= */

:root {
  color-scheme: light;

  --ink:    #16181A;
  --ink-2:  #5C5F63;
  --card:   #FBFAF7;
  --paper:  #FFFFFF;
  --line:   rgba(22, 24, 26, .16);
  --line-2: rgba(22, 24, 26, .07);

  --warm: #D6541F;
  --teal: #0E6F70;
  --deep: #1372A5;
  --iris: #7C3E9C;

  /* The archive trio. A replacement for the hue cycle, not an addition: the
     blocks that carry it use no --warm at all, because the accent belongs to
     work that is current. */
  --pine:   #1D6B5F;
  --indigo: #3C5C93;
  --plum:   #71466B;

  /* The ground behind a screenshot. White in both themes: the project
     thumbnails are exported on a transparent ground, so a dark plate behind a
     light screenshot reads as a hole. */
  --shot: #FFFFFF;

  /* The rule drawn ON --shot, and the reason it needs its own token.
     --ink flips with the theme, --shot does not. Use var(--ink) for a rule on
     a white plate and in dark mode you get a near-white rule on white paper:
     it disappears into the thing it is drawn on. A light gray disappears the
     same way, only more gently. What reads against BOTH a white plate and a
     near-black page is a mid gray, so that is what dark gets. Light keeps
     full ink, because there the rule is separating white paper from a white
     page and it has to carry that on its own. */
  --shot-rule: #16181A;

  --display: "Bricolage Grotesque", ui-sans-serif, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  --body:    "Instrument Sans", ui-sans-serif, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  --mono:    "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --ease:   cubic-bezier(.2, .9, .25, 1);   /* the house curve, for everything */
  --settle: cubic-bezier(.34, 1.4, .5, 1);  /* overshoot, only for coming to rest */

  /* The measure of the sheet. ONE number for the whole site: nav, footer, the
     three public pages and the case studies all sit on it, so the page's outer
     edge never moves as you navigate. It used to be 1120 here and 1360 on the
     case studies, which put the nav and footer on a different edge from the
     case body and made the content burst out around its own chrome.

     1360 is the case-study number, because that is the constrained one: the
     index rail takes 208px off the left and the diagram blocks want their
     column back. The three public pages are the unconstrained ones and simply
     follow. Blocks were drawn against 1120 and hold at either width; nothing
     in them is container-queried.

     --gut has to grow with it. --sheet is a MAX, so between about 1170 and
     1410 the sheet is fluid and the gutter is the ONLY outer margin there is.
     At the old 26px cap a 1280 laptop would have run nearly edge to edge the
     moment the sheet went to 1360. Scaling the gutter keeps real breathing
     room at every width instead of only on screens wide enough to cap out. */
  --sheet: 1360px;
  --gut: clamp(15px, 1rem + 2vw, 44px);

  /* The nav is sticky, so an anchor target has to clear it. One number, read
     by scroll-padding and by the index rail's script alike. */
  --nav-h: 72px;
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --ink:    #F1F0EB;
  --ink-2:  #9B9C9E;
  --card:   #17191C;
  --paper:  #101214;
  --line:   rgba(241, 240, 235, .18);
  --line-2: rgba(241, 240, 235, .08);

  /* --shot itself is NOT redefined here: it is white in both themes. Its rule
     is, because a white rule on white paper is no rule. */
  --shot-rule: #9B9C9E;

  --warm: #E8703C;
  --teal: #3FA39B;
  --deep: #4BA6D6;
  --iris: #B084DE;

  --pine:   #49A896;
  --indigo: #7C97D6;
  --plum:   #B383AC;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 20px);
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-tap-highlight-color: transparent;
  overflow-x: hidden;
}

img, svg, canvas { display: block; max-width: 100%; }
img { height: auto; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
h1, h2, h3, h4 { margin: 0; font-family: var(--display); }
p { margin: 0 0 1em; }
p:last-child { margin-bottom: 0; }

:focus-visible { outline: 2px solid var(--warm); outline-offset: 4px; }
:focus:not(:focus-visible) { outline: none; }
::selection { background: var(--warm); color: #fff; }

.sheet { width: 100%; max-width: var(--sheet); margin-inline: auto; padding-inline: var(--gut); }
.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* The section label above a block. Small, set in the display face, sitting on
   the same left edge as the block under it. */
.eyebrow {
  font-family: var(--display);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: -.01em;
  color: var(--ink);
  margin: 0 0 14px;
}

.band { padding-block: clamp(26px, 1.5rem + 2.2vw, 46px); }
.band--tight { padding-block: clamp(14px, .8rem + 1vw, 24px); }
.rule { border: 0; border-top: 1px solid var(--line-2); margin: 0; }

/* ══ NAV ══════════════════════════════════════════════════════════════════
   The live nav is a wordmark and three links. This keeps that and adds only
   what the site now needs and Webflow could not give it: a theme switch, a
   sheet menu on a phone, and one door out to the side project.

   Every moving part answers a question the visitor would otherwise have to
   work out: where does the sticker go (it says), where am I and where would
   this take me (one bar answers both), does this leave the site (an arrow
   says so, everywhere, the same way).
   ======================================================================== */

.nav {
  position: sticky;
  top: 0;
  z-index: 80;
  background: var(--paper);
  border-bottom: 1px solid transparent;
  transition: border-color .28s var(--ease), transform .28s var(--ease);
}
body.nav-stuck .nav { border-bottom-color: var(--line-2); }
/* Hides going down, returns going up. A case study runs to eight thousand
   pixels; a bar that never leaves is a bar you resent by the third section. */
body.nav-away .nav { transform: translateY(-100%); }

.nav__in {
  max-width: var(--sheet);
  margin-inline: auto;
  padding: 14px var(--gut);
  min-height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}

.brand { display: inline-flex; align-items: center; gap: 11px; }
.brand__face { position: relative; flex: 0 0 auto; }
/* A sticker, so it moves like one: it tips on its bottom edge and comes to
   rest with a little overshoot, the only place --settle is used on the bar. */
.brand__sticker {
  width: 34px; height: 34px; object-fit: contain;
  transform-origin: 50% 92%;
  transition: transform .46s var(--settle);
}
/* What the sticker says is where the link goes. On the home page it can only
   greet you, because clicking it goes nowhere new; everywhere else it names
   the destination. The tail is cut at the family 23 degrees. */
.brand__say {
  position: absolute;
  left: calc(100% - 6px);
  bottom: calc(100% - 4px);
  padding: 4px 7px 3px;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  white-space: nowrap;
  box-shadow: 2px 2px 0 0 var(--warm);
  transform-origin: 0 100%;
  transform: scale(.35) translate3d(0, 6px, 0);
  opacity: 0;
  pointer-events: none;
  transition: transform .34s var(--settle), opacity .14s var(--ease);
}
.brand__say::before { content: "Home"; }
body[data-page="home"] .brand__say::before { content: "Hi!"; }
.brand__say::after {
  content: "";
  position: absolute;
  left: 5px; top: 100%;
  width: 6px; height: 6px;
  background: var(--ink);
  clip-path: polygon(0 0, 100% 0, 0 100%);
}
@media (hover: hover) {
  .brand:hover .brand__sticker { transform: rotate(-9deg) translate3d(0, -2px, 0); }
  .brand:hover .brand__say { transform: none; opacity: 1; transition-delay: .06s; }
}
.brand:focus-visible .brand__say { transform: none; opacity: 1; }
.brand:active .brand__sticker { transform: rotate(-9deg) scale(.9); transition-duration: .12s; }
.brand__name {
  font-family: var(--display);
  font-weight: 600;
  font-size: 18px;
  letter-spacing: -.018em;
}

.nav__links { display: flex; align-items: center; gap: clamp(14px, 2vw, 26px); }

.nav__link {
  position: relative;
  font-family: var(--display);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: -.012em;
  color: var(--ink);
  padding-block: 4px;
}
/* The bar, rest scale .18, grown from the left. The cheapest emphasis mark in
   the vocabulary and the one the whole site already uses. */
.nav__link::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  background: var(--warm);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform .4s var(--ease);
}
@media (hover: hover) {
  .nav__link:hover { color: var(--warm); }
  .nav__link:hover::after { transform: scaleX(1); }
}
.nav__link[aria-current="page"] { color: var(--deep); }
.nav__link[aria-current="page"]::after { background: var(--deep); transform: scaleX(1); }

/* "This leaves the site." One glyph, one meaning, used the same way in the
   nav, the phone menu and the footer, so it never has to be learned twice. */
.nav__out {
  display: inline-block;
  margin-left: 2px;
  font-style: normal;
  font-family: var(--body);
  font-weight: 500;
  font-size: .8em;
  color: var(--ink-2);
  transition: transform .28s var(--settle), color .28s var(--ease);
}
@media (hover: hover) {
  .nav__link:hover .nav__out { color: var(--warm); transform: translate3d(2px, -2px, 0); }
}

/* ONE bar for "you are here" and "you would go here". With a bar per link,
   hovering Resume on the projects page lights two bars and the eye has to
   decide which one means what. One bar that leaves the current page, travels
   to the link under the pointer and comes home when the pointer goes says it
   as a single movement. Deep at rest, warm while it is pointing somewhere
   else: the same two hues the per-link bars used. shell.js drives it; without
   script the per-link bars below stand in, untouched. */
.nav__links { position: relative; }
.nav__links.has-ink .nav__link::after { display: none; }
.nav__ink {
  position: absolute;
  left: 0; top: 0;
  width: 10px; height: 2px;
  background: var(--deep);
  opacity: 0;
  pointer-events: none;
  transition: transform .42s var(--ease), width .42s var(--ease),
              opacity .2s var(--ease), background-color .28s var(--ease);
}
.nav__ink.is-on { opacity: 1; }
.nav__ink.is-away { background: var(--warm); }
.nav__ink.is-jump { transition: opacity .2s var(--ease), background-color .28s var(--ease); }

/* ── Play ─────────────────────────────────────────────────────────────────
   The one door out of the portfolio, so it is dressed as the one thing on the
   bar that is not work: a sticker slapped on a few degrees off level, the
   same object as the brand mark it mirrors across the bar. It straightens
   when you reach for it. Its relief is teal, not warm, so it cannot be read
   as a pair with the theme switch beside it: one is a place, one a setting.
   The destination drops in underneath, because a visitor deserves to know a
   link leaves the site before they press it, not after. */
.play {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 5px 9px 4px 10px;
  background: var(--card);
  border: 1px solid var(--ink);
  color: var(--ink);
  font-family: var(--display);
  font-weight: 800;
  font-size: 14px;
  letter-spacing: -.01em;
  line-height: 1.2;
  box-shadow: 3px 3px 0 0 var(--teal);
  transform: rotate(-4deg);
  transition: transform .46s var(--settle), box-shadow .28s var(--ease);
}
.play__arw {
  font-style: normal;
  font-family: var(--body);
  font-weight: 500;
  font-size: 12px;
  color: var(--teal);
  transition: transform .28s var(--settle);
}
.play__tip {
  position: absolute;
  top: calc(100% + 10px);
  left: 50%;
  padding: 4px 7px 3px;
  background: var(--paper);
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .04em;
  color: var(--ink-2);
  white-space: nowrap;
  opacity: 0;
  transform: translate3d(-50%, -4px, 0);
  pointer-events: none;
  transition: opacity .18s var(--ease), transform .28s var(--ease);
}
@media (hover: hover) {
  .play:hover { transform: rotate(0) translate3d(-2px, -2px, 0); box-shadow: 6px 6px 0 0 var(--teal); }
  .play:hover .play__arw { transform: translate3d(2px, -2px, 0); }
  .play:hover .play__tip { opacity: 1; transform: translate3d(-50%, 0, 0); }
}
.play:focus-visible { transform: rotate(0); }
.play:focus-visible .play__tip { opacity: 1; transform: translate3d(-50%, 0, 0); }
.play:active { transform: rotate(0) translate3d(3px, 3px, 0); box-shadow: 1px 1px 0 0 var(--teal); transition-duration: .12s; }

/* ── theme switch ──────────────────────────────────────────────────────────
   Two plates, warm and deep, one inked at a time. Square, hard-edged, and it
   carries the relief like everything else that can be pressed. */
.theme {
  display: inline-flex;
  border: 1px solid var(--ink);
  background: var(--card);
  box-shadow: 3px 3px 0 0 var(--warm);
  transition: transform .28s var(--ease), box-shadow .28s var(--ease);
  flex: 0 0 auto;
}
@media (hover: hover) {
  .theme:hover { transform: translate3d(-2px, -2px, 0); box-shadow: 6px 6px 0 0 var(--warm); }
}
.theme:active { transform: translate3d(3px, 3px, 0); box-shadow: 1px 1px 0 0 var(--warm); }
.theme__half {
  width: 26px; height: 24px;
  display: grid;
  place-items: center;
  color: var(--ink-2);
  transition: background-color .28s var(--ease), color .28s var(--ease);
}
.theme__half + .theme__half { border-left: 1px solid var(--line); }
.theme__half svg { width: 13px; height: 13px; transition: transform .5s var(--settle); }
/* the icon on the plate being inked turns in, so the press has a result you
   can see at the size of the thing you pressed, before the page follows */
.theme[data-now="light"] .theme__half--d svg,
.theme[data-now="dark"]  .theme__half--l svg { transform: rotate(-90deg) scale(.8); }
.theme[data-now="light"] .theme__half--l,
.theme[data-now="dark"]  .theme__half--d { background: var(--ink); color: var(--card); }

/* ── the phone menu ───────────────────────────────────────────────────────
   A sheet, not a drawer: it comes down off the top the way a print comes off
   a press. */
.burger {
  display: none;
  width: 38px; height: 34px;
  place-items: center;
  border: 1px solid var(--ink);
  background: var(--card);
  box-shadow: 3px 3px 0 0 var(--warm);
  flex: 0 0 auto;
}
.burger i { display: block; width: 15px; height: 1.5px; background: var(--ink); transition: transform .28s var(--ease), opacity .18s var(--ease); }
.burger i + i { margin-top: 4px; }
body.menu-open .burger i:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
body.menu-open .burger i:nth-child(2) { opacity: 0; }
body.menu-open .burger i:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }

.sheetmenu {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--paper);
  padding: calc(var(--nav-h) + 30px) var(--gut) 30px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  transform-origin: 50% 0;
  transform: scaleY(0);
  opacity: 0;
  visibility: hidden;
  transition: transform .28s var(--ease), opacity .18s var(--ease), visibility 0s linear .28s;
}
body.menu-open .sheetmenu {
  transform: scaleY(1); opacity: 1; visibility: visible;
  transition: transform .42s var(--ease), opacity .18s var(--ease), visibility 0s;
}
/* The setting-out field, at the family angle, behind the menu. */
.sheetmenu::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(67deg, var(--line-2) 0 1px, transparent 1px 46px);
  pointer-events: none;
}
.sheetmenu__list { position: relative; display: flex; flex-direction: column; gap: 6px; }
.sheetmenu__a {
  display: flex;
  align-items: baseline;
  gap: 14px;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(30px, 11vw, 46px);
  letter-spacing: -.035em;
  line-height: 1.12;
  opacity: 0;
  transform: translateY(12px);
  transition: opacity .28s var(--ease), transform .28s var(--ease);
}
body.menu-open .sheetmenu__a { opacity: 1; transform: none; transition-delay: var(--i, 0ms); }
.sheetmenu__a span {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .18em;
  color: var(--ink-2);
}
.sheetmenu__out {
  font-style: normal;
  font-family: var(--body);
  font-weight: 500;
  font-size: .5em;
  color: var(--ink-2);
  margin-left: -6px;
  align-self: flex-start;
  margin-top: .35em;
}
.sheetmenu__a--play em {
  align-self: center;
  margin-left: auto;
  font-style: normal;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .04em;
  color: var(--teal);
}
.sheetmenu__foot { position: relative; margin-top: 34px; padding-top: 18px; border-top: 1px solid var(--line-2); }

@media (max-width: 760px) {
  .nav__links { display: none; }
  .burger { display: grid; }
}
@media (min-width: 761px) {
  .sheetmenu { display: none; }
}

/* ══ PAGE HEADING ═════════════════════════════════════════════════════════
   Was the footer heading, borrowed by the gallery and contact pages as their
   h1. The footer has its own now; the name stays so those pages are untouched. */
.foot__h {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(21px, 2.5vw, 30px);
  line-height: 1.06;
  letter-spacing: -.03em;
  max-width: 16ch;
  margin: 0 0 12px;
}

/* ══ SITE FOOTER ══════════════════════════════════════════════════════════
   The footer's one job is the next step. It asks in the intro's own voice,
   and the thread under "untangle it" is knotted until you reach for the
   link, then pulls straight. That is the whole personality of it, and it is
   the claim the intro makes, acted out rather than restated. */

.foot {
  position: relative;
  /* the bottom clears the back-to-top button, which is fixed at 42px plus
     its offset: at the very end of the page it would otherwise sit on the
     colophon */
  padding-block: clamp(46px, 2.6rem + 3vw, 92px) calc(42px + 2 * clamp(14px, 2.5vw, 26px));
  border-top: 1px solid var(--line-2);
}
.foot__in {
  display: flex;
  flex-wrap: wrap;
  gap: 42px 64px;
  justify-content: space-between;
  align-items: flex-end;
}
.foot__k {
  margin: 0 0 14px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.foot__cta {
  margin: 0 0 28px;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(36px, 1.9rem + 3.4vw, 76px);
  line-height: 1;
  letter-spacing: -.042em;
}
.foot__go { display: inline; }
.foot__arw {
  display: inline-block;
  margin-left: .16em;
  color: var(--warm);
  transition: transform .42s var(--settle);
}
.foot__arw svg { display: inline-block; width: .46em; height: auto; vertical-align: .1em; transition: transform .42s var(--settle); }
@media (hover: hover) {
  .foot__go:hover .foot__arw { transform: translate3d(.14em, 0, 0); }
  .foot__go.is-up:hover .foot__arw { transform: translate3d(0, -.1em, 0); }
}
/* on the contact page the address is already on screen above, so the line
   points back up at it instead of reloading the page it is on */
.foot__go.is-up .foot__arw svg { transform: rotate(-90deg); }

.foot__mailrow { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; }
.foot__mail {
  display: block;
  font-family: var(--display);
  font-weight: 600;
  font-size: 17px;
  letter-spacing: -.015em;
  color: var(--ink);
  transition: color .28s var(--ease);
}
@media (hover: hover) { .foot__mail:hover { color: var(--warm); } }

/* Copy. The label changes and nothing else: the width is held so the row
   does not jump when "Copy" becomes "Copied". */
.foot__copy, .toast__b {
  min-width: 7.2em;
  padding: 7px 10px 6px;
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-2);
  transition: color .28s var(--ease), border-color .28s var(--ease),
              transform .28s var(--ease), box-shadow .28s var(--ease);
}
@media (hover: hover) {
  .foot__copy:hover, .toast__b:hover {
    color: var(--ink); border-color: var(--ink);
    transform: translate3d(-2px, -2px, 0);
    box-shadow: 4px 4px 0 0 var(--warm);
  }
}
.foot__copy:active, .toast__b:active { transform: translate3d(1px, 1px, 0); box-shadow: 0 0 0 0 var(--warm); }
.foot__copy.is-done, .toast__b.is-done { color: var(--warm); border-color: var(--warm); }

.foot__cols { display: flex; flex-wrap: wrap; gap: 28px clamp(40px, 5vw, 76px); }
.foot__col { display: flex; flex-direction: column; align-items: flex-start; gap: 9px; min-width: 110px; }
.foot__col a {
  position: relative;
  padding-bottom: 3px;
  font-family: var(--display);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: -.012em;
}
.foot__col a i { font-style: normal; font-family: var(--body); font-weight: 500; font-size: .8em; color: var(--ink-2); margin-left: 2px; }
.foot__col a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  background: var(--warm);
  transform: scaleX(.18);
  transform-origin: 0 50%;
  transition: transform .4s var(--ease);
}
@media (hover: hover) { .foot__col a:hover::after { transform: scaleX(1); } }

.foot__base {
  margin-top: clamp(40px, 4vw, 66px);
  padding-top: 14px;
  border-top: 1px solid var(--line-2);
  display: flex;
  flex-wrap: wrap;
  gap: 6px 24px;
  justify-content: space-between;
  font-size: 12.5px;
  color: var(--ink-2);
}

/* ══ THE THREAD ═══════════════════════════════════════════════════════════
   A line drawn under a phrase, knotted or straight. shell.js draws it as a
   prolate cycloid: a point on a rolling wheel's spoke, run past the rim, so
   it loops back on itself. Shrink the spoke to nothing and the same curve is
   a straight line, which is why a knot can pull taut here without a morphing
   library: it is one number going from 1 to 0. Used under "untangle it" in
   the footer, and on the contact page under "the hard part" (knotted) and
   "the easy part" (straight). */
.tangle { position: relative; display: inline-block; white-space: nowrap; }
.tangle__svg {
  position: absolute;
  left: -.04em;
  width: calc(100% + .08em);
  top: 86%;
  height: .44em;
  overflow: visible;
  pointer-events: none;
}
.tangle__svg path {
  fill: none;
  stroke: var(--tangle-c, var(--warm));
  stroke-width: 2;
  stroke-linecap: square;
  stroke-linejoin: round;
}

/* ══ THE MAIL TOAST ═══════════════════════════════════════════════════════ */
.toast {
  position: fixed;
  left: 50%;
  bottom: clamp(14px, 2.5vw, 26px);
  z-index: 95;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 16px;
  width: max-content;
  max-width: calc(100vw - 2 * var(--gut));
  padding: 10px 10px 10px 16px;
  background: var(--card);
  border: 1px solid var(--ink);
  box-shadow: 5px 5px 0 0 var(--warm);
  opacity: 0;
  transform: translate3d(-50%, 18px, 0);
  transition: transform .42s var(--settle), opacity .18s var(--ease);
}
.toast[hidden] { display: none; }
.toast.is-on { opacity: 1; transform: translate3d(-50%, 0, 0); }
.toast__t {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink);
}
.toast__b { min-width: 0; background: none; }

/* ══ BACK TO TOP ══════════════════════════════════════════════════════════ */

.totop {
  position: fixed;
  right: clamp(14px, 2.5vw, 26px);
  bottom: clamp(14px, 2.5vw, 26px);
  z-index: 60;
  width: 42px; height: 42px;
  display: grid;
  place-items: center;
  border: 1px solid var(--ink);
  background: var(--card);
  color: var(--ink);
  box-shadow: 4px 4px 0 0 var(--warm);
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity .28s var(--ease), transform .28s var(--ease), box-shadow .28s var(--ease);
}
body.scrolled-far .totop { opacity: 1; transform: none; pointer-events: auto; }
@media (hover: hover) {
  .totop:hover { transform: translate3d(-2px, -2px, 0); box-shadow: 8px 8px 0 0 var(--warm); }
}
.totop:active { transform: translate3d(3px, 3px, 0); box-shadow: 1px 1px 0 0 var(--warm); }

/* ══ THEME CHANGE ═════════════════════════════════════════════════════════
   Without this the whole page cuts from white to black in one frame and the
   eye loses its place. The new theme sweeps in from the switch's side of the
   screen along the family 23 degrees, top edge leading, so cause and effect
   read as one gesture. tan(23deg) = .4245, so an edge that leans 23 degrees
   off vertical moves 42.45vh sideways between the top of the screen and the
   bottom. Browsers without View Transitions keep the hard cut. */
::view-transition-old(root),
::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
::view-transition-new(root) { animation: md-wipe .72s cubic-bezier(.2, .9, .25, 1) both; }
@keyframes md-wipe {
  from { clip-path: polygon(100% 0, calc(100% + 43vh) 0, calc(100% + 43vh) 100%, calc(100% + 42.45vh) 100%); }
  to   { clip-path: polygon(-42.45vh 0, calc(100% + 43vh) 0, calc(100% + 43vh) 100%, 0 100%); }
}

/* ══ THE FIRST HELLO ══════════════════════════════════════════════════════
   The home page, first visit only, arrives in the order it should be read:
   the monogram (who this is) assembles, its planes sliding home along their
   own cuts (the block's data-assemble, set on its marker in index.html);
   the nav (the way round) drops in from the top edge as the last planes
   land; then "Who I am" rises into place. An introduction, not a splash:
   nothing is covered and nothing waits on it. All of it has settled by
   about a second and a half, and any scroll, key or tap finishes the nav
   and the intro on the spot (shell.js, THE FIRST HELLO). The monogram
   just carries on landing; nothing is waiting for it.

   The delays are set to the assembly: planes release over the first
   ~0.55s and are home by ~0.9s, so the nav starts at .7s on the tail.

   `backwards`, not `both`: once the entrance is over, the nav must be free
   to hide on scroll, which it does with its own transform. The class is
   only ever set by the head script, which skips anyone asking for less
   motion. */
html.hello .nav { animation: md-drop .46s var(--ease) .7s backwards; }
html.hello .monostage + .band { animation: md-rise .5s var(--ease) .94s backwards; }
@keyframes md-drop { from { transform: translateY(-100%); } }
@keyframes md-rise { from { opacity: 0; transform: translateY(14px); } }

/* ══ PAGE CHANGE ══════════════════════════════════════════════════════════
   Going to another page is the same gesture as changing the theme: the new
   page sweeps in along the 23 degree edge. Before this every link was a hard
   cut, so the motion inside each page stopped dead at its edge.

   Three things are held out of the sweep, each because it answers a question:
     the nav    it is the same bar on both pages. Sweeping it would say the
                whole site changed, when only the page under it did.
     the shot   the picture you clicked grows into the case study's hero, so
                there is no "which one did I open" moment on arrival. On the
                way back the hero shrinks into its card. The head script and
                shell.js pick which plate that is; .vt-shot is only ever on
                one element per page, and only for the length of the change.
     the knot   the wait (THE WAIT, below) pulls taut as the page lands.

   Back sweeps the other way: the direction is where you went. Browsers
   without cross-document View Transitions load the page the ordinary way,
   and so does anyone who has asked for less motion. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}
html.vt-page .nav { view-transition-name: nav; }
.vt-shot { view-transition-name: shot; }

html.vt-page::view-transition-new(root) { animation-duration: .6s; }
html.vt-back::view-transition-new(root) { animation-name: md-wipe-back; }
@keyframes md-wipe-back {
  from { clip-path: polygon(0 0, -43vh 0, -43vh 100%, -42.45vh 100%); }
  to   { clip-path: polygon(calc(100% + 42.45vh) 0, -43vh 0, -43vh 100%, 100% 100%); }
}

/* Slightly longer than the sweep so the picture is still arriving as the
   page settles round it, which is what makes it read as the thing that
   travelled. Both images fill the moving box: a thumbnail is square and a
   hero is wide, and letting either letterbox mid-flight shows a gap. */
::view-transition-group(shot) { animation-duration: .64s; animation-timing-function: var(--ease); }
::view-transition-old(shot),
::view-transition-new(shot) { height: 100%; object-fit: cover; }

/* ══ THE WAIT ═════════════════════════════════════════════════════════════
   A page that has not answered within a quarter second gets the thread from
   the footer and Contact, knotted, along the top edge. The knot is pushed
   along the line for as long as the wait lasts, in the hue of where you are
   going. When the page lands the knot pulls straight: the same "untangle it"
   the thread acts out everywhere else, now meaning "done".

   Nothing shows on a fast load. The quarter second is the point: a loader
   that flashes on every click is noise about a wait nobody had. Drawn by
   shell.js (THE WAIT) off the same curve as the underline. */
.wait {
  position: fixed;
  inset: 0 0 auto;
  z-index: 96;
  height: 16px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .2s var(--ease);
}
.wait.is-on { opacity: 1; view-transition-name: knot; }
.wait svg { display: block; width: 100%; height: 100%; overflow: visible; }
.wait path {
  fill: none;
  stroke: var(--tangle-c, var(--warm));
  stroke-width: 2;
  stroke-linecap: square;
  stroke-linejoin: round;
}
/* In a cross-document change the old page is a still picture, so the knot
   cannot be redrawn; flattening the picture of it reads as the same pull. */
::view-transition-old(knot) { animation: md-taut .46s var(--ease) both; }
::view-transition-new(knot) { animation: none; opacity: 0; }
@keyframes md-taut {
  60%  { transform: scaleY(.12); opacity: 1; }
  to   { transform: scaleY(.12); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .brand__sticker, .brand__say, .nav__ink, .nav__out, .play, .play__arw, .play__tip,
  .theme__half svg, .foot__arw, .foot__arw svg, .toast, .wait {
    transition-duration: .01ms !important;
  }
}
