/* RoomChat.

   The canvas is dark and the tool is a white panel on it, which is WeTransfer's
   shape and works for WeTransfer's reason: the thing that does the work is on
   screen from the first frame, so the words beside it can answer "what happens
   after I make this?" instead of trying to earn a scroll.

   GREEN, founder's call 2026-09-22. This product is a gateway to WhatsApp and
   Line, and it should look like the place it is taking you.

   That reverses an older decision recorded here and in brand/README.md: clay
   (#A63A2A) was chosen over WhatsApp green because green tested 1.98:1 on
   white against clay's 6.44:1. The reversal does not reintroduce that failure,
   because the green is used the way WhatsApp uses it rather than as a drop-in
   swap:

     --accent         #0b7a63  deep teal-green. TEXT, links, small marks.
                               4.8:1 on white, so it passes AA where clay did.
     --accent-strong  #25d366  WhatsApp's own green. LARGE FILLS ONLY, and
                               never under white text - it carries near-black
                               ink at 8.8:1, which is the send-button look.

   Putting white on #25d366 is the 1.98:1 failure. Do not do it. If a new
   control needs a green background, it takes --accent-strong-ink with it. */

:root {
  --canvas: #0a1512;
  --panel: #ffffff;
  --panel-2: #f2f7f4;
  --ink: #0d1f1a;
  --muted: #5d6b65;
  /* Everything inside a form step. Dark grey rather than --ink, which is
     near enough to black to read as black. 8.1:1 on white. */
  --form-ink: #39433f;
  --line: #dbe5e0;
  --accent: #0b7a63;
  /* THE MARK, AND NOTHING ELSE, on its own token.
     The logo does not follow the palette: it followed --accent once, the
     accent went green on 2026-09-22, and the door turned green with it. A
     brand mark that changes colour when a button does is not a brand mark.
     It was clay until 2026-09-24, when the founder asked for it in the
     colour of the headline's "Fill it." - the same teal, written here on
     its own, so a later change to the accent still cannot reach it. */
  --mark: #0b7a63;
  --accent-ink: #ffffff;
  /* Large fills only, and only with --accent-strong-ink on top. See the note
     at the top of this file: white on this green is 1.98:1 and is the exact
     failure the brand spent a year avoiding. */
  --accent-strong: #25d366;
  --accent-strong-ink: #04291f;
  --danger: #c0392b;
  --on-canvas: #eaf4ef;
  --on-canvas-muted: #93a8a0;
  --radius: 12px;
  /* The bar's height, and the one place the page's arithmetic lives. Both
     columns are sized against it. */
  --bar-h: 76px;
  --canvas-pad: 32px;
  /* How far the sparkle layer hangs below the panel. The panel is 90% of the
     space under the bar, so there is dark canvas beneath it - the room a
     burst's downward throws land in, since the panel itself is opaque and
     sits above the layer. Only the layer's extra height uses it. */
  --spark-gap: 150px;
  /* How much of the space under the bar the two columns take. Both use it, so
     their bottom edges stay level; one number to change if the panel wants to
     be taller or shorter again. */
  --col-fill: 0.9;
  --col-h: calc((100vh - var(--bar-h) - var(--canvas-pad)) * var(--col-fill));
}

* { box-sizing: border-box; }

/* THE BROWSER'S BLUE, EVERYWHERE THE BROWSER DRAWS.

   A radio's dot, a checkbox's tick, a focus ring, the highlight in a date
   picker - the browser paints all of them in its own accent unless it is
   told otherwise, and its own accent is blue. Nothing in this stylesheet
   reached them, so the one colour on the page that was not ours was the one
   marking every answer a person gave.

   The deep green, not the bright one: this sits under 9px of dot and a
   tick, both of which need to read against white. */
:root { accent-color: var(--accent); }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--canvas);
  color: var(--on-canvas);
  font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  -webkit-text-size-adjust: 100%;
}

/* STATIC ON A DESKTOP (founder, 2026-09-25): the front page never scrolls
   there, only the panel does, and the phone mock runs off the foot of the
   screen rather than lengthening the page. A phone-width screen scrolls as
   before.

   THE FRONT PAGE ONLY. This stylesheet is every page's, and a bare body
   here locked a desktop out of the lower half of a listing, the standalone
   form and the private page: none of them has a scrolling region of its own,
   the page is the scroll. The pitch column is the front page's alone. */
@media (min-width: 901px) {
  body:has(.pitch) { overflow: hidden; }
}

/* ---- the bar ---- */

.top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  height: var(--bar-h);
  padding: 0 24px;
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  color: var(--on-canvas);
  font-weight: 700;
  letter-spacing: -0.01em;
  text-decoration: none;
}
.mark { width: 26px; height: 26px; color: var(--mark); }
.top nav { display: flex; align-items: center; gap: 18px; font-size: 0.875rem; }
.top nav a { color: var(--on-canvas-muted); text-decoration: none; }
.top nav a:hover { color: var(--on-canvas); }
/* ---- the canvas ---- */

/* The pitch on the left and the tool on the right, and ONLY the tool scrolls.
   The pitch and the map stay put, which is what makes this read as one tool
   rather than as a long page: a poster is always looking at the form and
   never loses where they are. */
.canvas {
  position: relative;
  max-width: 1180px;
  margin: 0 auto;
  padding: 8px 24px 24px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 560px);
  gap: 56px;
  align-items: start;
}

/* BOTH COLUMNS GET THE HEIGHT EXPLICITLY.
   
   They were grid items relying on align-items: stretch to fill the row, and
   the panel did not — it came out the height of its first section, so six of
   the seven scrolled inside a box the size of the map. Whatever the exact
   reason, the fix is not to reason harder about grid row sizing in a file
   nobody can run a browser against: it is to say the height once, here, and
   have neither column depend on the other. */
/* ALL THREE ARE PLACED EXPLICITLY, OR NONE OF THEM CAN BE.
   The sparkle layer asks for column 2 row 1. The moment one item is placed,
   auto-placement puts the others in the next FREE cell - so the panel went
   to row 2 and the page came apart, live. Each item says where it goes. */
.pitch,
.panel {
  height: var(--col-h);
  grid-row: 1;
}

.panel {
  grid-column: 2;
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  color: var(--ink);
  border-radius: 18px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
  overflow: hidden;
}

/* The panel used to bob on a slow cycle. Removed at the founder's call,
   2026-09-22 - the sparks stayed. Removed rather than commented out: a
   keyframe nothing references outlives its reason, and the will-change that
   came with it was holding a compositor layer for an animation that no
   longer exists. */

/* THE PANEL IS OPAQUE AND SITS ABOVE THIS, so anything born inside its
   outline is behind it and stays behind it. The layer HANGS BELOW the panel
   by --spark-gap, so a burst's downward throws land on dark canvas instead
   of vanishing under the white box; throws off the other edges are seen
   because the layer does not clip.

   Bursts only, since 2026-09-28: this layer also dripped idly from the
   panel's foot, and that one emitter made the gap under the form read
   heavier than the rest of the page. The page-wide sparkfield (below) is
   the only ambient layer, so the density is even.

   align-self: start, or the grid would stretch it back to the row's height
   and take the overhang away. */
.sparkles {
  grid-column: 2;
  grid-row: 1;
  align-self: start;
  height: calc(var(--col-h) + var(--spark-gap));
  position: relative;
  z-index: 1;
  pointer-events: none;
}

/* One look for the form's sparks and the page's motes (below); only where
   they are born and which way they go differ. */
.spark,
.mote {
  position: absolute;
  width: var(--s);
  height: var(--s);
  border-radius: 50%;
  /* White at the core so it reads as a spark rather than a green dot, and
     transparent at the rim so it has no edge to look pasted on. */
  background: radial-gradient(circle, #ffffff 0%, var(--accent-strong) 45%, transparent 72%);
  opacity: 0;
  animation: drift var(--d) ease-out forwards;
}
/* Out from the birthplace along --dx/--dy, fading the whole way; which way
   is the spawner's choice. */
@keyframes drift {
  0% { transform: translate3d(0, 0, 0) scale(0.3); opacity: 0; }
  15% { opacity: 0.95; }
  100% { transform: translate3d(var(--dx), var(--dy), 0) scale(1); opacity: 0; }
}

/* THE FLOATING SPARKS ACROSS THE PAGE, on a layer of their own.
   Founder, 2026-09-24: the form's layer above throws from every edge on
   Next; these drift faintly everywhere, evenly - the page's only ambient
   sparks since 2026-09-28 - and do not react to anything. Fixed to the
   viewport at z-index -1,
   which puts it above the body's canvas (painted on the root) and under
   everything else: the bar, the pitch's words and the panel cover it
   without having to know it is there. Same look and keyframes as the
   form's sparks; only the birthplace and the direction differ. */
.sparkfield {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  opacity: 0.85;
  pointer-events: none;
}

/* The one scrolling region in the product. */
.panel .view { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }

/* Two jobs, one panel. A segmented control rather than links, because there
   is nowhere to navigate to. */
/* FLUSH WITH THE PANEL, on all three edges.
   No margin and no radius of its own: the panel is already rounded and
   already clips what overflows it, so the strip inherits the top corners
   for free and cannot disagree with them. Founder, 2026-09-22. */
/* BROWSER TABS. Founder, 2026-09-24.

   The strip is a shade darker than the progress row, and the chosen tab is
   cut from the progress row's own surface, so it reads as the front of the
   thing under it - the way a browser's active tab runs into its toolbar.
   The unchosen tab sits flat on the strip. Which one is chosen is also said
   by its name, bold and dark, with the other in the counter's grey.

   No padding on the strip: the tabs run to the panel's top and side edges,
   so the chosen one is part of the panel rather than a card inside it. */
.tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  margin: 0;
  padding: 0;
  background: var(--line);
}

