/* Forms and the things that report a state: inputs and selects, the gift item
 * picker, notices, an address to copy, and a cart line that changes under the
 * reader.
 *
 * Was the tail of motion.css and had nothing to do with motion. Split out on
 * 2026-08-01 at the seam its own banner already marked. Linked LAST, after
 * motion, which is where it sat when it was one file. */

/* ---- account, top up, and anything that reports a state ---------------- */

/* Held to the same 4.5:1 as body copy. Left unset the engine picks: Blink picks
   #757575, which is 4.14:1 on --bg-1 and fails. `opacity: 1` because Firefox
   still applies its own on top of whatever colour it is given.

   This used to say several placeholders were INSTRUCTIONS and give them contrast
   on that basis. Contrast was never the problem. A placeholder is clipped by the
   width of its input, so an instruction in one is read as far as it fits and
   stops: /rent showed "we sign in once and keep the ses" and "the long secret
   behind the QR co", and /swap told somebody an address had to be "on the
   network you chose abo". Screenshots 2026-08-02. An instruction now goes in
   `.field-hint` below the box, where it wraps and stays put while they type; a
   placeholder is an EXAMPLE or it is absent. */
::placeholder { color: var(--text-3); opacity: 1; }

/* The instruction that used to be crammed into a placeholder. Wraps, so its
   length is free, and stays on screen while the field is being filled - which
   the placeholder did not, since typing the first character erases it. */
.field-hint {
  /* No top margin: `.field` is a grid with `gap: var(--step-2)`, so one was
     already there and adding a second put the hint twice as far from its input
     as the label sits from it. */
  font-size: 13px; line-height: 1.5;
  color: var(--text-2);
}

/* The `?` beside a label: a CSS-only disclosure, because this site's CSP has no
   nonce and an inline handler would be dropped silently. `:focus-within` on the
   span is what makes it reachable by keyboard as well as by pointer. */
.help {
  position: relative; display: inline-flex;
  align-items: center; justify-content: center;
  width: 1.15rem; height: 1.15rem; margin-left: var(--step-2);
  border: 1px solid var(--border-fld); border-radius: 50%;
  font-size: 12px; line-height: 1; color: var(--text-2);
  cursor: help; vertical-align: middle;
}
.help:hover, .help:focus { color: var(--text-1); border-color: var(--accent); }
.help:focus { outline: 2px solid var(--accent); outline-offset: 2px; }

/* In FLOW, and a sibling of the `?` rather than its child. Both of those are
   forced by the same rule: `.qstep-in` is `overflow: hidden`, because `.qstep`
   collapses with the `grid-template-rows: 0fr -> 1fr` trick and that needs the
   inner box to clip. An absolutely positioned tip inside it is cut off at the
   card edge whatever its `z-index` says - the first version of this was, by
   about 300 of its 352 pixels. In flow it makes the step taller instead, which
   the same grid animates. A sibling because `.help` is a 1.15rem circle, and a
   block child of it would be a 1.15rem-wide column of text. */
.help-tip {
  display: none; margin-top: var(--step-2);
  padding: var(--step-3) var(--step-4);
  background: var(--surface-2); color: var(--text-1);
  border: 1px solid var(--border-fld); border-radius: var(--r-sm);
  box-shadow: var(--shadow-raised);
  font-size: 13px; line-height: 1.55; font-weight: 400;
  text-align: left; white-space: normal;    /* the label may be nowrap; this is not */
}
/* `display` and not `opacity`, because in flow a hidden-but-present box holds
   its own height open and the field below it would sit under a permanent gap. */
.help:hover + .help-tip, .help:focus + .help-tip { display: block; }

/* A select has to sit beside a text input without looking like a different
   control, so it borrows the input's box outright. The arrow is drawn here
   because the native one ignores the colour scheme on Windows. */
