/* A citation mark, the way a book sets one: the same triangle, shrunk and
   lifted clear of the x-height so it reads as a reference to the note at the
   foot of the step rather than as another word in the sentence. */
.ws .card ol.how .fn {
  width: 0.6rem;
  height: 0.6rem;
  position: relative;
  top: -0.42em;
  margin-left: 0.12em;
  color: var(--ink);
}

/* Inter, cut down to the alphabets this site actually writes in. The whole
   family is 352KB and carries Cyrillic, Greek, Vietnamese and more; the Latin
   cut is 72KB and covers every character any page here renders. The extended
   cut sits behind its own unicode-range, so a browser only reaches for it if
   an accent outside Latin-1 ever appears — English pages never pay for it. */
@font-face {
  font-family: "Inter";
  src: url("fonts/Inter-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD, U+2190, U+2192, U+25CB, U+25CF, U+2713-2714;
}
@font-face {
  font-family: "Inter";
  src: url("fonts/Inter-latin-ext.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
:root {
  --ink: #1a1a1a;
  --paper: #fcfcfa;
  --faint: #8a8a84;
  --line: #e6e6e0;
  /* Entering and leaving both use this. It is ease-out: fast at the start,
     settling at the end, which is what makes a thing feel like it answered
     rather than like it thought about it. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { font-size: 17px; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
  font-family: "Inter", ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", sans-serif;
  background: var(--paper);
  color: var(--ink);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
main {
  display: flex;
  flex-direction: column;
  gap: 4rem;
  /* 42rem, matched to what the page now carries: the artefacts are the
     argument, and a 300px Telegram panel inside a 544px column rendered
     everything smaller than it was drawn. The prose runs a little longer for
     it, which is the trade. */
  max-width: 42rem;
  margin: 0 auto;
  padding: 2rem 1.25rem 3rem;
}
/* The nameplate: a small grey home link, sitting above the headline the way a
   nav bar does. It is a link and nothing more — no sticky, no scroll effect. */
.who {
  align-self: flex-start;
  font-size: 0.95rem;
  font-weight: 500;
  /* The row has to outrank the feed below it, and a plain z-index on the panel
     cannot do that on its own. Naming an element for a view transition makes it
     a stacking context, and the nameplate and every project artefact are all
     named - so the panel's z-index is sealed inside the nameplate's own context
     and can never climb past a tile that comes later in the page. Lifting the
     whole row is what settles it. */
  position: relative;
  z-index: 50;
  /* the nameplate is its own thing, not the headline's first line: it sits
     near the top of the page, and then a clear run of nothing separates it
     from what the page actually says — 3rem, enough to read as a break with
     the headline still high on the page */
  margin-bottom: -1rem;
}
.who a {
  color: var(--faint);
  text-decoration: none;
  transition: color 0.2s ease;
}
.who a:hover { color: var(--ink); }

h1 {
  /* the one line in ink, and now the only headline — big enough to be the
     first thing read, capped so it never outgrows the column */
  font-size: clamp(2rem, 5vw, 2.8rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
  /* pulled toward the work below it — the headline introduces the projects,
     so it belongs to them rather than floating equidistant between the
     nameplate and the first one */
  margin-bottom: -1.6rem;
}
/* The sentence under the headline: the same grey as a project's one line,
   held to a width that breaks it in two rather than leaving a word behind.
   main spaces its children 4rem apart, which is right between projects and
   wrong between a headline and its own caption, so the intro pulls itself up
   to sit just under the line, and pulls the projects up after it by the same
   amount the headline used to, so they land where they always have. */
.intro { color: var(--faint); max-width: 30rem; line-height: 1.5; margin: -2.2rem 0 -1rem; text-wrap: balance; }
/* The comma appears and disappears, which is the joke: "I make less, work."
   and "I make less work." are both true. It cuts rather than fades — a hard
   step is what a terminal cursor does, and that is the reference. A fade was
   tried and read as softer than the page sounds. */
.comma {
  /* a sliver of width: comma + gap total ~0.4em, so the comma sits snug
     when showing and the word gap is only slightly wide when it's not */
  display: inline-block;
  width: 0.14em;
  animation: blink 3s step-end infinite;
}
@keyframes blink {
  0%, 55% { opacity: 1; }
  60%, 95% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .comma { animation: none; }
}
.projects {
  display: flex;
  flex-direction: column;
  /* Twice what it was. A project is a strip and the artefact under it, and the
     strip sits 1.1rem above its own artefact — so at 2.1rem the space between
     two projects was barely more than the space inside one, and the feed read
     as a single column of alternating text and pictures. The gap between
     things has to beat the gap within them or the grouping is not doing any
     work. The hover wash still bleeds past this on both sides, which is what
     the negative margins below are for. */
  gap: 4.4rem;
}
.project .name {
  display: block;
  color: var(--ink);
  font-weight: 600;
  text-decoration: none;
}
.project a.name {
  border-bottom: 1px solid var(--line);
  transition: border-color 0.2s ease;
}
.project a.name:hover { border-color: var(--ink); }
.project p {
  margin-top: 0.25rem;
  color: var(--faint);
  text-wrap: pretty;
}
/* one artefact per project — a still or a quiet loop. The app's own white
   is nearly the paper white, so the hairline is what gives it an edge. */
.project .thumb {
  display: block;
  width: 100%;
  height: auto;
  margin-top: 0.9rem;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: var(--paper);
}
/* ── the frame, and letting the shadow out of it ──
   An iframe is a hard paint boundary: nothing inside one can ever draw outside
   it. So the phone's shadow is not drawn inside it. It is drawn here, on the
   page, by a silhouette sitting behind the artefact — free to fall past the
   column the way a shadow should, while the artefact itself stays honestly
   inside 42rem.

   That lets the canvas be the artefact and nothing more: no reserved room, no
   negative margins undoing reserved room, and the drawing rendered as large as
   the column allows.

   What the artefact IS, in its own units. Everything below is arithmetic on
   these, done by the browser, so there is no number here tuned by hand. */
.project, .article {
  --canvas: 740;
  --canvas-h: 439;
  --phone-w: 197;
  --phone-h: 400;
  /* the body within the frame drawing, in the drawing's own 415-wide units.
     The drawing is wider than the phone is, because the buttons stand proud
     of it — so the silhouette has to be told where the phone actually ends,
     or it casts its shadow from a box slightly bigger and squarer than the
     thing it is standing in for, and leaves a lit rim in the difference. */
  --art-w: 415;
  --body-x: 3;
  --body-w: 409;
  --body-r: 62;
}
.frame {
  position: relative;
  width: 100%;
  aspect-ratio: var(--canvas) / var(--canvas-h);
  margin-top: 0.9rem;
}
/* vantage's canvas is the bb window and nothing else, drawn at its real size
   and scaled to whatever the column gives it. */
.project.vantage, .article.vantage { --canvas: 760; --canvas-h: 410; }
/* hiya's is the Codex window, plus the room under it the floating sequence
   pill and its shadow need */
.project.hiya, .article.hiya { --canvas: 760; --canvas-h: 582; }
/* and nothing of either hangs over its own edges, so there is no phone to cast
   a shadow out into the gutter */
.project.vantage .frame::before,
.article.vantage .frame::before,
.project.hiya .frame::before,
.article.hiya .frame::before { content: none; }
/* nfc's canvas is taller than the others, and on a handset taller again.
   Everything inside it is height-driven — a phone is as wide as its own height
   allows — so the only way to make the phone bigger is to give the frame more
   height. A 740 x 439 letterbox is the wrong shape to put a portrait object
   in, and on a 390px screen it left the middle handset barely 100px wide. */
.project.nfc, .article.nfc { --canvas: 740; --canvas-h: 520; }
@media (max-width: 640px) {
  /* Tall enough that a handset is legible, short enough that four of them
     still read as a row rather than one filling the screen. 800 put a single
     phone at 191px on a 390 screen, which was the whole tile. */
  .project.nfc, .article.nfc { --canvas-h: 620; }
}
/* nfc draws its own shadow under each device, inside each slot, so there is no
   silhouette to place out here — and with three of them one could only ever be
   right about the middle. */
.project.nfc .frame::before,
.article.nfc .frame::before { content: none; }
/* the phone's silhouette, behind the artefact and out of its reach. Only the
   shadow is ever seen: the phone itself is drawn over this, pixel for pixel. */
.frame::before {
  content: "";
  position: absolute;
  right: calc(var(--phone-w) * var(--body-x) / var(--art-w) / var(--canvas) * 100%);
  bottom: 0;
  width: calc(var(--phone-w) * var(--body-w) / var(--art-w) / var(--canvas) * 100%);
  height: calc(var(--phone-h) / var(--canvas-h) * 100%);
  border-radius: calc(var(--body-r) / var(--body-w) * 100%)
               / calc(var(--body-r) * var(--phone-w) / var(--art-w) / var(--phone-h) * 100%);
  box-shadow:
    0 0 1px rgba(14,17,22,.16),
    0 4px 8px -3px rgba(14,17,22,.18),
    0 13px 23px -8px rgba(14,17,22,.32),
    0 25px 46px -17px rgba(14,17,22,.34);
}
.frame iframe.thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  /* the hairline and the corner belong to the window inside, which draws its
     own; this element is just the pane the artefact is painted on */
  border: 0;
  border-radius: 0;
  background: transparent;
  overflow: hidden;
}
/* ══════════ a project that opens ══════════
   The tile becomes a strip: name and sentence on the left, "read more" on the
   right, centred against them. The words say it outright because an underline
   read as decoration rather than a door, and because a cue that only exists on
   hover is invisible on the phone, which is where the site gets shown. The
   artefact stays outside the strip — it has its own click. */
.project a.strip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem 2rem;
  color: inherit;
  text-decoration: none;
  /* On a narrow screen the sentence and the invitation stop fitting on one
     line, and squeezing them together turns the sentence into a column two
     words wide. So the invitation takes a line of its own instead: the words
     keep the row, "read more" drops below them, and space-between leaves it at
     the start of its own line rather than stranded out to the right. */
  flex-wrap: wrap;
}
.project a.strip > span:first-child { flex: 1 1 20rem; }
/* Both lines hug their own words and share the article's line-height, so each
   one's box is the same shape as its twin over there and the morph is an even
   scale rather than a stretch in one direction. */
.project a.strip .name,
.project a.strip .desc {
  width: fit-content;
  line-height: 1.2;
}
.project a.strip .name { font-size: 1rem; }
.project a.strip .desc { margin-top: 0.45rem; color: var(--faint); }
.project .more {
  flex: none;
  color: var(--faint);
  font-size: 0.92rem;
  transition: color 0.2s ease;
}
/* the arrow goes dark with the words and stays where it is — a nudge on hover
   read as the tile fidgeting */
.project .more::after {
  content: "\2192";
  display: inline-block;
  margin-left: 0.3em;
}
.project:has(a.strip:hover) .more { color: var(--ink); }

/* ── a strip that carries a mark ──
   nfc is a write-up at /projects/nfc/ and a working page at /nfc/, the address
   a sticker opens. It was briefly given a second phrase beside "read more",
   and two grey phrases with the same arrow read as one phrase broken in half:
   two actions are only confusable when they have the same form.

   So the live page stopped being an action. It is a mark against the name —
   a label first and a link second, saying this project is a running page,
   which the other three are not. The right-hand side is untouched, so the row
   still reads exactly like its neighbours.

   The row is a stretched link rather than a wrapping one, because the mark
   sits inside the row and an anchor cannot hold another anchor. Everything
   else about the layout is copied from a.strip above. */
.project .strip.marked {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem 2rem;
  flex-wrap: wrap;
}
.project .strip.marked .strip-link {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: 12px;
}
.project .strip.marked .says { flex: 1 1 20rem; }
.project .strip.marked .name,
.project .strip.marked .desc {
  width: fit-content;
  line-height: 1.2;
}
.project .strip.marked .name { font-size: 1rem; }
.project .strip.marked .desc { margin-top: 0.45rem; color: var(--faint); }
.name-row { display: flex; align-items: center; gap: 0.5rem; }

/* ── the mark ──
   It rides above the stretched link, which is the whole reason it can be a
   different destination from the row it sits in. */
.live {
  position: relative;
  z-index: 2;
  flex: none;
  /* ── the one relationship this box has ──
     The mark used to size itself: small text in generous padding, which came
     out 24px tall and 67px wide against a name that is 20 by 35. A label
     physically bigger than the thing it labels is neither a tag nor a button,
     and that is the whole of why it read as neither.

     It takes the name's height now instead of choosing one. 1.2rem is the
     name's line box, so the two share a top and a bottom edge and the row has
     one horizon rather than two. Change the name's size and the mark follows
     without a second number being touched. The letter-spacing came in and the
     arrow came down with it, because the width was the other half of it. */
  display: inline-flex;
  align-items: center;
  gap: 0.22em;
  height: 1.2rem;
  padding: 0 0.45rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--faint);
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  /* Hover is a state change, not a performance: fast enough to feel like the
     element answered rather than thought about it, and easing out so it
     arrives softly instead of stopping dead. Colour and background only —
     nothing here moves, so nothing can shift the line it sits on. */
  transition:
    color 0.16s ease-out,
    background-color 0.16s ease-out,
    border-color 0.16s ease-out,
    transform 0.16s ease-out;
}
.live:hover {
  color: var(--paper);
  background: #3d3d38;
  border-color: #3d3d38;
}
/* the press is the one thing that moves, and only a little — the mark is small
   enough that a bigger squeeze reads as a glitch */
.live:active { transform: scale(0.96); }
/* the arrow is a mark on a mark: at 0.7rem it was reading as loud as the word */
.live .arw { font-size: 0.85em; }

/* The row's own hover belongs to the row. Hovering the mark must not light the
   whole tile, or the tile would be promising to take you where the mark goes. */
.project:has(.strip-link:hover) .more { color: var(--ink); }

/* The hover lights the whole project, strip and artefact together, rather than
   boxing the strip alone: an even tint barely above the paper, so it reads as
   the tile warming rather than a panel appearing. */
.project:has(.strip) {
  position: relative;
  isolation: isolate; /* keeps the wash behind the artefact, not tinted over it */
  padding: 1.1rem 1.2rem 1.4rem;
  margin: -1.1rem -1.2rem -1.4rem;
  border-radius: 16px;
}
.project:has(.strip)::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: rgba(26, 26, 26, 0.032);
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
}
.project:has(a.strip:hover)::before,
.project:has(.strip-link:hover)::before { opacity: 1; }
.project a.strip + .thumb { margin-top: 1.1rem; }

