/* Stanley Ho — portfolio
   One stylesheet, no dependencies, no build. The page is deliberately colourless:
   every saturated thing on screen should be the work itself, which is dark UI,
   so the paper around it stays warm and quiet. */

:root {
  --paper:   #fbfaf8;
  --ink:     #14130f;
  --ink-2:   #45433d;
  --ink-3:   #86837a;
  --rule:    #e5e2da;
  --rule-2:  #efece5;
  --well:    #f3f1ea;   /* the recess an embed sits in */

  /* Swap this for a webfont line and the whole site changes.
     e.g. @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
     then put "Inter" at the front of --sans. */
  --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* The frame, and the only two widths inside it. A container is either the
     measure — 680, centred — or the whole frame. Nothing in between, and
     nothing sitting at an edge of its own. Two arbitrary widths read as
     haphazard; two declared ones read as a grid. */
  --frame:   1120px;
  --content: 680px;    /* the measure */
  --pad:     24px;

  --t: .18s cubic-bezier(.2,.7,.3,1);
}

@media (min-width: 700px)  { :root { --pad: 40px; } }
@media (min-width: 1100px) { :root { --pad: 64px; } }

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

@media (min-width: 700px) { body { font-size: 18px; } }

img { max-width: 100%; display: block; }

a { color: inherit; text-decoration: none; }

/* Links in prose keep their underline — it is the only ornament here. */
.prose a, .link {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: var(--ink-3);
  transition: text-decoration-color var(--t);
}
.prose a:hover, .link:hover { text-decoration-color: var(--ink); }

/* ---------------------------------------------------------------- shell -- */

.wrap  { max-width: var(--frame);   margin: 0 auto; padding: 0 var(--pad); }
.col   { max-width: var(--content); margin: 0 auto; }
.col-l { max-width: var(--content); margin: 0; }

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

.site-head {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(12px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--t);
}
.site-head.stuck { border-bottom-color: var(--rule); }

.site-head .wrap {
  display: flex; align-items: center; justify-content: space-between;
  height: 68px;
}
.brand { font-weight: 600; letter-spacing: -.01em; font-size: 16px; }
.brand span { color: var(--ink-3); font-weight: 400; margin-left: 6px; }

.nav { display: flex; gap: 22px; font-size: 14px; color: var(--ink-2); }
.nav a { position: relative; }
.nav a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -3px; height: 1px;
  background: var(--ink); transform: scaleX(0); transform-origin: left;
  transition: transform var(--t);
}
.nav a:hover::after { transform: scaleX(1); }
@media (max-width: 560px) { .nav a.opt { display: none; } }

/* ----------------------------------------------------------------- type -- */

.display {
  font-size: clamp(38px, 7vw, 66px);
  line-height: 1.03;
  letter-spacing: -.035em;
  font-weight: 500;
  margin: 0;
}

.lede {
  font-size: clamp(20px, 2.4vw, 25px);
  line-height: 1.42;
  letter-spacing: -.015em;
  color: var(--ink-2);
  margin: 0;
}

.label {
  font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .14em;
  color: var(--ink-3);
  margin: 0 0 22px;
}

h2.head {
  font-size: clamp(24px, 3vw, 30px);
  line-height: 1.2; letter-spacing: -.02em; font-weight: 550;
  margin: 0 0 14px;
}

h3.sub {
  font-size: 19px; font-weight: 600; letter-spacing: -.01em;
  margin: 0 0 8px;
}

.prose p { margin: 0 0 20px; }
.prose p:last-child { margin-bottom: 0; }
.prose strong { font-weight: 600; }
.prose ul { margin: 0 0 20px; padding-left: 20px; }
.prose li { margin-bottom: 8px; }
.prose li::marker { color: var(--ink-3); }

.muted { color: var(--ink-3); }

/* --------------------------------------------------------------- blocks -- */

section { padding: 84px 0; }
@media (min-width: 700px) { section { padding: 116px 0; } }
section + section { border-top: 1px solid var(--rule-2); }

.hero { padding-top: 76px; padding-bottom: 76px; }
@media (min-width: 700px) { .hero { padding-top: 132px; padding-bottom: 108px; } }

/* ------------------------------------------------------------ work list -- */

.work-list { display: flex; flex-direction: column; }

.work-item {
  display: grid; gap: 26px;
  grid-template-columns: 1fr;
  align-items: center;
  padding: 40px 0;
  border-top: 1px solid var(--rule);
}
.work-item:last-child { border-bottom: 1px solid var(--rule); }

@media (min-width: 860px) {
  .work-item { grid-template-columns: 1fr 224px; gap: 56px; padding: 52px 0; }
}

.work-item .idx {
  font-family: var(--mono); font-size: 12px; color: var(--ink-3);
  letter-spacing: .04em; margin-bottom: 14px;
}
.work-item h3 {
  font-size: clamp(26px, 3.6vw, 38px);
  line-height: 1.1; letter-spacing: -.03em; font-weight: 500;
  margin: 0 0 10px;
  transition: opacity var(--t);
}
.work-item .tag { color: var(--ink-2); margin: 0 0 16px; max-width: 46ch; }
.work-item .meta {
  display: flex; flex-wrap: wrap; gap: 8px 10px;
  font-size: 13px; color: var(--ink-3);
}
.work-item .meta span { display: inline-flex; }
.work-item .meta span + span::before { content: "·"; margin-right: 10px; color: var(--rule); }

.work-item:hover h3 { opacity: .55; }

.work-item .go {
  display: inline-flex; align-items: center; gap: 8px;
  margin-top: 18px; font-size: 14px; font-weight: 500;
}
.work-item .go svg { transition: transform var(--t); }
.work-item:hover .go svg { transform: translateX(4px); }

