/* ============================================================
   touchup mafia — the whole site
   Depends on tokens.css and base.css

   One screen. A field of drifting thumbnails, a two-item menu,
   and two overlays. No sections, no scroll narrative, no copy
   that isn't a name or a price.
   ============================================================ */

body {
  margin: 0;
  min-height: 100svh;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--face-ui);
  font-size: var(--text-base);
  line-height: var(--leading-ui);
  /* The index scrolls now: the frames run down a diagonal the viewport
     cannot hold at once, which is the point of an index. */
}


/* Links. No underline at rest and none on hover: the ink lifts ten percent
   instead. Inline links stay findable because the sentence around them is set
   quiet and the link is not — contrast does the work a rule was doing, and it
   does it without a second mark appearing under the cursor. */
a {
  color: inherit;
  text-decoration: none;
  transition: color var(--dur-hover) var(--ease);
}
a:hover { color: var(--ink-hover); }


/* ------------------------------------------------------------
   Masthead — logo, and exactly two words.
   ------------------------------------------------------------ */

.masthead {
  position: fixed;
  inset: 0 0 auto;
  z-index: 40;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--gutter);
}

/* No panel, no rule, no ground. Both float over the work and invert against
   whatever is beneath them: near-ink on paper, light over a dark photograph.
   `difference` of white against a backdrop is |backdrop - 255|, so the source
   has to be pure white — this is the one place on the site where it is. */
.masthead,
.menu {
  mix-blend-mode: difference;
  color: #fff;
}

/* Difference has one failure and it is not cosmetic: a mid-grey backdrop
   inverts to mid-grey, so contrast goes to nothing — and skin tones sit exactly
   there. BOOK disappearing over a face is the most expensive bug this page
   could have.

   The rescue is a scrim of the ground colour at each edge, and it has to be a
   SIBLING: put it inside the blended element and it inverts with everything
   else, which is a black band, not a rescue. It sits one layer under the two
   bars and is the paper's own colour, so on paper there is nothing to see. */
.veil {
  position: fixed;
  inset: 0;
  z-index: 39;
  pointer-events: none;
  background:
    linear-gradient(var(--ground) 0, transparent 92px),
    linear-gradient(transparent calc(100% - 78px), var(--ground) 100%);
}

/* The chips lift on hover with a transform, which would make each one its own
   stacking context and hide it from the blend. Keeping the stage flat is what
   lets the header see it. */
.stage { isolation: auto; }

.masthead__mark { width: 118px; color: inherit; }
.masthead__mark svg { width: 100%; height: auto; fill: currentColor; }

.menu {
  display: flex;
  gap: var(--space-4);
}

.menu button {
  padding: var(--space-1) 0;
  background: none;
  border: 0;
  font-family: var(--face-meta);
  /* There are three words on this entire site that are not a name or a price.
     Setting them at 11px was treating them as furniture. */
  font-size: clamp(1.0625rem, 0.85rem + 1vw, 1.5rem);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--dur-hover) var(--ease);
}
/* The whole site hovers the same way: the colour lifts ten percent and
   nothing else happens. A rule arriving under a word is a second mark drawn
   for an event the colour has already reported.

   Here the ten percent is taken out of white, not out of the ink: these two
   bars are composited with `difference`, where the source has to be pure white
   to invert correctly. Handing them --ink-hover would send near-black through
   the blend, and the word would vanish instead of lifting. */