.field select {
  /* --step-10, not --step-9: there is no 9 on the scale, and a var() naming a
     token that does not exist voids the WHOLE shorthand, so this padding was
     silently 0 on all four sides. The select's text sat on its left border
     while every input beside it was indented 16px. 40px on the right is what
     clears the arrow drawn below, which reaches 26px in from the edge. */
  height: 50px; width: 100%; padding: 0 var(--step-10) 0 var(--step-4);
  background: var(--bg-1); color: var(--text-1);
  border: 1px solid var(--border-fld); border-radius: var(--r-sm);
  box-shadow: var(--shadow-inset);
  /* It borrows the input's BOX, but not its type. An input here holds a number
     and 18px mono suits that; a select holds a sentence, and at 18px mono only
     about 35 characters fit the widest box this site has — so "Take it out of
     that figure, pay me on the rest" was cut off, and Chrome does not ellipsize
     a closed select on Windows, it just truncates. On a phone the box is 18
     characters. The customer choosing who absorbs Roblox's 30% could not read
     either option. */
  font-family: inherit; font-size: 16px;
  -webkit-appearance: none; appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--text-2) 50%),
    linear-gradient(135deg, var(--text-2) 50%, transparent 50%);
  background-position: right var(--step-5) center, right var(--step-4) center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}
.field select:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The item picker: a game, a search box, and up to ten hits.
   Buttons rather than a second select, because the whole point is to search a
   catalog too long to sit in a dropdown, and a native dropdown cannot show a
   price beside a name. Ten is the cap on RESULTS; the box is shorter than ten
   rows on purpose, so a full result set reads as a list to scroll rather than
   a 550px slab that pushes the rest of the order form off the screen. */
/* A search box with its own mark. The icon is not decoration: it is what says
   this field is searched rather than filled in, which matters when the four
   fields under it look identical and are all typed into. */
.find { position: relative; }
.find-ic {
  position: absolute; left: var(--step-4); top: 50%; transform: translateY(-50%);
  color: var(--text-3); pointer-events: none;
  transition: color 160ms ease-out;
}
.find input { padding-left: 48px; }
.find:has(input:not(:placeholder-shown)) .find-ic { color: var(--accent); }

/* HEIGHT, not max-height, and this is the whole reason the page stops moving.
   The card is the tallest thing in the head, so the head's height was the
   card's height: every keystroke that changed the number of results resized
   the card, and the headline and lede beside it visibly jumped. A results area
   that is the same size holding nothing, three items or ten costs some empty
   space at the bottom and buys a page that does not move while it is read. */
.hits {
  list-style: none; margin: var(--step-4) 0 0;
  display: grid; gap: var(--step-2); align-content: start;
  height: 272px; overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: thin;
  /* The rows ended flush against the bar, which read as one control cut off
     rather than a list beside its scrollbar. The gutter is held open whether
     or not the list actually overflows: padding alone would inset a short list
     from nothing, so the rows would change width the moment a search narrowed
     them past the point of scrolling. */
  padding: 0 var(--step-3) 0 0;
  scrollbar-gutter: stable;
}
/* Nothing to show is a state with something to say, so it is centred in the
   box rather than pinned to the top of an otherwise empty one. */
/* `outline` and not `border`, because the box is 272px whether or not anything
   is in it and a border would move every row by a pixel the moment a search
   matched. An outline is painted outside the box model entirely, so this
   appears and disappears without shifting the list. Dashed, and at the faintest
   border token, because it is saying "this is where results land", not drawing
   a control: an empty fixed-height box with no edge reads as a slab of nothing
   the page failed to fill. */
.hits.is-empty {
  align-content: center; justify-items: center;
  outline: 1px dashed var(--border-str); outline-offset: -1px; border-radius: inherit;
}
.hits-msg {
  display: grid; justify-items: center; gap: var(--step-4);
  padding: 0 var(--step-6); text-align: center;
  font-size: 14px; color: var(--text-3);
  animation: hit-in 300ms cubic-bezier(.16,.84,.44,1) backwards;
}
/* Faint and oversized. It fills a box that is deliberately the same height
   whether or not it holds anything, so it has to read as a state rather than
   as a control somebody forgot to draw. */