/* the small live cover that sits beside a work row */
.cover {
  justify-self: start;
  border-radius: 14px; overflow: hidden;
  background: #000;
  box-shadow: 0 1px 2px rgba(20,19,15,.08), 0 12px 34px -10px rgba(20,19,15,.22);
  transition: transform var(--t), box-shadow var(--t);
}
.work-item:hover .cover {
  transform: translateY(-3px);
  box-shadow: 0 1px 2px rgba(20,19,15,.1), 0 20px 46px -12px rgba(20,19,15,.3);
}
@media (max-width: 859px) { .cover { justify-self: center; } }

/* ------------------------------------------------------------- contact -- */

.rows { border-top: 1px solid var(--rule); }
.row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 20px;
  padding: 20px 0; border-bottom: 1px solid var(--rule);
}
.row .k { font-size: 13px; color: var(--ink-3); flex: 0 0 auto; }
.row .v { font-size: 17px; text-align: right; }
.row a.v { position: relative; }
.row a.v::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -2px; height: 1px;
  background: var(--ink); transform: scaleX(0); transform-origin: right;
  transition: transform var(--t);
}
.row a.v:hover::after { transform: scaleX(1); transform-origin: left; }


/* --------------------------------------------------------------- embeds -- */

/* An embed is a real page in an iframe. It mounts only when it is nearly on
   screen — a case study can hold twenty of these and each one is a document
   booting its own scripts, so mounting them all at load makes the page crawl. */

.embed { margin-top: 0; margin-left: 0; margin-right: 0; }
.embed + .embed { margin-top: 44px; }

.embed .stage {
  position: relative;
  display: flex; justify-content: center;
  background: var(--well);
  border: 1px solid var(--rule);
  border-radius: 16px;
  padding: 34px 20px;
}
.embed.bare .stage { background: transparent; border: 0; padding: 0; }

.embed .frame {
  position: relative;
  border-radius: 20px;
  overflow: hidden;
  background: #000;
  box-shadow: 0 1px 2px rgba(20,19,15,.1), 0 24px 60px -18px rgba(20,19,15,.35);
}
.embed.desktop .frame { border-radius: 10px; }
.embed.flat .frame { border-radius: 8px; box-shadow: 0 1px 2px rgba(20,19,15,.08); }

.embed iframe {
  border: 0; display: block;
  transform-origin: top left;
  background: #000;
}

/* Before it mounts: a hairline placeholder at the exact final size, so nothing
   on the page moves when the real thing arrives. */
.embed .frame::after {
  content: "";
  position: absolute; inset: 0;
  background: var(--well);
  opacity: 1; transition: opacity .3s ease;
  pointer-events: none;
}
.embed.ready .frame::after { opacity: 0; }

/* An interactive embed is inert until it is clicked, otherwise scrolling the
   page past a phone hands your scroll to the phone. */
.embed .shield {
  position: absolute; inset: 0; z-index: 2;
  border: 0; padding: 0; margin: 0;
  background: transparent;
  cursor: pointer;
  font: inherit; color: #fff;
  display: flex; align-items: flex-end; justify-content: center;
  padding-bottom: 22px;
  opacity: 0; transition: opacity var(--t);
}
.embed .shield b {
  font-size: 12px; font-weight: 500; letter-spacing: .01em;
  background: rgba(10,10,10,.78);
  backdrop-filter: blur(6px);
  padding: 7px 13px; border-radius: 100px;
}
.embed .frame:hover .shield { opacity: 1; }
.embed.live .shield { display: none; }

/* Slots and tables caption themselves the same way an embed does. */
/* Every figure captions the same way. This was a list of selectors and every
   new block type forgot to join it — four times. One rule now. */
.cap {
  margin: 14px auto 0;
  font-size: 13.5px; line-height: 1.5; color: var(--ink-3);
  text-align: center; max-width: 60ch;
}
.cap b { color: var(--ink-2); font-weight: 500; }

.embed .tools {
  position: absolute; right: 14px; top: 14px; z-index: 3;
  display: flex; gap: 6px;
  opacity: 0; transition: opacity var(--t);
}
.embed .stage:hover .tools { opacity: 1; }
.embed .tools button, .embed .tools a {
  font: inherit; font-size: 12px; line-height: 1;
  padding: 7px 10px; border-radius: 100px;
  border: 1px solid var(--rule);
  background: var(--paper); color: var(--ink-2);
  cursor: pointer;
  transition: color var(--t), border-color var(--t);
}
.embed .tools button:hover, .embed .tools a:hover { color: var(--ink); border-color: var(--ink-3); }

/* a row of embeds side by side */
.row-embeds { display: grid; gap: 18px; }
.row-embeds .embed + .embed { margin-top: 0; }
.row-embeds .stage { padding: 26px 14px; }

/* ----------------------------------------------------------- case study -- */

.case-head { padding-top: 60px; padding-bottom: 44px; }
@media (min-width: 700px) { .case-head { padding-top: 92px; padding-bottom: 64px; } }

.back-wrap { max-width: var(--content); margin: 0 auto; }

.back {
  display: inline-flex; align-items: center; gap: 9px;
  font-size: 14px; color: var(--ink-2); margin-bottom: 44px;
}
.back svg { transition: transform var(--t); }
.back:hover svg { transform: translateX(-4px); }

.case-head .col-l { max-width: var(--content); margin: 0 auto; }

.facts {
  max-width: var(--content); margin-left: auto; margin-right: auto;
  display: grid; gap: 22px 40px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  border-top: 1px solid var(--rule);
  padding-top: 26px; margin-top: 44px;
}
.facts .fact { min-width: 0; }
.facts dt { font-size: 11px; text-transform: uppercase; letter-spacing: .12em; color: var(--ink-3); margin-bottom: 6px; }
.facts dd { margin: 0; font-size: 15px; line-height: 1.45; }