.menu button:hover { color: color-mix(in srgb, #fff 90%, #000); }

/* 11px text with an 11px target fails on any touch screen. The label keeps
   its size; the tappable area does not. */
.menu button { position: relative; }
.menu button::after {
  content: "";
  position: absolute;
  inset: -15px -12px;
}

/* ------------------------------------------------------------
   The index

   One run of frames, every one cropped to the same portrait
   ratio, each with its year off the left edge and the client off
   the right. Nothing is positioned by hand: the order the build
   writes is the order on the page.

   A phone runs it down the page. From --break up it lies on its
   side and the same wheel carries it along, because a strip of
   portraits is the shape of a desk, not of a phone.
   ------------------------------------------------------------ */

.stage {
  position: relative;
  z-index: 10;
}

.strip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  padding: calc(92px + var(--space-5)) 0 var(--space-section);
}

.index__head {
  width: 100%;
  padding: 0 var(--gutter) var(--space-6);
  pointer-events: none;
}

/* One column, one ratio, and the frames overlapping by a sliver so the run
   reads as a stack of prints on a table rather than a list of cards. The two
   words that used to sit out at the page edges are gone from here: they
   belong to the frame a visitor has actually opened, and they arrive with it. */
.frame {
  /* Sized so five frames are on screen at once — the one being looked at and
     two on either side of it, which is the depth the stack needs to read as a
     stack. The step between frames is what sets that count: at 0.46 of the
     width, five boxes intersect a phone screen. */
  --frame-w: min(70vw, 330px);
  --overlap: calc(var(--frame-w) * 0.46);
  display: grid;
  grid-template-columns: minmax(0, var(--frame-w));
  justify-content: center;
  width: 100%;
  padding: 0 var(--gutter);
}

.frame + .frame { margin-top: calc(var(--overlap) * -1); }

/* Kept in the document for screen readers and for the API to write into —
   the portfolio still knows who and when, it just does not say so until the
   photograph is open. */
.frame__side {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The run quietens when one frame is being looked at. */
/* No fade on the field any more. The focus view blurs what is behind it, and
   dimming the same frames a second time only made the blur look like fog. */

/* ------------------------------------------------------------
   Desktop: the same run, on its side

   A strip of portraits wants the long edge of the screen. The
   wheel is translated in script, since a wheel on a desktop still
   means "further along the work".
   ------------------------------------------------------------ */
/* ------------------------------------------------------------
   Overlays

   Native <dialog>: focus trapping, Escape, and inertness on the
   page behind it all come free and correct. Rebuilding that by
   hand is the most common way a modal ends up inaccessible.
   ------------------------------------------------------------ */

dialog {
  margin: auto;
  padding: 0;
  border: 0;
  max-width: min(92vw, 560px);
  max-height: 88svh;
  background: var(--ground);
  color: var(--ink);
  box-shadow: 0 1px 2px oklch(0% 0 0 / 0.06),
              0 24px 64px oklch(0% 0 0 / 0.16);
}

dialog::backdrop {
  background: oklch(14% 0 0 / 0.72);
  backdrop-filter: blur(2px);
}

/* Entry animation via @starting-style rather than a keyframe that begins
   at opacity 0. A keyframe that never runs leaves the dialog invisible;
   this way the resting state is "visible" and the motion is the extra. */
dialog {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-in) var(--ease),
              transform var(--dur-in) var(--ease),
              overlay var(--dur-in) var(--ease) allow-discrete,
              display var(--dur-in) var(--ease) allow-discrete;
}

@starting-style {
  dialog[open] { opacity: 0; transform: translateY(8px); }
}

dialog:not([open]) { opacity: 0; transform: translateY(8px); }

.sheet {
  display: grid;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-5) var(--space-4);
  max-height: 88svh;
  overflow-y: auto;
}

.sheet__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--hairline);
}

/* A sheet says its own name the way the index says WORK: one word, set large.
   As a 10px label it read as a form field's caption — the sheet looked like a
   panel of something else rather than the thing the menu had just named. */
.sheet__title {
  margin: 0;
  font-family: var(--face-ui);
  font-size: var(--text-display-sm);
  letter-spacing: var(--track-display);
  line-height: var(--leading-tight);
  text-transform: uppercase;
  color: var(--ink);
  font-weight: var(--weight-bold);
}

/* The mark, not the word: a cross needs no translating and takes the width of
   a cross rather than the width of a title. */
.sheet__close {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  flex: 0 0 32px;
  padding: 0;
  background: none;
  border: 0;
  color: var(--ink-quiet);
  cursor: pointer;
  transition: color var(--dur-hover) var(--ease);
}
.sheet__close:hover { color: var(--ink-hover); }

.sheet__close .x {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.25;
  stroke-linecap: round;
}

.sheet__close { position: relative; }
.sheet__close::after {
  content: "";
  position: absolute;
  inset: -15px -14px;
}

/* --- Focus view: one photograph, nothing around it -----------

   The white card is gone. A photograph framed in paper is a
   thumbnail that got bigger; this one is simply the thing on
   screen, with the rest of the work blurred behind it and the
   credit laid over its own bottom edge.
   ------------------------------------------------------------ */

.focus {
  width: 100vw;
  max-width: none;
  height: 100svh;
  max-height: none;
  padding: 0;
  background: transparent;
  box-shadow: none;
  display: grid;
  place-items: center;
  overflow: hidden;
}

/* The blur IS the rest of the index: ::backdrop filters what is behind it, so
   the frames the visitor was just scanning go soft and stay where they were.
   A flat scrim would have hidden the same thing and told them less. */
.focus::backdrop {
  background: color-mix(in oklch, var(--ground) 80%, transparent);
  backdrop-filter: blur(26px) saturate(0.82);
  -webkit-backdrop-filter: blur(26px) saturate(0.82);
}

.focus__fig {
  position: relative;
  margin: 0;
  /* Narrow enough that the credit on the middle line has a page edge to sit
     in rather than lying across the middle of the frame. */
  max-width: min(72vw, 980px);
  line-height: 0;
}

.focus__img {
  display: block;
  width: auto;
  max-width: 100%;
  max-height: 82svh;
  object-fit: contain;
  background: none;
}