.hits-msg-ic {
  width: 40px; height: 40px;
  color: rgb(var(--accent-rgb) / .30);
  stroke-width: 1.3;
}
.hit {
  width: 100%; display: flex; align-items: center;
  gap: var(--step-3); padding: var(--step-3) var(--step-4);
  background: var(--bg-1); color: var(--text-1);
  border: 1px solid var(--border-str); border-radius: var(--r-sm);
  font: inherit; font-size: 15px; text-align: left; cursor: pointer;
  transition: border-color 140ms ease-out, background-color 140ms ease-out,
              transform 140ms ease-out;
  /* Each row is dealt a beat after the one above it, so a result set reads as
     arriving rather than blinking into place whole. `backwards` is what holds a
     row invisible through its own delay instead of flashing first. */
  animation: hit-in 280ms cubic-bezier(.16,.84,.44,1) backwards;
  animation-delay: calc(var(--i, 0) * 24ms);
}
@keyframes hit-in {
  from { opacity: 0; transform: translateY(7px) scale(.99); }
  to { opacity: 1; transform: none; }
}
.hit:hover { border-color: rgb(var(--accent-rgb) / .45); transform: translateX(2px); }
.hit[aria-pressed="true"] {
  border-color: var(--accent); background: rgb(var(--accent-rgb) / .12);
}
/* Always in the layout, only ever faded in, so choosing a row cannot shift the
   name beside it by the width of a tick. */
.hit-ic {
  width: 18px; height: 18px; color: var(--accent);
  opacity: 0; transform: scale(.55);
  transition: opacity 160ms ease-out, transform 220ms cubic-bezier(.16,.84,.44,1);
}
.hit[aria-pressed="true"] .hit-ic { opacity: 1; transform: none; }
.hit-n { flex: 1 1 auto; min-width: 0; }
.hit-p {
  font-family: var(--font-mono); font-size: 13px; color: var(--text-3);
  white-space: nowrap;
}
.hit[aria-pressed="true"] .hit-p { color: var(--accent); }
.hits-note { margin-top: var(--step-3); min-height: 20px; font-size: 13px; color: var(--text-3); }

/* A button that cannot do anything yet says so, and stops pretending to lift
   off the page when pressed. */
/* .6 and not .45: at 45% the accent button's ink measures about 2.4:1 on
   what is painted behind it, and on /gifting that button's LABEL is the only
   thing telling the customer what is missing - `gift.js` swaps it to "Pick a
   gift first". WCAG exempts an inactive control, so this is not a conformance
   failure; it is the least readable text on the page being the instruction. */
.btn:disabled { opacity: .6; cursor: not-allowed; }
.btn:disabled:hover { transform: none; filter: none; }

@media (prefers-reduced-motion: reduce) {
  .hit, .hits-msg { animation: none; }
  .hit:hover { transform: none; }
  .hit-ic { transition: opacity 160ms ease-out; transform: none; }
}

/* A label sits 8px above its own control, so the space BETWEEN two fields has
   to be clearly bigger than that or the label reads as belonging to the control
   above it. At the old 16px it was barely double, and "Amount in USD" looked
   like a caption on the currency select. */
.topup { display: grid; gap: var(--step-6); margin-top: var(--step-6); }
.topup .btn { margin-top: var(--step-2); }

/* The one number a customer opens this page to read. */
.balance-line {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: 0 var(--step-4); margin-top: var(--step-5);
}
/* `overflow-wrap: normal` undoes the `anywhere` every `.mono` carries. At 38px
   a thirteen character balance is about 296px against a 280px wrap on a 320px
   phone, and `anywhere` breaks it mid-number: $1,234,56 / 7.89. A number that
   wraps is a different number. */