/* A flat gap between every block gave a heading the same air as a list that
   belongs to the paragraph above it. Blocks sit close; a section opens a gap. */
.case-body > * { margin-bottom: 34px; }
.case-body > *:last-child { margin-bottom: 0; }
.case-body > .sec { margin-top: 62px; }
.case-body > .sec:first-child { margin-top: 0; }
@media (min-width: 700px) {
  .case-body > * { margin-bottom: 40px; }
  .case-body > .sec { margin-top: 88px; }
}

.case-body .prose { max-width: var(--content); margin-left: auto; margin-right: auto; }

.pull {
  max-width: var(--content); margin-left: auto; margin-right: auto;
  font-size: clamp(21px, 2.6vw, 27px); line-height: 1.38; letter-spacing: -.02em;
  color: var(--ink); border-left: 2px solid var(--ink); padding-left: 24px;
}
.pull cite { display: block; font-style: normal; font-size: 14px; color: var(--ink-3); margin-top: 14px; }

.divider { border: 0; border-top: 1px solid var(--rule); max-width: var(--content); margin: 0 auto; }

.next {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  padding: 34px 0; border-top: 1px solid var(--rule);
}
.next .k { font-size: 12px; text-transform: uppercase; letter-spacing: .12em; color: var(--ink-3); }
.next .v { font-size: clamp(20px, 3vw, 28px); letter-spacing: -.02em; font-weight: 500; }

/* --------------------------------------------------------------- footer -- */

.site-foot {
  border-top: 1px solid var(--rule);
  padding: 44px 0 56px;
  font-size: 14px; color: var(--ink-3);
  display: flex; flex-wrap: wrap; gap: 14px; justify-content: space-between;
}

/* ------------------------------------------------------------ niceties -- */

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
  html { scroll-behavior: auto; }
}

/* Things that fade up as they arrive. Held to one subtle move. */
.rise { opacity: 0; transform: translateY(10px); transition: opacity .5s ease, transform .5s cubic-bezier(.2,.7,.3,1); }
.rise.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .rise { opacity: 1; transform: none; } }

::selection { background: #14130f; color: #fbfaf8; }

:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 3px; }

/* An embed is scenery until it is clicked: without this a cover inside a work
   row swallows the click meant for the link, and scrolling the page over a
   phone scrolls the phone. */
.embed iframe { pointer-events: none; }
.embed.live iframe { pointer-events: auto; }

/* On a phone the nav and the brand's trailing role were touching. */
@media (max-width: 600px) { .brand span { display: none; } }

/* Two dense paragraphs at the full column width ran to ~79 characters a line,
   past the point where the eye reliably finds the next one. */
/* The container centres the way the contact block does; the words inside stay
   flush left, because two paragraphs this long are for reading, not for
   admiring the symmetry of. */
.about-centre { max-width: var(--content); margin: 0 auto; }


/* ======================================================== long-form blocks ==
   A written case study needs more than paragraphs and screens: the numbers that
   carry the argument, the tables the workshop produced, the proposals and the
   trade-off each one turned on. */

