/* The live builder.

   Loaded after platform.css, which owns the tokens. This file owns one screen
   and it is an EDITOR SHELL rather than a page: an app bar, a control panel
   that owns its own scroll on the left, and a studio stage on the right that
   never scrolls and holds nothing but the customer's card. Everything here
   traces to platform/DESIGN.md.

   Two grammars meet on this screen and neither is diluted.

   The SHELL is Linear's: a dark ground built out of luminance steps rather
   than borders, whisper-thin translucent white edges, ghost controls at 2 to
   6 per cent white, restrained density, and depth from stacking rather than
   from drop shadows. That is the vocabulary a tool is built in.

   The SURFACE is the site's Apple language, unchanged: SF stack, #0071e3 for
   actions and selection only, capsule buttons, border-led containment on the
   white panel. Landing and builder still read as one product.

   Three rules run through all of it.

   The card is not styled here. What sits on the stage is the engine's own
   markup, inline-styled, table-nested, at 508px. This file gives it a ground,
   a scale and a shadow and never reaches inside it, except to grey a
   placeholder and to drive the mirrors' animation from --accent.

   Nothing about the card's motion is a number in this file. Every window,
   easing and distance comes from /api/spec and is written into generated
   keyframes at run time. What IS here is the interface's own motion, which is
   deliberately small: a press, a hover, a gliding tile marker, a blur roll on
   one stateful button, and one slow pulse on the live-preview dot.

   --accent is the customer's colour and it appears ONLY inside the card. No
   piece of chrome on this screen is ever tinted by it. */

:root {
  /* the dark side: one ground, then luminance steps on top of it, because a
     drop shadow on a near-black surface communicates nothing */
  --stage: #0a0a0b;
  --stage-glow: rgba(255, 255, 255, .07);
  --lift-2: rgba(255, 255, 255, .04);
  --lift-3: rgba(255, 255, 255, .06);
  /* `--bar` and `--edge` moved to platform.css when the app bar became one
     component on every page. They are still exactly what they were. */

  /* the light side. `--panel`, `--panel-edge`, `--tile` and `--blue-wash`
     moved to platform.css when the app shell's panels took the same hairline
     this file's tiles wear. Same values, same roles, same argument as `--bar`
     and the motion tokens below: a token two stylesheets depend on belongs in
     the one every page loads. */

  --accent: #a57f38;          /* overwritten on .build by the customer's pick */

  /* Watermark strength. One number, Ramon's to set: raise it and every band
     on every template follows. .14 is his "way more visible" call. */
  --mark-ink: rgba(0, 0, 0, .14);
  --mark-ink-soft: rgba(0, 0, 0, .09);

  /* `--t-fast`, `--t-base`, `--t-swap`, `--t-glide`, `--ease-out` and
     `--ease-glide` moved to platform.css when the app shell needed the same
     press and hover timings the builder has always used. Same values, same
     roles, same argument as `--bar` and `--edge` above: a token two
     stylesheets depend on belongs in the one every page loads. */

  --bar-h: 52px;
  --panel-w: 400px;
  --gutter: 22px;

  /* The card's scale on the stage, stepped by breakpoint. Below the
     two-column width the steps are there so a 510px card fits a 375px phone;
     above it they are there because the whole point of the stage is that the
     product is the biggest thing on the screen. It cannot be solved fluidly as
     `min(1, (100vw - 44px) / 510)`: a length over a number is a length, and
     both `zoom` and `scale()` want a number, so the declaration is dropped.

     It is two properties rather than one because there are two constraints and
     they are not the same constraint. `--card-step` is the WIDTH ladder, which
     wants the card as large as the stage will hold. `--card-cap` is what a
     SHORT window allows, which wants it smaller. The scale is whichever is
     less, so a short window narrows the card by the amount it has to and no
     more. It used to overwrite the ladder outright, which was survivable while
     the ladder topped out at 1.6 and is not now that it reaches 2.36. */
  --card-step: .64;
  --card-cap: 9;
  --card-scale: min(var(--card-step), var(--card-cap));
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ----------------------------------------------------------- the app bar */
/* The service's app bar, which is now in its working clothes everywhere:
   `--bar`, a hairline in `--edge`, no frost. What is left here is the one
   thing that is still the builder's alone, which is that the shell's bar runs
   the full width rather than sitting inside a centred container. */
body.builder .nav-inner {
  max-width: none;
  padding: 0 20px;
  height: var(--bar-h);
}
/* The bar's wordmark is the drawn lockup now, so the 9px square this file used
   to set beside the word goes the way the bar's own did: two marks on one link
   is one mark too many. The size comes from `platform.css` with everything
   else about it; the hover fade stays, because a shell whose every control
   answers to the pointer should not have one link that does not. */
body.builder .wordmark {
  display: flex;
  align-items: center;
  transition: opacity var(--t-base) var(--ease-out);
}
body.builder .wordmark:hover { opacity: .72; }
body.builder .nav-right { font-size: 12px; }

/* -------------------------------------------------------------- the shell */
/* Below 1040px the document scrolls, the stage sits on top and goes sticky so
   the card stays on screen while the fields move under it, and the ask sticks
   to the bottom of the viewport. Above it, nothing scrolls but the panel. */
.build {
  display: flex;
  flex-direction: column;
  background: var(--light);
}

/* ---------------------------------------------------------- the stage */
/* platform.css owns `.stage` for the wait page's progress line, which is a
   19px/600 label. These three declarations put the builder's stage back to
   being a surface rather than a heading. */
.build .stage {
  margin-top: 0;
  font-size: 13px;
  font-weight: 400;

  position: sticky;
  top: var(--bar-h);
  z-index: 1;
  /* Sized to what is in it rather than to a share of the window: the chrome,
     the card, the caption and one gap under it. A fixed 44dvh band left about
     180px of dead black under the caption on a phone, which is the stage
     reading as a void instead of as a pool. 44dvh survives as the CEILING, so
     a tall card can never push the controls off the screen. */
  height: auto;
  max-height: 44dvh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  /* the implicit column would otherwise be sized to the card rather than to
     the stage, and a narrow phone gets a pool that runs off the right edge */
  grid-template-columns: minmax(0, 1fr);
  overflow: hidden;
  color: var(--ink-inv);
  letter-spacing: -.01em;
  /* The pool has to be wider than the thing floating on it or the card's own
     edges hang out of the light and the ground reads as a void rather than as
     a lit stage. At the top of the ladder the card is 73 per cent of the
     stage's width, so the glow is 84. */
  background:
    radial-gradient(84% 62% at 50% 46%, var(--stage-glow), transparent 74%),
    var(--stage);
}

/* Flat dark is sterile dark. A fixed grain at three and a half per cent is
   under the threshold of being seen and over the threshold of being felt. */
.stage-grain {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: .035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* the chrome that makes it a tool rather than a poster */
.stage-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 13px 16px 0;
  position: relative;
  z-index: 2;
}
.stage-chip {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted-dark);
}
.stage-chip i {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: #5fd08a;
  animation: livedot 2.6s var(--ease-out) infinite;
}
@keyframes livedot {
  0%, 100% { opacity: .35; transform: scale(.82); }
  50%      { opacity: 1;   transform: scale(1); }
}