.tab {
  position: relative;
  appearance: none;
  border: 0;
  border-radius: 10px 10px 0 0;
  padding: 10px 12px 8px;
  font: inherit;
  font-size: 1.125rem;
  font-weight: 400;
  color: var(--muted);
  background: transparent;
  cursor: pointer;
}
.tab:hover { background: color-mix(in srgb, var(--panel-2) 45%, transparent); }

.tab[aria-selected="true"] {
  font-weight: 700;
  color: var(--ink);
  background: var(--panel-2);
}

/* The flares: the two concave curves where a browser tab meets the bar
   under it. Each is a 10px square beside the tab's foot, filled with the
   tab's surface everywhere except a quarter circle, which leaves the curve.
   The panel clips what overflows it, so the outer one is cut cleanly at
   the edge - which, with no padding on the strip, is where the outer one
   always is. */
.tab[aria-selected="true"]::before,
.tab[aria-selected="true"]::after {
  content: "";
  position: absolute;
  bottom: 0;
  width: 10px;
  height: 10px;
  pointer-events: none;
}
.tab[aria-selected="true"]::before {
  left: -10px;
  background: radial-gradient(circle at 0 0, transparent 9.5px, var(--panel-2) 10px);
}
.tab[aria-selected="true"]::after {
  right: -10px;
  background: radial-gradient(circle at 100% 0, transparent 9.5px, var(--panel-2) 10px);
}

.pane[hidden] { display: none; }

/* Inside a stepped pane the nav's Next IS the search, so the filter form's
   own submit button would be the same action twice on one screen. It stays
   in the markup because without JavaScript nothing is stepped, the nav is
   hidden, and that button is the only way to run a search at all. */
.stepped .filters button[type="submit"] { display: none; }

/* The search pane's own button sits under the map, not at the end of a form. */
#search-here { width: 100%; margin: 12px 0 4px; }

.progress[hidden] { display: none; }

/* How much of the listing is filled in — not a gate. A listing needs only a
   pin, a number and the terms to go up; this says how much of it a seeker
   will get to read. */
.progress {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--line);
  background: var(--panel-2);
  /* OVER THE SCROLLER, AND CASTING ONTO IT. Founder, 2026-10-01: the frame
     is static and its content moves, and nothing said so - content ran up
     to the line and stopped, which reads as the end of the page rather than
     as the top of a scroll. A tight shadow under the chrome says the layer
     above is in front. It is deliberately slight; the line still does the
     separating and this only gives it depth.

     The z-index is not decoration: this sits BEFORE the scroller in the
     flow, so without it the scrolling content paints on top of the shadow. */
  position: relative;
  z-index: 1;
  /* No negative spread: it pulled the shadow in from both sides, so the
     panel's edges got a fade where the middle got a shadow. At zero the
     shadow box is the row itself and the blur runs off both edges, where
     the panel clips it - full width, even weight. Founder, 2026-10-01:
     a tenth of the strength it shipped at an hour earlier. */
  box-shadow: 0 6px 12px rgba(12, 32, 27, 0.05);
}
.progress-bar {
  flex: 1;
  height: 6px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
  /* It grows into the charged state rather than snapping to it. */
  transition: height 260ms ease, background 260ms ease;
}
.progress-bar span {
  display: block;
  width: 0;
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 220ms ease, background 220ms ease, box-shadow 220ms ease;
}

/* THE LAST STEP.

   The bar keeps the mass and the colour it gains at full - 9px, and a fill
   that runs deep at the tail, bright at the head and hot at the tip. What it
   does NOT do is move. It shook, then it breathed and swept, and both were
   the same mistake in different clothes: a bar that keeps moving after it is
   finished is asking to be watched rather than acted on.

   The counter goes instead. At the end it reads 4 / 4, which is a fact with
   nothing left in it, and what matters there is what the step is FOR - both
   flows end in a message. So the number is replaced by a speech bubble. */
/* Slightly thicker and nothing else. The row is already taller than either
   bar height - the counter's slot governs it - so this cannot move anything
   above or below it, and the slot rule above stops it moving anything to
   the side. */
/* THICKNESS AND NOTHING ELSE.

   This has been a shiver, a brightness flicker, a breath and a sweep, a
   gradient, then a brighter fill with a glow around it. Every one of them
   was the bar trying to say something the thing beside it already says.

   The colour is the colour at every step. Changing it at the end made a
   full bar read LIGHTER than a half-full one, which is backwards, and the
   glow spread that lightness past its own edge. All that changes now is
   that it gets slightly heavier. */
.progress.is-charged .progress-bar { height: 8px; }

/* The same grey the numbers were. It stands in for the counter rather than
   announcing itself, and a green mark beside a green bar would read as a
   second thing lighting up. */
.chat {
  width: 18px;
  height: 18px;
  display: block;
  color: var(--muted);
}

/* ONE SLOT, TWO OCCUPANTS, IDENTICAL BOX.

   The counter and the chat mark swap in the same place, so they have to
   reserve the same space or the bar beside them changes length when they
   trade. The mark is narrower than "4 / 4", so without this the bar visibly
   grew on the last step - a horizontal jump nobody asked for.

   tabular-nums does the same job one digit down: 1 / 4 and 4 / 4 are the
   same width even though 1 and 4 are not. */