.prose ol { margin: 0 0 20px; padding-left: 22px; }
.prose ol li { margin-bottom: 10px; }
.prose ol li::marker { color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* --- the numbers ---------------------------------------------------------- */

.stats {
  max-width: var(--content); margin-left: auto; margin-right: auto;
  display: grid; gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: 12px;
  overflow: hidden;
}
.stat { background: var(--paper); padding: 22px 20px; }
.stat b {
  display: block;
  font-size: clamp(22px, 2.8vw, 29px);
  line-height: 1.1; letter-spacing: -.03em; font-weight: 500;
  font-variant-numeric: tabular-nums;
  margin-bottom: 8px;
}
.stat span { display: block; font-size: 13px; line-height: 1.45; color: var(--ink-3); }

/* --- tables --------------------------------------------------------------- */

/* Never zero the bottom in these rules: they sit after the case-body rhythm and
   carry the same specificity, so a shorthand margin silently wins and the block
   ends up flush against whatever follows. Sides and top only. */
.tbl-wrap { margin-top: 0; margin-left: 0; margin-right: 0; }
/* A two-column table stretched across 1140px reads as mostly empty. */
.tbl-wrap.narrow { max-width: var(--content); margin-left: auto; margin-right: auto; }
.tbl-scroll {
  overflow-x: auto;
  border: 1px solid var(--rule);
  border-radius: 12px;
  -webkit-overflow-scrolling: touch;
}
.tbl { border-collapse: collapse; width: 100%; font-size: 14.5px; line-height: 1.5; }
.tbl th, .tbl td {
  text-align: left; vertical-align: top;
  padding: 14px 18px;
  border-bottom: 1px solid var(--rule-2);
}
.tbl thead th {
  font-size: 11px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-3);
  border-bottom-color: var(--rule);
  white-space: nowrap;
}
.tbl tbody tr:last-child th, .tbl tbody tr:last-child td { border-bottom: 0; }
.tbl tbody th { font-weight: 550; }
.tbl td b, .tbl th b { font-weight: 600; }
.tbl .num { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* A tick or a cross is a shape to count, not a word to read, so it leads the
   cell and keeps its own size whatever the row says after it. */
.tbl .mark { font-size: 13px; font-weight: 400; margin-right: 6px; line-height: 1; }
.tbl td:has(> .mark:only-child) { text-align: center; }

/* --- proposals and their trade-offs --------------------------------------- */

.items {
  max-width: var(--content); margin-top: 0; margin-left: auto; margin-right: auto; padding: 0;
  list-style: none;
  counter-reset: item;
  border-top: 1px solid var(--rule);
}
.items li {
  counter-increment: item;
  padding: 22px 0 22px 44px;
  border-bottom: 1px solid var(--rule-2);
  position: relative;
}
.items li::before {
  content: counter(item, decimal-leading-zero);
  position: absolute; left: 0; top: 25px;
  font-family: var(--mono); font-size: 12px; color: var(--ink-3);
}
.items.plain { counter-reset: none; }
.items.plain li { padding-left: 0; }
.items.plain li::before { content: none; }
.items h4 {
  margin: 0 0 8px;
  font-size: 17px; font-weight: 550; letter-spacing: -.01em; line-height: 1.4;
}
.items p { margin: 0; font-size: 15px; line-height: 1.6; color: var(--ink-2); }
/* A body can run to more than one paragraph; they should not touch. */
.items p + p { margin-top: 9px; }
.items p b { color: var(--ink); font-weight: 550; }

/* --- timeline ------------------------------------------------------------- */

.timeline {
  max-width: var(--content); margin-top: 0; margin-left: auto; margin-right: auto;
  display: grid; grid-template-columns: max-content 1fr;
  gap: 0 26px;
  /* No rule across the top: the label above it already opens the block, and a
     line under a line reads as a mistake. */
}
.timeline dt {
  padding: 12px 0; font-size: 13px; color: var(--ink-3);
  font-variant-numeric: tabular-nums; white-space: nowrap;
  border-bottom: 1px solid var(--rule-2);
}
.timeline dd {
  margin: 0; padding: 12px 0; font-size: 15px;
  border-bottom: 1px solid var(--rule-2);
}
.timeline dt:last-of-type, .timeline dd:last-of-type { border-bottom: 0; }

/* --- a screen that does not exist yet ------------------------------------- */

.slot { margin-top: 0; margin-left: 0; margin-right: 0; }
.slot .stage {
  display: flex; justify-content: center;
  background: var(--well);
  border: 1px solid var(--rule);
  border-radius: 16px;
  padding: 34px 20px;
}
.slot-box {
  max-width: 100%;
  border: 1px dashed var(--ink-3);
  border-radius: 18px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; padding: 20px; text-align: center;
  color: var(--ink-3);
}
.slot-box b { font-size: 14px; font-weight: 550; color: var(--ink-2); }
.slot-box span { font-size: 12.5px; line-height: 1.5; max-width: 28ch; }

/* Section headings need more air above them than a paragraph does when they
   follow a table or a run of screens. */
.case-body .prose h2.head { margin-top: 8px; }


/* ============================================================ the figure ===
   Stanley stands next to his own name. Half the size he was in the About
   column, because up here he is punctuation rather than a portrait — and on a
   transparent cut-out the only thing that reads as "picture" is the figure
   itself, so there is no frame, no radius and no grey behind him. */

.hero-grid {
  /* The text takes the measure it needs; he takes the middle of what is left,
     with equal air either side of him. Against the right edge he was stranded;
     tucked up against the sentence he was crowding it. Auto margins let the
     leftover space split itself evenly at every width. */
  display: flex; align-items: flex-end; justify-content: flex-start;
  gap: 0;
}
.hero-grid .col-l { flex: 0 1 auto; }

/* A jump to a section should land below the sticky header, not behind it. */
section[id] { scroll-margin-top: 84px; }

/* ============================================================== contact ====
   Centred: the section is four short rows and a sentence, and left-aligning it
   against a 1140 wrap left it stranded in the corner of an empty band. */

.contact-centre { max-width: var(--content); margin: 0 auto; text-align: center; }
.contact-centre .head { margin-bottom: 34px; }
.contact-centre .rows { text-align: left; }


/* A slot is a drawing of a screen that does not exist yet, so it obeys the same
   rule as a screen: the measure for one, the frame for a wide diagram. */
.slot-box { width: 100%; max-width: 100%; }

/* The body continues the head rather than opening a new band. */
.case-flow { padding-top: 0; border-top: 0; }


/* ================================================================== tag ====
   A study that is not finished says so, rather than leaving the reader to work
   it out from thin content. Hollow and quiet: it is a note about the page, not
   a thing on the page. */

.badge {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--sans);
  font-size: 10.5px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-3);
  border: 1px solid var(--rule);
  border-radius: 100px;
  padding: 4px 10px;
  white-space: nowrap;
}
.badge::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--ink-3); opacity: .55;
}

/* on a work row it shares the line with the number */
.work-item .idx { display: flex; align-items: center; gap: 12px; }

/* in a case study it sits above the title */
.badge-line { margin-bottom: 18px; }

/* A block label, set exactly as the facts labels are. */
.block-label {
  max-width: var(--content); margin-left: auto; margin-right: auto;
  font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .12em;
  color: var(--ink-3);
}
.case-body > .block-label { margin-bottom: 16px; }

/* A sub heading belongs to the block under it, so it sits at list spacing from
   it rather than at the gap that separates one block from the next. */
.case-body > .subhead { margin-bottom: 16px; }
.case-body > .subhead h3.sub { margin-bottom: 0; }

/* ================================================================ images ==
   Same stage as a screen sits in, so a board and a phone read as the same
   kind of exhibit. Full frame by default — a FigJam board at the measure is
   unreadable — with .narrow for anything that does not need the width. */

.imgblock { margin-top: 0; margin-left: 0; margin-right: 0; }
.imgblock .stage {
  display: flex; justify-content: center;
  background: var(--well);
  border: 1px solid var(--rule);
  border-radius: 16px;
  padding: 20px;
}
.imgblock a { display: block; }
.imgblock img {
  max-width: 100%; height: auto;
  border-radius: 6px;
  background: #fff;
}