/* The credit sits on the window's middle line, out at both edges: the client
   to the left of the photograph and the date to its right. It is the only
   place on the site these two words appear — the index says nothing about a
   frame until someone opens it.

   `difference` against pure white is what keeps them legible whether the line
   crosses paper or crosses the photograph, the same trick the masthead uses,
   and it means no scrim has to be laid over the work. */
.focus__cap {
  margin: 0;
  position: fixed;
  inset: 50% 0 auto 0;
  transform: translateY(-50%);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-4);
  padding: 0 var(--gutter);
  pointer-events: none;
  /* Ink, not a blend. `difference` vanished wherever the line crossed a
     mid-grey patch of the blurred work behind it — which is exactly where a
     photograph's shoulder sits. The backdrop below is lightened enough that
     ink reads on every part of it. */
  color: var(--ink);
  font-family: var(--face-meta);
  font-size: var(--text-micro);
  letter-spacing: var(--track-caps);
  line-height: var(--leading-ui);
  text-transform: uppercase;
}

.focus__title { max-width: 14ch; }

.focus__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
}

/* The way out sits off the photograph, in the corner of the window. */
.focus__close {
  position: fixed;
  top: var(--space-3);
  right: var(--gutter);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  cursor: pointer;
  transition: color var(--dur-hover) var(--ease);
}
.focus__close:hover { color: var(--ink-hover); }
.focus__close .x {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.25;
  stroke-linecap: round;
}

/* The photograph arrives by growing the last fraction of the way, which reads
   as the frame coming forward rather than a card appearing. */
.focus { transition: opacity var(--dur-in) var(--ease),
                     overlay var(--dur-in) var(--ease) allow-discrete,
                     display var(--dur-in) var(--ease) allow-discrete; }
.focus .focus__fig {
  transform: scale(1);
  opacity: 1;
  transition: transform 340ms var(--ease), opacity 240ms var(--ease);
}
@starting-style {
  .focus[open] .focus__fig { transform: scale(0.965); opacity: 0; }
}
.focus:not([open]) .focus__fig { transform: scale(0.985); opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .focus .focus__fig,
  .focus[open] .focus__fig,
  .focus:not([open]) .focus__fig { transform: none; transition: none; }
}

@media (max-width: 560px) {
  .focus__fig { max-width: 74vw; }
  .focus__img { max-width: 74vw; max-height: 72svh; }
  .focus__title { max-width: 8ch; }
}

/* ------------------------------------------------------------
   Full-screen sheets — Info and Book

   A centred box is how a product asks a question. These two are
   not questions: Info is the masthead of the practice and Book is
   a call sheet, and both are the only thing a visitor is doing
   while they are open. So they take the whole window, set their
   own name at the size the index sets WORK, and rule their
   content the way the invoice rules a line.

   The authored moment is the opening: the paper is drawn down
   over the work, top edge to bottom, once. Nothing else on these
   two sheets moves.
   ------------------------------------------------------------ */

dialog.full {
  width: 100vw;
  max-width: none;
  height: 100svh;
  max-height: none;
  box-shadow: none;
  opacity: 1;
  transform: none;
  clip-path: inset(0 0 0 0);
  transition: clip-path 460ms var(--ease),
              overlay 460ms var(--ease) allow-discrete,
              display 460ms var(--ease) allow-discrete;
}

@starting-style {
  dialog.full[open] { clip-path: inset(0 0 100% 0); opacity: 1; transform: none; }
}
dialog.full:not([open]) { clip-path: inset(0 0 100% 0); opacity: 1; transform: none; }

/* The sheet already covers the window, so a dimmed, blurred layer under it is
   work the compositor does for nobody. */
dialog.full::backdrop { background: var(--ground); backdrop-filter: none; }

@media (prefers-reduced-motion: reduce) {
  dialog.full,
  dialog.full:not([open]),
  dialog.full[open] { clip-path: none; transition: none; }
}

.sheet--full {
  max-height: none;
  height: 100%;
  padding: var(--space-4) var(--gutter) calc(var(--space-5) + env(safe-area-inset-bottom));
  grid-template-rows: auto 1fr auto;
  gap: var(--space-5);
}

/* The name, at the size the index sets its own. Six rem is the ceiling: past
   that the word stops being a title and starts being the page. */
.sheet__title--xl {
  font-size: clamp(2.5rem, 1.2rem + 5.4vw, 6rem);
}

.sheet__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--hairline-soft);
}
.sheet__sig { width: 108px; color: var(--ink-faint); }
.sheet__sig svg { width: 100%; height: auto; fill: currentColor; }

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

   An editorial spread: the statement on the left at reading size,
   the four ways to reach her ruled down the right. Nothing is
   centred, because a practice is not an announcement.
   ------------------------------------------------------------ */

.spread {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--space-6) var(--space-7);
  /* The spread sits in the middle of its own band rather than clinging to the
     rule under the title: on a full screen, starting at the top leaves the
     sheet bottom-heavy with nothing in it. */
  align-content: center;
  align-items: start;
  overflow-y: auto;
}