/* ══════════ the article a project opens into ══════════ */
.article { gap: 0; }
/* the nameplate leans on the feed's 4rem gap to clear what follows it; an
   article has no such gap, so it makes the room itself */
.article .who { margin-bottom: 0; }
.article .body { margin-top: 3rem; }
/* the tile's name, arrived: a label now, not a door, so it steps back and
   hands the page to the cover line */
.eyebrow {
  display: block;
  width: fit-content;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--faint);
}
/* the article's name row, where a project carries a mark. .name-row is a flex
   line and .eyebrow is display:block with fit-content, so the two sit together
   without either one stretching. */
.article .name-row { width: fit-content; }
/* the tile's sentence, grown into the headline */
.cover { width: fit-content; margin-top: 0.7rem; }
/* the feed's headline pulls the projects up under it with a negative margin;
   an article has a sentence under its headline instead, and that sentence has
   to sit below it rather than in it */
.article .cover { margin-bottom: 0; }
/* the tile's sentence, arrived unchanged: it said the thing on the feed and
   it says it here, so the page never argues with its own tile */
.article .desc { margin-top: 0.7rem; width: fit-content; color: var(--faint); }
/* the artefact, at exactly the size it had in the feed */
.article .frame { margin-top: 2.2rem; }
/* the one artefact that answers back. It is a frame like any other, but its
   height is whatever it says it is - the thing inside knows, and a fixed box
   would either clip it or leave a hole under it. */
.tour { margin-top: 3.2rem; }
.tour iframe {
  display: block;
  width: 100%;
  height: 560px;
  border: 0;
  background: transparent;
  transition: height 0.25s ease;
}
.article .prose { margin-top: 3.2rem; line-height: 1.72; }
/* hiya's artefact ends before its canvas does: the drawing keeps 68 of its own
   units under the floating pill for the shadow to fall into, so measuring the
   gap from the frame's edge measures it from empty paper and the prose starts
   a paragraph too low. The room is taken back in the drawing's own units, so
   it stays right at every width. */
.article.hiya .prose { margin-top: calc(3.2rem - 68 / 760 * 100%); }
.article .prose p { margin-bottom: 1.4rem; }
/* The article's own two levels. The page's h1 is the cover line above, so a
   section inside the prose starts at h2 and there is only ever one headline.
   Two levels, far enough apart to be told apart mid-paragraph without looking
   twice: one opens a part of the piece, the other marks a turn inside it. */
.article .prose h2 {
  font-size: 1.42rem;
  font-weight: 600;
  letter-spacing: -0.017em;
  line-height: 1.28;
  /* the run of nothing above a section is what makes it read as a break */
  margin: 3.4rem 0 1rem;
}
.article .prose h3 {
  font-size: 1.08rem;
  font-weight: 600;
  letter-spacing: -0.005em;
  line-height: 1.4;
  margin: 2.4rem 0 0.65rem;
}
/* on the work page the three offers are separated outright: a hairline above
   each one but the first, so nobody has to infer where one ends */
.article.work .prose h2:not(:first-child) {
  margin-top: 2.4rem;
  padding-top: 2.4rem;
  border-top: 1px solid var(--line);
}
/* a section heading landing straight after another needs no second gap */
.article .prose h2 + h3 { margin-top: 1.4rem; }
/* a heading that opens the piece should not be pushed away from it */
.article .prose > :first-child { margin-top: 0; }
.article .prose ul, .article .prose ol { margin: 0 0 1.4rem 1.15rem; }
.article .prose li { margin-bottom: 0.45rem; padding-left: 0.25rem; }
.article .prose li::marker { color: var(--faint); }

/* The examples are texture more than reading: a run of grey shorthand under
   each offer. When there are more of them than fit, the row becomes a real
   scroll area that drifts on its own - see the page's own script. The drift
   moves the scroll position rather than a transform, so a finger, a trackpad
   and the drift are all moving the same one thing and none of them can fight. */
.article .prose ul.cases {
  /* pulled up under the sentence it belongs to: the paragraph's own bottom
     margin is a gap between paragraphs, and these are not one */
  margin: -1.05rem 0 0;
  list-style: none;
  color: var(--faint);
  /* The file writes one item per line, and between two inline items that line
     break is a real space in the page: it lands to the right of every dot, and
     a selection dragged across the labels paints it. Narrowing it was not
     enough - a space of no width is still a space that highlights. Nulling the
     font size here removes it outright, and each label sets its own back. The
     markup cannot be fixed instead: the desk rewrites it one item per line
     every time it saves. */
  font-size: 0;
  line-height: 0;
}
.article .prose ul.cases li {
  display: inline;
  margin: 0;
  padding: 0;
  font-size: 1rem;
  line-height: 1.5;
}
/* every item carries its own trailing dot, so the set repeats exactly - that
   exactness is what makes the loop invisible. The last dot is dropped when the
   row is standing still, where it would be a full stop that isn't one. */
.article .prose ul.cases li::after {
  /* Drawn rather than typed: the middle-dot glyph is tiny and sits on the
     x-height, which reads low next to lowercase. A circle takes a real size,
     and vertical-align puts it on the true middle of the line - which only
     works while it is in the line, so it stays in flow. */
  content: "";
  display: inline-block;
  width: 0.2em;
  height: 0.2em;
  border-radius: 50%;
  /* Fixed, not currentColor: a dot belongs to the row, not to the label it
     happens to follow, so it must not darken when that label is hovered or
     opened - which read as one dot changing while its neighbour did not. Mixed
     to the same strength as the rule under the labels, so the row's quiet marks
     are one weight rather than two. */
  background: color-mix(in srgb, var(--faint) 38%, transparent);
  vertical-align: middle;
  margin: 0 0.6em;
  -webkit-user-select: none;
  user-select: none;
}
.article .prose ul.cases li:last-child::after { content: none; }

/* looping: one line, scrollable, with its ends dissolved rather than cut so
   the row reads as continuing rather than as clipped */