/* ======================================================== one column, one ==
   Nothing in a case study is wider than the text it belongs to. Tables, boards,
   screens and slots all stop at the measure and centre on it, so the document
   has a single column and two edges rather than four. A table too wide for it
   scrolls inside its own box; a board too detailed for it opens full size in a
   tab. */

.case-body > * { max-width: var(--content); margin-left: auto; margin-right: auto; }

/* A section heading now sits in its own wrapper, so it binds to what follows
   the way a sub heading does. */
.case-body > .sec { margin-bottom: 16px; }
.case-body > .sec h2.head { margin-bottom: 0; }


/* ==================================================== annotated screens ====
   A screen with its parts ringed and a note beside each one. The rings live in
   an SVG drawn in the screen's own coordinates, scaled with it, so a note never
   drifts off its element. Below 620 there is no room for a gutter on each side,
   so the whole thing stacks: screen first, notes as a list under it. */

.annot { margin-top: 0; margin-left: auto; margin-right: auto; }

.annot-stage {
  position: relative;
  background: var(--well);
  border: 1px solid var(--rule);
  border-radius: 16px;
  padding: 28px 0;
}

.annot-frame {
  position: absolute;
  border-radius: 18px; overflow: hidden; background: #000;
  box-shadow: 0 1px 2px rgba(20,19,15,.1), 0 18px 44px -16px rgba(20,19,15,.3);
}
.annot-frame iframe { border: 0; display: block; transform-origin: top left; pointer-events: none; }