/* Linear's ghost button: nearly transparent fill, whisper-thin white edge,
   6px radius. The one control on the stage, and it earns its place because
   4000ms of the 5180ms loop is a hold, so a first look is often a still card. */
.ghost {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: -.005em;
  color: #d8d8dd;
  background: var(--lift-2);
  border: 1px solid var(--edge);
  border-radius: 6px;
  cursor: pointer;
  transition: background var(--t-base) var(--ease-out),
              border-color var(--t-base) var(--ease-out),
              color var(--t-base) var(--ease-out),
              transform var(--t-fast) var(--ease-out);
}
.ghost:hover { background: var(--lift-3); border-color: rgba(255, 255, 255, .14); color: #fff; }
.ghost:active { transform: scale(.97); }
.ghost svg { flex: none; }

/* The pool of light, and the card floating on it. The card and the line that names it travel together and are centred as one
   object, so the note reads as a caption under the artwork rather than as a
   footer stranded at the bottom of a dark field. */
.stage-pool {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* on a very short window the card is taller than the pool, and plain
     centring would clip it at both ends instead of only the bottom */
  justify-content: safe center;
  gap: 20px;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
  padding: 10px 24px 20px;
  position: relative;
  z-index: 1;
}
/* `zoom` rather than `transform: scale()`, which is DESIGN.md's standing call
   and it is load-bearing here for a second reason: a transformed card keeps
   its unscaled 508px layout box, so the pool centres the wrong rectangle and
   the caption underneath is laid out against a box the card has already grown
   past. Zoom scales the box with the artwork, so centring and the caption are
   both honest, and the card's own @2x stills re-rasterise crisply into it. */
.stage-card {
  position: relative;
  zoom: var(--card-scale);
  /* a tinted shadow to sit it on the ground, and a whisper-thin ring so the
     card's white edge does not blow out against the dark */
  box-shadow:
    0 28px 70px -16px rgba(0, 0, 0, .75),
    0 3px 10px rgba(0, 0, 0, .5),
    0 0 0 1px rgba(255, 255, 255, .07);
}
.stage-card img { max-width: none; }
.cardslot { display: none; }
.cardslot.is-on { display: block; }

/* An empty field shows what belongs there, greyed, so the card is never a
   hollow rectangle and never claims the sample is the customer's. */
.stage [data-empty="1"] { opacity: .38; }

/* The watermark is CSS on the card and never touches an asset, which is why
   the paid install page simply does not have one. Sizes divide by the card's
   own scale so every mark stays the size it was drawn at however far the card
   is scaled up.

   STRENGTH IS RAMON'S CALL, 2026-08-20. It ran at five per cent on one line
   and he judged it "hardly noticeable", so it is now three banded lines at
   fourteen per cent with a solid chip. The argument against a loud watermark
   is that a screenshot is close to worthless anyway, since the product being
   bought is the hosted animation and the install markup. He owns the theft
   risk, so he owns this number: it lives in --mark-ink and nowhere else.

   The bands still avoid the name and the eyes. Two run through the contact
   block and one under the logo row, so the card is unmistakably marked while
   the part a customer is admiring stays clean. */
.mark-chip, .mark-line {
  position: absolute;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}
.mark-chip {
  right: calc(7px / var(--card-scale));
  bottom: calc(7px / var(--card-scale));
  padding: calc(3px / var(--card-scale)) calc(7px / var(--card-scale));
  border-radius: calc(4px / var(--card-scale));
  background: rgba(0, 0, 0, .68);
  color: #fff;
  font-size: calc(10px / var(--card-scale));
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: .09em;
  text-transform: uppercase;
}
/* Six of the seven cards keep their bottom-right corner clear, so that is
   where the chip lives. Frame is the exception: its portrait's brand border
   stops eight pixels short of the card's right edge and seven of its foot,
   which is less than the chip is tall, so on that one card the chip crosses to
   the bottom-left, where the icon rail has already ended. */
.build[data-active="frame"] .mark-chip {
  right: auto;
  left: calc(8px / var(--card-scale));
}
/* ...unless the card is carrying a booking button, because then the
   bottom-left is the button's. A watermark sitting on the one thing on the
   card that asks to be pressed is worse than a watermark crossing eight
   pixels of portrait border, so on that card it goes back to the right. */
.build[data-active="frame"] .stage-card:has(
    .cardslot.is-on [data-opt="booking_url"]:not([hidden])) .mark-chip {
  right: calc(7px / var(--card-scale));
  left: auto;
}
.mark-line {
  left: 50%;
  white-space: nowrap;
  color: var(--mark-ink);
  font-size: calc(13px / var(--card-scale));
  font-weight: 700;
  letter-spacing: .30em;
  text-transform: uppercase;
}
.mark-line-a { top: 62%; transform: translate(-50%, -50%) rotate(-11deg); }
.mark-line-b { top: 84%; transform: translate(-52%, -50%) rotate(-11deg); }
/* The third band rides above the identity block, under the logo row, where
   every template has ruled space. It is dimmer than the other two so the
   mark reads as a receding pattern instead of three equal stripes. */
.mark-line-c {
  top: 22%;
  transform: translate(-48%, -50%) rotate(-11deg);
  color: var(--mark-ink-soft);
}

.stage-note {
  margin: 0;
  max-width: min(100%, 70ch);
  text-align: center;
  font-size: 12px;
  line-height: 1.5;
  color: #75757c;
}
.stage-note b { font-weight: 600; color: var(--muted-dark); }

/* --------------------------------------------------------------- panel */
/* The panel's own bottom padding stays at zero and that is deliberate: a
   sticky element is held inside its containing block, which is the panel's
   CONTENT box, so bottom padding on the panel does not clear the rail, it
   lifts the rail that far off the bottom of the screen and leaves a white
   band under it. The clearance goes on the block above the rail instead, and
   §7 of DESIGN.md is where that is written down. */
.panel {
  background: var(--panel);
  padding: 18px var(--gutter) 0;
  min-width: 0;
}
.panel-head h1 { font-size: 18px; line-height: 1.2; }
.panel-head .sub { font-size: 13px; line-height: 1.4; margin-top: 4px; }

/* platform.css zeroes the first group's top margin, which is right on a form
   sheet and wrong in a panel where a group follows the template picker. */
.panel .group { margin-top: 22px; }
.group-label {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted-light);
  margin-bottom: 10px;
  padding: 0;
}
/* a hairline that runs out from the eyebrow to the panel's edge, which is
   cheaper than a rule above it and reads as a section rather than a divider */