.balance-figure {
  overflow-wrap: normal;
  font-size: clamp(38px, 7vw, 60px); line-height: 1;
  color: var(--text-1); font-variant-numeric: tabular-nums;
}
.balance-line span:last-child { color: var(--text-3); font-size: 14px; }

/* Says what happened without shouting. Used for a refusal we caused and for a
   state the customer is waiting on, so it is informative rather than alarming. */
.notice {
  margin-top: var(--step-5); padding: var(--step-4) var(--step-5);
  background: rgb(var(--accent-rgb) / .10);
  border: 1px solid rgb(var(--accent-rgb) / .34);
  border-radius: var(--r-sm);
  color: var(--text-1); font-size: 14px; max-width: 62ch;
}
.quote .notice { margin: 0 0 var(--step-5); line-height: 1.55; }

/* An address is copied, not read, so it breaks anywhere rather than overflowing
   its panel on a phone. */
.addr {
  margin-top: var(--step-3); padding: var(--step-4);
  background: var(--bg-0); border: 1px solid var(--border-str);
  border-radius: var(--r-sm);
  font-size: 14px; line-height: 1.5;
  /* `break-word` first, `anywhere` second: an engine that does not know the
     newer keyword drops that declaration and keeps the older one, rather than
     falling back to `normal` and letting a 42-char chain address overflow the
     panel on a phone. Same reason `-webkit-user-select` is paired below --
     without it a tap does not select the whole address, and a customer
     hand-selecting part of a crypto address is a partial-copy on a money path. */
  overflow-wrap: break-word; overflow-wrap: anywhere;
  -webkit-user-select: all; user-select: all;
}

/* A player's state on a cart, which changes under the reader while the gifts go
   out. The words are the information; this is only what stops a line four rows
   down changing unnoticed. It lights the accent and fades back, once per change.

   Nothing is needed for reduced motion: the block further up this sheet already
   switches every animation off with `!important`, so somebody who asked for less
   movement gets the new words with none of it. That is the right way round --
   the highlight is the decoration and the text is the answer. */
.line-state.is-new { animation: line-new 1400ms ease-out; }
@keyframes line-new {
  from { color: var(--accent); }
  to { color: var(--text-1); }
}

.muted { color: var(--text-3); }
.rowlist dd .muted { margin-left: var(--step-3); font-size: 12px; }
.rowlist dt .muted { font-size: 12px; }

/* `.muted` is 12px `--text-3`, this site's footnote size and footnote colour,
   and it was carrying two different jobs. A timestamp belongs there. The word
   that says what the row IS does not: a deposit's "Waiting", an order's
   "unpaid", a swap's "open" and, on the review-before-you-pay page, the gift
   itself were all set smaller and dimmer than the figure beside them - the
   what quieter than the how much, and on the cart the what quieter than the
   who. Same row, one step up in size and colour, still under the figure. */
.rowlist .state { font-size: 14px; color: var(--text-2); }
.rowlist dd .state { margin-left: var(--step-3); }

/* A deposit row links to its address, and an inline anchor is 19px tall, which
   is a miss on a phone. Same treatment as .mark: the link carries the target,
   the row grows to hold it. */
.rowlist dt a, .rowlist dd a {
  display: inline-flex; align-items: center; min-height: 44px;
  color: var(--text-1); text-decoration: underline;
  text-decoration-color: rgb(var(--accent-rgb) / .5); text-underline-offset: 3px;
}
.rowlist dt a:hover, .rowlist dd a:hover { text-decoration-color: var(--accent); }

/* An anchor sitting INSIDE a sentence. Note what this is not: `.link` is a
   flex divider ornament with gradient pseudo elements, so putting it on an
   inline anchor turned the anchor into a block and pushed the sentence's full
   stop onto a line of its own. That is the stray dot that was under the last
   homepage section. Prose links get this instead, and it stays inline. */