.annot-lines { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
.annot-ring { fill: none; stroke: #d8452f; stroke-width: 1.5; opacity: .9; }
.annot-lead { stroke: #d8452f; stroke-width: 1; opacity: .55; }
.annot-dot  { fill: #d8452f; opacity: .85; }

.annot-note {
  font-size: 11px; line-height: 1.5; color: var(--ink-2);
}
.annot-note--right { padding-right: 20px; }
.annot-note--left  { padding-left: 20px; text-align: right; }
/* Only the leading number is the number. A <b> inside the note's own text is
   emphasis, and was inheriting the mono red block treatment. */
.annot-note > b:first-child {
  display: block; font-family: var(--mono);
  font-size: 10.5px; letter-spacing: .04em; color: #d8452f;
  margin-bottom: 4px; font-weight: 500;
}
.annot-note b { font-weight: 600; color: var(--ink); }
.annot-note i { font-style: italic; }

/* the alternative button copies, drawn as the buttons they would be */
.annot-chip {
  display: inline-block; margin: 7px 0 0 6px;
  padding: 5px 12px; border-radius: 100px;
  background: #fe2c55; color: #fff;
  font-size: 10.5px; font-weight: 600; line-height: 1.2;
  white-space: nowrap;
}
.annot-note--left .annot-chip { margin: 7px 6px 0 0; }

/* stacked, on anything too narrow for two gutters */
.annot.stacked .annot-stage { display: flex; flex-direction: column; align-items: center; gap: 0; padding: 24px 20px; }
.annot.stacked .annot-frame { position: relative !important; left: auto !important; top: auto !important; }
/* Stacked keeps the rings — it is the badges, not the leader lines, that tie a
   note to its element there. The SVG sits over the screen only. */
.annot.stacked .annot-lines { position: absolute; inset: 0; height: auto; }
.annot-badge { fill: #d8452f; }
.annot-badge-t { fill: #fff; font-family: var(--mono); font-size: 9px; font-weight: 600;
  text-anchor: middle; dominant-baseline: central; }
.annot.stacked .annot-note {
  transform: none; width: 100% !important; text-align: left;
  padding: 14px 0 0; border-top: 1px solid var(--rule);
  margin-top: 14px;
}
.annot.stacked .annot-note--left { text-align: left; padding-left: 0; }
.annot.stacked .annot-note--right { padding-right: 0; }


/* ===================================================== the cropped window ==
   Fixed height whatever it holds, so seventeen proposals of different shapes
   still read as one column. */

.shot { margin-top: 16px; }

.shot-win {
  position: relative; overflow: hidden;
  background: var(--well);
  border: 1px solid var(--rule);
  border-radius: 10px;
  transition: height .3s cubic-bezier(.2,.7,.3,1);
}
@media (prefers-reduced-motion: reduce) { .shot-win { transition: none; } }
.shot-layer {
  position: absolute; inset: 0;
  opacity: 0; visibility: hidden;
  transition: opacity .22s ease;
}
.shot-layer.on { opacity: 1; visibility: visible; }
.shot-layer iframe {
  border: 0; display: block;
  transform-origin: top left;
  pointer-events: none;          /* a detail, not a demo */
  background: #000;
}
/* …unless the proposal is an interaction, in which case the frame has to let
   you perform it. The screen inside does not scroll, so a wheel over it still
   scrolls the page. */
.shot.live .shot-layer iframe { pointer-events: auto; }
.shot.live .shot-win { cursor: pointer; }

.shot-switch { display: flex; gap: 4px; margin-top: 8px; align-items: center; }
.shot-grow { margin-left: auto; }
.shot-note { font-size: 11.5px; color: var(--ink-3); line-height: 1.4; }
.shot-tab {
  font: inherit; font-size: 10.5px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
  padding: 5px 11px; border-radius: 100px;
  border: 1px solid var(--rule); background: transparent;
  color: var(--ink-3); cursor: pointer;
  transition: color .18s, border-color .18s, background .18s;
}
.shot-tab:hover { color: var(--ink-2); }
.shot-tab.on { background: var(--ink); border-color: var(--ink); color: var(--paper); }


/* ============================================================== flow map ==
   Two orderings of the same journey, stacked. Reading down the two tracks is
   the whole point, so they keep a common left edge and scroll together only if
   the column cannot hold them. */

/* Inside a proposal it follows the trade-off, so it needs the same air a
   window gets. */
.flow { margin: 16px auto 0; }
.flow-row + .flow-row { margin-top: 22px; }

.flow-label {
  font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .12em;
  color: var(--ink-3);
  margin-bottom: 10px;
}

.flow-track {
  display: flex; align-items: stretch; gap: 5px;
  overflow-x: auto; padding-bottom: 4px;
  -webkit-overflow-scrolling: touch;
}

.flow-step {
  flex: 0 0 auto; min-width: 0;
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  padding: 8px 10px; border-radius: 9px;
  border: 1px solid var(--rule);
  background: var(--paper);
  color: var(--ink-2);
  transition: border-color .18s, color .18s;
}
.flow-step b { font-size: 12px; font-weight: 550; line-height: 1.3; color: var(--ink); white-space: nowrap; }
.flow-step span { font-size: 10px; line-height: 1.3; color: var(--ink-3); white-space: nowrap; }

a.flow-step:hover { border-color: var(--ink-3); }

/* the two steps that changed places */
.flow-step.is-moved { border-color: #d8452f; background: #fdf0ed; }
.flow-step.is-moved b { color: #a5321f; }
.flow-step.is-moved span { color: #b4614f; }

/* a stretch of the journey the map does not need to spell out */
.flow-step.is-group { border-style: dashed; background: var(--well); }

.flow-arrow {
  flex: 0 0 auto; align-self: center;
  font-size: 12px; color: var(--ink-3);
}


/* a sequence with more than one ordering */
.rowset { margin-top: 16px; }
.rowset-switch { margin: 0 0 12px; }
.rowset-row { display: none; }
.rowset-row.on { display: grid; }


/* =============================================================== matrix ===
   One board, three scopes. Everything out of scope stays on the plot, greyed:
   what a position leaves out is as much the point as what it takes. */

.mx { margin: 16px auto 0; }
.mx-switch { margin: 0 0 14px; align-items: center; }
.mx-count { margin-left: auto; font-size: 11px; color: var(--ink-3); white-space: nowrap; }

.mx-plot {
  position: relative;
  height: 380px;
  margin: 0 0 0 26px;
  border-left: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  background: var(--well);
  border-radius: 0 12px 0 0;
}

.mx-rule { position: absolute; background: var(--rule); }
.mx-rule--v { left: 50%; top: 0; bottom: 0; width: 1px; }
.mx-rule--h { bottom: 50%; left: 0; right: 0; height: 1px; }

.mx-axis {
  position: absolute;
  font-size: 10.5px; font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-3);
}
.mx-axis--y { left: -26px; top: 50%; transform: translateY(-50%) rotate(-90deg); transform-origin: center; }
.mx-axis--x { bottom: -22px; left: 50%; transform: translateX(-50%); }

.mx-quad {
  position: absolute; font-size: 10.5px; color: var(--ink-3);
  letter-spacing: .04em; pointer-events: none;
}
.mx-quad--tl { top: 8px; left: 10px; }
.mx-quad--tr { top: 8px; right: 10px; }
.mx-quad--bl { bottom: 8px; left: 10px; }
.mx-quad--br { bottom: 8px; right: 10px; }

/* a plotted item */
.mx-dot {
  /* The mark sits on the point, not the middle of the mark-plus-label — which
     is what pushed labels off both edges of the plot. */
  position: absolute; transform: translate(-4.5px, 50%);
  display: flex; align-items: center; gap: 7px;
  border: 0; background: transparent; padding: 0; cursor: default;
  font: inherit; text-align: left;
}
.mx-dot-mark {
  flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%;
  background: var(--paper); border: 1.5px solid var(--ink-3);
  transition: background .2s, border-color .2s, transform .2s;
}
.mx-dot-label {
  font-size: 11px; line-height: 1.25; color: var(--ink-3);
  white-space: nowrap;
  opacity: 0; transition: opacity .2s, color .2s;
  pointer-events: none;
}
.mx-dot.in .mx-dot-label,
.mx-dot:hover .mx-dot-label,
.mx-dot:focus-visible .mx-dot-label { opacity: 1; }
.mx-dot.in .mx-dot-label { color: var(--ink); font-weight: 600; }

.mx-dot.in .mx-dot-mark { transform: scale(1.25); }
.mx-dot:not(.in) .mx-dot-mark { background: var(--paper) !important; }
.mx-dot { cursor: default; z-index: 1; }
.mx-dot:hover { z-index: 3; }
.mx-dot.in { z-index: 2; }

/* labels on the right half read inward so nothing runs off the plot */
.mx-dot--flip { flex-direction: row-reverse; transform: translate(calc(-100% + 4.5px), 50%); }
.mx-dot--flip .mx-dot-label { text-align: right; }

/* the three layers, so the shape of the argument is visible without reading */
.mx-dot--content .mx-dot-mark { border-color: #d8452f; }
.mx-dot--content.in .mx-dot-mark { background: #d8452f; }
.mx-dot--ui .mx-dot-mark { border-color: #2f7dd8; }
.mx-dot--ui.in .mx-dot-mark { background: #2f7dd8; }
.mx-dot--feature .mx-dot-mark { border-color: #7a7a7a; }
.mx-dot--feature.in .mx-dot-mark { background: #7a7a7a; }

.mx-key { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0 0 12px 26px;
  font-size: 11px; color: var(--ink-3); }
.mx-key-item { display: inline-flex; align-items: center; gap: 6px; }
.mx-key-item .mx-dot-mark { width: 9px; height: 9px; border-radius: 50%;
  border-width: 1.5px; border-style: solid; }

.mx-note {
  margin: 30px 0 0; font-size: 13px; line-height: 1.55; color: var(--ink-2);
}
.mx-note b { color: var(--ink); font-weight: 600; }

@media (max-width: 560px) {
  .mx-plot { height: 260px; }
  .mx-dot-label { display: none; }
}

/* A hovered label can land on top of an in-scope one — seventeen points on one
   plot leaves no arrangement where it can't. It reads cleanly if it carries its
   own ground rather than by moving the point off its own value. */
.mx-dot:hover .mx-dot-label,
.mx-dot:focus-visible .mx-dot-label {
  background: var(--paper);
  box-shadow: 0 0 0 3px var(--paper);
  border-radius: 2px;
  color: var(--ink);
}


/* =============================================================== rounds ===
   Both arms of a round are measured against the control, so both can win. The
   toggles under a round say what did, and every later round redraws, because
   its control is the accumulation of everything that beat control before it. */

/* No margin shorthand here: it would beat the auto side-margins that centre
   every block in the measure, and the whole tournament would sit left. */
.rounds { margin-top: 0; }
.round { margin: 0 0 44px; }
.round:last-of-type { margin-bottom: 0; }
.round h3 { margin: 0 0 10px; }
.round .prose { margin: 0 0 22px; }

.round-ask {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: 16px 0 0;
}
.round-ask-lb {
  font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-3); margin-right: 2px;
}
.round-arm {
  font: inherit; font-size: 12px; line-height: 1;
  padding: 7px 12px; border-radius: 999px;
  border: 1px solid var(--rule); background: var(--paper); color: var(--ink-2);
  cursor: pointer; transition: background .15s, color .15s, border-color .15s;
}
.round-arm:hover { border-color: var(--ink-3); color: var(--ink); }
.round-arm.on { background: var(--ink); border-color: var(--ink); color: var(--paper); }

.round-carry {
  margin: 12px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--ink-3);
}
.round-carry b { color: var(--ink-2); font-weight: 600; }

/* The arm that has not been drawn yet. It holds its place at the right size so
   the row reads as three, and says plainly that it is not a screen. */
.embed.is-blank { opacity: .72; }
.embed.is-blank .frame { border-style: dashed; }

/* ================================================================= lock ===
   The password layer. It covers the page rather than sitting inside it,
   because there is nothing underneath yet: the writing arrives encrypted and
   is only decrypted once the reader gets the password right. */

.is-locked { overflow: hidden; }
.is-locked .site-head,
.is-locked main,
.is-locked .site-foot { display: none; }

.lock {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center;
  padding: 24px; background: var(--paper);
}
.lock-card { width: 100%; max-width: 380px; }
.lock-card h1 { margin: 0 0 8px; font-size: 19px; font-weight: 600; letter-spacing: -.01em; }
.lock-card > p { margin: 0 0 22px; font-size: 14.5px; line-height: 1.6; color: var(--ink-2); }

.lock label {
  display: block; margin: 0 0 7px;
  font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3);
}
.lock input {
  width: 100%; font: inherit; font-size: 15px; padding: 11px 13px;
  border-radius: 9px; border: 1px solid var(--rule);
  background: transparent; color: var(--ink);
}
.lock input:focus { outline: none; border-color: var(--ink-3); }
.lock button {
  width: 100%; margin-top: 10px; font: inherit; font-size: 15px; font-weight: 550;
  padding: 11px; border-radius: 9px; border: 0;
  background: var(--ink); color: var(--paper); cursor: pointer;
}
.lock button[disabled] { opacity: .6; cursor: default; }
.lock-err { margin: 12px 0 0; font-size: 13px; color: #d8452f; }
.lock-back { display: inline-block; margin-top: 22px; font-size: 13px; color: var(--ink-3); }

/* The intro is a short list of claims, not a paragraph. The first is what he
   is, set as a standfirst in the same treatment as a section label, so it reads
   as a title over the description rather than a first sentence of it. */
.lede .hero-p { display: block; }
.lede .hero-p + .hero-p { margin-top: 16px; }
.hero-lead { margin: 0 0 14px; }

/* =============================================================== the room ===
   The first thing the site says is not a sentence, it is a picture: Stanley
   standing among the things you can open. Every object is a link and every
   label names the section it goes to, so the header does not repeat them.

   On paper, not on black. The reference this came from puts the figures on a
   black stage; here the page is warm paper everywhere else, and one black
   screen at the top would read as a different site rather than the front of
   this one. The objects are pale, so the light ground costs them a little
   contrast — the shadow under each one buys it back.

   The arrangement is one picture, not five positioned elements, so every
   coordinate is a percentage of the stage and every size is a share of --u,
   the stage's SHORT axis. Sizing off the width would blow the objects up on a
   wide screen and shrink him to nothing on a narrow one. */

.intro { padding: 0; }

/* The room is a picture at the top of the page, not a splash screen: it stops
   short of the fold so the sentence under it is already showing. One height,
   declared once — the stage sizes the whole arrangement off it, so the two can
   never drift apart. */
.room {
  --room-h: clamp(380px, 62svh, 620px);
  min-height: var(--room-h);
  display: flex; justify-content: center;
  overflow: hidden;
}

/* Capped at the frame, like everything else on the page. Left to fill a wide
   monitor the objects would drift out to the edges and stop being a group. */
.room-stage {
  position: relative;
  flex: 0 1 var(--frame);
  width: min(100%, var(--frame));
  --u: min(var(--room-h), 100vw);
}

/* An absolutely positioned box shrinks to fit the room left of the viewport
   edge, which quietly caps anything at 50% to half the stage. Giving the box
   its width outright takes the picture out of the browser's hands. */
.thing {
  position: absolute;
  left: var(--x); top: var(--y);
  width: calc(var(--u) * var(--size) / 100);
  transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center;
  gap: 14px;
  text-align: center;
  --amp: 5px;
  transition: --amp .45s cubic-bezier(.2,.7,.3,1);
}

/* Two nested boxes, because the two motions must not fight: the outer runs the
   idle drift for ever, the inner takes the hover lift. One element trying to do
   both means the transition stamps on the keyframes. */
.thing-lift {
  display: block;
  width: 100%;
  animation: float var(--dur, 6s) ease-in-out var(--delay, 0s) infinite;
}

.thing-img {
  display: block;
  width: 100%;
  height: auto;
  transform: translateY(0) scale(1);
  filter: drop-shadow(0 3px 6px rgba(20, 19, 15, .16));
  transition: transform .45s cubic-bezier(.2,.7,.3,1), filter .45s ease;
  /* The shadow is on the image; the drift is on the parent — so it travels
     with the object rather than staying put on the ground. That is the right
     trade here: these things have no floor under them. */
  will-change: transform;
}

/* The label is allowed to be wider than the object it names — "LinkedIn" under
   a coffee cup — so it never wraps and simply overhangs, centred. */
.thing-label {
  font-size: 14px;
  letter-spacing: .01em;
  white-space: nowrap;
  color: var(--ink-3);
  transition: color var(--t);
}

/* Hover does two things at once and they must not be confused for each other:
   the object rises and stays risen, and the idle drift carries on underneath it
   — wider now, because at ±5px on a lifted, scaled object the breathing was
   there in the numbers and invisible on screen. */
.thing:hover, .thing:focus-visible { --amp: 11px; }

.thing:hover .thing-img,
.thing:focus-visible .thing-img {
  transform: translateY(-14px) scale(1.06);
  filter: drop-shadow(0 20px 18px rgba(20, 19, 15, .2));
}
.thing:hover .thing-label,
.thing:focus-visible .thing-label { color: var(--ink); }

.thing:focus-visible { outline: none; }
.thing:focus-visible .thing-label {
  text-decoration: underline; text-underline-offset: 3px;
}

/* He is the anchor, so at rest he stands still — but the drift runs on him the
   whole time at zero amplitude, and hover simply opens it. Starting the
   animation on hover instead would snap him up by the amplitude on the first
   frame; opening it from nothing eases him off the floor. */
.thing.t-me { --amp: 0px; }
.thing.t-me:hover, .thing.t-me:focus-visible { --amp: 9px; }
.thing.t-me .thing-lift { animation-duration: 7.2s; animation-delay: -1.6s; }
.thing.t-me .thing-img { filter: drop-shadow(0 8px 16px rgba(20, 19, 15, .14)); }
.thing.t-me:hover .thing-img,
.thing.t-me:focus-visible .thing-img {
  transform: translateY(-10px) scale(1.01);
  filter: drop-shadow(0 26px 24px rgba(20, 19, 15, .18));
}

/* Each object drifts on its own clock. Shared timing reads as a carousel;
   staggered timing reads as five separate things in the air. */
.thing.t-work     { --dur: 6.4s; --delay: -.4s; }
.thing.t-resume   { --dur: 7.6s; --delay: -2.1s; }
.thing.t-contact  { --dur: 5.8s; --delay: -1.2s; }
.thing.t-linkedin { --dur: 8.2s; --delay: -3.4s; }

/* Registered, so it can be transitioned rather than snapped. Without
   @property the value still applies — the drift just widens in one step
   instead of easing open, which is a fair thing to lose. */
@property --amp {
  syntax: '<length>';
  inherits: true;
  initial-value: 5px;
}

@keyframes float {
  0%, 100% { transform: translateY(calc(var(--amp, 5px) * -1)); }
  50%      { transform: translateY(var(--amp, 5px)); }
}

/* Until an object's file lands. It shares .thing-img, so it inherits the lift
   and the drift and only has to say how it looks. */
.thing-img.ghost {
  aspect-ratio: 1;
  border: 1px dashed var(--rule);
  border-radius: 10px;
  filter: none;
}
.thing:hover .thing-img.ghost,
.thing:focus-visible .thing-img.ghost { filter: none; }

/* Portrait windows. The coordinates themselves are swapped in JS from the
   config — an inline style would beat anything written here — so this block
   carries only what a media query can honestly own. */
@media (max-aspect-ratio: 1/1) {
  .thing { gap: 10px; }
  .thing-label { font-size: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  .thing-lift { animation: none; }
  .thing-img { transition: none; }
}

/* The sentence sits under the room in the same section, so what divides them
   is a rule the width of the frame — the short one. A full-bleed line across
   the window is what a section boundary looks like here, and this is not one. */
.intro-rule {
  border: 0; border-top: 1px solid var(--rule-2);
  margin: 0;
}
.intro .hero { padding-top: 76px; padding-bottom: 76px; }
@media (min-width: 700px) {
  .intro .hero { padding-top: 108px; padding-bottom: 108px; }
}

/* ------------------------------------------------------------- the clock -- */

/* His time, not the reader's — that is the point of putting it up there. */
.clock {
  display: flex; align-items: baseline; gap: 10px;
  font-size: 14px; color: var(--ink-2);
}
.clock-time {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: .01em;
  color: var(--ink-3);
}
@media (max-width: 460px) { .clock-city { display: none; } }