.group-label::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--panel-edge);
}

/* the fields, denser than the site's forms because this is a working panel */
.panel .field { margin-top: 14px; }
.panel .group > .field:first-of-type { margin-top: 0; }
.panel .field label { font-size: 13px; margin-bottom: 6px; }
.panel .field .opt { font-size: 12px; }
.panel .field input[type="text"],
.panel .field input[type="email"],
.panel .field input[type="url"] {
  height: 40px;
  padding: 0 12px;
  font-size: 15px;
  border-radius: 8px;
}
.panel .hint { margin-top: 6px; font-size: 12px; line-height: 1.4; }
/* A note about the whole group rather than about one control, so it sits
   under the eyebrow instead of hanging off the last field above it. */
.panel .group-note { margin-top: -2px; margin-bottom: 12px; }
.panel .err { font-size: 12px; }
.panel .row2 { grid-template-columns: 1fr 1fr; gap: 12px; }

.panel .swatch { gap: 10px; }
.panel .swatch input[type="color"] { width: 44px; height: 34px; border-radius: 8px; }
.panel .swatch code { font-size: 12px; }
.panel .swatch .hint { margin-top: 0; min-width: 0; }

.panel .choices { gap: 8px; }
.panel .choices .choice { padding: 10px 12px; border-radius: 10px; align-items: center; }
.panel .choices .choice-body strong { font-size: 13.5px; }
.panel .choices .choice-body span { font-size: 12px; }