.lede {
  margin: 0 0 var(--space-4);
  max-width: var(--measure);
  font-size: var(--text-lede);
  line-height: var(--leading-read);
}

/* The first paragraph is the statement of the practice and carries the page's
   reading weight; the one under it is a condition, set at reading size. */
.lede:first-child {
  font-size: clamp(1.25rem, 0.95rem + 1.3vw, 1.875rem);
  line-height: 1.35;
  letter-spacing: -0.012em;
  max-width: 22ch;
}
.lede:last-child { margin-bottom: 0; }

.ledger { margin: 0; display: grid; }

/* A contact is a row you can hit, not a word you have to find. The whole row
   is the target and the rule under it is the only furniture it gets. */
.ledger__row {
  display: grid;
  grid-template-columns: 11ch minmax(0, 1fr);
  gap: var(--space-3);
  align-items: baseline;
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--hairline-soft);
}
.ledger__row:first-child { border-top: 1px solid var(--hairline); }

.ledger__row dt {
  font-size: var(--text-micro);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-quiet);
}
.ledger__row dd {
  margin: 0;
  font-size: var(--text-read);
}

.ledger__row a {
  color: inherit;
  text-decoration: none;
  transition: color var(--dur-hover) var(--ease);
}
.ledger__row a:hover { color: var(--ink-hover); }

/* Kept for the Analyze sheet, which is still a centred box. */
.info__body {
  max-width: var(--measure);
  font-size: var(--text-read);
  line-height: var(--leading-read);
}
.info__body p { margin: 0 0 var(--space-3); }

/* ------------------------------------------------------------
   Analyze

   A selfie, read back. The sheet is a sequence and reads as one:
   what this is, the picker, the photograph, the verdict. DO and
   DON'T sit side by side because they are one judgement seen
   from two directions, and a column each is how a call sheet
   would set them.
   ------------------------------------------------------------ */

.an {
  display: grid;
  gap: var(--space-3);
  padding-top: var(--space-4);
}

.an__lede {
  margin: 0;
  font-size: var(--text-read);
  line-height: var(--leading-read);
  max-width: var(--measure);
}

.an__note {
  margin: 0;
  font-size: var(--text-micro);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-quiet);
}

/* The picker is a ruled plate, not a button: it is where a photograph goes,
   so it is the shape of one. */
.an__pick { display: block; cursor: pointer; }
.an__pick-face {
  display: grid;
  place-items: center;
  min-height: 112px;
  padding: var(--space-4);
  border: 1px dashed var(--hairline);
  font-size: var(--text-small);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-quiet);
  transition: border-color var(--dur-hover) var(--ease), color var(--dur-hover) var(--ease);
}
.an__pick:hover .an__pick-face { border-color: var(--ink-faint); color: var(--ink); }
.an__pick:focus-within .an__pick-face { border-color: var(--ink); color: var(--ink); }

.an__shot { margin: 0; }
.an__shot[hidden] { display: none; }
.an__shot img {
  width: 100%;
  max-height: 46svh;
  object-fit: contain;
  background: var(--ground-sunk);
}

.an__out { display: grid; gap: var(--space-4); }
.an__out[hidden] { display: none; }

/* The four readings, ruled like the contact rows in Info — same object, so
   the same furniture. */
.an__facts { margin: 0; display: grid; gap: 0; }
.an__row {
  display: grid;
  grid-template-columns: 9ch 1fr;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--hairline-soft);
  font-size: var(--text-base);
}
.an__row dt {
  font-size: var(--text-micro);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-quiet);
  padding-top: 2px;
}
.an__row dd { margin: 0; }

.an__read {
  margin: 0;
  font-size: var(--text-read);
  line-height: var(--leading-read);
}

.an__lists {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
}

.an__list ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--space-2);
  font-size: var(--text-base);
  line-height: var(--leading-ui);
}

.an__list li {
  padding-left: 14px;
  position: relative;
}
.an__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 7px;
  height: 1px;
  background: currentColor;
}

.an__list-t {
  margin: 0 0 var(--space-2);
  padding-bottom: var(--space-1);
  border-bottom: 1px solid var(--hairline);
  font-size: var(--text-small);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  font-weight: var(--weight-bold);
}

/* The one place the accent earns its keep on this sheet: DON'T is state, the
   same way an overdue invoice is. */
.an__list--dont .an__list-t { color: var(--signal); border-bottom-color: var(--signal); }
.an__list--dont li::before { background: var(--signal); }

.an__foot {
  margin: 0;
  font-size: var(--text-micro);
  line-height: 1.5;
  color: var(--ink-faint);
  max-width: var(--measure);
}

@media (max-width: 560px) {
  /* Two columns of four-word lines is not a call sheet, it is a crossword. */
  .an__lists { grid-template-columns: 1fr; gap: var(--space-3); }
}