.article .prose ul.cases {
  white-space: nowrap;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
/* An end is dissolved only when there is something past it. Fading an edge
   with nothing behind it makes the first word look damaged rather than
   continuing, which is how it read before this. */
.article .prose ul.cases.has-right {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 3rem), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 3rem), transparent);
}
.article .prose ul.cases.has-left {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 2.5rem);
  mask-image: linear-gradient(to right, transparent, #000 2.5rem);
}
.article .prose ul.cases.has-left.has-right {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 2.5rem, #000 calc(100% - 3rem), transparent);
  mask-image: linear-gradient(to right, transparent, #000 2.5rem, #000 calc(100% - 3rem), transparent);
}
.article .prose ul.cases::-webkit-scrollbar { display: none; }
/* Only the words open an example. The separator remains on the list item, as a
   sibling of this button, so its visual gap is not a mysterious tap target. */
.article .prose ul.cases li[data-more] > button {
  appearance: none;
  border: 0;
  padding: 0;
  background: none;
  color: inherit;
  font: inherit;
  line-height: inherit;
  vertical-align: baseline;
  cursor: pointer;
  /* A dotted rule under a word already means there is something behind it.
     It has to be visible at rest, not on hover: a thumb has no hover, and a
     thumb is what will be holding this at a conference. */
  text-decoration: underline dotted;
  text-decoration-color: color-mix(in srgb, var(--faint) 38%, transparent);
  text-underline-offset: 0.28em;
  text-decoration-thickness: 1px;
  /* dark on the way in without waiting, so the press reads as the cause; the
     ease is kept only for letting go, where nothing is being answered */
  transition: color 0.35s ease, text-decoration-color 0.35s ease;
}
.article .prose ul.cases li[data-more] > button:hover,
.article .prose ul.cases li.is-open > button { transition-duration: 0s; }
.article .prose ul.cases li[data-more] > button:hover { text-decoration-color: var(--ink); }
/* the one that is answering says so outright, in the page's own ink */
.article .prose ul.cases li.is-open > button {
  text-decoration-style: solid;
  text-decoration-color: var(--ink);
}
.article .prose ul.cases li[data-more] > button:hover,
.article .prose ul.cases li[data-more] > button:focus-visible { color: var(--ink); outline: none; }
.article .prose ul.cases li.is-open > button { color: var(--ink); }
/* the sentence turns over rather than cutting; it holds its own height, so the
   page never moves under the thumb that pressed it */
/* a row that can be taken hold of says so before it is taken hold of */
.article .prose ul.cases.has-left, .article .prose ul.cases.has-right { cursor: grab; }
.article .prose ul.cases.is-held { cursor: grabbing; }

/* On this page a heading, its sentence and its examples are one block, so the
   two gaps inside it are made equal - the heading's bottom margin comes in by
   the half-line of air it and the sentence each leave between them. Scoped to
   this page: the articles want their headings to breathe. */
.article.work .prose h2 { line-height: 1.2; margin-bottom: 0.35rem; }
/* The row sits under the sentence, so a new sentence rises from the row's
   side: the movement points back at the thing that was pressed. Out is quick
   and downward - it is only bookkeeping - and in is slower and eased out,
   because the arriving line is the answer. */
.article .prose p.says {
  line-height: 1.5;
  transition: opacity 0.22s ease-out, transform 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}
.article .prose p.says.is-turning {
  opacity: 0;
  transform: translateY(4px);
  transition-duration: 0.1s;
  transition-timing-function: ease-in;
}
/* Scroll already supplies the timing. Easing each scroll-driven value again
   makes the sentence chase the finger and visibly judder on a phone. */
.ledger.is-morphing .prose p.says { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .article .prose p.says, .article .prose p.says.is-turning { transform: none; }
}

/* The price, as a receipt that the page turns into.
 *
 * The three offers are pinned while a tall section scrolls past them - they
 * have to still be on screen to travel from - then everything but the headings
 * leaves, the headings fly down into the receipt as its line items, and the
 * question the reader was just asked becomes its header. The whole thing is
 * driven by scroll position in the page's own script, so it can be scrubbed
 * backwards and never plays on its own. */
.ledger { position: relative; }
/* The sticky box itself must be no taller than the viewport. A tall sticky box
   has two competing edges on a phone: Safari pushes its bottom edge down,
   then changes direction and pins its top. The writing may overflow this
   frame; the frame is only the stable coordinate system for the morph. */
.stage { position: sticky; top: 0; height: 100svh; padding-top: 2.4rem; }
/* Only the three headings are lifted above the paper, because only they land on
   it. Lifting the whole block, as this once did, leaves the sentences and the
   label rows showing through the slip as it slides over them. */
.article.work .prose h2 { position: relative; z-index: 3; }
/* scaled from their own top-left corner, or a shrinking heading drifts right
   and down as it flies and lands beside its line rather than on it */
.article.work .prose h2 { transform-origin: left top; will-change: transform; }

/* The question that ends the offers, and becomes the receipt's title.
 *
 * It is the thing already in the reader's head, so it is written the way a
 * thought is: lighter than the page's own voice, not heavier, lowercase, grey.
 * No bubble and no italic - there is no italic cut of this font, and a browser
 * slanting the roman itself is a worse lie than not slanting it at all.
 *
 * The thought arrives word by word, each one out of focus and settling: things
 * occur to people in pieces, and a line that simply fades in whole reads as the
 * page talking rather than the reader thinking. */
.article .ask {
  margin: 0 0 2.4rem;
  font-size: clamp(1.4rem, 3vw, 1.7rem);
  font-weight: 400;
  letter-spacing: -0.01em;
  line-height: 1.3;
  color: var(--ink);
}
.article .ask .asking { transform-origin: left top; will-change: transform; display: inline-block; }
.article .ask .word {
  display: inline-block;
  white-space: pre;
}
.article .ask.forming .word {
  opacity: 0;
  filter: blur(7px);
  transform: translateY(7px);
  letter-spacing: 0.08em;
}
.article .ask.thought .word {
  opacity: 1;
  filter: blur(0);
  transform: none;
  letter-spacing: inherit;
  transition: opacity 0.5s ease-out, filter 0.6s ease-out,
              transform 0.6s cubic-bezier(0.22, 1, 0.36, 1), letter-spacing 0.6s ease-out;
}
.article .ask.thought .word:nth-child(2) { transition-delay: 0.11s; }
.article .ask.thought .word:nth-child(3) { transition-delay: 0.22s; }

/* The slip itself. White against the site's off-white paper - the only place
   on the site where those two are set against each other - so it reads as
   something laid on the page rather than printed into it. The lines are then
   printed onto it as the reader scrolls, which is what makes it a receipt
   rather than a box with a list in it. */
.receipt {
  position: relative;
  z-index: 2;
  margin-top: 3.6rem;
  /* a receipt is a narrow column, not a slide - the proportion is most of what
     tells you what you are looking at before you read a word of it */
  max-width: 34.5rem;
  margin-left: auto;
  margin-right: auto;
  padding: 2.6rem 2rem 3rem;
  background: #fff;
  /* two shadows: a tight one for the edge it sits on, a wide soft one for the
     air under it. One shadow alone reads as either a sticker or a fog. */
  box-shadow: 0 2px 4px rgba(26, 26, 26, 0.05), 0 16px 44px rgba(26, 26, 26, 0.09);
  will-change: transform;
}
/* invisible twins: they hold the places the real words are flying to, so the
   flight is measured against something on the page rather than a guess */
/* A line item: the label with its dots running out, and the sum on the line
   beneath, right-aligned. The sums are written as arithmetic rather than as
   sentences - shorter, funnier, and it prices the thing against what they are
   already paying. They sit beside their labels rather than beneath: a price
   under its item is not what a receipt looks like. The sums are set a size
   down, which is what buys the room for that. */
.receipt .slot {
  display: flex; align-items: baseline; gap: 0.5rem;
  padding: 0.8rem 0;
}
.receipt .ghost { font-size: 1.02rem; font-weight: 400; line-height: 1.5; white-space: nowrap; visibility: hidden; }
/* The leader dots. Bigger and closer than a hairline, which reads as a smudge
   rather than as the run of dots that makes a line a line item. */
.receipt .dots {
  order: 1; flex: 1 1 1.6rem; min-width: 1.6rem; height: 2px;
  align-self: flex-end; margin-bottom: 0.3em;
  background-image: radial-gradient(circle at center, var(--line) 1.2px, transparent 1.3px);
  background-size: 5px 2px; background-repeat: repeat-x;
  transform: scaleX(0); transform-origin: left;
}
.receipt .amount {
  order: 2;
  color: var(--ink); font-size: 0.85rem;
  white-space: nowrap;
  opacity: 0;
}
/* the rule is a mark, not a word, so it takes the light shade with the dots */
.receipt .cut { height: 1px; margin-top: 1.1rem; background: var(--line); transform: scaleX(0); transform-origin: left; }
/* The total is a line item like the others - same label, same dots, same right
   edge - because that is what a total is. It carries more ink and more size,
   which is the only thing that has to say it is the last line. */
.receipt .total {
  display: flex; align-items: baseline; gap: 0.5rem;
  padding-top: 1.3rem; opacity: 0;
}
.receipt .ghost-total {
  font-size: 1.02rem; line-height: 1.5; font-weight: 600; color: var(--ink);
}
/* the total's amount is the same size as the sums above it and carries the
   weight of its own label - a total is not a bigger price, it is a firmer one */
/* the total's own leader is dropped: the rule above it already separates it,
   and a second run of dots on the line that closes the receipt is one mark too
   many */
.receipt .total .dots { background: none; }
.receipt .total .amount {
  order: 2; opacity: 1; color: var(--ink);
  font-size: 1.02rem; font-weight: 600;
}

/* Standing still it is simply an article followed by a receipt. */
@media (prefers-reduced-motion: reduce) {
  .ledger { height: auto !important; }
  .stage { position: static; height: auto; }
  .receipt { transform: none; }
  .receipt .dots, .receipt .cut { transform: scaleX(1); }
  .receipt .amount, .receipt .total { opacity: 1; }
  .receipt .ghost { visibility: visible; }
}

/* A phone does not have enough line length for a receipt label and its full
   calculation to share one row. The calculation moves beneath the label and
   keeps the receipt's right edge; nothing is clipped or made unreadably small. */
@media (max-width: 34rem) {
  .receipt {
    width: 100%;
    padding: 2.2rem 1.35rem 2.5rem;
  }
  .receipt .slot {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0.15rem;
    padding: 0.72rem 0;
  }
  .receipt .ghost { white-space: normal; }
  .receipt .slot .dots { display: none; }
  .receipt .amount {
    max-width: 100%;
    white-space: normal;
    line-height: 1.4;
    text-align: left;
  }
  .receipt .total { justify-content: space-between; gap: 1rem; }
  .receipt .total .dots { display: none; }
}

/* The receipt resolves the cost question; this is the page's final chapter,
   large enough to feel like its answer rather than a utility added below it. */
.inquiry {
  width: 100%;
  margin: 5.8rem 0 0;
  padding-bottom: 2.5rem;
}
.inquiry h2 {
  max-width: 12ch;
  font-size: clamp(2.15rem, 6vw, 3rem);
  font-weight: 600;
  letter-spacing: -0.035em;
  line-height: 1.08;
  text-wrap: balance;
}
.work-form { margin-top: 2.4rem; }
.work-fields {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.45rem 2rem;
}
.work-fields label { display: block; min-width: 0; }
.work-fields label > span {
  display: block;
  margin-bottom: 0.35rem;
  color: var(--faint);
  font-size: 1rem;
}
.work-fields .message-field { grid-column: 1 / -1; }
.work-fields input,
.work-fields textarea {
  display: block;
  width: 100%;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  padding: 0.15rem 0 0.55rem;
  background: transparent;
  color: var(--ink);
  font: inherit;
  line-height: 1.5;
  outline: none;
  transition: border-color 0.18s ease;
}
.work-fields textarea {
  field-sizing: content;
  min-height: calc(1.5em + 0.7rem);
  max-height: calc(12em + 0.7rem);
  overflow-y: auto;
  resize: vertical;
}
.work-fields input:hover,
.work-fields textarea:hover { border-color: var(--faint); }
.work-fields input:focus-visible,
.work-fields textarea:focus-visible { border-color: var(--ink); }
.form-trap {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
  clip: rect(0 0 0 0) !important;
  clip-path: inset(50%) !important;
  white-space: nowrap !important;
}
.form-action {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: 2.6rem;
  margin-top: 1.2rem;
}
.work-form button {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  border: 1px solid var(--ink);
  border-radius: 999px;
  padding: 0.8rem 1.15rem;
  background: var(--ink);
  color: var(--paper);
  font: 600 0.95rem/1 Inter, system-ui, sans-serif;
  cursor: pointer;
  transform-origin: center;
  transition: transform 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
}
.work-form button:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.work-form button:disabled { background: var(--faint); border-color: var(--faint); color: var(--paper); cursor: wait; }
.send-arrow {
  position: relative;
  display: inline-block;
  width: 1em;
  height: 1em;
  overflow: hidden;
}
.send-arrow span {
  position: absolute;
  inset: 0;
  line-height: 1;
  transition: transform 0.2s ease;
}
.send-arrow span:last-child { transform: translateX(-100%); }
@media (hover: hover) and (pointer: fine) {
  .work-form button:not(:disabled):hover {
    border-color: #3e3e3b;
    background-color: #3e3e3b;
    transform: translateY(-1px);
  }
  .work-form button:not(:disabled):hover .send-arrow span:first-child { transform: translateX(100%); }
  .work-form button:not(:disabled):hover .send-arrow span:last-child { transform: none; }
}
.work-form button:not(:disabled):active {
  transform: scale(0.97);
  transition-duration: 0.15s;
  transition-timing-function: var(--ease-out);
}
.form-status {
  color: var(--faint);
  font-size: 0.88rem;
  line-height: 1.4;
}
.form-status[data-state="error"] { color: var(--ink); }

@media (max-width: 34rem) {
  .inquiry { margin-top: 4.5rem; padding-bottom: 3rem; }
  .inquiry h2 { font-size: 2.35rem; }
  .work-form { margin-top: 2.2rem; }
  .work-fields { grid-template-columns: minmax(0, 1fr); gap: 1.25rem; }
  .work-fields .message-field { grid-column: auto; }
  .work-form button { padding: 0.85rem 1.2rem; }
  .form-action { align-items: flex-start; flex-direction: column; gap: 0.65rem; }
}

@media (prefers-reduced-motion: reduce) {
  .work-fields input, .work-fields textarea, .work-form button, .send-arrow span { transition: none; }
  .work-form button:not(:disabled):hover,
  .work-form button:not(:disabled):active { transform: none; }
  .work-form button:not(:disabled):hover .send-arrow span:first-child { transform: none; }
  .work-form button:not(:disabled):hover .send-arrow span:last-child { transform: translateX(-100%); }
}
/* a quoted line steps out of the column just enough to be seen as borrowed */
.article .prose blockquote {
  margin: 1.8rem 0;
  padding-left: 1.1rem;
  border-left: 2px solid var(--line);
  color: var(--faint);
}
/* a block of code keeps its own line breaks, and sits in its own field */
.article .prose pre {
  margin: 1.8rem 0;
  padding: 0.9rem 1rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: #f6f6f2;
  overflow-x: auto;
}
.article .prose pre code {
  padding: 0;
  background: none;
  font-size: 0.88rem;
  line-height: 1.6;
  white-space: pre;
}
.article .prose del { color: var(--faint); }
.article .prose code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em;
  background: #f2f2ee;
  border-radius: 4px;
  padding: 0.1em 0.32em;
}
.article .prose figure { margin: 2rem 0; }
/* an artefact set into the writing: the same 760-wide drawing the feed uses,
   given the column's full width and its own height, so it sits in the prose
   the way a picture does rather than as a window onto another page */