.err-block { margin-top: 10px; }

/* ------------------------------------------------------------ added links */
/* One control instead of eight fields. Everything here reuses the panel's own
   vocabulary — the field's 40px control height, the field's 8px radius, the
   hairline the groups are drawn with — because a second visual language for a
   row that holds a URL is a second thing to learn for no gain.

   The rows are all in the document from the start and hidden until asked for,
   which is what lets the card's glyph appear on a keystroke rather than on a
   round trip. `display: flex` on the class has to beat the UA sheet's
   `[hidden] { display: none }`, so the hidden rule is restated here for the
   same reason `.drop[hidden]` restates it. */
.adds { margin-top: 16px; }
.addlist { list-style: none; margin: 0; padding: 0; }

.addrow {
  /* Not decoration, and it fails in a way nothing on screen shows. A row's
     label is `.sr-only`, which is `position: absolute` with no offsets, and
     without a positioned ancestor its containing block is the INITIAL one —
     so it is laid out at its static position deep inside the panel's scroll
     content while belonging to the viewport, which `.panel`'s overflow
     therefore cannot clip. The document then grows to reach it: measured at
     1440x900, showing one row made the page scrollable by 269px with every
     visible box still inside the shell. `shots.py`'s "nothing below the fold"
     check is what caught it. */
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin-bottom: 8px;
}
.addrow[hidden] { display: none; }
.addrow-mark {
  flex: none;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 1px solid var(--panel-edge);
  border-radius: 8px;
  background: var(--light);
  color: var(--muted-light);
}
.addrow-mark svg { width: 17px; height: 17px; }
.addrow-body { flex: 1; min-width: 0; }
/* the panel's own control, restated: these inputs are not inside a `.field`,
   because a row is an icon, a value and a way out rather than a labelled
   field, and the label they do carry is for a screen reader */