/* ------------------------------------------------------------
   Booking form
   ------------------------------------------------------------ */

/* ------------------------------------------------------------
   Book — a call sheet, not a contact form

   Fields are ruled rows: label in the left column, the answer in
   the right, one hairline under each. It is how the invoice sets
   a line and how Info sets a contact, which is the point — the
   booking is the first document of the job.

   The decision sits in its own column on the right and stays put
   while the questions scroll: what it costs, what the conditions
   are, the way out to WhatsApp, and the button.
   ------------------------------------------------------------ */

.form { display: grid; gap: var(--space-3); }

.book {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--space-5) var(--space-7);
  align-items: start;
  overflow-y: auto;
}

.book__fields { display: grid; }

.book__rail {
  position: sticky;
  top: 0;
  display: grid;
  gap: var(--space-4);
}

.field {
  display: grid;
  grid-template-columns: 11ch minmax(0, 1fr);
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--hairline-soft);
  transition: border-color var(--dur-hover) var(--ease);
}
/* No rule above the first field: the head is already ruled, and two hairlines
   a space apart read as an empty row. */

/* The rule under the row is the field's own focus ring: it is already there,
   it is the width of the answer, and it moves nothing when it darkens. */
.field:focus-within { border-bottom-color: var(--ink); }

/* Date and time are one question each, not a pair of boxes side by side. */
.field--pair { display: contents; }

/* Divides who the client is from what they are booking. Six fields at one
   weight read as a wall; two groups read as two questions. */
.form__rule {
  height: 1px;
  border: 0;
  margin: var(--space-2) 0;
  background: var(--hairline-soft);
}

/* The caps label is the document's voice and stays caps — but 10px with
   0.09em tracking is a credit line, not a question someone has to answer on a
   phone. 11px is the site's own nav size and the floor for a label here. */
.field > label,
.group__legend {
  font-family: var(--face-meta);
  font-size: var(--text-small);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-quiet);
}

.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 0;
  background: none;
  color: var(--ink);
  border: 0;
  border-radius: var(--radius-ui);
  font-family: var(--face-meta);
  font-size: var(--text-read);
  line-height: 1.35;
  caret-color: var(--signal);
}
.field input::placeholder,
.field textarea::placeholder { color: var(--ink-faint); }
.field :is(input, select, textarea):focus { outline: none; }

/* A select with no border needs the one mark that says it opens something.
   Drawn at the hairline weight, like every other mark on the site. */
.field select {
  appearance: none;
  cursor: pointer;
  padding-right: 1.6em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1.5L6 6.5l5-5' fill='none' stroke='%23000' stroke-opacity='.55' stroke-width='1.25' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0 center;
  background-size: 12px 8px;
}
[data-scheme="dark"] .field select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1.5L6 6.5l5-5' fill='none' stroke='%23fff' stroke-opacity='.7' stroke-width='1.25' stroke-linecap='round'/%3E%3C/svg%3E"); }

.field textarea { resize: vertical; min-height: 3em; }

/* Errors. The accent already means "state" on this site, so it carries
   these too — and nothing else inside the form is allowed to use it. */
.error {
  margin: 0;
  font-size: var(--text-base);
  line-height: 1.45;
  color: var(--signal);
  text-wrap: pretty;
}
.error[hidden] { display: none; }
.field .error { grid-column: 2; }

/* A bad answer colours the row it is in, which is the only thing on the sheet
   the accent is allowed to do. */
.field:has([aria-invalid="true"]) { border-bottom-color: var(--signal); }
.field [aria-invalid="true"] { color: var(--ink); }

/* --- Place --------------------------------------------------- */

/* The suggestions are the field's own rows continued downward, not a widget
   dropped on top of it: same hairlines, same two weights, same ground. A
   Google-styled panel here would be the one object on the sheet that came
   from somewhere else, and it would say so. */
.place { position: relative; }

.place__list {
  position: absolute;
  z-index: 5;
  inset: calc(100% + var(--space-3)) 0 auto;
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--ground);
  border: 1px solid var(--hairline);
  /* The only shadow on the sheet, and it is doing a job no hairline can: the
     list floats over the rows beneath it and has to read as nearer. Offset
     down, blurred wide, barely there. */
  box-shadow: 0 12px 32px -12px oklch(0% 0 0 / 0.45);
  max-height: 16rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.place__list[hidden] { display: none; }

.place__item {
  display: grid;
  gap: 2px;
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  border-bottom: 1px solid var(--hairline-soft);
}
.place__item:last-child { border-bottom: 0; }

/* Hover may be quiet: the cursor is already sitting on the row and saying so.
   The arrow keys have no cursor, and this tint is the only thing telling
   someone which of five venues Enter is about to choose — so it is the louder
   of the two, and it takes the address up to full ink with it. */
.place__item:hover { background: var(--fill); }