.article .prose figure.art { margin: 2.6rem 0; }
.article .prose figure.art.cone iframe { aspect-ratio: 760 / 380; }
.article .prose figure.art iframe {
  display: block;
  width: 100%;
  aspect-ratio: 760 / 404;
  border: 0;
  background: transparent;
}
/* placeholder screenshots, carrying the artefact's own proportions so the page
   is judged at the size the real ones will be */
.article .prose .shot {
  aspect-ratio: var(--canvas) / var(--canvas-h);
  border: 1px solid var(--line);
  border-radius: 12px;
  background:
    repeating-linear-gradient(-45deg, transparent, transparent 14px, #f4f4f0 14px, #f4f4f0 15px),
    var(--paper);
  display: grid;
  place-items: center;
  color: var(--faint);
  font-size: 0.9rem;
}
/* a real screenshot, once one exists, takes the placeholder's exact place */
.article .prose figure img {
  display: block;
  width: 100%;
  border: 1px solid var(--line);
  border-radius: 12px;
}
.article .prose .filler { color: var(--faint); }
/* The end of what is written so far. Grey said "footnote" and this is not a
   footnote - it is the last thing the page says, and it has something for the
   reader to do. So: ink, a size up, and a rule above it to part it from the
   writing rather than let it trail off. */
.article .prose .next {
  margin-top: 3rem;
  padding-top: 1.7rem;
  border-top: 1px solid var(--line);
  color: var(--ink);
  font-size: 1.16rem;
  font-weight: 500;
  letter-spacing: -0.005em;
  line-height: 1.5;
}
/* a link in the writing wears the page's own ink and an underline the colour
   of a rule - the browser's blue is the one colour on this site nobody chose */
.article .prose a {
  /* Ink, not a colour. The site has no accent and a link is not the place to
     invent one: inside a grey line the black is the thing that stands out,
     and the rule under it says why. */
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--line);
  text-underline-offset: 3px;
  transition: text-decoration-color 0.2s ease;
}
.article .prose a:hover { text-decoration-color: currentColor; }
.article .prose figcaption { margin-top: 0.6rem; color: var(--faint); font-size: 0.92rem; }
/* a link back up the page is a move, not a jump - the artefact it lands on
   is the thing being pointed at */
html { scroll-behavior: smooth; }
.article .back {
  display: inline-block;
  margin-top: 4rem;
  color: var(--faint);
  text-decoration: none;
  transition: color 0.2s ease;
}
.article .back::before { content: "\2190"; margin-right: 0.35em; }
.article .back:hover { color: var(--ink); }

/* ══════════ the feed becoming the article ══════════
   Three acts: what is leaving bows out, what persists glides to its new place,
   what is arriving breathes in last. The nameplate never moves or fades — one
   still landmark is what makes everything else read as the same surface
   rearranging rather than a different page. Chrome and Safari animate;
   Firefox changes pages instantly, which is the fallback. */
@view-transition { navigation: auto; }

.who { view-transition-name: nameplate; }
/* named per project, since the feed shows several at once and a name must be
   unique on the page */
.project.firsthand .name, .article.firsthand .eyebrow { view-transition-name: firsthand-name; }
.project.firsthand .desc, .article.firsthand .cover  { view-transition-name: firsthand-cover; }
.project.firsthand iframe.thumb, .article.firsthand .thumb { view-transition-name: firsthand-art; }
.project.vantage .name, .article.vantage .eyebrow { view-transition-name: vantage-name; }
.project.vantage .desc, .article.vantage .cover  { view-transition-name: vantage-cover; }
.project.vantage iframe.thumb, .article.vantage .thumb { view-transition-name: vantage-art; }
.project.hiya .name, .article.hiya .cover { view-transition-name: hiya-name; }
/* The sentence is not morphed, and firsthand's page is why: there, the tile's
   sentence IS the headline, so one thing travels. Here the name becomes the
   headline and the sentence stays a sentence - two boxes travelling side by
   side, growing at different rates, and for a third of a second the headline
   swells straight through the line under it. So the sentence arrives with the
   page instead. Nothing to collide with. */
.project.hiya .thumb, .article.hiya .thumb { view-transition-name: hiya-art; }
.project.nfc .name, .article.nfc .eyebrow { view-transition-name: nfc-name; }
.project.nfc .desc, .article.nfc .cover  { view-transition-name: nfc-cover; }
.project.nfc iframe.thumb, .article.nfc .thumb { view-transition-name: nfc-art; }
/* the mark is on both sides of the flight, so it travels rather than blinking
   out on one page and in on the other */
.project.nfc .live, .article.nfc .live { view-transition-name: nfc-live; }

::view-transition-group(nameplate) { animation: none; }
::view-transition-old(nameplate) { display: none; }
::view-transition-new(nameplate) { animation: none; opacity: 1; }

::view-transition-old(root) { animation: page-out 0.28s ease both; }
::view-transition-new(root) { animation: page-in 0.42s cubic-bezier(0.32, 0.72, 0, 1) 0.18s both; }
@keyframes page-out { to { opacity: 0; } }
@keyframes page-in { from { opacity: 0; transform: translateY(12px); } }

/* long enough to be watched, still fast off the mark */
::view-transition-group(firsthand-name),
::view-transition-group(firsthand-cover),
::view-transition-group(firsthand-art),
::view-transition-group(vantage-name),
::view-transition-group(vantage-cover),
::view-transition-group(vantage-art),
::view-transition-group(hiya-name),
::view-transition-group(hiya-art),
::view-transition-group(nfc-name),
::view-transition-group(nfc-cover),
::view-transition-group(nfc-art),
::view-transition-group(nfc-live) {
  animation-duration: 0.68s;
  animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
}
/* ONE element morphed, not two crossfaded. A crossfade always draws both
   copies at once, which reads as one thing shooting out while another swells
   in its place. So the departing copy is dropped and only the arriving one is
   drawn, stretched to the travelling box for the whole flight: it starts life
   exactly where and how big the tile's line was and grows into the headline.
   The artefact is carried whole for the same reason — crossfading a thing with
   itself is only blur. */
/* hiya was named in the group above but not here, so it kept the default and
   crossfaded: two copies of a 760x582 artefact drawn and rescaled every frame
   of the flight, where firsthand pays for one. On a laptop that is invisible.
   On a phone it is the stutter. */
::view-transition-old(firsthand-name),
::view-transition-old(firsthand-cover),
::view-transition-old(firsthand-art),
::view-transition-old(vantage-name),
::view-transition-old(vantage-cover),
::view-transition-old(vantage-art),
::view-transition-old(hiya-name),
::view-transition-old(hiya-art),
::view-transition-old(nfc-name),
::view-transition-old(nfc-cover),
::view-transition-old(nfc-art),
::view-transition-old(nfc-live) { display: none; }
::view-transition-new(firsthand-name),
::view-transition-new(firsthand-cover),
::view-transition-new(firsthand-art),
::view-transition-new(vantage-name),
::view-transition-new(vantage-cover),
::view-transition-new(vantage-art),
::view-transition-new(hiya-name),
::view-transition-new(hiya-art),
::view-transition-new(nfc-name),
::view-transition-new(nfc-cover),
::view-transition-new(nfc-art),
::view-transition-new(nfc-live) {
  animation: none;
  opacity: 1;
  width: 100%;
  height: 100%;
  object-fit: fill;
}

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

footer {
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  color: var(--faint);
  font-size: 0.92rem;
}
footer a { color: inherit; }
footer a:hover { color: var(--ink); }

/* The nameplate row: the name and the one word that opens something. */
.plate { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem; }
.plate a { color: var(--ink); }

/* Everything in the row answers a press the same way, because a row where one
   word dips and its neighbour does not reads as one live control parked among
   dead ones. The dip is felt before anything opens - the one piece of feedback
   that must never wait for a panel to arrive. */
.topbar a, .topbar button { transition: color 0.2s ease, transform 0.16s var(--ease-out); }
.topbar a:active, .topbar button:active { transform: scale(0.97); }
/* an icon is small enough that 0.97 is invisible on it, so it goes a little
   further to travel the same apparent distance as a word */
footer .social a { transition: color 0.2s ease, transform 0.16s var(--ease-out); }
footer .social a:active { transform: scale(0.92); }

/* ══════════ workshops ══════════
   Public sessions are neither a page nor a section of one: three dated links
   do not carry a page, and set inside the writing they cut it up. So they sit
   behind one word in the nameplate row, on every page rather than on one, and
   the list drops out of the word that opened it. */