.prose-link {
  display: inline; color: var(--text-1); text-decoration: underline;
  text-decoration-color: rgb(var(--accent-rgb) / .55); text-underline-offset: 3px;
  transition: text-decoration-color 140ms ease-out;
}
.prose-link:hover { text-decoration-color: var(--accent); }

/* The order form, in parts ------------------------------------------------- */

/* Open is the default state and it is written here rather than in the script,
   which is what makes the whole form work with nothing running: every part is
   on screen until gift-steps.js shuts one. The `hidden` attribute is what shuts
   it, because it is the one thing that takes a part out of the picture and out
   of the reading order at the same time. */
.qstep { position: relative; display: grid; grid-template-rows: 1fr; }
/* The clip the unfold needs. The row is what shrinks, and a grid item spills
   out of a row too short for it unless the item itself clips. The padding is
   room for a focus ring, which the clip would otherwise shave off the outside
   edge of every control in here, and the matching negative margin puts the box
   back where it was.

   The number is outline width plus outline offset, and it has to clear the
   WIDEST ring in here. That is the global `:focus-visible` in app.css at
   2px + 2px = 4px, which Back, Continue, `.btn-add` and the `.btn-block` submit
   all use; the fields' own 2px + 1px = 3px is the narrower one. It was 3px
   until 2026-08-02 and clipped every button ring by exactly a pixel - on both
   sides of the two that are `width: 100%`, which touch both edges. Change
   either rule and change this to match. */
.qstep-in { position: relative; min-height: 0; overflow: hidden; padding: 4px; margin: -4px; }
.qstep + .qstep { margin-top: var(--step-6); }
/* One line of the cart, and one account the Robux may come out of. Five of
   either stacked with nothing between reads as one long form of fifteen
   controls, so each is given a rule and a number.
   The margin is on every line and not only between them: without it the first
   number sits flush under the part heading and reads as part of it. */
.qline, .qacct { margin-top: var(--step-8); }
.qline + .qline, .qacct + .qacct {
  padding-top: var(--step-8); border-top: 1px solid var(--border);
}
/* Showing one line at a time is the script's doing, and a rule drawn above the
   line that is showing separates it from nothing: the lines before it are
   display:none, which an adjacent sibling selector does not care about. */
.qline[hidden] + .qline { padding-top: 0; border-top: 0; }
/* Pulled up by exactly the top margin the field below it carries, so the number
   belongs to its line rather than floating between two. */
.qline-n {
  display: block; margin-bottom: calc(var(--step-6) * -1);
  font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-3);
}
/* `fit-content`, because this heading is a focus target. A block level heading
   takes the whole card, and the ring drawn round it read as an empty text field
   sitting above the real one rather than as the name of the part you were just
   moved to. Round the words it is unmistakably a heading. */
.qstep-t {
  width: fit-content;
  font-family: var(--font-display); font-size: 17px; font-weight: 600;
  letter-spacing: -0.01em; color: var(--text-1);
}
/* Which part of how many. A form that opens one part at a time hides its own
   length, so somebody deciding whether to start cannot tell what they are
   agreeing to; this is the whole of the answer.

   Inside the heading rather than above it, and that is the reason it is a span
   and not another `.qline-n`: the heading is the focus target and the one thing
   a screen reader announces on arrival, so a number outside it is a number only
   sighted readers ever get. It has to name its own font, because the heading
   sets the display face and everything in here would otherwise inherit it. */
.qstep-n {
  margin-left: var(--step-3);
  font-family: var(--font-body); font-size: 12px; font-weight: 500;
  letter-spacing: .08em; text-transform: uppercase; color: var(--text-3);
}

/* The one moment on this page that is allowed to be a moment: a hairline in the
   accent draws itself across the gap, and the new part unfolds under it. Both
   are the page's own vocabulary rather than a new one -- the nav underline
   already sweeps in from the left, and the result rows already arrive on this
   curve. Drawn in the middle of the gap between two parts, and only between
   two: nothing draws a line above the first. */