.progress-label,
.progress-done {
  flex: 0 0 auto;
  min-width: 36px;
  height: 18px;
  margin: 0;
  color: var(--muted);
  font-size: 0.8125rem;
  /* Matches the height, so the contents sit centred without the box needing
     to be a flex container. It cannot be one: the counter is three text
     nodes with spaces between them, and flex makes each an item and eats
     the whitespace - which turned 3 / 4 into 3/4. */
  line-height: 18px;
  /* CENTRED, both of them, and that is what makes them swap in place.
     Right-aligned they shared an EDGE but not a middle: the counter is
     about 26px of the 36px slot and the mark is 18px, so pushing both to
     the right put their centres 4px apart and the mark sat further out
     than the numbers it replaces. Sharing the midpoint is the only
     alignment where two things of different widths land in the same
     place. */
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* WHICH OF THE TWO IS SHOWING, and it must be stated after the shared box
   above rather than before it. Both are single class selectors, so source
   order decides, and a display set up there beat the one set down here -
   which is why the mark appeared on every step instead of the last. */
.progress-label { display: block; }
.progress-done { display: none; }
.progress.is-charged .progress-label { display: none; }
.progress.is-charged .progress-done {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* A flex column, so the phone under the steps can be the one thing that
   gives way when the column is short (see .illo).

   No position and no z-index of its own. It sat at 2, above the sparks'
   layer, from when a drawing was anchored to it; once Next began throwing
   sparks from every edge of the form (2026-09-24), the ones thrown this way
   flew behind the phone mock and vanished. Unpositioned, it paints under
   the sparks and the sparks still paint under the panel. */
.pitch {
  grid-column: 1;
  padding-top: 16px;
  display: flex;
  flex-direction: column;
}
.pitch h1 {
  margin: 0 0 28px;
  font-family: ui-serif, Georgia, "Times New Roman", serif;
  /* Capped where the column stops widening (~1280): past it a bigger
     headline only wraps onto more lines. */
  font-size: clamp(2.1rem, 3.6vw, 2.85rem);
  /* "Your room, in one / link." left a word alone at 1280 wide. */
  text-wrap: balance;
  line-height: 1.02;
  letter-spacing: -0.02em;
  font-weight: 600;
}
.pitch h1 em { font-style: normal; color: var(--accent); }
/* The why, under the what, and a sentence rather than a second headline:
   the body's sans at reading size, so the serif above it is the only
   display type in the pitch (founder's pick, 2026-09-24). */
.pitch h1 .why {
  display: block;
  margin-top: 14px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-weight: 500;
  font-size: clamp(1.0625rem, 1.6vw, 1.25rem);
  line-height: 1.5;
  letter-spacing: 0;
  /* WhatsApp green, the same as Next and the sparks, so the three line up
     (founder, 2026-09-25). 9.4:1 on the canvas. */
  color: var(--accent-strong);
}

/* ---- the floaters: what the product makes, not stock shapes ---- */

@media (max-width: 900px) {
  /* A phone has one column and the page itself scrolls: pinning a panel
     inside a viewport that is already the panel's height gains nothing and
     costs the address bar's worth of room. */
  .canvas { grid-template-columns: minmax(0, 1fr); height: auto; gap: 28px; padding: 4px 14px 48px; }
  /* One column here, so an explicit column 2 would invent a second one and
     put the panel in it. Both go back to being placed in order. */
  .pitch,
  .panel { height: auto; }
  /* One column here, so an explicit column 2 would invent a second one and
     put the panel in it. */
  .pitch,
  .panel { grid-column: 1; grid-row: auto; }
  /* The mock shows here too since 2026-09-29 (Skylar: critical to
     understanding the product; hidden from the first drawing until then).
     Its mobile sizing lives BELOW the base .illo rule, not here: same
     specificity, so source order decides, and written here the base rule's
     flex-fill won and the box rendered at zero height. */
  /* One column here, so there is no second cell to sit over - and a phone
     pays for an animation in battery. */
  .sparkles { display: none; }
  .sparkfield { display: none; }
  .panel .view { overflow: visible; }
  .pitch { padding-top: 8px; align-self: start; }
  .pitch h1 { margin-bottom: 18px; }
  .top { padding: 14px 16px; }
  .top nav { gap: 12px; }
}

/* The phone mock, IN FLOW, straight under the steps.

   It was absolutely positioned for a reason that still holds: the first
   drawing, in normal flow, made the pitch column taller than its fixed
   height, the overflow cascaded, and the form panel came out the height of
   its first section. But pinned to the column's foot the phone drifted far
   below the steps on a tall screen (founder, 2026-09-25: "too much space").

   So it follows the steps, in a flex column that keeps its explicit height,
   and fills what is left of it: the phone fades out just past the form's
   bottom edge (founder, 2026-09-25) and scrolls inside its box, so it
   changes no height - the explicit height is what stops the cascade, and
   it stays. It
   hides when the viewport is too short for it, and on phones, where a mock
   is not what a poster needs above the form. */
.illo {
  /* Fills the rest of the column and no more, so the box ends by the
     form's bottom edge - the two columns share the same explicit height -
     and the phone fades out there (founder, 2026-09-25). The phone inside
     runs on past it and scrolls. */
  flex: 1 1 0;
  min-height: 0;
  /* It scrolls in its own box, so the rest of the reply can be read; the
     page still never does (founder, 2026-09-25), and a wheel anywhere off
     the form scrolls it (the script). No scrollbar: the fades say there is
     more. */
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  width: min(430px, 100%);
  /* Above: the same visible gap as between the teal line and step 1
     (founder, 2026-09-25), the headline's 28px margin plus the teal line's
     leading below its text, measured at 33px. A margin, outside the
     scroller, so the phone scrolls out where its top sits at rest, not up
     against the steps (founder, 2026-09-26).
     Below: 40px past the form's foot, flush having looked worse than
     expected (founder, 2026-09-25), 10px past still a touch high
     (2026-09-26), and 20px leaving too much dark canvas under the phone
     (2026-09-25) - so the phone runs down rather than the gap closing, the
     column height and the panel both untouched. A negative margin in a flex
     column grows the box by as much, and the page below has the room. */
  margin: 33px 0 -40px;
  /* A fade of 33px at the foot, rather than a hard cut, and one at the
     top as deep as the phone has scrolled, up to the same 33px: the script
     sets --top-fade, so at rest there is none and the phone's top is
     whole. */
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 var(--top-fade, 0px), #000 calc(100% - 33px), transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 var(--top-fade, 0px), #000 calc(100% - 33px), transparent);
}
.illo::-webkit-scrollbar { display: none; }



/* The phone mock: the top of a phone, showing a dark-mode WhatsApp chat
   with the link the poster sent, as WhatsApp draws it: photo, bold title,
   two lines of details, the site, and the link itself under the card; and
   under that the applicant's reply, grey, received. Founder, 2026-09-25: a
   white box, a green bubble with a face, a dark chat, a phone, then this.

   Only the top of the phone: a whole one at this height would shrink the
   text past reading, so it rises from the bottom edge of the screen, and
   the reply under the link shows only its top.

   The colours are WhatsApp's dark theme: a near-black chat, #144d37 for a
   sent bubble with its link card a shade darker, near-white text, a grey
   time and day pill, the link and read ticks in WhatsApp's blue. The
   texture is a faint dot grid, NOT WhatsApp's doodle wallpaper, which is
   their artwork. The type is sized in vh so a short screen gets a smaller
   bubble rather than none: the first drawing hid below 860px of viewport,
   five pixels above a common laptop, and never appeared on the machine it
   was built for. */
.illo .phone {
  /* At least as tall as the box, so the phone runs off its foot however
     tall the window is; the screen fills it. */
  display: flex;
  flex-direction: column;
  min-height: 100%;
  width: min(400px, 100%);
  padding: 7px 7px 0;
  border: 1px solid rgba(233, 237, 239, 0.14);
  border-bottom: 0;
  border-radius: 40px 40px 0 0;
  background: #161a19;
}
.illo .phone-screen {
  display: flex;
  flex-direction: column;
  flex: 1 0 auto;
  /* Ends just after the reply, so scrolling stops with its last line clear
     of the bottom fade rather than running on into an empty screen. */
  padding: 0 12px 48px;
  border-radius: 32px 32px 0 0;
  background-color: #111313;
  background-image: radial-gradient(rgba(233, 237, 239, 0.05) 1px, transparent 1.5px);
  background-size: 14px 14px;
}
.illo .phone-bar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  height: 26px;
  /* Room before the first message: below 900px of viewport the day pill
     hides and the link bubble sat right under the bar (founder,
     2026-09-26). */
  margin-bottom: 14px;
  padding: 0 10px;
  color: #e9edef;
  font-size: 0.75rem;
  font-weight: 600;
}
.illo .phone-island { width: 72px; height: 18px; border-radius: 10px; background: #000; }
.illo .phone-status { justify-self: end; width: 38px; height: 12px; fill: #e9edef; color: #e9edef; }
.illo .wa-day {
  align-self: center;
  margin: 4px 0 10px;
  padding: 3px 10px;
  border-radius: 8px;
  background: #1f2121;
  color: #e9edef;
  font-size: 0.75rem;
  font-weight: 600;
}
.illo .wa-bubble {
  position: relative;
  align-self: flex-end;
  margin: 0 8px 0 0;
  max-width: 82%;
  padding: 3px 3px 22px;
  border-radius: 10px 10px 0 10px;
  background: #144d37;
  color: #e9edef;
  font-size: clamp(0.72rem, 1.55vh, 0.8125rem);
  line-height: 1.45;
}
/* The reply: a RECEIVED message, WhatsApp's dark-mode grey, on the left,
   its tail at the bottom-left. The fade cuts it off until it is scrolled. */
.illo .wa-in {
  align-self: flex-start;
  margin: 8px 0 0 8px;
  max-width: 88%;
  padding: 7px 10px 20px;
  border-radius: 10px 10px 10px 0;
  background: #242626;
}
/* More specific than .illo .wa-tail below, so its order cannot matter. */
.illo .wa-in .wa-tail { right: auto; left: -11px; fill: #242626; }
/* The tail swoops out of the bottom-right corner, curved rather than a
   triangle, the way WhatsApp draws a sent message now. */
.illo .wa-tail {
  position: absolute;
  right: -11px;
  bottom: 0;
  width: 12px;
  height: 16px;
  fill: #144d37;
}
.illo .wa-preview { border-radius: 7px; overflow: hidden; background: #0f3a2a; }
/* 3:2, the proportions WhatsApp gives a card's photo (founder, 2026-09-26,
   from a real card), not the 3:1 strip the drawing was. */
.illo .wa-photo { display: block; width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; }
.illo .wa-preview-text { display: flex; flex-direction: column; gap: 2px; padding: 6px 8px 7px; }
/* The title and details in full, never clamped: the card is there to show
   every data point a poster gets (founder, 2026-09-26). The phone scrolls
   for the length. */
.illo .wa-preview-text strong { color: #e9edef; font-weight: 600; }
.illo .wa-details { color: rgba(233, 237, 239, 0.65); }
.illo .wa-domain { display: flex; align-items: center; gap: 4px; margin-top: 2px; color: rgba(233, 237, 239, 0.65); }
.illo .wa-domain svg { width: 12px; height: 12px; }
/* The message's own lines, under the card: one fact per line with its
   emoji, the way shareMessage writes them (founder, 2026-10-12). Inset
   7px, WhatsApp's text inset, so they line up with the link below them;
   the bubble's 3px padding is the card's, not the text's. */
.illo .wa-text { display: block; padding: 6px 7px 0; }
.illo .wa-text strong { font-weight: 600; }
/* The last line of that message, which is also the link the card is drawn
   from: WhatsApp's blue, and it breaks mid-URL rather than widening the
   bubble past its 82%. */
.illo .wa-link { display: block; padding: 5px 7px 0; color: #53bdeb; overflow-wrap: anywhere; }
.illo .wa-ticks { width: 16px; height: 11px; color: #53bdeb; }
.illo .wa-time {
  position: absolute;
  right: 9px;
  bottom: 5px;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: rgba(233, 237, 239, 0.55);
  font-size: 0.6875rem;
}

/* A down chevron hovering under the phone, to say it scrolls (founder,
   2026-09-27). No height, so the phone's box and its fade are what they
   were. It starts where the phone's negative margin pulls it, 40px up
   inside the phone's box, so it is pushed 50px down to sit 10px under the
   fade; as wide as the phone, to centre on it. */
.illo-more {
  flex: none;
  height: 0;
  width: min(400px, 100%);
  display: flex;
  justify-content: center;
  color: var(--on-canvas-muted);
}
.illo-more svg { width: 22px; height: 22px; margin-top: 50px; }
/* It points the way the phone can still scroll: up at its foot, and not at
   all with nothing to scroll (founder, 2026-09-27). The script sets both.
   The path turns about the view box's centre, so the svg keeps its bob. */
.illo-more path { transform-origin: 12px 12px; }
.illo-more.is-end path { transform: rotate(180deg); }
.illo-more.is-still { visibility: hidden; }

@media (prefers-reduced-motion: no-preference) {
  .illo-more svg { animation: bob 1.8s ease-in-out infinite; }
  .illo-more path { transition: transform 0.25s ease; }

  @keyframes bob {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(6px); }
  }
}

/* On a phone (Skylar, 2026-09-29): the sent link and its card, static, in
   flow, and the page scrolls past. A scroll-ride (sticky box, the chat
   driven from the page's scroll by script) lived for a few hours that day
   and was removed rather than tuned: on real glass, momentum scrolling
   runs on the compositor and a script-set scrollTop lags a frame behind,
   so the chat jittered against the page - a race script cannot win. Ana's
   reply went the same day ("the card is the story") and CAME BACK the same
   evening, founder's call: the reply is the promise - a message that
   arrives with the applicant's details - and without it the mock sold a
   link, not the answers. The screen ends just after it (base rule), so the
   page simply scrolls past the whole conversation.

   Below EVERY base rule it overrides (.illo, .illo .phone, .phone-screen)
   on purpose: same specificity throughout, so source order decides, and
   placed in the one-column block up top the base flex-fill silently won
   and the box rendered at zero height. flex: none for that same flex-fill;
   overflow: visible makes the box no scroll container at all - it takes
   its content's height and nothing traps a finger. The phone still TRAILS
   OFF at its foot - base bottom fade, open frame - rather than closing: a
   fully closed phone (bottom bezel, corners, no fade) shipped for an hour
   on 2026-09-29 and lost the fade the founder's eye expected. The screen's
   own 48px bottom padding keeps the bubble's text clear of the 33px fade,
   and that padded-then-fading foot IS the bottom spacing. The chevron
   goes: nothing scrolls inside the box. */
@media (max-width: 900px) {
  .illo {
    flex: none;
    overflow: visible;
    margin: 33px 0 0;
  }
  .illo-more { display: none; }
}

/* Only when there is genuinely no room. Below this the steps and the drawing
   would overlap however small it is made. It was 620px until the pitch grew
   a why line and a trust line on 2026-09-24; at 1280x720 the trust line then
   sat on the drawing. */
/* Desktop windows only: every phone is under 900px of viewport height, and
   on a phone the page scrolls, so nothing overlaps (2026-09-29). */
@media (max-height: 760px) and (min-width: 901px) {
  .illo { display: none; }
  .illo-more { display: none; }
}

/* Below 900px of viewport the phone gives up the day pill, so the top of
   the reply still shows above the screen's edge: the Spanish headline runs
   to three lines and takes the room. Desktop only, same as above: the
   mobile box picks its own height, and the pill has the room. */
@media (max-height: 900px) and (min-width: 901px) {
  .illo .wa-day { display: none; }
}

/* ---- what happens after you make this ---- */

.how { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.how li {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  color: var(--on-canvas-muted);
  font-size: 1rem;
  line-height: 1.45;
}
/* An emoji bullet, not a number: the four lines are reasons, not steps
   (founder, 2026-09-28). */
.how .emo { flex: 0 0 auto; width: 24px; text-align: center; }

/* A shared link arrives with no canvas around it: the same content, on the
   panel, centred. */
.app {
  max-width: 620px;
  margin: 24px auto;
  background: var(--panel);
  color: var(--ink);
  border-radius: 18px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
  /* clip, not hidden, where the browser knows it: hidden makes the card a
     scroll container, and a sticky footer inside one sticks to the card
     instead of the screen - it never moves. clip rounds the corners
     without that. hidden stays first as the fallback. */
  overflow: hidden;
  overflow: clip;
}
@media (max-width: 900px) { .app { margin: 12px; } }

/* ---- the map, and the two things you can do from it ---- */

.map-pane {
  position: relative;
  /* 10% off both, 2026-09-21. The floor moves with the height or a short
     viewport quietly keeps the old size. */
  height: 39.6vh;
  min-height: 216px;
  background: var(--panel);
}
/* IN THE PANEL, THE MAP TAKES WHAT THE FIRST STEP LEAVES.

   Founder, 2026-10-02, on a screenshot with a band of white under the note:
   "extend the map visible zone down a little further so that the form first
   page is mostly filled". A share of the window could not do that: the panel
   grows by 0.9 of every extra pixel of window and the map grew by 0.396, so
   the band widened with the screen (63px at 884 high, 223px at 1200).

   So here the map is the panel's own height less everything else in it.
   329px is that everything, measured in real Chrome at five window sizes and
   the same at each: the tabs, the progress row, the step's top padding, the
   line and the note under the map, and the nav come to 285px, and the form
   and the view keep 44px of padding under every step. Any less than 329 and
   the first step starts to scroll - 297 was tried and scrolled by 32px. What
   is left under the note is that 44px, about what sits above the map.

   Never smaller than it was: max() keeps the old share of the window where
   that is the bigger of the two, which is any window under about 846px
   tall. There the fit would have SHRUNK the map to stop a small scroll that
   was already there, and nobody asked for a smaller map.

   Only where the panel has a fixed height. On a phone the page scrolls and
   the base rule's share of the window is still the right size. After the
   base rule, because source order decides between them. */
@media (min-width: 901px) {
  .panel .map-pane { height: max(39.6vh, calc(var(--col-h) - 329px)); }
}
#map { position: absolute; inset: 0; }
#crosshair {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -60%);
  font-size: 26px;
  color: var(--danger);
  text-shadow: 0 0 3px var(--bg);
  pointer-events: none;
}
.map-missing { display: flex; align-items: center; justify-content: center; height: 100%; padding: 20px; }
.map-missing p { color: var(--muted); text-align: center; margin: 0; }

/* THE CITY BOX. On the map, along its top edge, with its list under it.

   Founder, 2026-10-02: type a city and the map jumps there. It is inside
   the map's pane rather than above it, so it costs the panel no height and
   moves between the two tabs with the map as one node.

   z-index 2 puts it over the crosshair and the map's own controls. Only the
   box and the list take a tap: the strip between them is the map's.

   16px in the box, here and not only from the step rule: under 16px an
   iPhone zooms the whole page when the box is tapped.

   The list's buttons undo the page-wide button rule (full width, bold,
   centred), because a suggestion is a line of text, not a call to action.

   (No backticks in here, ever.) */
.citybox {
  position: absolute;
  top: 8px;
  left: 8px;
  right: 8px;
  z-index: 2;
}
.citybox input {
  display: block;
  width: 100%;
  margin: 0;
  /* Room on the left for the looking glass. */
  padding: 10px 12px 10px 38px;
  font-family: inherit;
  font-size: 16px;
  font-weight: 400;
  line-height: 1.4;
  color: var(--ink);
  /* The panel's white, 30% see-through (founder, 2026-10-02), so the map
     reads through the box it sits on. The list under it stays solid: its
     lines have to be read over street names. */
  background-color: rgba(255, 255, 255, 0.7);
  /* And the map behind it blurred, frosted glass (founder, 2026-10-02), so
     street names under the box do not fight the text typed into it. The
     prefixed line is Safari's. */
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  /* A grey looking glass, so the box says what it is for before anyone
     reads the hint (founder, 2026-10-02). Drawn here rather than in the
     markup: it is decoration, and a background cannot be tapped or read
     out. The colour is spelled out because a data URI cannot see a
     variable. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%239aa5a0' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='8.5' cy='8.5' r='5.5'/%3E%3Cpath d='M13 13l4.5 4.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 12px center;
  background-size: 18px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 2px 8px rgba(12, 32, 27, 0.18);
  appearance: none;
  -webkit-appearance: none;
}
.citybox ul {
  list-style: none;
  margin: 4px 0 0;
  padding: 4px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 2px 8px rgba(12, 32, 27, 0.18);
}
.citybox ul[hidden] { display: none; }
.citybox li button {
  display: block;
  width: 100%;
  padding: 10px 8px;
  font-weight: 400;
  text-align: left;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 8px;
}
.citybox li button:hover,
.citybox li button:focus-visible { background: var(--panel-2); }

.bar {
  position: sticky;
  top: 0;
  z-index: 5;
  display: grid;
  grid-template-columns: 48px 1fr 1fr;
  gap: 8px;
  padding: 10px 12px;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}

.primary, .secondary, button, a.primary {
  display: block;
  width: 100%;
  padding: 12px;
  font: inherit;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  border-radius: var(--radius);
  border: 1px solid transparent;
  cursor: pointer;
}
.primary, a.primary { background: var(--accent-strong); color: var(--accent-strong-ink); }
.primary.alt, a.primary.alt { background: transparent; color: var(--accent); border-color: var(--accent); }
.secondary { background: transparent; color: var(--ink); border-color: var(--line); }
button.danger { color: var(--danger); border-color: var(--line); }
.contact { display: block; width: 100%; padding: 14px; font-weight: 600; text-align: center; text-decoration: none; border-radius: var(--radius); background: var(--accent-strong); color: var(--accent-strong-ink); border: 0; cursor: pointer; }

/* THE COUNT, as the button's second line (founder, 2026-10-09: "keep it as a
   subtext, and part of the reason to message").
   It came OFF the photograph to get here. The Gutenberg diagram calls the
   top-right corner it used to sit in the strong fallow area, the one the eye
   attends least, which is a poor home for a number meant to make somebody
   act; the terminal area at the foot is where an action belongs, and this is
   the action. Baymard's rule bit too: a badge that is always there stops
   reading as a badge, and that one was on every live room.
   --accent-strong-ink, NOT white: white on this green is 1.98:1 and is the
   exact failure the brand spent a year avoiding. Faded with opacity rather
   than a lighter colour, so it stays the same hue as the label above it. */
.contact .asked {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 3px;
  font-size: 0.8125rem;
  font-weight: 500;
  opacity: 0.72;
}
.contact .asked .chipmark { width: 14px; height: 14px; flex: 0 0 auto; }

/* ---- whatever the page is showing ---- */

.view { padding: 24px 28px 8px; }
/* A shared link opened on its own has no Back/Next under it, so the space
   under the last button matches the space above the photos (founder,
   2026-09-24) - and then the message button became a footer pinned like
   /find's nav, flush with the card's bottom edge, so no bottom padding. */
.app .view { padding: 36px 28px 0; }
.view:empty { display: none; }
/* WAITING IS THREE DOTS THAT PULSE IN TURN, not a typed ellipsis.

   It was the character U+2026, which does not move, so a slow fetch and a
   dead one looked identical. These are WhatsApp's typing dots on purpose:
   this product is a door into WhatsApp and the wait should feel like the
   place it is taking you.

   role=status on the element, so a screen reader is told something is
   happening rather than being read three empty spans. */
.waiting {
  display: flex;
  gap: 7px;
  justify-content: center;
  padding: 26px 0;
  margin: 0;
}

.waiting span {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0.25;
}

@media (prefers-reduced-motion: no-preference) {
  .waiting span { animation: blip 1.25s ease-in-out infinite; }
  .waiting span:nth-child(2) { animation-delay: 0.16s; }
  .waiting span:nth-child(3) { animation-delay: 0.32s; }

  @keyframes blip {
    0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
    30% { opacity: 1; transform: translateY(-5px); }
  }
}
.failed { color: var(--danger); }
.said { color: var(--muted); margin: 0 0 12px; }
/* The two ways out of an empty search. Both are .secondary, which is already
   a full-width block, so the only thing stacking them needs is the gap a lone
   Clear filters never did. */
.empty-way + .empty-way { margin-top: 10px; }
/* The group row (web/group-row.ts). Nothing but the gap: it is a .secondary,
   which is already a full-width block, and it lands under a form, a results
   list or a share button depending on the surface, none of which leaves room
   below itself. */
.grouprow { margin-top: 12px; }
/* **Without this the hidden row shows.** .secondary is display: block, and a
   block declaration beats the browser's own [hidden] rule, which is why
   .pane, .progress, .roommap and the city list each carry one of these. The
   posting form ships the row hidden and the script un-hides it once the
   crosshair is inside a group's city. */
.grouprow[hidden] { display: none; }
/* **Inactive, and it has to LOOK inactive.** Founder, 2026-10-14: the row
   "is fully rendered when its inactive, it needs to be very greyed out". A
   .secondary carries the page's own ink and border, so disabled it read as a
   live control that simply did nothing. Greyed harder than the Share button
   beside it (.finish .contact:disabled is 0.45) because an outline button
   fades less than a filled one: the same opacity on the two does not look
   like the same state. */
.grouprow:disabled {
  opacity: 0.4;
  color: var(--muted);
  border-color: var(--line);
  background: transparent;
  cursor: default;
}
/* The taken page: the heading, the rooms offered in its place, and View more
   under them. It ends in a button rather than a message bar, so it adds the
   bottom room the view does not give it. */
.gone { padding-bottom: 16px; }
.gone .secondary, .gone .primary { margin-top: 12px; }
/* The heading's larger line box left 21.5px of white above the line under it
   against 15px below that line (founder, 2026-10-07: "too far down"). Half
   the usual 12px evens the two, measured off a render. */
.gone .room-name { margin-bottom: 6px; }
.help { display: block; margin-top: 4px; }

/* ---- results ---- */

.results .said { font-size: 0.9375rem; }
.card {
  display: flex;
  gap: 12px;
  align-items: center;
  padding: 10px;
  margin-bottom: 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
}
/* A PHOTO ARRIVES WHOLE OR NOT AT ALL.

   A big JPEG paints top down, a band at a time, and watching a room assemble
   itself in strips is worse than watching nothing happen. So the slot holds
   a quiet placeholder at the photo's exact size, the image sits at zero
   opacity while it decodes, and it fades in once - complete - when it is
   ready.

   The placeholder lives on a WRAPPER and not on the image, because an image
   at zero opacity takes its own background with it. A gallery shot was
   already wrapped in an anchor of that shape, so only the thumbnail in a
   result row needed one adding.

   No shimmer. A placeholder that animates is a second thing to watch while
   waiting for the first. */
.ph {
  display: block;
  overflow: hidden;
  border-radius: 6px;
  background: color-mix(in srgb, var(--accent) 9%, var(--panel-2));
}

.row .ph { width: 76px; height: 76px; flex: 0 0 76px; }

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

.thumb,
.shot img {
  opacity: 0;
  transition: opacity 260ms ease;
}

.thumb.is-loaded,
.shot img.is-loaded { opacity: 1; }

/* THE PLACEHOLDER BREATHES WHILE IT WAITS.

   A flat grey box is indistinguishable from a box that has given up. The
   slow pulse between two greys is the standard skeleton, and it says the
   same thing a spinner does without spinning: something is coming.

   It STOPS the moment the photo is in. Left running it would animate
   forever underneath an opaque image - invisible, repainting a layer per
   frame for nothing, on a page that may hold a dozen of these. :has is what
   lets a parent answer for its child, and this file already leans on it for
   a chosen pill.

   Gated, like everything else here that moves. Reduced motion keeps the
   plain placeholder, which was already enough on its own. */
@media (prefers-reduced-motion: no-preference) {
  .ph,
  .shot {
    animation: skeleton 1.5s ease-in-out infinite;
  }

  .ph:has(img.is-loaded),
  .shot:has(img.is-loaded) {
    animation: none;
  }

  @keyframes skeleton {
    0%, 100% { background-color: color-mix(in srgb, var(--accent) 11%, var(--panel-2)); }
    50% { background-color: color-mix(in srgb, var(--accent) 3%, var(--panel)); }
  }
}
.card-text { min-width: 0; }
.card-text p { margin: 0 0 2px; }
.card-title { font-weight: 600; }
.card-room, .card-far { color: var(--muted); font-size: 0.875rem; }
.more {
  display: block;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--accent);
  font-weight: 600;
  text-align: center;
  text-decoration: none;
}

/* ---- a room ---- */

.room-name { font-size: 1.25rem; font-weight: 600; margin: 0 0 12px; }
.gallery { display: grid; gap: 4px; margin-bottom: 16px; position: relative; }
.gallery-one { grid-template-columns: 1fr; }
.gallery-many { grid-template-columns: repeat(2, 1fr); }
/* :first-of-type, NOT :first-child. The live strip is the gallery's first
   child since 2026-10-09, and while it was :first-child these two rules
   stopped matching anything: the lead photo silently lost its full-width span
   and its 16/10 crop, and the pills meant to sit on it floated above it
   instead. The shots are the only anchors in here, so of-type still means
   "the lead". */
.gallery-many .shot:first-of-type { grid-column: 1 / -1; }
.shot {
  display: block;
  border-radius: 6px;
  overflow: hidden;
  background: color-mix(in srgb, var(--accent) 9%, var(--panel-2));
}
.shot img { display: block; width: 100%; height: 100%; object-fit: cover; aspect-ratio: 4 / 3; }
.gallery-many .shot:first-of-type img { aspect-ratio: 16 / 10; }

.facts { list-style: none; margin: 0 0 16px; padding: 0; }
.facts li { display: flex; justify-content: space-between; gap: 16px; padding: 9px 0; border-bottom: 1px solid var(--line); }
.facts li:last-child { border-bottom: 0; }
.facts .label { color: var(--muted); }
.facts .value { text-align: right; }
.description { white-space: pre-wrap; margin: 0 0 16px; }
/* The poster's own words, in their own grey area: set apart from the
   details above it and read first (founder, 2026-09-24). */
.room .description {
  margin: 16px 0;
  padding: 14px 16px;
  background: var(--panel-2);
  border-radius: var(--radius);
  line-height: 1.5;
}

/* The message buttons as the room's footer: pinned to the bottom of the
   screen and drawn like /find's Back and Next, the full width of the card.
   Always live: a tap with something missing scrolls to the empty box and
   outlines it instead of greying this out (enquiry-script.ts). */
.msgbar {
  position: sticky;
  bottom: 0;
  z-index: 2;
  margin: 20px -28px 0;
  padding: 14px 28px 18px;
  background: var(--panel-2);
  border-top: 1px solid var(--line);
}

/* What was ticked, as tags with the card's emojis. */
.tags { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.tags li {
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  font-size: 0.9375rem;
  white-space: nowrap;
}
.area { font-weight: 600; margin: 0 0 2px; }
.approx { color: var(--muted); font-size: 0.875rem; margin: 0 0 20px; }
.video { display: inline-block; margin-bottom: 12px; color: var(--accent); font-weight: 600; }

/* ---- forms ---- */

/* A GROUP IS LABELLED LIKE A FIELD, NOT BOXED LIKE A FORM.

   Type and Stay were bordered fieldsets with a notched legend sitting beside
   unboxed Price and Move-in labels, so one form looked like two. The
   fieldset stays for the semantics - these radios belong to each other and
   a screen reader should hear the group's name with each - and is drawn as
   the same small muted label every other field uses. */
fieldset { border: 0; min-width: 0; margin: 0 0 12px; padding: 0; }
legend {
  padding: 0;
  margin-bottom: 4px;
}
/* ONE SIZE AND ONE COLOUR FOR EVERYTHING INSIDE A STEP.

   Founder, 2026-09-22, after four rounds of tuning individual classes
   failed to make the form look like one thing. Every label, legend, pill,
   checkbox, hint and input in a step is 16px in the same dark grey. Not
   black: --ink is near enough to black to read as it, and nothing in a form
   needs that much contrast against white.

   NOT :where(). It was, and that left a hole: a step does not only hold
   fields, it also holds whatever the panel loads into it - a list of rooms
   in step 3, one room in step 4 - and those fragments carry their own
   classes from the page they were built for. At zero specificity every one
   of them won, so a line like the approximate-location note inside a room
   kept the smaller size it has on its own page.

   So this has real specificity and beats them. On its own page at
   /l/<code> that listing keeps its hierarchy, because nothing there is
   inside a step. Inside the panel it is one size, like everything else.

   The tabs, the progress row and the step nav are outside a step and keep
   their own sizes. */
.step,
.step * {
  font-size: 1rem !important;
  color: var(--form-ink) !important;
}

/* !important, deliberately, and this is the argument for it.

   A step holds fragments built for other pages, and those carry selectors
   of every shape: .facts .label is two classes, .results .said is two,
   .manage .code is two. Each of them beats a one-class-plus-one-element
   rule, so this was chased three times - a size, then a colour, then this
   colour - and each fix only moved the next one into view.

   Scoped to .step, it says one thing and says it completely: inside the
   form there is one size and one colour. Outside it nothing changes, so
   the listing at /l/<code> and the manage view keep their own type.

   Adding an exception means adding it HERE, in the open, rather than
   winning a specificity argument somewhere else in the file. There are
   four, and all carry meaning rather than decoration: a refusal is red,
   a link is a link, a link drawn as a green button keeps the ink its fill
   needs, and so does the share link's Copy. The last slide holds Confirm
   on WhatsApp and the link places; without the last two, one was green on
   green with no label at all and the other dark grey on deep green. */
.step .error,
.step .error * { color: var(--danger) !important; }
.step a { color: var(--accent) !important; }
.step a.contact,
.step a.primary { color: var(--accent-strong-ink) !important; }
/* The two section headings, which name a section rather than ask for an
   answer. Founder, 2026-10-14: the titles and the box are not a state, only
   the buttons are. The one-colour rule inside a step turned the ad's heading
   the form's grey, which is what "greyed out" meant, and left the edit
   link's a different grey from the one it has once the listing is live. */
.step .sharelink .said { color: var(--accent) !important; }
.step .keylink .said { color: var(--muted) !important; }

/* And one size. A note is a footnote, not a field: it explains what happens
   to an answer, or what an answer should look like, rather than asking for
   one - so it is smaller and quieter than the line it sits under. */
.step .note,
.step .note * {
  font-size: 0.8125rem !important;
  color: var(--muted) !important;
}

/* One weight too, written out so nothing inherits a bolder one. */
.opt,
.field,
legend,
.check {
  font-weight: 400;
}

.opt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-right: 14px;
}

.field { display: block; margin-bottom: 12px; }

/* ONE RULE FOR EVERY TEXT CONTROL, select included.

   background was var(--bg), a token this stylesheet has never defined, so
   every input and every textarea has been drawing with no background at all.
   It only looked right because what is behind them happens to be white.

   appearance: none on the select, or the browser draws it with its own
   metrics and it comes out shorter and tighter than the inputs above it -
   which is exactly how it looked. The chevron is drawn here instead, so the
   select matches and still says it opens. */
.field input, .field select, textarea {
  display: block;
  width: 100%;
  margin-top: 4px;
  padding: 12px;
  /* LONGHANDS, NOT THE FONT SHORTHAND.

     This used the font shorthand with inherit as the family. A CSS-wide
     keyword cannot be one
     component of a shorthand - it is only legal as the entire value - so
     that whole declaration was invalid and thrown away. Every input, select
     and textarea fell back to the browser's own control font: around 13px,
     in the UA's family, beside 16px labels in ours. It reads as a size
     difference and a colour difference at once, because a different family
     at a smaller size looks lighter.

     Written out, each one stands or falls on its own.

     And the SIZE and colour, which this rule never set: on /find the
     .step * rule covered for it, but a shared link has no steps, and its
     enquiry boxes came out in the browser's 13px black (2026-09-24). Under
     16px an iPhone also zooms the whole page when a box is tapped. */
  font-family: inherit;
  font-size: 1rem;
  color: inherit;
  font-weight: 400;
  line-height: 1.4;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/* ASKED AGAIN. Founder, 2026-10-02: "highlight the ones that are empty in a
   reddish outline." The message button stays green and answers a tap by
   taking you to what is missing, so this mark is the whole of the refusal and
   has to read as one. enquiry-script.ts adds it on the browser's own invalid
   event and takes it off on the next keystroke.

   The ring as well as the border, because a 1px border going from grey to red
   is a change almost nobody notices on a phone held at arm's length. */
.field input.missing,
.field textarea.missing,
.field select.missing {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.14);
}

.field select {
  appearance: none;
  padding-right: 38px;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 20px) calc(50% + 2px), calc(100% - 14px) calc(50% + 2px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}
textarea { resize: vertical; }
/* A DATE INPUT IS NOT ONE FIELD, IT IS A ROW OF THEM.

   WebKit builds it out of separate month, day and year sub-fields with their
   own colours, which do not reliably inherit the colour of the input around
   them - so the date drew pale against everything beside it while the number
   field above drew in ink. Each part is named here.

   The min-height is separate: WebKit also gives the control its own inner
   height, so without this it sits a couple of pixels shorter than the number
   field directly above it. */
.field input[type="date"] {
  min-height: 3rem;
  color: var(--form-ink);
  -webkit-text-fill-color: var(--form-ink);
  /* The one control where the page accent leaks into TEXT rather than into
     a tick or a dot. Left on, the segments draw in a washed-out green. */
  accent-color: auto;
}

/* COLOUR ALONE DOES NOT WIN HERE.

   Setting the colour on these was not enough and the date still drew pale:
   WebKit dims unedited segments with its own opacity, and on a date that
   has not been touched that is most of them. So each part is given the ink
   with -webkit-text-fill-color, which outranks an ordinary colour on these
   the dimming is switched off outright.

   The wrapper is in the list on purpose - it was missing before, and it is
   the element the separators live in, which is why the slashes stayed pale
   even where a digit had darkened. */
.field input[type="date"]::-webkit-datetime-edit,
.field input[type="date"]::-webkit-datetime-edit-fields-wrapper,
.field input[type="date"]::-webkit-datetime-edit-text,
.field input[type="date"]::-webkit-datetime-edit-month-field,
.field input[type="date"]::-webkit-datetime-edit-day-field,
.field input[type="date"]::-webkit-datetime-edit-year-field {
  color: var(--form-ink);
  -webkit-text-fill-color: var(--form-ink);
  opacity: 1;
}

/* The calendar button is the browser's own glyph and comes out mid-blue-grey
   against a green page. */
.field input[type="date"]::-webkit-calendar-picker-indicator {
  opacity: 0.55;
  cursor: pointer;
}
.check {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}
details.more { margin-bottom: 14px; }
details.more summary { padding: 10px 0; font-weight: 600; color: var(--accent); cursor: pointer; }
.error { padding: 12px; margin-bottom: 14px; border-radius: var(--radius); background: rgba(192,57,43,0.12); color: var(--danger); font-weight: 600; }

/* ---- the two links ---- */

.manage .code { color: var(--muted); font-size: 0.875rem; margin: -6px 0 12px; }
.manage .stats { font-weight: 600; }
/* The rule between sections, and NOT across the ad box, which draws its own
   edges. The :not() is load-bearing and the reason is a regression this
   stylesheet caused on 2026-10-14: .sharelink lost its .manage prefix so the
   posting form could draw the same box, which dropped it to one class, which
   let this rule's border-top win on specificity. The box came out with a grey
   top edge and green sides, and the founder saw it on a handset within the
   hour. Fighting that with a restated border-top here would be the same trap
   one layer down. */
.manage section:not(.sharelink) { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line); }
/* Room under the last thing on the page (founder, 2026-10-09). The private
   link is the final section and it sat flush against the bottom edge of the
   panel, which read as cut off rather than finished. Padding and not margin:
   a margin on the last child collapses out through the panel and changes
   nothing. */
.manage { padding-bottom: 28px; }
.manage section:last-child { margin-bottom: 8px; }
.manage section:last-child .linkrow { margin-bottom: 4px; }
/* The post's last screen starts with its first step, not with a gap. */
.manage.posting > :first-child { margin-top: 0; }

/* THE LAST SLIDE (founder, 2026-09-24): the terms, the button, and the
   two link places at the bottom, the way the posted screen had them - the
   share link in the green box, the edit link under a rule. An empty place
   and an unreachable button are quiet rather than hidden. */
.finish .terms { margin-bottom: 14px; }
.finish .contact:disabled { opacity: 0.45; cursor: default; }
.finish .copy:disabled { opacity: 0.45; cursor: default; }
/* A date box that reads day first (date-field.ts): the text box people type
   in, the calendar button at its right-hand end, and the native picker it
   opens, present but invisible - showPicker needs it rendered, not display
   none - and never in the way of a tap. */
.dateinput { position: relative; display: block; }
.dateinput input[type="text"] { padding-right: 48px; }
.dateinput .datepick {
  position: absolute;
  right: 6px;
  top: calc(50% + 2px);
  transform: translateY(-50%);
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  background: transparent;
  font-size: 1.1rem;
  cursor: pointer;
}
.dateinput .datenative {
  position: absolute;
  right: 6px;
  bottom: 0;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* The main-photo picker (photo-field.ts): tappable thumbnails once two or
   more files are chosen, the tapped one marked with a ring and a star badge
   rather than left to guess from order alone. */
.photothumbs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.photothumbs[hidden] { display: none; }
.photothumb {
  position: relative;
  width: 64px;
  height: 64px;
  padding: 0;
  border: 2px solid transparent;
  border-radius: var(--radius);
  overflow: hidden;
  cursor: pointer;
  background: var(--panel-2);
}
.photothumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photothumb.is-main { border-color: var(--accent); }
.photothumb.is-main::after {
  content: "★";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  line-height: 16px;
  font-size: 11px;
  text-align: center;
  color: var(--accent-ink);
  background: var(--accent);
  border-radius: 50%;
}

/* Share on WhatsApp, under the link it sends. */
.finish .linkrow + .contact { margin-top: 10px; }

/* Where a shared listing is: a still map with a circle, never a point
   (listing-map-script.ts). Hidden until the script draws it, so no script
   means no empty box. */
/* THE STRIP at the top of a room (2026-10-09). A live green dot, the word,
   and how many people have asked, in that order. It is the first thing on the
   page because it answers what a seeker arriving from a week-old forward
   wants to know first: is this still going.
   Nothing here touches a meta tag, so the card that unfurls is unchanged. */
.livestrip {
  /* In the flow, scrolling away like everything else (founder, 2026-10-09,
     after seeing it pinned). It was sticky for two rounds; the band and the
     bleed to the panel edges went with the pinning, because an opaque
     background is something only a pinned bar needs. */
  display: flex;
  align-items: center;
  gap: 8px;
  /* EVEN AIR, top and bottom. The view sets the padding above the room — 36px
     inside the app shell, 24px outside it — and the room's photos follow
     straight after, so without this the strip had 36 above and 10 below.
     Each rule pulls back to the same 16, which is why the two numbers differ
     and why they are not magic: they are the view's own, minus 16. */
  margin: 0 0 16px;
  font-size: 0.8125rem;
}
/* The child combinator is load-bearing: these two pull the IN-FLOW strip up
   through the view's padding, and a margin still applies to an absolutely
   positioned box. While they reached the overlay copy as well, its top: 10px
   resolved to -10 and the pills sat above the photograph instead of on it. */
.view .room > .livestrip { margin-top: -8px; }
.app .view .room > .livestrip { margin-top: -20px; }

/* The pills, which are the poster's chips brought onto the page (founder,
   2026-10-09: "I need them in this pill format with the design as it is").
   Solid --ink rather than the poster's see-through black: that one sat on a
   photograph and needed to let it through, this one sits on white. */
.livestrip .chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px;
  border-radius: 999px;
  background: var(--ink);
  color: #ffffff;
  font-weight: 600;
  line-height: 1.1;
}
.livestrip .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent-strong);
  flex: 0 0 auto;
}
/* chipmark, not mark: .mark is the brand door and sets color: var(--mark).
   See the note in web/listing-html.ts. */
.livestrip .chipmark { width: 15px; height: 15px; flex: 0 0 auto; }

/* OVER THE LEAD PHOTO (founder, 2026-10-09). The lead always spans the
   gallery's full width and sits at its top, in both gallery shapes, so the
   two pills land on its top-left and top-right corners.
   Absolute, so the grid's cells are untouched. pointer-events none, so a tap
   meant for the photo still opens the photo and does not land on a pill that
   does nothing.
   A listing with NO photo has nothing to sit on and renders the strip in the
   flow instead, where the solid pill above still applies. */
.gallery .livestrip {
  position: absolute;
  top: 10px;
  left: 10px;
  right: 10px;
  z-index: 1;
  margin: 0;
  pointer-events: none;
}
/* See-through over the room, the way the poster's chips were, rather than the
   solid ink they take on white. The blur is what keeps white words legible on
   a photograph nobody has seen: a flat tint over a bright duvet is not
   enough, which is the same thing the poster learned in October. */
.gallery .livestrip .chip {
  background: rgba(13, 31, 26, 0.74);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

.roommap { height: 200px; margin: 16px 0 6px; border-radius: var(--radius); overflow: hidden; }
.roommap[hidden] { display: none; }
.manage .actions form { margin-bottom: 8px; }

/* The share link is the product, so it is the loud one. The private link
   below it is a credential and reads like a receipt. A poster who cannot tell
   them apart at a glance hands out the wrong one, which is exactly what
   happened while this page showed only the private link. */
/* **The box and its heading are NOT a state.** Founder, 2026-10-14: "dont
   dynamicly greey out the section title 'your ad' or the green line. only the
   buttons should be dynamic." These two rules were scoped to .manage, so the
   posting form's own last step rendered the same section as a muted .said
   with no border at all, which reads as the whole thing greyed out while the
   listing is a draft. One ad, one box, wherever it is shown; the only thing
   that changes before the code arrives is whether the two buttons work. */
.sharelink { border-radius: var(--radius); border: 1px solid var(--accent); padding: 14px 16px 16px; margin-top: 24px; }
.sharelink .said { font-size: 1.0625rem; font-weight: 700; color: var(--accent); }
.keylink .said { font-weight: 600; }
/* **The edit-link section is one section, not two states.** Founder,
   2026-10-14: "the line break and Edit link section is still dynamic". The
   rule above it came from the .manage section rule, and the heading's weight
   from a .manage-scoped one, so on the posting form's last step there was no
   line and no bold heading — and then both appeared the moment the code
   arrived and the posted fragment replaced it. Only the buttons change now. */
.finish .keylink { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line); }

/* A room that is off is not an invitation, so it loses the accent border and
   the accent heading. Same section, stated rather than offered. */
.sharelink.off { border-color: var(--line); }
.sharelink.off .said { color: var(--ink); }
.manage .sharelink.off .secondary { margin-top: 14px; }

.linkrow { display: flex; gap: 8px; align-items: stretch; margin-top: 8px; }
.sharemsg { margin-top: 18px; }
.sharemsg .contact { margin-top: 10px; }
.linkbox {
  flex: 1 1 auto;
  min-width: 0;
  padding: 11px;
  font: 0.8125rem ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
/* The bare button rule above sets width 100%, which every other button in
   this product wants. Here it made the copy button demand the whole row and
   squeezed the URL to a sliver. Both overrides are needed: flex for the
   growing, width for the demand. */
.copy {
  flex: 0 0 auto;
  width: auto;
  padding: 0 16px;
  /* Longhands, for the reason the input rule gives: a CSS-wide keyword
     cannot be one component of a shorthand, so this whole declaration was
     invalid and dropped, and the copy button drew in the browser's own
     button font. */
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1;
  color: #fff;
  background: var(--accent);
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
}
.copy:hover { filter: brightness(1.08); }
.keylink .copy { color: var(--ink); background: transparent; border: 1px solid var(--line); }

/* A photo or a video opens OVER the page. There is no tab in this product and
   no reload in it either — a full-size shot is a layer, and the layer closes. */
.overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(0, 0, 0, 0.92);
  cursor: zoom-out;
}
.overlay img { max-width: 100%; max-height: 100%; border-radius: 6px; }
.overlay iframe {
  width: 100%;
  max-width: 900px;
  aspect-ratio: 16 / 9;
  border: 0;
  border-radius: 6px;
}
body.locked { overflow: hidden; }

/* Drawers. When more space is needed the page opens it where you are; it does
   not take you somewhere. A card is the handle, the drawer is the room. */
.row { margin-bottom: 8px; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.row .card { margin: 0; border: 0; border-radius: 0; }
/* The room is a step of its own now, so it needs no rule here. */

/* The bar under the map: one control and one instruction. */
.bar { grid-template-columns: 48px 1fr; align-items: center; }
.where { margin: 0; color: var(--muted); }

/* The enquiry. The reason RoomChat is in the middle: a poster gets a named
   person, a date and a line about them before they have typed anything. */
.enquiry { margin-top: 4px; }
.enquiry .said { margin-bottom: 12px; }
.copybox {
  width: 100%;
  margin: 8px 0 14px;
  padding: 12px;
  /* Longhands: see the input rule. The shorthand with inherit in it was
     invalid, so the share link drew in the browser's own textarea font. */
  font-family: inherit;
  font-size: 0.9375rem;
  line-height: 1.4;
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  resize: vertical;
}

/* Six sections. The number is not decoration: a poster deciding whether to
   start needs to see how much there is, and six short things reads as
   finishable where one column of eleven fields does not. */
.post-form { padding-bottom: 24px; }
.step { padding: 20px 0 4px; border-top: 1px solid var(--line); }
.step:first-of-type { border-top: 0; padding-top: 8px; }
/* A step used to open with a circled number and a title. Both are gone -
   the progress bar states the same fact - so the rules that drew them are
   gone with them. The circled number in the how-it-works list is a
   different thing and keeps its own rule. */
.step .map-pane { margin-bottom: 10px; border-radius: var(--radius); }

/* The map's own credit, kept and made small.

   OpenStreetMap and OpenFreeMap require attribution, so this is never
   removed - but expanded it runs a line of small print across the foot of
   the map, over the ground a crosshair is most often pointing at. Collapsed
   it is the standard mark in the corner, and one tap opens it.

   The selectors are MapLibre's own, so they are as stable as the version
   pinned in home-page.ts and no more. If the credit ever disappears
   entirely, suspect a MapLibre upgrade renaming these - and fix it rather
   than leaving the tiles uncredited. */
.maplibregl-ctrl-attrib.maplibregl-compact:not(.maplibregl-compact-show) .maplibregl-ctrl-attrib-inner {
  display: none;
}

.maplibregl-ctrl-attrib.maplibregl-compact {
  min-height: 24px;
  background: color-mix(in srgb, var(--panel) 82%, transparent);
  border-radius: 999px;
}

.maplibregl-ctrl-bottom-right { margin: 0 4px 4px 0; }

/* The map's parking space. Hidden, never replaced, outside every pane. */
#map-park[hidden] { display: none; }

/* Where a pane asks for the map. Empty until the script moves it in, and
   it holds no height of its own so an empty slot is invisible. */
.map-slot { display: block; }

/* One section at a time — but only once the script says so. Without
   JavaScript the whole form stays on one scroll and still submits, so the
   default here has to be "everything visible". */
/* Not scoped to the posting form any more: the search pane steps too, and
   the stepper puts the marker on whichever PANE it is driving.
   (No backticks in here, ever. One ends the template literal this whole
   stylesheet lives in, and the file stops parsing. Four times now.) */
.stepped .step { display: none; }
.stepped .step.is-current { display: block; padding-top: 4px; border-top: 0; }

/* Back and Next, pinned under the scrolling section. */
/* The same surface as the progress row at the other end of the panel. The
   two are a matched pair - one says where you are, the other moves you -
   and they bracket the part that scrolls between them. Drawn in the panel's
   own white, the footer read as more of the form rather than as its edge. */
.stepnav {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  padding: 14px 28px 18px;
  border-top: 1px solid var(--line);
  background: var(--panel-2);
  /* The mirror of the progress row above, thrown upward: the same depth
     under the same static frame. This one paints over the scroller for
     free, being after it in the flow, but it is positioned anyway so the
     two ends of the frame are described the same way. */
  position: relative;
  z-index: 1;
  box-shadow: 0 -6px 12px rgba(12, 32, 27, 0.05);
}
.stepnav[hidden] { display: none; }
.stepnav button:disabled { opacity: 0.4; cursor: default; }
.stepnav #next[hidden] { display: none; }
/* With Next gone on the last section, Back should not stretch across. */
.stepnav:has(#next[hidden]) { grid-template-columns: 1fr; }

/* THE ROOM'S BUTTONS ARE THE PANEL'S BUTTONS. Founder, 2026-10-01: "make it
   matching the poster's UI".

   A room arrives as the same fragment a shared link serves, so it brings its
   own pinned message bar. On a shared link that is the whole footer and it
   is right. Inside the panel the footer already exists, so the page ended
   up with two bands and a white gap between them, and a Back stretched
   alone across the lower one. home-html.ts moves the buttons up here
   instead, where Next would be, and the step reads Back | Message as the
   posting flow reads Back | Next.

   Auto columns rather than a second explicit pair: a poster can offer three
   messengers, and the row has to take Back plus however many arrive. It must
   also sit AFTER the rule above, which would otherwise win on source order
   and put everything in one column. */
.stepnav.with-actions {
  grid-template-columns: none;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
}

/* Choices as a row of pills, which is faster to hit than a stack of radios. */
.choices { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
/* A room's own questions, gone the moment Whole flat is picked. Shown by
   default and hidden for a flat rather than the other way round, so a
   browser without :has() shows both - harmless - instead of hiding them
   from everyone. The route drops them for a flat either way. */
.step:has(input[name="type"][value="flat"]:checked) .room-only { display: none; }
.step:has(input[name="type"][value="room"]:checked) .flat-only { display: none; }
.choices .opt {
  margin: 0;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
/* Marked by its border and a wash, never by its text. It used to thicken to
   600 and take the accent colour, so the chosen answer was a different
   weight AND a different colour from the ones beside it - two of the three
   differences in a row of pills that should read as one set. */
.choices .opt:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 9%, transparent);
}

/* A footnote with a mark beside it. The text is indented past the icon
   rather than wrapping under it, which is what makes it read as one aside
   instead of a bullet.

   This replaced three classes - privacy, help and hint - which was three
   chances to drift, and they had drifted into two sizes and two colours
   before anyone looked. The manage view keeps its own help class: it is not
   inside a step and is not a footnote under a field. */
.note {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 8px 0;
  padding: 10px 12px;
  border-radius: var(--radius);
  background: var(--panel-2);
  border: 1px solid var(--line);
  line-height: 1.45;
}

/* Instructional, and therefore neither a bubble NOR smaller.

   It belongs to the control above it rather than commenting on it, so it is
   an ordinary line of the form and takes the form's one size like every
   other line in a step. Only a note is set apart, because only a note is
   an aside. */
.guide { display: block; margin: 8px 0 0; line-height: 1.45; }

.note .info {
  flex: 0 0 15px;
  width: 15px;
  height: 15px;
  margin-top: 1px;
}

.choices-stack { flex-direction: column; align-items: stretch; }
.choices-stack .opt { border-radius: var(--radius); }
.channels { display: grid; gap: 8px; }