/* the right of the nameplate: the way to the work, then the workshops */
.holds { position: relative; display: inline-flex; align-items: center; gap: 1.1rem; }
/* a word, not a button - the page it opens is not a pitch either */
.hire {
  color: var(--faint); text-decoration: none;
  transition: color 0.2s ease;
}
.hire:hover { color: var(--ink); }
.opens {
  font: inherit;
  color: var(--faint);
  background: none; border: 0; padding: 0;
  cursor: pointer;
}
.opens:hover, .opens[aria-expanded="true"] { color: var(--ink); }

/* Nothing on the page is dimmed - only the line the panel came out of. The
   name and the aside step back while the workshops are being read, then come
   forward again: focus handled at the scale of the thing that changed. */
.topbar .plate > * { transition: opacity 0.4s ease; }
.topbar.aside .plate > * { opacity: 0.28; }

.sheet {
  position: absolute;
  top: calc(100% + 0.6rem);
  right: 0;
  z-index: 40;
  /* sized by the longest line it holds, so each workshop's date, venue and
     price stay on the one row they read as - a width picked in advance wraps
     the price onto its own line and makes one fact into two half-facts */
  width: max-content;
  max-width: min(34rem, calc(100vw - 2.5rem));
  padding: 1.1rem 1.2rem 1.15rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--paper);
  /* Slight enough to be read as lift rather than seen as a shadow: one wide
     layer for the lift, one tight for the contact edge, both cast in the
     site's own ink rather than black, which would grey the paper. */
  box-shadow: 0 8px 26px rgba(26, 26, 26, 0.07),
              0 1px 2px rgba(26, 26, 26, 0.05);
  text-align: left;

  /* It grows out of the word that opened it, not out of its own middle - the
     default origin is the commonest thing wrong with a dropdown. And it starts
     at 0.96 rather than 0, because nothing resumes from no size at all. */
  transform-origin: top right;
  opacity: 0;
  transform: scale(0.96) translateY(-4px);
  visibility: hidden;
  /* leaving is faster than arriving: someone who has closed a thing has
     already stopped looking at it */
  transition: opacity 0.15s var(--ease-out),
              transform 0.15s var(--ease-out),
              visibility 0s linear 0.15s;
}
/* Transitions rather than keyframes, because a transition can be interrupted
   and retargeted mid-flight. Pressing the word twice quickly with keyframes
   restarts from nothing; this turns around from wherever it had got to. */
.sheet.open {
  opacity: 1;
  transform: none;
  visibility: visible;
  transition: opacity 0.22s var(--ease-out),
              transform 0.22s var(--ease-out),
              visibility 0s;
}

/* The three arrive in order, a beat apart, so the panel reads as three things
   being set down rather than one box switching on. They leave together - a
   staggered exit is three separate goodbyes to something already dismissed. */