.qstep + .qstep::before {
  content: ""; position: absolute; left: 0; right: 0;
  top: calc(var(--step-3) * -1); height: 1px; transform-origin: left;
  background: linear-gradient(90deg, var(--accent), rgb(var(--accent-rgb) / .06));
}
/* Only ever set by the script, and only on a part it has just opened, so a
   page with no script never animates anything here. `backwards` holds the
   first frame through the delay: without it the part would sit open for
   120ms and then jump shut to start. */
.qstep.is-new { animation: qstep-open 460ms cubic-bezier(.16,.84,.44,1) 120ms backwards; }
.qstep.is-new::before { animation: qstep-rule 440ms cubic-bezier(.16,.84,.44,1); }
@keyframes qstep-open {
  from { grid-template-rows: 0fr; opacity: 0; }
  to { grid-template-rows: 1fr; opacity: 1; }
}
@keyframes qstep-rule { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* The bulk ladder on the listing form: three optional rows, each an order size
   beside what that size pays. A fieldset, so the three rows read as one
   question and its name is announced on the way into any of them; the browser's
   own border and padding go, because the card already draws the box this sits
   in, and `min-width` because a fieldset defaults to min-content and would
   refuse to shrink inside one.

   Flex and not a grid: a row is a pair of boxes that has to fall to one column
   on a narrow phone, and wrapping at the basis below does that on its own with
   no breakpoint to keep in step with every other one in these sheets. */
.tiers { margin-top: var(--step-8); padding: 0; border: 0; min-width: 0; }
.tiers legend { padding: 0; font-size: 13px; color: var(--text-2); }
.tier-row { display: flex; flex-wrap: wrap; gap: 0 var(--step-4); }
/* Wraps at 2 x 9rem plus the gap, so the pair falls to one column under a 304px
   card and needs no breakpoint. `min-width: 0` is what lets it: a flex item's
   automatic minimum is its own min-content, and a `.field` holding a text input
   reports the input's `size` attribute - 20 characters of 18px mono, about
   250px. Without this the two rows stacked correctly and then each one
   overflowed the card by 34px on a 320px phone, which is 216px of usable
   width. */
.tier-row .field { flex: 1 1 9rem; min-width: 0; }

/* Moving between the parts. Built by the script and only by the script, over a
   card that is showing one part at a time; with nothing running there are no
   parts to move between and none of this exists. Back is as wide as its word
   and Continue takes the rest, because they are not the same size of decision. */
.qnav { display: flex; gap: var(--step-3); margin-top: var(--step-5); }
.qnav .btn { margin-top: 0; }
/* Why Continue did nothing, said above it. A notice normally HEADS a card, so
   `.quote .notice` takes its top margin off; this one sits mid card between a
   field and the buttons and needs that space back. Same specificity, later in
   the sheet, which is the whole of why it wins. */
.notice.qnav-why { margin-top: var(--step-5); }
.btn-next { flex: 1; }
/* --border-int rather than the inherited --border-str: this one drops the
   raised fill for the page's own background, so the boundary is the only
   thing left saying it is a button. */
.btn-back {
  flex: 0 0 auto; color: var(--text-2); background: var(--bg-0);
  box-shadow: none; border-color: var(--border-int);
}
.btn-back:hover { color: var(--text-1); }

/* Opening a slot that is already on the page, which is a different act from
   moving to the next part, so it may not look like the button that does. An
   empty well rather than a raised button: the same inset the text fields carry,
   because what it opens IS another one of those. Only ever built by the script,
   over slots that were served folded away for having nothing in them. */
.btn-add {
  width: 100%; margin-top: var(--step-5);
  background: var(--bg-0); border: 1px solid var(--border-int); border-style: dashed;
  box-shadow: var(--shadow-inset);
  color: var(--text-2); font-size: 14px; font-weight: 500;
}
.btn-add:hover { border-color: rgb(var(--accent-rgb) / .55); color: var(--text-1); }

/* The cart: one short row per gift added, above the single entry row that adds
   them. This is the whole of what five gifts looks like now. Before, "add
   another gift" unfolded a second entry form under the first and a third under
   that, so five gifts was five stacked forms and the button that builds the
   order sat somewhere below the fold.
   Capped and scrolled rather than left to grow, for the same reason the results
   list further up is a fixed height: what the card does to the page around it
   must not depend on how much is in it. */
/* `hidden auto` and not a bare `overflow-y`: with the other axis left at
   `visible` the spec computes it to `auto`, so a row wider than the box grew a
   horizontal scrollbar under a list that only ever scrolls down. Nothing is
   wide enough to need it now that both halves of a row truncate, and stating it
   is what keeps it that way.
   Padded and pulled back for the same 4px arithmetic as `.qstep-in` above: this
   is a second clip inside that one, and an outline is not part of scrollable
   overflow, so the ring on a focused `.qcart-l` or `.qcart-x` was cut off at
   both edges however much room the step around it left. */
.qcart {
  list-style: none; margin: calc(var(--step-5) - 4px) -4px -4px; padding: 4px;
  display: grid; gap: var(--step-2);
  max-height: 208px; overflow: hidden auto; overscroll-behavior: contain;
  scrollbar-width: thin; scrollbar-gutter: stable;
}
.qcart li { display: flex; align-items: center; gap: var(--step-2); }
/* The row IS the edit control. Pressing it puts that gift back in the entry row
   above, which is the same one row it was typed in the first time: editing a
   gift may not cost the page a second form any more than adding one may. */
.qcart-l {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: baseline; gap: var(--step-3);
  padding: var(--step-3) var(--step-4);
  background: var(--bg-1); color: var(--text-1);
  border: 1px solid var(--border-str); border-radius: var(--r-sm);
  font: inherit; font-size: 15px; text-align: left; cursor: pointer;
  transition: border-color 140ms ease-out;
}
.qcart-l:hover { border-color: rgb(var(--accent-rgb) / .45); }
.qcart-w {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The gift, and the long half of the row: an item name runs to the 80
   characters `ITEM_MAX` allows. Truncated exactly like the name beside it -
   `nowrap` on its own only guaranteed the row could not fit, and with no
   `min-width: 0` a flex item cannot shrink under its own content either. */
.qcart-g {
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--font-mono); font-size: 13px; color: var(--accent);
}
/* No border and no fill at rest, and the 44px stays. The row is the object; the
   cross is an affordance ON it. Drawn as a bordered box it was a second control
   of equal weight sitting against a bordered row, so a line the reader is only
   scanning arrived as two things to decide between. The edge comes back on hover
   and on focus, which is where a control has to say it is one.

   `transparent` rather than dropping the border, so nothing moves by a pixel
   when it appears. The 44px is the pointer target and is not negotiable at any
   visual weight - it is the whole reason this is comfortable with a thumb. */
.qcart-x {
  flex: 0 0 auto; width: 44px; height: 44px;
  background: none; color: var(--text-3);
  border: 1px solid transparent; border-radius: var(--r-sm);
  font: inherit; font-size: 20px; line-height: 1; cursor: pointer;
  transition: border-color 140ms ease-out, color 140ms ease-out, background-color 140ms ease-out;
}
.qcart-x:hover, .qcart-x:focus-visible {
  background: var(--bg-0); border-color: rgb(var(--accent-rgb) / .55); color: var(--text-1);
}

/* Where the Robux come from: a ladder of prices whose top rung is the
   customer's own account. A listing carries no username, no Roblox id and no
   owner, deliberately, so the price is not one column of a row about an account
   -- it IS the row, and the layout says so by setting it at the size a heading
   would be. Anything else here would be decoration filling a gap that is
   supposed to be empty.
   A label rather than a card with a control in it: the whole rung is the target,
   which is what makes these usable with a thumb. */
.rents { list-style: none; margin: var(--step-5) 0 0; padding: 0; display: grid; gap: var(--step-2); }
.rent {
  display: grid; grid-template-columns: auto auto 1fr; align-items: baseline;
  gap: var(--step-1) var(--step-3);
  padding: var(--step-3) var(--step-4);
  background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--r-sm);
  cursor: pointer; transition: border-color 140ms ease-out;
}
.rent:hover { border-color: rgb(var(--accent-rgb) / .45); }
.rent:has(.g-src:checked) { border-color: rgb(var(--accent-rgb) / .55); background: var(--surface-1); }
/* Arriving on a rung and having chosen it are two different states, and a
   keyboard reader is in the first one for as long as it takes to read the
   prices. The row is the control, so the row is what takes the ring - the 16px
   radio was carrying it alone inside a rung it is one twelfth of.
   A ring rather than the fill above, because a person can be on a rung they
   have not picked, and one of the two states may not read as the other.
   The radio's own ring goes only where this one replaces it: written this way
   round, an engine without `:has()` drops both declarations together and keeps
   the native ring rather than losing every trace of focus. */