.place__item[aria-selected="true"] { background: var(--fill-strong); }
.place__item[aria-selected="true"] .place__where { color: var(--ink); }

.place__name {
  font-size: var(--text-base);
  line-height: 1.35;
  color: var(--ink);
}

/* The address under the name. Quiet, and clipped rather than wrapped: a venue
   in Jakarta carries four commas of city and province and the list is for
   recognising a name, not reading a postal address. */
.place__where {
  font-size: var(--text-small);
  line-height: 1.35;
  color: var(--ink-quiet);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Google requires attribution wherever its Places data is shown outside a
   Google map. Set as the smallest thing in the list, which is also honestly
   what it is. */
.place__by {
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--hairline-soft);
  font-size: var(--text-micro);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Add-ons. Hidden until a service that offers them is chosen, so
   the form never shows a choice that does not apply. */
.group {
  display: grid;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-3) 0;
  border: 0;
  border-bottom: 1px solid var(--hairline-soft);
}
.group[hidden] { display: none; }
.group__legend { padding: 0 0 var(--space-1); }

.addon {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-2);
  padding-bottom: var(--space-2);
  font-size: var(--text-read);
  line-height: 1.35;
  cursor: pointer;
}
/* `display: grid` on the row beats the browser's own [hidden] rule, so every
   add-on showed for every service — including the ones that service does not
   offer. The attribute has to win here. */
.addon[hidden] { display: none; }

.addon input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--signal);
  cursor: pointer;
}

.addon__price {
  font-family: var(--face-meta);
  font-size: var(--text-base);
  font-variant-numeric: tabular-nums;
  color: var(--ink-quiet);
}

.addon input[type="number"] {
  width: 5.5ch;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--hairline);
  background: var(--ground);
  color: var(--ink);
  caret-color: var(--signal);
  font-family: var(--face-meta);
  font-size: var(--text-base);
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* --- Estimate + terms --------------------------------------- */

/* Hidden until a service is chosen. An em dash sitting under the
   heaviest rule on the sheet was weight spent on nothing. */
.estimate[hidden] { display: none; }

.estimate {
  display: grid;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--ink);
  font-family: var(--face-meta);
}
.estimate__label {
  font-size: var(--text-small);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-quiet);
}

/* The number is the headline of this column, so it is set like one: the
   brand's display tracking, leading near 1, and the figures lining up as
   tabular because the amount changes while someone is still choosing. */
.estimate__value {
  font-size: var(--text-figure-lg);
  line-height: 1.04;
  letter-spacing: var(--track-display);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  text-wrap: balance;
}

/* The conditions a client must read before they commit. Above the button,
   not in a footnote — and at reading size, because 11px grey is how a site
   says "nobody is meant to read this" and a 50% deposit is the opposite of
   that. One sentence: the two bold phrases carry the hierarchy, so the type
   does not have to shout to be found. A filled panel here was the only
   saturated block on the sheet and pulled more attention than the button
   underneath it, so it stays plain text. */
.terms {
  margin: 0;
  font-size: var(--text-read);
  line-height: var(--leading-read);
  letter-spacing: var(--track-meta);
  color: var(--ink-quiet);
  text-wrap: pretty;
}
.terms b { font-weight: var(--weight-bold); color: var(--ink); }

/* The way out of the form, sat where someone stalls: one line above the
   button. "Ask me directly" is the whole link — a phone number read out
   mid-sentence is a string to copy, not an invitation, and the number is
   already in Info for anyone who wants it.

   Set at the terms' size with a rule between them, because at a step smaller
   and a gap away it read as the last clause of the deposit condition rather
   than the offer it is. The rule is the one the whole site uses to say two
   things are two things. */
.ask {
  margin: 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--hairline-soft);
  font-size: var(--text-read);
  line-height: var(--leading-read);
  letter-spacing: var(--track-meta);
  color: var(--ink-quiet);
  text-wrap: pretty;
}

/* The site refuses underlines that arrive on hover — a second mark for an
   event the colour already reported. A link that is the only affordance on
   the sheet is the other case: it wears its rule all the time, in its own
   ink, so hover moves the text and the rule together as one object. At
   hairline alpha on the dark ground the rule was there in the stylesheet and
   nowhere on the screen. */
.ask a {
  color: var(--ink);
  font-weight: var(--weight-bold);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.24em;
  text-decoration-skip-ink: none;
  /* On a phone the phrase broke after "Ask" and took its underline with it,
     which turned one affordance into two fragments. It wraps as a whole or
     not at all. */
  white-space: nowrap;
  transition: color var(--dur-hover) var(--ease);
}
.ask a:hover { color: var(--ink-hover); }

/* Ink, not accent.

   The accent is reserved for state, and validation errors are state — so
   the accent belongs to them. A primary action wearing the same red as the
   five messages above it reads as another warning. Full width, filled, and
   last in the sheet is already unmistakable without borrowing the colour. */