.sheet li {
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.sheet.open li {
  opacity: 1;
  transform: none;
  transition: opacity 0.3s var(--ease-out), transform 0.3s var(--ease-out);
}
.sheet.open li:nth-child(1) { transition-delay: 0.04s; }
.sheet.open li:nth-child(2) { transition-delay: 0.10s; }
.sheet.open li:nth-child(3) { transition-delay: 0.16s; }

.sheet ul { list-style: none; margin: 0; padding: 0; }
.sheet li + li {
  margin-top: 0.95rem; padding-top: 0.95rem;
  border-top: 1px solid var(--line);
}
.sheet .what, .hello .drawer .what {
  display: block; color: var(--ink); text-decoration: none;
  font-size: 0.98rem; font-weight: 500;
  letter-spacing: -0.005em; line-height: 1.35;
}
.sheet .what::after, .hello .drawer .what::after {
  content: "\2197"; margin-left: 0.3em;
  color: var(--faint); font-size: 0.82em;
  display: inline-block;
  transition: transform 0.2s var(--ease-out), color 0.2s ease;
}
/* the mark leans the way the link goes - the whole of the delight budget for
   something a visitor may open three times */
.sheet .what:hover::after, .hello .drawer .what:hover::after { color: var(--ink); transform: translate(1px, -1px); }

/* when, where, what it costs - always that order, so after the first line the
   eye stops reading the icons and only reads the values */
.sheet .meta, .hello .drawer .meta {
  display: flex; flex-wrap: wrap; gap: 0.3rem 0.85rem;
  margin-top: 0.35rem;
  color: var(--faint); font-size: 0.85rem; line-height: 1.5;
}
/* the value and its icon must never be split across a wrap - an orphaned pin
   above an address reads as a broken glyph */
.sheet .meta span, .hello .drawer .meta span { display: inline-flex; align-items: center; gap: 0.35em; white-space: nowrap; }
.sheet .meta svg, .hello .drawer .meta svg { width: 1em; height: 1em; flex: none; fill: currentColor; opacity: 0.75; }
/* the venue is a link, but it stays the colour of the meta line: it is a place
   to check, not the thing being offered */
.sheet .where, .hello .drawer .where {
  color: inherit; text-decoration: underline;
  text-decoration-color: var(--line); text-underline-offset: 0.2em;
  transition: color 0.2s ease, text-decoration-color 0.2s ease;
}
.sheet .where:hover, .hello .drawer .where:hover { color: var(--ink); text-decoration-color: currentColor; }

/* On a phone the row stacks, so the panel is pinned to the column rather than
   hung off a word that has drifted in from the edge. */
/* Wide enough, the panel hangs off .holds, which already sits inside whatever
   margin the page keeps. Stacked, it hangs off the row itself - and a row that
   pads itself has its own edge at the window's, so the page states that padding
   as --bar-gutter and the panel lands on the margin rather than the glass. */
@media (max-width: 34rem) {
  .holds { position: static; }
  .topbar { position: relative; }
  .sheet {
    right: var(--bar-gutter, 0px); left: var(--bar-gutter, 0px);
    width: auto; max-width: none; transform-origin: top center;
  }
}

/* Reduced motion means gentler, not none: the fade stays so the panel is still
   seen to arrive, and everything that moves in space stops moving. */
@media (prefers-reduced-motion: reduce) {
  .sheet, .sheet.open { transform: none; }
  .sheet li, .sheet.open li { transform: none; transition-delay: 0s; }
  .topbar a:active, .topbar button:active,
  footer .social a:active,
  .sheet .what:hover::after { transform: none; }
}

/* The page that is not a page. It borrows the headline and the nameplate so a
   wrong address still lands somewhere that is plainly the site. On the home
   page the headline is pulled down toward the work it introduces; here there
   is no work under it, only the way back, so the pull comes off and the two
   sit a clear 4rem apart. */
.notfound h1 { margin-bottom: 0; }
.back {
  color: var(--faint); text-decoration: none; font-size: 0.92rem;
  width: fit-content; border-bottom: 1px solid var(--line);
  transition: color 0.2s ease, border-color 0.2s ease;
}
.back:hover { color: var(--ink); border-color: var(--faint); }

/* The two places to find him, at the foot of every page. They were in the
   nameplate row as well; once a word that says what it opens needed that
   space, a mark repeated forty pixels from itself was what had to give. The
   marks are the platforms' own, filled, because a brand drawn in somebody
   else's hand reads as an approximation of itself. */
.topbar {
  align-self: stretch;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
}
.social { display: inline-flex; align-items: center; gap: 1.05rem; }
.social a { color: var(--faint); display: inline-flex; transition: color 0.2s ease; }
.social a:hover { color: var(--ink); }
/* sized to the nameplate's letters rather than to the row, so they read as
   part of the line instead of buttons parked beside it */
.social svg { width: 0.95rem; height: 0.95rem; display: block; }
/* The envelope is naturally wider and leaves more air above and below its
   drawing. A small optical correction gives it the same weight as LinkedIn. */
.social svg.mail-icon { width: 1.1rem; height: 1.1rem; }
footer { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
@media (max-width: 34rem) {
  .topbar { align-items: flex-start; }
}

/* ── workshop worksheet ──
   One step per 💻 slide, same words as the slide. The page is a single column
   of steps separated by hairlines, not a stack of cards: the only surface with
   its own colour is the prompt, because the prompt is the thing to copy and
   nothing else on the page competes with it. */
.ws .body, .ws .prose { max-width: 100%; overflow-wrap: anywhere; }
/* The worksheet is read during the session, on a phone or beside Claude. The
   one job the nameplate has here is the way back and the next session, so the
   hire link steps out: nobody in the room is shopping. */
.ws .holds .hire { display: none; }
/* The contents no longer sits on the page — it stayed the length of a screen
   on a phone before the first step. It lives on as the pill's source of truth
   and nothing else, so a step exists in one place only. */
.ws .toc { display: none; }

/* A step is a heading, a grey line saying why, and the prompt. The hairline
   between two steps is the whole separation: a border all the way round each
   one boxed the prompt inside a second box and the page read as bricks. */
.ws .card { margin: 2.6rem 0 0; }
.ws .card + .card { margin-top: 2.8rem; padding-top: 2.8rem; border-top: 1px solid var(--line); }
/* the role field already ends in a rule of its own; a second hairline right
   under it reads as a mistake */
.ws .toc + .card { margin-top: 3rem; }
/* The last card is not a thirteenth step, so it takes no number, and the extra
   air above it is the only thing that says the workshop is over and this one is
   for later. */
.ws .card.end { margin-top: 4.4rem; padding-top: 3.4rem; }
.ws .card.end p { margin-bottom: 0; }
/* the robot is the only picture on the page, and it is here because this card
   is the one people meet after the numbers stop */
.ws .card.end h3 .ico, .ws .card.activity h3 .ico { font-size: 1.1rem; line-height: 1; }
/* the star and its title wrap as one line of words, so a long title on a phone
   keeps its star in front instead of leaving it alone on a line above */
.ws .card.activity > h3 .t { min-width: 0; }
.ws .card.activity > h3 .t .ico { margin-right: 0.2rem; }
/* An activity is the workshop handed back: the steps just shown, done on a
   task of the participant's own. It is not one of the twelve, so it takes no
   number, here or in the steps list. */
.ws .card .aside { color: var(--faint); }
.ws .card p.aside { margin-top: 1.1rem; }
/* Help for both ends of the room at once: whoever cannot think of a task, and
   whoever has already finished one. It stays shut until asked, so the activity
   itself is still seven lines. */
.ws .card details.more { margin-top: 1.4rem; }
.ws .card details.more summary {
  display: inline-flex; align-items: flex-start; gap: 0.55rem;
  cursor: pointer; list-style: none; line-height: 1.5;
  font-size: 0.95rem; font-weight: 600; color: var(--faint);
  transition: color 0.16s ease-out;
}
.ws .card details.more summary::-webkit-details-marker { display: none; }
.ws .card details.more summary::before {
  /* on the first line of the question, not halfway down a wrapped one */
  content: ""; flex: none; width: 0.38rem; height: 0.38rem; margin-top: 0.5rem;
  border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
  transform: rotate(-45deg); transition: transform 0.16s ease-out;
}
.ws .card details.more[open] summary { color: var(--ink); }
.ws .card details.more[open] summary::before { transform: translateY(-0.12rem) rotate(45deg); }
.ws .card details.more ol.how { list-style: decimal; margin: 0.8rem 0 0 1.5rem; }
/* A line of help, not a warning: the note style brings a triangle with it, and
   nothing here is going wrong. It sits hard against the block it describes,
   because a gap would let it read as the end of the paragraph above. */
.ws .card p.hint { font-weight: 600; font-size: 0.95rem; margin: 0 0 -0.1rem; }
/* A step title is the thing people scan for when they look up mid-demo, so it
   is nearer a section heading than a line of body text. */
.ws .card > h3 {
  display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
  font-size: 1.24rem; letter-spacing: -0.012em; line-height: 1.3;
  margin: 0 0 0.55rem 0;
}
/* Which app this step happens in, wearing the mark the feed puts beside a
   project name. It is on every step rather than only where the tool changes,
   because the pill exists so people can jump — and a step arrived at sideways
   still has to say where it is. */
/* The sprite holds the two logos and draws nothing itself. It sits outside
   main, so scoping it to .ws left it at an svg's default 300x150 - a screen
   of blank paper above the nameplate on every page load. */
svg.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.ws .card > h3 .app {
  display: inline-flex; align-items: center; gap: 0.25rem;
  height: 1.1rem; padding: 0 0.42rem 0 0.34rem;
  font-size: 0.58rem; font-weight: 700; letter-spacing: 0.055em; text-transform: uppercase;
  line-height: 1; color: var(--faint);
  border: 1px solid var(--line); border-radius: 999px;
}
/* The maker's own mark, in the maker's own colour — the two places on this
   site where a logo is quoted rather than drawn, so they are quoted straight.
   The word beside it stays grey; the colour is the badge, not the label. */
.ws .card > h3 .app .lg { width: 0.6rem; height: 0.6rem; }
.ws .card > h3 .app.claude .lg { fill: #d97757; }
.ws .card > h3 .app.gpt .lg { fill: var(--ink); }
/* an activity works in whatever AI someone already has, so it names no maker
   and carries no mark */
.ws .card > h3 .app.any { padding: 0 0.42rem; }
/* the tag sits at the right edge of the column, off the end of the title */
.ws .card > h3 .app { margin-left: auto; }
/* Numbers, not a laptop on every line. Eleven identical icons said only "this
   is a step", which every one of them already was; a number says which one,
   and that is the thing anyone asks. They count themselves, so inserting a
   step never leaves a wrong number behind. */
.ws .prose { counter-reset: step; }
.ws .card:not(.you):not(.end):not(.activity) > h3::before {
  counter-increment: step;
  content: counter(step) ".";
  color: var(--faint);
  /* close enough to belong to the title rather than to sit apart from it */
  margin-right: 0.3rem;
  font-variant-numeric: tabular-nums;
}
.ws .card p { margin: 0 0 0.8rem 0; }
.ws .card p:last-child { margin-bottom: 0; }
.ws .card .name { font-weight: 600; }
/* the one field on the page: a rule under a line of typing, nothing more. The
   browser's own box would be the only rectangle left on the page. */
.ws .you h3 { margin-bottom: 0.2rem; }
.ws .you label { display: block; margin-top: 0.4rem; }
.ws .you input {
  width: 100%;
  font: inherit;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: transparent;
  padding: 0.3rem 0;
  outline: none;
  -webkit-appearance: none;
  appearance: none;
}
.ws .you input:focus { border-color: var(--ink); }
/* What to press, in order, for anyone who has never opened this screen. Tight
   and grey-numbered so it stays a set of directions rather than a second body
   of writing above the prompt. */
/* Lettered, not numbered: the step already owns a number, and a "1." inside
   step 1 makes a reader work out which one is being counted. */
.ws .card ol.how { list-style: lower-alpha; margin: 0.35rem 0 1.1rem 1.5rem; }
.ws a.file + ol.how { margin-top: 0; }
/* A prompt belongs to the line above it, so it sits at one line's distance from
   that line - and at two from whatever comes next, because the step continues
   there. The list drops its own bottom margin first, or the two would stack. */
.ws .card ol.how:has(+ .prompt), .ws .card ol.how:has(+ a.file) { margin-bottom: 0; }
/* Read standing up, on a phone, while a demo runs on the projector. The step
   lines carry a little more size and a clear gap between them, so an eye
   coming back to the page finds its place instead of its paragraph. */
.ws .card ol.how li { font-size: 1.05rem; line-height: 1.6; margin-bottom: 0.85rem; padding-left: 0.2rem; }
.ws .card ol.how li:last-child { margin-bottom: 0; }
.ws .card ol.how li::marker { color: var(--faint); }
/* the same triangle, small, at the end of the line it belongs to: the note at
   the foot of the step is then plainly about this instruction and not the card */
.ws .card ol.how .fn { width: 0.72rem; height: 0.72rem; vertical-align: baseline; color: var(--ink); }
/* What they are agreeing to: last, once, and in the same voice as the rest. */
/* The one line here that has to be read rather than skimmed. Every other box
   came off this page, which is exactly why this one works: it is the only
   outline left, so it cannot be mistaken for a container the page happens to
   use. Hairline and open field, the same rule that separates the steps. */
.ws .card p.note {
  display: flex;
  gap: 0.55rem;
  align-items: flex-start;
  margin: 1.6rem 0 0;
  padding: 0.75rem 0.95rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  color: var(--ink);
  font-size: 0.92rem;
  line-height: 1.55;
}
.ws .card p.note svg { flex: none; width: 0.9rem; height: 0.9rem; margin-top: 0.25rem; color: var(--ink); }

/* The padding is trimmed at the top rather than set equal. A line box begins
   above the letters and ends below them, so equal padding reads top-heavy:
   the first line shows its leading and the last line fills the gap with its
   descenders. These two numbers look like the same space. */
.ws .prompt { position: relative; background: #f4f4f0; border-radius: 14px; padding: 0.88rem 1.2rem 1.12rem; margin: 0.85rem 0 1.7rem; }
/* the article's code block brings its own border and field; here the prompt
   surface is the field, so the pre is only text */
.ws .prompt pre { margin: 0; padding: 0 2rem 0 0; border: 0; border-radius: 0; background: none; overflow-x: visible; white-space: pre-wrap; font: inherit; font-size: 0.92rem; line-height: 1.6; }
/* The text itself fades — the glyphs thin out and vanish. An overlay laid on
   top could only ever be a panel of the field's own colour, which is a grey
   block by another name, and it needed height to look gradual. A mask needs
   none: the last line is half there and then it is not. */
/* The newsletter is the one thing here longer than a screen, so it gets a
   window of its own to scroll inside rather than pushing the next step off the
   page. Nothing else on this page scrolls: a prompt of a few lines should be
   read whole. */
.ws .prompt.long .rich, .ws .prompt.long pre {
  max-height: 17rem;
  overflow-y: auto;
  padding-right: 0.8rem;
  overscroll-behavior: contain;
}
/* the icons step inward by the width of the scrollbar, so the bar has a lane
   of its own instead of running straight through them */
.ws .prompt.long button.copy { right: 1.35rem; }
.ws .prompt.long .gmail { right: 3.3rem; }
.ws .prompt.long .rich::-webkit-scrollbar, .ws .prompt.long pre::-webkit-scrollbar { width: 10px; }
.ws .prompt.long .rich::-webkit-scrollbar-thumb, .ws .prompt.long pre::-webkit-scrollbar-thumb {
  background: rgba(0, 0, 0, 0.14);
  border: 3px solid #f4f4f0;
  border-radius: 999px;
}
.ws .prompt.long .rich::-webkit-scrollbar-track, .ws .prompt.long pre::-webkit-scrollbar-track { background: transparent; }
.ws .prompt.long .rich, .ws .prompt.long pre { scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.18) transparent; }
/* A newsletter is written, not typed: it keeps its headings, its bold and its
   quotes here so that what is copied is what lands in the mail. Sized down to
   the prompt's own voice — this is still material to copy, not to read. */
.ws .prompt .rich { font-size: 0.92rem; line-height: 1.6; }
.ws .prompt .rich > :first-child { margin-top: 0; }
.ws .prompt .rich h2 { font-size: 1.08rem; font-weight: 600; margin: 1.1rem 0 0.3rem; letter-spacing: -0.01em; }
.ws .prompt .rich h3 { font-size: 1rem; font-weight: 600; margin: 1rem 0 0.3rem; }
.ws .prompt .rich h4 { font-size: 0.95rem; font-weight: 600; margin: 0.9rem 0 0.3rem; color: var(--faint); }
.ws .prompt .rich p { margin: 0 0 0.7rem; }
.ws .prompt .rich strong { font-weight: 600; }
.ws .prompt .rich ol { margin: 0 0 0.7rem 1.2rem; padding: 0; }
.ws .prompt .rich li { margin-bottom: 0.3rem; }
.ws .prompt .rich blockquote {
  margin: 0 0 0.7rem; padding-left: 0.9rem;
  border-left: 2px solid var(--line); color: var(--faint);
}
.ws .prompt .rich hr { border: 0; border-top: 1px solid var(--line); margin: 1rem 0; }

.ws .prompt b { font-weight: 600; }
.ws .prompt .v { border-bottom: 1px dashed var(--faint); }
.ws .prompt .v.empty { color: var(--faint); }
/* Copy is an icon and nothing else, the way it is in ChatGPT and Claude: no
   border, no pill, no word. It is faint until the pointer is on the prompt,
   and it swaps to a tick for a moment once the text is on the clipboard. */
.ws button.copy {
  position: absolute; top: 0.55rem; right: 0.55rem;
  display: grid; place-items: center;
  width: 2rem; height: 2rem;
  border: 0; border-radius: 8px; background: transparent;
  color: var(--faint); cursor: pointer;
  opacity: 0.55; transition: opacity 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.ws .prompt:hover button.copy, .ws button.copy:focus-visible { opacity: 1; }
/* the way to a compose window, sitting beside the copy it belongs to */
.ws .prompt .gmail {
  position: absolute; top: 0.55rem; right: 2.5rem;
  display: grid; place-items: center;
  width: 2rem; height: 2rem;
  border-radius: 8px;
  color: var(--faint);
  opacity: 0.55; transition: opacity 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.ws .prompt:hover .gmail, .ws .prompt .gmail:focus-visible { opacity: 1; }
.ws .prompt .gmail svg { width: 1.05rem; height: 1.05rem; display: block; }
.ws button.copy svg { width: 1.05rem; height: 1.05rem; display: block; }
.ws button.copy .tick, .ws button.copy.done .mark { display: none; }
.ws button.copy.done .tick { display: block; animation: tick-in 0.24s var(--ease-out); }
@keyframes tick-in { from { opacity: 0; transform: scale(0.7); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .ws button.copy.done .tick { animation: none; } }
.ws button.copy.done { opacity: 1; color: var(--ink); }

/* A file to take away, drawn the way a chat window draws an attachment: the
   kind of file, its name, and the way to get it, across the full width of the
   column. No fill — the hairline is enough on a page made of hairlines. */
.ws a.file {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  width: 100%;
  /* the list above ends on its own bottom margin, so the row only owes the
     space beneath it — that is what makes the two gaps read as one size */
  margin: 0.85rem 0 1.7rem;
  padding: 0.6rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  color: var(--ink);
  text-decoration: none;
}
/* hover washes the row rather than drawing a black line round it: it says
   pressable without saying selected */
.ws a.file:active { transform: scale(0.995); }
/* ── the two gates ──
   Not steps: the thing everyone must have open before a run of steps will work
   at all. Same row as a file to take away, with the app's own mark and an
   arrow that leaves the page rather than one that comes down into it. */
.ws .gate { margin: 3rem 0 2.8rem; }
.ws .gate .lab {
  display: block; margin-bottom: 0.5rem;
  color: var(--faint);
  font-size: 0.72rem; font-weight: 600; letter-spacing: 0.07em; text-transform: uppercase;
}
.ws .gate a.file { margin: 0; }
.ws a.file .kind.brand { fill: currentColor; color: var(--ink); stroke: none; }
/* the maker's own colour, quoted the same way the step tags quote it */
.ws a.file .kind.brand.claude { color: #d97757; }
.ws a.file .kind { flex: none; width: 1.35rem; height: 1.35rem; color: var(--faint); }
.ws a.file .what { display: flex; flex-direction: column; gap: 0; min-width: 0; line-height: 1.35; }
.ws a.file .fname { font-size: 0.95rem; font-weight: 500; overflow-wrap: anywhere; }
.ws a.file .sub { color: var(--faint); font-size: 0.8rem; }
.ws a.file .go {
  flex: none; width: 1.05rem; height: 1.05rem; margin-left: auto;
  color: var(--faint);
  transition: color 0.16s ease-out;
}
.ws a.file { transition: background-color 0.16s ease-out, transform 0.16s ease-out; }

/* ── the steps pill ──
   A sidebar needs a window this page rarely gets: it is read on a phone, or on
   half a laptop screen beside Claude. So the contents becomes one small thing
   in the corner instead, the same on both — and it wears the mark the feed
   already uses, a hairline pill, rather than a black slab. */
.ws-steps {
  position: fixed;
  right: max(1rem, env(safe-area-inset-right));
  bottom: max(1rem, env(safe-area-inset-bottom));
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.55rem;
  /* Out of the way until the contents at the top has gone by. Leaving is
     quicker than arriving and eases in rather than out: a thing on its way out
     should not linger over the words. */
  opacity: 0;
  transform: translateY(0.4rem);
  pointer-events: none;
  transition: opacity 0.14s ease-in, transform 0.14s ease-in;
}
.ws-steps.is-on {
  opacity: 1;
  transform: none;
  transition: opacity 0.24s var(--ease-out), transform 0.24s var(--ease-out);
}
/* The shut panel still takes up its full room here, invisibly. On a phone that
   room is most of the screen, and every press meant for the page landed on it
   instead. Nothing but the pill, and the panel once open, may take a press. */
.ws-steps.is-on .pill { pointer-events: auto; }
.ws-steps .pill {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font: inherit; font-size: 0.82rem; font-weight: 600; letter-spacing: 0.02em;
  padding: 0.42rem 0.85rem 0.42rem 0.7rem;
  color: var(--faint);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  transition:
    color 0.16s ease-out,
    background-color 0.16s ease-out,
    border-color 0.16s ease-out,
    transform 0.16s ease-out;
}
/* Open is the same fill hover gives, so the two states cannot contradict
   each other - the old rule set ink text on a dark fill the moment a pointer
   landed on an already-open pill. */
.ws-steps.open .pill, .ws-steps.open .pill:hover {
  color: var(--paper); background: #3d3d38; border-color: #3d3d38;
}
.ws-steps .pill:active { transform: scale(0.96); }
.ws-steps .pill svg { width: 0.95rem; height: 0.95rem; }
.ws-steps .list {
  /* the panel grows out of the pill: the corner it scales from is the corner
     the pill sits in, so it reads as the pill opening rather than a card
     appearing from nowhere */
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 0.9rem 1.1rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
  /* room for all eleven where the screen allows it — the cap is only there
     so it can never run off the top of a short window */
  max-height: calc(100vh - 6rem);
  overflow-y: auto;
  position: relative;
  width: max-content;
  max-width: min(20rem, calc(100vw - 2rem));
  opacity: 0;
  transform: translateY(0.4rem) scale(0.97);
  transform-origin: bottom right;
  pointer-events: none;
  transition: opacity 0.13s ease-in, transform 0.13s ease-in;
}
.ws-steps.open .list {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  transition: opacity 0.22s var(--ease-out), transform 0.22s var(--ease-out);
}
/* The list stays open when a step is chosen — the room jumps, reads, and jumps
   again — so it needs a way out of its own: this, or a press anywhere off it. */
.ws-steps .list .shut {
  /* on the first label's own line rather than above it: a row kept for nothing
     but a close button is a row of the list spent on the list */
  position: absolute; top: 0.6rem; right: 0.6rem;
  display: grid; place-items: center;
  width: 1.75rem; height: 1.75rem;
  border: 0; border-radius: 8px; background: transparent;
  color: var(--faint); cursor: pointer;
  transition: color 0.16s ease-out, background-color 0.16s ease-out;
}
.ws-steps .list .shut svg { width: 0.95rem; height: 0.95rem; }
.ws-steps .list ul { list-style: none; margin: 0; padding: 0; counter-reset: step; }
.ws-steps .list li {
  margin: 0; padding: 0; font-size: 0.95rem;
  /* the steps arrive one after another rather than all at once — 16ms apart,
     which is felt as a sweep rather than counted as eleven events */
  opacity: 0;
  transform: translateY(0.35rem);
  transition: opacity 0.16s ease-in, transform 0.16s ease-in;
}
.ws-steps.open .list li {
  opacity: 1;
  transform: none;
  transition: opacity 0.2s var(--ease-out), transform 0.2s var(--ease-out);
  transition-delay: calc(var(--i, 0) * 16ms);
}
/* the same split, so the list says which app to open before anyone jumps */
.ws-steps .list li.grp {
  margin: 0.75rem 0 0.2rem;
  color: var(--faint);
  font-size: 0.72rem; font-weight: 600; letter-spacing: 0.07em; text-transform: uppercase;
}
.ws-steps .list li.grp:first-child { margin-top: 0; padding-right: 2rem; }
.ws-steps .list a { display: block; padding: 0.28rem 0; text-decoration: none; border: 0; color: var(--ink); }
.ws-steps .list a::before {
  counter-increment: step;
  content: counter(step) ".";
  /* the numbers hang in a column of their own, so ten and eleven do not shunt
     their own words out of line with the rest */
  display: inline-block;
  width: 1.5em;
  text-align: right;
  color: var(--faint);
  margin-right: 0.45rem;
  font-variant-numeric: tabular-nums;
}
/* where the room is now */
.ws-steps .list a.here { font-weight: 600; }
/* the last entry is the takeaway, which happens after the workshop rather than
   during it: it keeps its emoji and its words, and gives up its number */
.ws-steps .list li.end { margin-top: 0.75rem; }
.ws-steps .list li.end a::before { content: none; counter-increment: none; }
.ws-steps .list li.activity a::before { content: none; counter-increment: none; }
@media (prefers-reduced-motion: reduce) {
  .ws-steps, .ws-steps .list, .ws-steps .list li, .ws-steps .pill,
  .ws a.dl, .ws a.dl svg { transition: none; }
  .ws-steps.open .list li { transition-delay: 0s; }
}

/* ── hover belongs to pointers ──
   On a touch screen :hover latches onto whatever was last tapped and stays
   there, so the steps pill kept its pressed fill long after it was closed.
   These are the states a finger should never trigger. */
@media (hover: hover) and (pointer: fine) {
  .ws-steps .pill:hover { color: var(--paper); background: #3d3d38; border-color: #3d3d38; }
  .ws-steps .list .shut:hover { color: var(--ink); background: rgba(0, 0, 0, 0.05); }
  .ws-steps .list a:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
  .ws button.copy:hover { color: var(--ink); background: rgba(0, 0, 0, 0.05); }
  .ws .prompt .gmail:hover { color: var(--ink); background: rgba(0, 0, 0, 0.05); }
  .ws a.file:hover { background: rgba(0, 0, 0, 0.035); }
  .ws a.file:hover .go { color: var(--ink); }
  .ws .gate a.file:hover .go { color: var(--ink); }
}

/* ── the top of the worksheet ──
   The headline, the one question, the field. On a phone the article's own
   opening spacing left a hole between them, and the question was set at step
   size when it is not a step - it is the one thing to fill in. Body size,
   bold: the same weight the page gives a name, no louder. */
.ws .body { margin-top: 2.2rem; }
.ws .cover { margin-bottom: 0; }
.ws .prose { margin-top: 1.8rem; }
.ws .card.you > h3 {
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1.45;
  margin-bottom: 0.35rem;
}
.ws .toc + .card, .ws .card.you + .card { margin-top: 2.6rem; }
@media (max-width: 30rem) {
  .ws .body { margin-top: 1.6rem; }
  .ws .prose { margin-top: 1.5rem; }
}

/* ── the card page ──
   What the card opens. Built for a phone held in one hand, a minute after
   meeting: who this was, how to reach him, and the card itself. */
.hello { max-width: 26rem; gap: 0; }

.hello h1 { margin: 3rem 0 0; }
.hello .line { color: var(--faint); margin-top: 0.55rem; max-width: 22rem; }

/* every way to reach him, one row each, all the same weight. The workshops
   are something to open rather than an address to keep, so they carry a
   caret where the others carry copy. */
.hello .ways { list-style: none; margin-top: 2.4rem; border-top: 1px solid var(--line); }
.hello .ways > li { display: flex; flex-wrap: wrap; align-items: center; gap: 0 0.35rem; border-bottom: 1px solid var(--line); }
.hello .ways .way {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.9rem;
  padding: 1rem 0.1rem;
  border: 0; background: none; font: inherit; text-align: left; cursor: pointer;
  color: var(--ink); text-decoration: none;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.16s var(--ease-out);
}
.hello .ways .way:active { transform: scale(0.98); }
/* Filled, the way the footer draws them: the footer's own mail and LinkedIn
   marks, and Phosphor's filled globe and cap beside them. Filled
   shapes weigh more than outlines, so they sit a size down. */
/* every icon holds the same 1.1rem column so the labels line up; the boxy
   ones are drawn smaller inside it, because a square reads bigger than a
   circle of the same width */
.hello .ways .ic { width: 0.95rem; height: 0.95rem; flex: none; margin: 0 0.075rem; }
.hello .ways .ic.mail { width: 1rem; height: 1rem; margin: 0 0.05rem; }
.hello .ways .ic.globe, .hello .ways .ic.cap { width: 1.1rem; height: 1.1rem; margin: 0; }
.hello .ways .ic { color: var(--faint); transition: color 0.2s ease; }
.hello .ways .way:hover .ic { color: var(--ink); }
.hello .ways .what { font-weight: 500; }
.hello .ways .detail {
  margin-left: auto; min-width: 0; color: var(--faint); font-size: 0.88rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* the caret sits exactly where a copy button would, inside the row so the
   whole row is still the thing pressed */
.hello .ways .end {
  flex: none; display: grid; place-items: center;
  width: 2.25rem; height: 1.25rem; margin: 0 -0.55rem 0 auto;
  color: var(--faint); transition: color 0.2s ease, transform 0.35s var(--ease-out);
}
.hello .ways .detail + .end { margin-left: -0.55rem; }
.hello .ways .end svg { width: 1.05rem; height: 1.05rem; display: block; }
.hello .ways .way:hover .end { color: var(--ink); }
/* a small phone still has to show all of a LinkedIn address */
@media (max-width: 380px) {
  .hello .ways .way { gap: 0.7rem; }
  .hello .ways .detail { font-size: 0.8rem; }
}

/* The workshops open inside the list, under their row, rather than over the
   page: the row lets go of its date and turns its caret, the list grows to
   hold them, and they are set down one after another at the list's full
   width, with no indent of their own. Closing is quicker and all at once. */
.hello .ways .coming .detail { transition: opacity 0.2s ease, transform 0.3s var(--ease-out); }
.hello .ways .coming.open .detail { opacity: 0; transform: translateX(6px); }
.hello .ways .coming.open .end { color: var(--ink); transform: rotate(180deg); }
.hello .ways .drawer {
  flex: 1 1 100%;
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows 0.28s var(--ease-out);
}
.hello .ways .coming.open .drawer { grid-template-rows: 1fr; transition-duration: 0.45s; }
.hello .ways .drawer > div { min-height: 0; overflow: hidden; }
.hello .ways .drawer ul { list-style: none; padding: 0 0.1rem 1.1rem; }
.hello .ways .drawer li + li { margin-top: 1rem; }
.hello .ways .drawer li {
  opacity: 0; transform: translateY(-6px);
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.hello .ways .coming.open .drawer li {
  opacity: 1; transform: none;
  transition: opacity 0.35s var(--ease-out), transform 0.4s var(--ease-out);
}
.hello .ways .coming.open .drawer li:nth-child(1) { transition-delay: 0.08s; }
.hello .ways .coming.open .drawer li:nth-child(2) { transition-delay: 0.15s; }
.hello .ways .coming.open .drawer li:nth-child(3) { transition-delay: 0.22s; }
.hello .ways .coming.open .drawer li:nth-child(n+4) { transition-delay: 0.29s; }

/* gentler, not none: the workshops still fade in, but nothing moves or grows */
@media (prefers-reduced-motion: reduce) {
  .hello .ways .drawer li, .hello .ways .coming.open .drawer li,
  .hello .ways .coming.open .detail { transform: none; transition-delay: 0s; }
  .hello .ways .drawer { transition: none; }
}

/* copy sits beside each way, the same quiet control the worksheets use: no
   border and no word, a tick for a moment once it is on the clipboard */
.hello .ways button.copy {
  flex: none; display: grid; place-items: center;
  width: 2.25rem; height: 2.25rem; margin-right: -0.45rem;
  border: 0; border-radius: 8px; background: transparent;
  color: var(--faint); cursor: pointer;
  transition: color 0.2s ease, background 0.2s ease, transform 0.16s var(--ease-out);
}
.hello .ways button.copy:hover { color: var(--ink); background: rgba(0, 0, 0, 0.05); }
.hello .ways button.copy:active { transform: scale(0.92); }
.hello .ways button.copy svg { width: 1.05rem; height: 1.05rem; display: block; }
.hello .ways button.copy .tick, .hello .ways button.copy.done .mark { display: none; }
.hello .ways button.copy.done { color: var(--ink); }
.hello .ways button.copy.done .tick { display: block; animation: tick-in 0.24s var(--ease-out); }

/* the card is theirs now. The card itself sits beside the words, lifted off
   the paper, so the thing in their hand and the thing on screen are plainly
   the same object. */
.hello .keep {
  margin-top: 2.8rem;
  display: flex; align-items: center; gap: 1.6rem;
}
.hello .keep .words { min-width: 0; }
.hello .keep p { font-weight: 500; }

/* The card turns in 3D. It arrives face up, then turns over on its own and
   stays on the back, so the tag is beside the sentence about it and nobody
   has to know a card can be turned to find out. A tap turns it again, and on
   a phone a finger can drag it round; neither needs a hover. */
.hello .flip {
  flex: none; width: 8.5rem; aspect-ratio: 305 / 408;
  perspective: 900px;
  cursor: pointer; touch-action: pan-y;
  -webkit-tap-highlight-color: transparent;
  user-select: none; -webkit-user-select: none;
}
@property --card-ry { syntax: "<angle>"; inherits: true; initial-value: 0deg; }
@property --card-tx { syntax: "<angle>"; inherits: true; initial-value: 6deg; }
@property --card-ty { syntax: "<angle>"; inherits: true; initial-value: -10deg; }
/* The angles are registered and it is they that are eased, not the transform:
   an eased transform would leave the light, which reads the same angle,
   jumping straight to where the card ends up. */
.hello .flip .turn {
  position: relative; width: 100%; height: 100%;
  transform-style: preserve-3d;
  transform: rotateX(var(--card-tx)) rotateY(calc(var(--card-ry) + var(--card-ty))) rotateZ(-2deg);
  transition: --card-ry 0.9s cubic-bezier(0.34, 1.25, 0.5, 1), --card-tx 0.5s var(--ease-out), --card-ty 0.5s var(--ease-out);
}
.hello .flip.dragging .turn { transition: none; }
.hello .flip .face {
  position: absolute; inset: 0;

  border-radius: 7px; overflow: hidden;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
  background: #fff;
  /* a hairline and two shadows: a tight one where the card nearly touches
     the paper and a wide soft one for the lift */
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.06),
    0 2px 4px rgba(0, 0, 0, 0.06),
    0 14px 28px -8px rgba(0, 0, 0, 0.18);
}
/* Each face also hides itself once it has turned away. backface-visibility
   should do that alone, but a phone's browser will sometimes flatten the card
   and paint the front mirrored on the back; the angle is animated, so this
   follows every frame of a turn. */
.hello .flip .front {
  transform: rotateY(0deg);
  opacity: clamp(0, cos(calc(var(--card-ry) + var(--card-ty))) * 1000, 1);
}
.hello .flip .rear { opacity: clamp(0, cos(calc(var(--card-ry) + var(--card-ty))) * -1000, 1); }
.hello .flip .front img { display: block; width: 100%; height: 100%; pointer-events: none; }

/* Light. One soft band crosses each face as the card turns, worked out from
   how far round it is, so the card reads as a thing catching a lamp rather
   than a picture spinning. */
.hello .flip .face::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(115deg,
    rgba(255, 255, 255, 0) 30%, rgba(255, 255, 255, 0.55) 48%, rgba(255, 255, 255, 0) 64%),
    linear-gradient(160deg, rgba(255, 255, 255, 0.35), rgba(0, 0, 0, 0.035));
  background-size: 300% 100%, 100% 100%;
  background-position: calc(50% + sin(var(--card-ry)) * 60%) 0, 0 0;
  opacity: 0.4;
}

/* The back, drawn from the real one: plain card, a round tag, and a square of
   clear tape over it holding it on. The tag's antenna shows through its paper
   only as a faint ghost. */
.hello .flip .rear {
  transform: rotateY(180deg);
  background: linear-gradient(160deg, #ffffff, #f2f2ee);
}
.hello .flip .rear .disc {
  position: absolute; left: 22%; top: 25%; width: 58%; aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, #ffffff, #f6f6f3 70%, #efefeb);
  /* raised off the card: a crisp edge, a close shadow, and a wider soft
     one, so it reads as a thing stuck on rather than printed */
  box-shadow:
    0 0 0 0.75px rgba(0, 0, 0, 0.18),
    0 1.5px 2.5px rgba(0, 0, 0, 0.22),
    0 4px 12px rgba(0, 0, 0, 0.2);
}
.hello .flip .rear .disc svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.hello .flip .rear .disc svg { filter: blur(0.25px); }
.hello .flip .rear .coil { fill: none; stroke: #000; stroke-opacity: 0.2; stroke-width: 0.9; }
.hello .flip .rear .ant { fill: #000; fill-opacity: 0.15; }
.hello .flip .rear .ant .hole { fill: #fff; fill-opacity: 1; }
.hello .flip .rear .tape {
  position: absolute; left: 15%; top: 21%; width: 74%; height: 50%;
  transform: rotate(-1.5deg);
  border-radius: 1.5px;
  background:
    linear-gradient(115deg,
      rgba(255, 255, 255, 0) 25%, rgba(255, 255, 255, 0.75) 44%, rgba(255, 255, 255, 0) 60%),
    rgba(236, 236, 231, 0.22);
  background-size: 320% 100%, auto;
  background-position: calc(50% - sin(var(--card-ry)) * 70%) 0, 0 0;
  box-shadow:
    0 0 0 0.75px rgba(0, 0, 0, 0.13),
    inset 0 0 0 0.75px rgba(255, 255, 255, 0.9),
    0 1px 2.5px rgba(0, 0, 0, 0.12);
}
/* the corner that has started to lift, catching the light on its crease */
.hello .flip .rear .tape::after {
  content: ""; position: absolute; right: -0.5px; bottom: -0.5px;
  width: 16%; height: 7%;
  background: linear-gradient(160deg, rgba(255, 255, 255, 0.95), rgba(0, 0, 0, 0.05));
  clip-path: polygon(100% 0, 100% 100%, 0 100%);
  filter: drop-shadow(-0.5px -0.5px 0.5px rgba(0, 0, 0, 0.12));
}

/* the tag answers when it comes into view, the same rings the NFC page uses */
.hello .flip .rear .rings { position: absolute; left: 22%; top: 25%; width: 58%; aspect-ratio: 1; overflow: visible; }
.hello .flip .rear .ring { fill: none; stroke: var(--ink); stroke-width: 1; opacity: 0; transform-origin: 50% 50%; }
.hello .flip.showing-back .rear .ring { animation: hello-ring 1.3s var(--ease-out) 0.35s forwards; }
.hello .flip.showing-back .rear .ring:nth-of-type(2) { animation-delay: 0.53s; }
.hello .flip.showing-back .rear .ring:nth-of-type(3) { animation-delay: 0.71s; }
@keyframes hello-ring {
  0% { opacity: 0.35; transform: scale(0.5); }
  100% { opacity: 0; transform: scale(1.6); }
}

@media (prefers-reduced-motion: reduce) {
  .hello .flip .turn { transition-duration: 0.01s; }
  .hello .flip.showing-back .rear .ring { animation: none; }
}
.hello .keep a {
  display: inline-block; margin-top: 0.35rem;
  color: var(--faint); text-decoration: underline;
  text-decoration-color: var(--line); text-underline-offset: 0.2em;
  transition: color 0.2s ease;
}
.hello .keep a:hover { color: var(--ink); }

/* the rows are set down one after another, a beat apart */
.hello > *, .hello .ways > li { animation: hello-arrive 0.45s var(--ease-out) both; }
.hello h1 { animation-delay: 0.02s; }
.hello .line { animation-delay: 0.08s; }
.hello .ways > li:nth-child(1) { animation-delay: 0.14s; }
.hello .ways > li:nth-child(2) { animation-delay: 0.19s; }
.hello .ways > li:nth-child(3) { animation-delay: 0.24s; }
.hello .ways > li:nth-child(4) { animation-delay: 0.29s; }
.hello .ways > li:nth-child(5) { animation-delay: 0.34s; }
.hello .keep { animation-delay: 0.42s; }
@keyframes hello-arrive { from { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) {
  .hello > *, .hello .ways > li { animation: none; }
}

/* The preference control shares the footer's quiet treatment. */
.analytics-control { appearance: none; border: 0; padding: 0; background: none; font: inherit; color: inherit; text-decoration: underline; cursor: pointer; }
.analytics-control:disabled { text-decoration: none; cursor: default; }
.analytics-preference { margin: 2rem 0 0; font-size: 0.85rem; color: var(--faint); text-align: center; }