.rent:has(.g-src:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.rent:has(.g-src:focus-visible) .g-src { outline: none; }
.g-src { accent-color: var(--accent); width: 16px; height: 16px; align-self: center; }
.rent-p {
  font-family: var(--font-mono); font-size: 19px; color: var(--text-1);
  white-space: nowrap;
}
.g-src:checked ~ .rent-p { color: var(--accent); }
.rent-u { font-size: 14px; color: var(--text-3); }

/* Three columns do not fit a phone. At 320px the card's content box is about
   216px, and after the radio, the gaps and a nowrap price the `1fr` track that
   holds "per 1,000 Robux, one of ours" gets roughly 87px - three or four
   wrapped lines against a one line price sitting on the first baseline, so
   every option in the list reads as a broken row. Two columns instead, and the
   caption on its own full width line under the price. */
@media (max-width: 30rem) {
  .rent { grid-template-columns: auto 1fr; }
  .rent-u { grid-column: 2 / -1; }
}

/* The receipt on the last part: one row per player, their gifts added up.
   Nothing writes it without a script, and an empty description list still
   carries the margin every `.bill` has, which would read as a gap somebody
   left in the card. */
#g-sum:empty { display: none; }
#g-sum dt { color: var(--text-1); font-size: 15px; min-width: 0; }
#g-sum dd { font-size: 14px; color: var(--accent); }

/* Somebody who asked for less movement gets the part outright. The block near
   the top of this sheet already switches every animation off with `!important`,
   which is what makes that happen; this is here so the two are read together,
   and because the unfold is a HEIGHT animation -- the one kind that leaves a
   part collapsed rather than merely unanimated if it is ever half disabled. */
@media (prefers-reduced-motion: reduce) {
  .qstep, .qstep.is-new { animation: none; grid-template-rows: 1fr; opacity: 1; }
  .qstep.is-new::before { animation: none; transform: none; }
}

/* the recording opt-in ---------------------------------------------------- */
/* One checkbox, and the only one on the site, which is why it is a rule here
   rather than a shared control: a second one earns the abstraction, this one
   does not. Sits inside the pay form, above the button, so the price it names
   is read before the press it changes. */
.check-row {
  display: flex; align-items: flex-start; gap: var(--step-3);
  margin-bottom: var(--step-4); text-align: left;
  font-size: 0.92rem; line-height: 1.45; color: var(--text-2);
  cursor: pointer;
}
/* Big enough to hit on a phone without a tap target rule of its own, and not
   shrunk by the flex row when the label beside it wraps to three lines. */
.check-row input { flex: none; width: 18px; height: 18px; margin-top: 2px; accent-color: var(--accent); }