.addrow-body input[type="text"] {
  width: 100%;
  height: 40px;
  padding: 0 12px;
  font-family: inherit;
  font-size: 15px;
  line-height: 1.3;
  color: var(--ink);
  background: var(--white);
  border: 1px solid var(--hair);
  border-radius: 8px;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.addrow-body input::placeholder { color: #a1a1a6; }
.addrow-body input:focus,
.addrow-words select:focus {
  outline: 0;
  border-color: var(--blue);
  box-shadow: 0 0 0 3px rgba(0, 113, 227, .18);
}
.addrow-body input[aria-invalid="true"] { border-color: var(--alert); }
.addrow .err { margin-top: 5px; }

/* The button's words, under the address they belong to. A select and a text
   field that take turns, so the four presets are one tap and a custom label is
   still reachable without a second control on screen the whole time. */
.addrow-words { display: flex; gap: 8px; margin-top: 8px; }
.addrow-words select {
  flex: 1;
  min-width: 0;
  height: 36px;
  padding: 0 8px;
  font-family: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--white);
  border: 1px solid var(--hair);
  border-radius: 8px;
}
.addrow-words input[type="text"] {
  flex: 1;
  min-width: 0;
  height: 36px !important;
  font-size: 13px !important;
}
.addrow-words input[hidden] { display: none; }

.addrow-off {
  flex: none;
  width: 30px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 0;
  background: none;
  color: var(--muted-light);
  border-radius: 8px;
  cursor: pointer;
  transition: color var(--t-base) var(--ease-out),
              background var(--t-base) var(--ease-out);
}
.addrow-off:hover { color: var(--alert); background: rgba(215, 0, 21, .06); }
.addrow-off:focus-visible { outline: 3px solid var(--blue); outline-offset: 1px; }

/* the control itself, and the list it opens */
.addbar { position: relative; }
.ghost-add {
  color: var(--blue);
  background: var(--white);
  border: 1px dashed var(--panel-edge);
  border-radius: 8px;
  height: 38px;
  padding: 0 14px;
  font-size: 13px;
  font-weight: 600;
}
.ghost-add:hover:not(:disabled) {
  color: var(--blue);
  background: var(--blue-wash);
  border-color: var(--blue);
  border-style: solid;
}
.ghost-add:disabled {
  color: var(--muted-light);
  background: var(--light);
  border-color: var(--panel-edge);
  cursor: default;
}
.ghost-add:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }

.addmenu {
  position: absolute;
  left: 0;
  bottom: calc(100% + 6px);
  z-index: 4;
  min-width: 200px;
  padding: 5px;
  background: var(--white);
  border: 1px solid var(--panel-edge);
  border-radius: 10px;
  box-shadow: 0 10px 28px rgba(20, 24, 28, .14);
}
.addmenu[hidden] { display: none; }
.addopt {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 8px 9px;
  border: 0;
  background: none;
  border-radius: 7px;
  font-family: inherit;
  font-size: 13.5px;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.addopt[hidden] { display: none; }
.addopt:hover { background: var(--blue-wash); }
.addopt:focus-visible { outline: 3px solid var(--blue); outline-offset: -2px; }
.addopt-mark {
  flex: none;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  color: var(--muted-light);
}
.addopt-mark svg { width: 16px; height: 16px; }

#add-note { margin-top: 8px; }
/* the cap, once it has actually been passed. Said in the alert colour because
   it is now a thing to do rather than a thing to know. */
.panel .hint.is-warn { color: var(--alert); font-weight: 500; }

/* --------------------------------------------------- the template picker */
/* Ramon's call and the convention: the picker belongs with the other edits,
   at the top of the panel. Seven live cards, each one playing its own motion
   at a size where the LAYOUT is legible. The blurbs came off the tiles: seven
   sentences is a wall of text, so the active one is said once, underneath.

   It is a THUMBNAIL grid now, not seven small cards. Two columns of 66px wells
   with a 6px surround cost 497px, which was the panel's whole first screen:
   the only field a customer met at 1440x900 was their name, sitting against
   the rail. Three columns of 104px wells with no surround at all cost 297,
   which puts the name, the title and the company on that first screen. The
   mini is smaller and that is the trade: at this size a tile says WHICH
   ARRANGEMENT, and the stage says what it looks like. */
.switch { margin-top: 14px; }
/* `min-inline-size: 0` is load-bearing and it fails silently. A <fieldset>
   carries a UA `min-inline-size: min-content`, so the tiles set a floor the
   page cannot shrink below and the whole document grows sideways at 375px. */
.switch fieldset { border: 0; padding: 0; margin: 0; min-inline-size: 0; }
/* a <legend> defaults to an inline-ish box, and the eyebrow's hairline is a
   flex child, so the display has to be restated or the rule never draws */
.switch legend { display: flex; width: 100%; float: none; }

/* `auto-fill` rather than a column count, because the panel is 400px wide on
   the shell and full-bleed under it: one rule gives three columns in the rail
   and at 390, and a single row of seven across a tablet, which is the same
   thumbnail either way. 104px is the floor the mini's zoom is solved against. */
.tpl-grid {
  position: relative;
  min-width: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: 7px;
}

/* The gliding active marker, beui.dev `tabs` layoutId mechanism done with one
   element: JS writes its transform and size, the transition does the glide,
   and a mid-flight change retargets instead of queueing. In a grid it travels
   on both axes, which is the same mechanism and a longer journey. */
.tpl-mark {
  position: absolute;
  left: 0;
  top: 0;
  height: 0;
  border-radius: 10px;
  border: 2px solid var(--blue);
  pointer-events: none;
  opacity: 0;
  transition: transform var(--t-glide) var(--ease-glide),
              width var(--t-glide) var(--ease-glide),
              height var(--t-glide) var(--ease-glide),
              opacity var(--t-base) var(--ease-out);
}
.tpl-mark.is-ready { opacity: 1; }

/* No padding: the well runs to the tile's own border and the name sits on a
   strip under it, so the tile is the thumbnail plus its caption and nothing
   else. `overflow: hidden` is what lets the square-cornered well sit inside a
   rounded tile without a second radius to keep in step. */
.tpl {
  position: relative;
  min-width: 0;
  background: var(--tile);
  border: 1px solid var(--panel-edge);
  border-radius: 10px;
  overflow: hidden;
  cursor: pointer;
  transition: transform var(--t-base) var(--ease-out),
              border-color var(--t-base) var(--ease-out),
              background var(--t-base) var(--ease-out);
}
.tpl:hover { transform: translateY(-1px); border-color: #c9c9ce; background: #fff; }
.tpl input {
  position: absolute;
  opacity: 0;
  width: 1px; height: 1px;
  margin: 0;
}
.tpl:has(input:focus-visible) {
  outline: 3px solid var(--blue);
  outline-offset: 2px;
}
.tpl.is-on { background: var(--blue-wash); border-color: transparent; }

/* Each tile holds a whole live card, shrunk. `zoom` rather than a transform:
   the card is a table of pixel-sized inline-styled boxes and a transform would
   leave a full-size bounding box behind it. */
.tpl-stage {
  display: block;
  height: 50px;
  background: #fff;
  border-bottom: 1px solid #eceef1;
  overflow: hidden;
  position: relative;
}
.tpl-card {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* Two numbers meet here and both are floors. The narrowest well the grid can
     hand out is 104px, and the widest card is 510px, so .2 is as large as the
     zoom can be without a card running out of its well sideways. The tallest
     card is script's 252px, which lands at 50.4px: the well's own height. */
  zoom: .2;
  pointer-events: none;
}
.tpl-name {
  display: block;
  padding: 4px 5px 5px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: -.01em;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tpl-say {
  margin-top: 8px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--muted-light);
}

/* --------------------------------------------------------- file controls */
.drop {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 46px;
  padding: 10px 12px;
  border: 1px dashed var(--hair);
  border-radius: 8px;
  background: var(--light);
  color: var(--muted-light);
  cursor: pointer;
  transition: border-color var(--t-base) var(--ease-out),
              background var(--t-base) var(--ease-out),
              color var(--t-base) var(--ease-out);
}
.drop-slim { min-height: 42px; margin-top: 10px; }
/* `display: flex` on a class beats the UA sheet's `[hidden] { display: none }`
   no matter how specific the UA rule is, because an author sheet wins the
   cascade first. Without this the logo drop is on the screen the whole time,
   asking for a file the customer has not said they want to send, and it is the
   last focusable control in the panel while it is there. */
.drop[hidden] { display: none; }
.drop:hover, .drop.is-over { border-color: var(--blue); background: #fff; color: var(--ink); }
.drop input[type="file"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}
.drop:has(input:focus-visible) { outline: 3px solid var(--blue); outline-offset: 2px; }
.drop-face { display: flex; align-items: center; gap: 9px; min-width: 0; }
.drop-label {
  font-size: 13.5px;
  font-weight: 600;
  color: inherit;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.drop.is-working { border-style: solid; border-color: var(--blue); background: #fff; }
.drop.is-done { border-style: solid; border-color: var(--good); background: #fff; color: var(--ink); }
.drop.is-bad { border-style: solid; border-color: var(--alert); background: #fff; }
.drop-say {
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--muted-light);
  min-height: 0;
}
.drop-say:empty { margin-top: 0; }
.drop-say.is-bad { color: var(--alert); font-weight: 500; }
.drop-say.is-good { color: var(--good); }

/* ------------------------------------------------------------- the rail */
/* The one ask, stuck to the bottom of whatever is scrolling: the panel above
   1040px, the viewport below it. Same element, same place in the document, so
   there is never a second button to wonder about.

   The rail's height, carried as padding under the last control, is what makes
   the bottom of the panel reachable: scrolled all the way down, the last thing
   a customer can press clears the rail instead of sitting behind it. It is
   measured by builder.js rather than typed, because an error line under the
   button changes it. The fallback is the rail at its usual size.

   The ask here is the step to the plan page, which carries the money. */
.panel-body { padding-bottom: var(--foot-h, 152px); }
.panel-foot {
  position: sticky;
  bottom: 0;
  z-index: 3;
  margin: 0 calc(var(--gutter) * -1);
  padding: 12px var(--gutter) calc(12px + env(safe-area-inset-bottom));
  background: rgba(255, 255, 255, .94);
  -webkit-backdrop-filter: saturate(180%) blur(16px);
  backdrop-filter: saturate(180%) blur(16px);
  border-top: 1px solid var(--panel-edge);
  box-shadow: 0 -10px 22px -16px rgba(0, 0, 0, .35);
}
/* Whatever is mid-scroll under the rail meets its hairline and is guillotined
   by it: half a field, half an eyebrow. A short scrim above the edge lets the
   panel fade into the rail instead, so the cut reads as the page continuing
   rather than as a layout mistake. */
.panel-foot::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 22px;
  background: linear-gradient(to top, var(--panel), rgba(255, 255, 255, 0));
  pointer-events: none;
}
.plans { display: flex; gap: 8px; margin-bottom: 10px; }
.planpick {
  flex: 1 1 0;
  position: relative;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 5px;
  padding: 8px;
  border: 1px solid var(--hair);
  border-radius: 8px;
  cursor: pointer;
  transition: border-color var(--t-base) var(--ease-out),
              background var(--t-base) var(--ease-out);
}
.planpick input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.planpick b { font-size: 15px; font-weight: 600; letter-spacing: -.015em; }
.planpick span { font-size: 12px; color: var(--muted-light); }
.planpick:hover { background: var(--light); }
.planpick:has(input:checked) { border-color: var(--blue); background: var(--blue-wash); }
.planpick:has(input:focus-visible) { outline: 3px solid var(--blue); outline-offset: 2px; }

.pill-block { padding: 13px 28px; font-size: 16px; }
.submit-note { margin-top: 8px; font-size: 12px; }
/* The hosting term and the two legal links, quieter than the price above them
   but on the same rail, because that is where the money is asked for. */
.foot-terms { margin-top: 6px; font-size: 11px; line-height: 1.45; color: var(--muted-light); }
.foot-links { display: inline-flex; gap: 10px; margin-left: 4px; white-space: nowrap; }
.foot-terms a { color: var(--muted-light); text-decoration: underline; text-underline-offset: 2px; }
.foot-terms a:hover { color: var(--ink); }

/* The stateful button, beui.dev `button/stateful`: the label blur-rolls out
   and the next one rolls in, and the control's width animates so the row does
   not pop in a single frame. Reduced motion swaps the roll for a plain change. */
.pill-state { position: relative; overflow: hidden; }
.pill-slot {
  display: inline-block;
  transition: transform var(--t-swap) var(--ease-out),
              opacity var(--t-swap) var(--ease-out),
              filter var(--t-swap) var(--ease-out);
}
.pill-slot.is-out {
  transform: translateY(-105%);
  opacity: 0;
  filter: blur(6px);
}
.pill-slot.is-in {
  transform: translateY(105%);
  opacity: 0;
  filter: blur(6px);
}
.pill[disabled] { background: var(--hair); color: #fff; cursor: default; }
.pill[disabled]:hover { background: var(--hair); }
.pill.is-bad { background: var(--alert); }
.pill.is-bad:hover { background: var(--alert); }

/* --------------------------------------------------------- breakpoints */
/* a 508px card cannot keep any ground at all on the narrowest phones */
@media (max-width: 400px) { :root { --gutter: 16px; } }
@media (max-width: 380px) { :root { --card-step: .55; } }

/* One column, tighter chrome: the stage is a band at the top of the screen
   and the panel scrolls under it. Each step leaves the card at least 40px of
   ground either side of it at the narrowest width the step covers. The band's
   height is its contents, so the chrome around the card is what decides how
   much of the phone the stage takes: it is packed rather than padded. */
@media (max-width: 1039px) {
  .stage-bar { padding: 12px 16px 0; }
  .stage-pool { gap: 14px; padding: 10px 20px 16px; }
  .stage-note { font-size: 11.5px; }
}
@media (min-width: 440px) { :root { --card-step: .78; } }
@media (min-width: 600px) { :root { --card-step: .95; } }
@media (min-width: 760px) { :root { --card-step: 1; } }

/* The editor shell. The window is the frame, the panel owns the only
   scrollbar on the screen, and the stage holds still. */
@media (min-width: 1040px) {
  :root { --card-step: 1; --gutter: 24px; }

  body.builder {
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
  }
  body.builder .nav { position: relative; flex: 0 0 var(--bar-h); }

  .build {
    flex: 1;
    min-height: 0;
    display: grid;
    grid-template-columns: var(--panel-w) minmax(0, 1fr);
  }
  .panel {
    grid-column: 1;
    grid-row: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    border-right: 1px solid var(--panel-edge);
    scrollbar-width: thin;
  }
  .panel::-webkit-scrollbar { width: 9px; }
  .panel::-webkit-scrollbar-thumb {
    background: #d8d8dd;
    border: 3px solid #fff;
    border-radius: 999px;
  }
  .build .stage {
    grid-column: 2;
    grid-row: 1;
    position: relative;
    top: auto;
    height: auto;
    max-height: none;
    min-height: 0;
  }
  .stage-bar { padding: 16px 20px 0; }
  .stage-note { padding-bottom: 18px; }
}

/* The card is the hero, so it grows with the room it is given, and the ladder
   is DERIVED rather than chosen. Each step is

       0.755 x (its own width - the 400px panel) / 508

   which opens the step with the card at about three quarters of the stage and
   lets it fall to about seven tenths before the next step takes over. That
   band is why the steps sit roughly eight per cent apart: 0.755 / 0.695 is
   1.086, so each stage width is the last one times 1.086. Four fixed steps
   used to cover this whole range, which is how 1440 ended up showing a 635px
   card on a 1040px stage: 61 per cent, and the rest of it dark.

   The ladder stops at 1990. Past about 2130 the share falls away again and
   that is the deliberate ceiling: a signature drawn wider than about 1200px
   has stopped being a preview of an email signature. */
@media (min-width: 1100px) { :root { --card-step: 1.04; } }
@media (min-width: 1160px) { :root { --card-step: 1.13; } }
@media (min-width: 1220px) { :root { --card-step: 1.22; } }
@media (min-width: 1290px) { :root { --card-step: 1.32; } }
@media (min-width: 1370px) { :root { --card-step: 1.44; } }
@media (min-width: 1450px) { :root { --card-step: 1.56; } }
@media (min-width: 1540px) { :root { --card-step: 1.69; } }
@media (min-width: 1640px) { :root { --card-step: 1.84; } }
@media (min-width: 1750px) { :root { --card-step: 2; } }
@media (min-width: 1860px) { :root { --card-step: 2.17; } }
@media (min-width: 1990px) { :root { --card-step: 2.36; } }

/* The other constraint, and it is a ceiling rather than a replacement: a
   window can be wide and short at once, and the card has to fit the pool
   whatever the width ladder wants. Each cap is the scale at which the tallest
   card, script's 252px, takes 65 per cent of the stage's height, so the pool
   still reads as a pool and the chrome around it still has its 148px. `min()`
   in --card-scale is what picks between the two, which is why a short 1920
   window now loses a little of the card rather than most of it. */
@media (min-width: 1040px) and (max-height: 900px) { :root { --card-cap: 2.18; } }
@media (min-width: 1040px) and (max-height: 800px) { :root { --card-cap: 1.92; } }
@media (min-width: 1040px) and (max-height: 700px) { :root { --card-cap: 1.67; } }
@media (min-width: 1040px) and (max-height: 620px) { :root { --card-cap: 1.46; } }
@media (min-width: 1040px) and (max-height: 560px) { :root { --card-cap: 1.31; } }

@media (prefers-reduced-motion: reduce) {
  .tpl, .tpl-mark, .pill-slot, .drop, .planpick, .ghost { transition: none !important; }
  .tpl:hover { transform: none; }
  .stage-chip i { animation: none; opacity: 1; transform: none; }
  /* nothing on the card moves, so there is nothing to replay */
  #replay { display: none; }
}