/* An 11px word in a 48px block reads as a caption someone has centred. The
   label takes the weight the action has: 12px bold caps, and a block tall
   enough to be the obvious end of the sheet on a phone. */
.submit {
  width: 100%;
  min-height: 56px;
  padding: var(--space-3) var(--space-4);
  background: var(--ink);
  color: var(--ground);
  border: 0;
  border-radius: var(--radius-ui);
  font-family: var(--face-meta);
  font-size: var(--text-base);
  font-weight: var(--weight-bold);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  cursor: pointer;
  transition: filter var(--dur-hover) var(--ease),
              transform var(--dur-hover) var(--ease);
}
.submit:hover { filter: brightness(1.35); transform: translateY(-1px); }
.submit:disabled { filter: grayscale(1) opacity(0.5); cursor: wait; transform: none; }

/* --- Success ------------------------------------------------ */

.done {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-4);
  text-align: center;
}
.done[hidden] { display: none; }

.done__mark { width: var(--mark-lg); margin: 0 auto; color: var(--ink); }
.done__mark svg { width: 100%; height: auto; fill: currentColor; }

/* The one moment on the sheet that is allowed to be a sentence rather than a
   field, so it is set as one. */
.done__line {
  margin: 0;
  max-width: 30ch;
  margin-inline: auto;
  font-size: var(--text-lede);
  line-height: 1.45;
  letter-spacing: var(--track-meta);
  text-wrap: balance;
}

.done__note {
  font-family: var(--face-meta);
  font-size: var(--text-small);
  line-height: var(--leading-read);
  color: var(--ink-quiet);
}

.form[hidden] { display: none; }

/* ------------------------------------------------------------
   Narrow viewports

   The drift is a desktop pleasure. On a phone the chips are
   bigger, calmer, and allowed to scroll.
   ------------------------------------------------------------ */

/* One ratio for every frame. The crop is here and not in the files, so a
   photograph keeps its own proportions in the focus view and only the index
   is regimented. 3:4 is the reference's, and the tallest crop that still
   leaves the stack reading as a stack. */
.chip {
  display: block;
  width: 100%;
  /* A grid item's automatic minimum is its content, and the photograph inside
     is wider than the column it has been given. */
  min-width: 0;
  aspect-ratio: 3 / 4;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.chip__img {
  display: block;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background-size: cover;
  background-position: center;
  transition: transform var(--dur-hover) var(--ease);
}

.chip img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--dur-reveal) linear;
}
.chip img.is-loaded { opacity: 1; }

.chip:hover  { z-index: 20; }
.chip:hover .chip__img,
.chip:focus-visible .chip__img { transform: scale(1.02); }

/* ------------------------------------------------------------
   The one motion on the index

   The frames are a stack seen almost edge-on: the one at the
   middle of the window stands square to the viewer at full size,
   and the ones above and below lean away from it, foreshortened
   by the same perspective. Scrolling turns the stack through
   that angle rather than sliding a list past a window.

   It is driven by the scroll itself — no listener, no library —
   so it is exactly as fast as the page and stops when it stops.
   `perspective()` lives inside each frame's own transform: a
   perspective on the parent would make that parent the
   containing block for the focus view's fixed credit.
   ------------------------------------------------------------ */
@supports (animation-timeline: view()) {
  @keyframes bring {
    from {
      transform: perspective(900px) translateZ(-260px) rotateX(-38deg);
      opacity: 0.26;
      z-index: 0;
    }
    /* The square-on moment is brief on purpose: hold it and the stack reads
       as a list that pauses, not as a stack being turned through. */
    44%, 56% {
      transform: perspective(900px) translateZ(0) rotateX(0deg);
      opacity: 1;
      z-index: 2;
    }
    to {
      transform: perspective(900px) translateZ(-260px) rotateX(38deg);
      opacity: 0.26;
      z-index: 0;
    }
  }

  .frame {
    animation: bring linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 100%;
    transform-origin: 50% 50%;
    will-change: transform, opacity;
  }

  @media (prefers-reduced-motion: reduce) {
    .frame { animation: none; }
  }
}

/* Micro-interactions on the frame itself. The photograph answers the hand
   before it answers the click: it lifts a hair under the cursor, presses in
   when held, and the press is the shorter of the two because a control that
   releases slowly feels broken. */
.chip {
  transition: transform 180ms var(--ease);
}
.chip:active { transform: scale(0.985); }
.chip:active .chip__img { transform: scale(1); }

/* The index names the frame under the cursor without the index saying
   anything: the neighbours step back half a shade. */
.frame:hover .chip__img { transform: scale(1.015); }

/* ------------------------------------------------------------
   Desktop: the same stack, turned through ninety degrees

   A screen that is wider than it is tall has its depth across,
   not down. So the stack lies on its side here and the frames
   lean away to the left and right of the one in the middle,
   through the same angle. The wheel is translated to the
   sideways scroll in script, since a wheel still means
   "further along the work".
   ------------------------------------------------------------ */
@media (min-width: 900px) {
  /* The page itself does not scroll down; the run scrolls across. */
  body { overflow-y: hidden; }

  .strip {
    flex-direction: row;
    align-items: center;
    height: 100svh;
    padding: 0 38vw;
    gap: 0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .strip::-webkit-scrollbar { display: none; }

  .index__head {
    flex: 0 0 auto;
    width: min(30vw, 420px);
    padding: 0 var(--space-7) 0 0;
  }

  .frame {
    --frame-w: min(26vw, 380px);
    --overlap: calc(var(--frame-w) * 0.2);
    flex: 0 0 auto;
    width: var(--frame-w);
    padding: 0;
  }

  .frame + .frame {
    margin-top: 0;
    margin-left: calc(var(--overlap) * -1);
  }

  @supports (animation-timeline: view()) {
    @keyframes bring-sideways {
      from {
        transform: perspective(900px) translateZ(-260px) rotateY(38deg);
        opacity: 0.26;
        z-index: 0;
      }
      44%, 56% {
        transform: perspective(900px) translateZ(0) rotateY(0deg);
        opacity: 1;
        z-index: 2;
      }
      to {
        transform: perspective(900px) translateZ(-260px) rotateY(-38deg);
        opacity: 0.26;
        z-index: 0;
      }
    }

    .frame {
      animation-name: bring-sideways;
      /* The inline axis: the run's own sideways scroll, not the page's. */
      animation-timeline: view(inline);
      animation-range: cover 0% cover 100%;
    }
  }
}

/* Where the timeline is unsupported the stack simply sits still, which is
   already the layout: nothing depends on the motion to be legible. */

/* Below the spread's width the two columns become one: the statement then the
   contacts, the questions then the decision. The rail stops being sticky
   because on a phone it is simply the end of the sheet. */
@media (max-width: 900px) {
  /* One column, and one scroll: the sheet scrolls, its contents do not scroll
     inside it. Two nested scrollers on a phone means a thumb that sometimes
     moves the page and sometimes does not. */
  .spread,
  .book { grid-template-columns: minmax(0, 1fr); gap: var(--space-5); overflow: visible; }
  .book__rail { position: static; }
  .sheet--full { gap: var(--space-4); }

  /* Stacked, the rail is no longer a column beside the questions — it is the
     last thing on the sheet, read in one pass: what it costs, what the terms
     are, how to ask, then the button. More air between those four than the
     side-by-side layout needed. */
  .book__rail { gap: var(--space-5); }

  /* A note read at arm's length on a phone, in a column the full width of the
     sheet. The measure, not the size, is what needed adjusting. */
  .terms { max-width: 46ch; }
}

/* An 11ch label column in front of a date input leaves the date nowhere to
   go, so the label moves above the answer and the rule stays. */
@media (max-width: 560px) {
  .field,
  .ledger__row { grid-template-columns: minmax(0, 1fr); }
  .field .error { grid-column: 1; }
}

@media (max-width: 720px) {
  body { overflow-y: auto; }

  /* On a phone the signature is the whole header. Sticky rather than fixed,
     so it takes its own height out of the flow and the stage below needs no
     magic number to clear it — the mark's height follows the viewport width
     and a hard-coded offset would be wrong at every size but one. */
  .masthead {
    position: sticky;
    inset: 0 auto auto;
    display: block;
  }
  .masthead__mark { width: 100%; }

  /* The two words move to the bottom edge, where a thumb already is. */
  .menu {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 40;
    gap: 0;
    padding: var(--space-3) var(--gutter) calc(var(--space-3) + env(safe-area-inset-bottom));
  }
  .menu button { flex: 1; }
  .menu button:first-child { text-align: left; }
  .menu button:last-child { text-align: right; }

  .stage {
    min-height: 100svh;
  }

  /* The sticky header occupies real space here, so the run only has to clear
     the fixed footer and the home indicator under it. */
  .strip {
    padding: var(--space-4) 0
             calc(var(--space-6) + var(--space-5) + env(safe-area-inset-bottom));
  }

  .sheet { padding: var(--space-4); }
  .field--pair { grid-template-columns: 1fr; }

  /* `.sheet` above is later in the file than `.sheet--full`, so it was
     overriding the full sheet's padding — and with it the home-indicator
     inset. On a phone that put the Book button under the indicator. The full
     sheet keeps its own padding here. */
  .sheet--full {
    padding: var(--space-4) var(--gutter)
             calc(var(--space-4) + env(safe-area-inset-bottom));
  }

  /* Taller again where the tap target is a thumb rather than a cursor. */
  .submit { min-height: 60px; }
}

@media (prefers-reduced-motion: reduce) {
  dialog[open] { animation: none; }
}
