/* Inner pages: the forms and panels behind a sign in, and the widgets on them.
 *
 * Split out of app.css on 2026-08-01, which was 1522 lines against a ceiling of
 * 750. The cut is a contiguous slice, not a regroup, so the cascade is byte for
 * byte what it was.
 *
 * ORDER MATTERS. base.html links tokens, then app, then this, then motion, and a
 * rule here beats the same selector in app.css only because it is linked after.
 * Reordering the links is a redesign, not tidying. */

/* inner pages -------------------------------------------------------------- */

.page-head { position: relative; padding: var(--step-20) 0 var(--step-12); }
/* The glow is anchored to the CONTENT, not to the viewport, and above about
   1550px those are two different places. `.wrap` stops growing at 1180px and
   centres, so the text column walks right as the window widens while a plain
   `left: 12%` walks right more slowly: measured at 2560px the 900px box sat at
   307-1207 with the column at 690-1870, which put most of the glow in the empty
   left margin and left the whole right half of the head unlit. `50% - 590px` is
   the column's own left edge, and `max()` keeps the old value below the width
   where the two cross, so nothing moves at any width this was drawn at. */
.page-head::after {
  content: ""; position: absolute; pointer-events: none;
  left: max(12%, calc(50% - 590px)); top: -280px; width: 900px; height: 620px;
  background: radial-gradient(45% 45% at 50% 50%, rgb(var(--accent-rgb) / .13), transparent 72%);
}
/* CENTRE, not start or end. Every page head is the same shape: a short text
   column beside a tall form card. Aligning the short column to the top pooled
   all the leftover height into one dead area under the lede (measured on
   /rent: text ended at 390px against a card running to 800px, so 400px of
   nothing), and aligning it to the bottom just moves the same hole upwards.
   Centred, the slack splits evenly above and below and reads as margin rather
   than as a gap where something failed to load. Same fix on all four pages
   because all four have the same shape. */
.page-head .wrap {
  position: relative; z-index: 1;
  display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.85fr);
  column-gap: var(--step-10); align-items: center;
}
.page-head .eyebrow { grid-column: 1; grid-row: 1; }
.page-head h1 { grid-column: 1; grid-row: 2; font-size: clamp(38px, 5.6vw, 66px); margin-top: var(--step-4); }
/* The heading's own lede, and nothing else. Two separate escapes had to close.
   Matching every DESCENDANT `p` claimed the paragraphs inside the quote card
   beside the heading; narrowing to a direct child fixed those pages and missed
   the ones with no quote card — `/account`, `/gifting/{id}`, `/rent/{id}`,
   `/swap/{id}` — where the notice IS a direct child of `.wrap`. There it landed
   in `grid-column: 2 / grid-row: 2`, the cell the lede already occupies, so the
   two superimposed and the 10%-alpha notice let the paragraph read straight
   through it, at 17px over its own 14px with half its padding.
   `:not(.notice)` is the fix rather than more specificity: a notice is a status
   message, not a lede, so the rule must not MATCH it — outweighing it would
   still leave it in the wrong grid cell. */
.page-head .wrap > p:not(.notice) {
  grid-column: 2; grid-row: 2; color: var(--text-2);
  font-size: 19px; line-height: 1.5; max-width: 40ch; padding-bottom: 6px;
}
/* Under the title, in the left column. Every paragraph in a page head is placed
   into column two row two by the rule above, so a second one lands on top of
   the first: the date sat across the middle of the lede. Same specificity as
   that rule and written after it, which is what makes it win. */
.page-head .wrap > p.doc-date {
  grid-column: 1; grid-row: 3; margin-top: var(--step-4);
  font-size: 13px; color: var(--text-3); max-width: none; padding-bottom: 0;
}
/* The balance, under the name that owns it. Third paragraph to need its own
   cell for the same reason as the date above: /account carries two direct
   paragraphs, the figure and the note explaining USDT, and the lede rule put
   both in column two row two — a 44px number printed straight through a 19px
   sentence. It reads better here anyway. The number is what somebody opened
   this page for, so it belongs under their name at the top left, and the note
   about what USDT is stays the lede on the right where every other head puts
   its explanation. */
.page-head .wrap > p.balance-line {
  grid-column: 1; grid-row: 3; margin-top: var(--step-5);
  max-width: none; padding-bottom: 0;
}

/* A notice in a page head is the message that CAUSED the redirect - "Not enough
   on your balance", "That order is already paid" - so it must be the first thing
   read, and until now it was the last. The rule above deliberately does not
   match it, which left it with no cell at all, so it was auto-placed into the
   first free one: column two, ROW ONE, the top right corner. Worse, the head's
   `align-items: center` then sized row one to it, leaving the 11px eyebrow
   floating in the middle of a 90px band of nothing.
   Full width and under the title: a status message is not a column of a layout.
   Below 980px `motion.css` collapses to one column and source order already put
   it last, which is why this was only ever wrong on a wide screen.
   `:not(.has-quote)` because those three heads run a six row track where row 4
   is the LEDE, and they place their notice inside the card anyway. The three
   pages that carry a direct notice - /account, /gifting/{id}, /gifting/cart/{id},
   /rent/{id}, /swap/{id} - are all plain heads. */
.page-head:not(.has-quote) .wrap > p.notice {
  grid-column: 1 / -1; grid-row: 4; margin-top: var(--step-5);
}

/* The quote card is taller than the three text rows beside it, so the row
   track has slack to give away and the only question is WHERE it goes.
   `auto auto 1fr` put every pixel of it under the lede: on /rent the text
   ended at 390px against a card running to 800px, so the left half of the
   head was 400px of nothing and read as a page that had failed to finish
   loading. Bottom-aligning only moves the same hole above the eyebrow.
   A flexible row on BOTH ends splits the slack, so the text block sits against
   the middle of the card and the leftover reads as margin. All four heads are
   this same shape, so this is the one place it needs fixing.
   The two rows are not equal. Geometric centring reads low, and the taller the
   card the worse it gets: /swap's is 888px, and an even split put 308px of
   nothing above the eyebrow, so the page opened on an empty top left quadrant.
   At 0.55fr over 1fr the headline sits a little above centre, which is where
   the eye expects it. Fixing the top row instead was tried and is wrong: it
   makes the slack asymmetric again on the shorter heads. */
.page-head.has-quote .wrap {
  align-items: start; grid-template-rows: 0.55fr auto auto auto auto 1fr;
}
/* The gifting card is the only one on the site that changes height while it is
   being used: gifts are added to it. Splitting the leftover height above and
   below the text column, which is what those two flexible rows do, makes the
   position of the headline a function of the card's height, so every gift added
   moved the heading down the page under the customer while they were reading
   it. Here the text starts at the top and the whole of the slack pools below
   it: the heading holds still whatever the card does. The other three heads
   keep the centring, because their cards are one height from open to submit. */
.svc-gift .page-head.has-quote .wrap { grid-template-rows: auto auto auto auto auto 1fr; }
.page-head.has-quote .eyebrow { grid-column: 1; grid-row: 2; }
.page-head.has-quote h1 { grid-column: 1; grid-row: 3; align-self: start; }
.page-head.has-quote .wrap > p:not(.notice) {
  grid-column: 1; grid-row: 4; padding-bottom: 0;
  margin-top: var(--step-5); max-width: 46ch;
}
/* Three facts under the lede, so the column beside a tall card carries proof
   instead of air. Every one is answerable from the page's own body copy, and
   none of them repeats a figure the card already shows: a second copy of the
   price is not proof, it is the same sentence twice. This is also why it is
   three short facts rather than another paragraph. The row is optional; a head
   without one leaves an auto row at zero height. */
.proof {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--step-5) var(--step-6); margin-top: var(--step-6);
  padding-top: var(--step-6); border-top: 1px solid var(--border);
}
.page-head.has-quote .proof { grid-column: 1; grid-row: 5; }
.proof > div { display: grid; gap: var(--step-2); align-content: start; }
.proof b {
  font-family: var(--font-display); font-size: 17px; font-weight: 600;
  letter-spacing: -0.01em; color: var(--text-1);
}
.proof span { font-size: 13px; line-height: 1.5; color: var(--text-3); }

/* `1 / -1`, not a span count: the row list above can grow without this
   silently leaving the card short of the last row. */
.page-head.has-quote .quote { grid-column: 2; grid-row: 1 / -1; align-self: start; }
/* One column, and the rows have to move too: the card spans the whole track
   beside the heading, so without this it stacks straight on top of the heading
   and hides it. Order is reading order. Putting the card above the lede was
   tried, to keep the form reachable without scrolling, and it costs more than it
   buys: the lede is the sentence that says what the page sells, and a phone
   then met the form before it. Two lines of context is a cheap fifty pixels.
   The flexible rows are dropped here: on one column there is no taller
   neighbour to centre against, so they would only add dead height. */
@media (max-width: 980px) {
  .page-head.has-quote .wrap { grid-template-rows: repeat(5, auto); }
  .page-head.has-quote .eyebrow { grid-row: 1; }
  .page-head.has-quote h1 { grid-row: 2; }
  .page-head.has-quote .wrap > p:not(.notice) { grid-column: 1; grid-row: 3; margin-top: var(--step-4); }
  .page-head.has-quote .proof { grid-row: 4; }
  .page-head.has-quote .quote { grid-column: 1; grid-row: 5; margin-top: var(--step-6); }
  /* Three facts across a phone leaves four words per line. */
  .proof { grid-template-columns: 1fr; }
}

/* Off screen without being positioned off screen: a negative offset inside a
   relatively positioned body grows the scroll area. */
.skip { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.skip:focus {
  position: fixed; width: auto; height: auto; clip-path: none;
  left: var(--step-4); top: var(--step-4); z-index: 40;
}

/* Natural height, not stretched. This was `stretch` so a pair would end on the
   same line, and that is the nicer of the two when both panels carry about the
   same amount. When they do not, stretch has to put the difference somewhere,
   and every somewhere is inside a bordered box: measured at 1920, /rent held
   396px of nothing, /swap 197px, /policies 127px. A void inside a
   card reads as broken. The same void under the card is just background, so the
   ragged edge is the cheaper flaw. Pushing the slack to one chosen row instead
   was tried and is worse: it moves the hole into the middle of the copy.
   A sticky aside still opts out below, and now agrees with the default. */
.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(340px, 420px); gap: var(--step-8); align-items: start; }
.split > .sticky { align-self: start; }
/* Even halves, for a pair where neither side is the subordinate one. The
   default split is 1fr beside a 420px rail, which put the eight row chain
   minimums in the narrow column where they could only ever be one column
   tall, and stretched the panel beside it to match all that height with
   nothing to put in it. */
.split-even { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.sticky { position: sticky; top: 90px; }

.panel {
  position: relative;
  border: 1px solid var(--border); border-radius: var(--r-lg);
  background: linear-gradient(180deg, rgba(255,255,255,.028), rgba(255,255,255,0) 30%), var(--surface-1);
  box-shadow: var(--shadow-raised);
  padding: var(--step-8);
}
/* --step-8, to match the gap `.split` puts between its two columns. Below 980px
   that split collapses into one column, so the main panel and the aside end up
   32px apart in the same vertical stack where the aside's own panels sat 16px
   apart - one column, two rhythms. */
.panel + .panel { margin-top: var(--step-8); }
/* As direct children of the grid they are already spaced by its gap. */
.split > .panel + .panel { margin-top: 0; }
.panel h2 { font-size: 24px; }
.panel .eyebrow + h2 { margin-top: var(--step-3); }
/* A panel is 728px wide with 32px of padding, and a 24px heading sits above
   this. At 15px the copy under it read as a caption somebody forgot to finish,
   which is the note the same reviewer wrote four times. The measure stays wide
   on purpose: at 18px a 62ch line fills the panel instead of running thin down
   the middle of it. Raising the size without the measure only moves the gap. */
.lede { color: var(--text-2); margin-top: var(--step-4); font-size: 18px; line-height: 1.55; max-width: 62ch; }
.foot-note { color: var(--text-3); font-size: 15px; line-height: 1.55; margin-top: var(--step-5); max-width: 64ch; }
.panel-lit { border-color: rgb(var(--accent-rgb) / .22); background: linear-gradient(180deg, var(--surface-2), var(--surface-1)); }

/* A recording of a gift being handed over. The browser draws the controls, so
   the only thing to say here is that a 960px capture stays inside its panel on
   a phone instead of running out of the side of it. The black is what the frame
   is before any of it has loaded, and it is a literal rather than a token
   because it is a mat and not a theme colour: it is under a video, it is what a
   letterboxed frame shows at its own edges, and it would still be black on a
   site painted any other hue. Everything with a hue in these sheets is a token
   and there is a test. */
/* An eyebrow is an inline label with no margin of its own, which is right at the
   top of a panel and cramped against the rows above it in the middle of one. */
.clip-head { margin-top: var(--step-6); }
.clip {
  display: block; width: 100%; margin-top: var(--step-4);
  border: 1px solid var(--border); border-radius: var(--r-md); background: #000;
}

/* A panel header that carries its own mark. The pages behind /rent and
   /swap were paragraphs and definition lists all the way down, so a reader
   scanning for the one panel they wanted had nothing but headings to aim at.
   Same framed plate as .tile-icon, so the two pages read as one product. */
.panel-head { display: flex; align-items: flex-start; gap: var(--step-5); }
.panel-head > div { min-width: 0; }
.panel-head h2 { margin-top: var(--step-2); }
.p-ic {
  flex: none; width: 46px; height: 46px; display: grid; place-items: center;
  border-radius: var(--r-md); color: var(--accent);
  background: linear-gradient(180deg, rgb(var(--accent-rgb) / .14), rgb(var(--accent-rgb) / .04));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.10), inset 0 0 0 1px rgb(var(--accent-rgb) / .18);
}
.p-ic .ic { width: 22px; height: 22px; }
.tag {
  font-family: var(--font-mono); font-size: 12px; color: var(--text-2);
  padding: 4px var(--step-3); border: 1px solid var(--border-str);
  border-radius: var(--r-pill); background: var(--bg-1);
}

.rowlist { display: grid; margin: var(--step-5) 0 0; }
.rowlist dt, .rowlist dd { margin: 0; }
/* Wraps rather than crushes, same reason as .bill: on a narrow phone the term
   was being squeezed to one word per line to keep its value on the same row. */
.rowlist div {
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline;
  gap: 0 var(--step-5); padding: var(--step-4) 0; color: var(--text-2); font-size: 16px;
}
.rowlist dt { min-width: 11ch; }
.rowlist dd { margin-left: auto; }
.rowlist .mono { color: var(--text-1); font-size: 16px; }

/* quote widget ------------------------------------------------------------- */

.quote {
  border: 1px solid var(--border-str); border-radius: var(--r-lg);
  background: var(--surface-glass);
  -webkit-backdrop-filter: blur(20px) saturate(150%);
          backdrop-filter: blur(20px) saturate(150%);
  box-shadow: var(--shadow-raised); padding: var(--step-8);
}
/* An eyebrow on the left and the page's own tag on the right, and on /rent that
   tag is the longest on the site: "$3.50 to $8.00 per 1,000" is about 199px of
   12px mono inside 24px of pill padding, beside 125px of letterspaced eyebrow.
   340px of content, and a 320px phone leaves this card 216px - the wrap is 40px
   off `.wrap` and 32px of padding either side. With nothing allowed to wrap,
   both sides were squeezed to two and three lines each.
   `flex-wrap` rather than a breakpoint, same argument as `.tier-row`: the rule
   fires off the width it actually needs, on whichever of the four pages has the
   long tag, with no number to keep in step. Below about a 444px viewport the
   tag drops to its own line and both sides fit whole. */
.quote-head {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--step-4); margin-bottom: var(--step-6);
}
.field { display: grid; gap: var(--step-2); }
/* Every other child of the quote card carries its own top margin; a field did
   not, and only ever looked spaced because whatever preceded it had a bottom
   margin. After the total, which has none, the "Roblox user id" label sat flush
   against the figure. Margins collapse in this block, so the fields that
   already had room keep exactly the room they had. */
.quote .field { margin-top: var(--step-6); }
/* And the same for a form that lives in a panel instead of a quote card, which
   is where the code box on a sign in sits. Without it the label sat flush under
   the sentence above it and read as the last line of that paragraph rather than
   as the name of the box below. Scoped to a form so a `.field` used as a plain
   row inside a panel keeps whatever spacing its own container gives it. */
.panel form .field { margin-top: var(--step-6); }
.field label { font-size: 13px; color: var(--text-2); }
.field input {
  height: 50px; width: 100%; padding: 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);
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 18px;
}
.field input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Itemised, because a small order total looks exactly like a worse rate if you
   only ever show the sum. Flex, not grid: a label beside its figure is never
   two columns worth of text. */
.bill { margin: var(--step-6) 0 0; display: grid; gap: var(--step-2); }
/* Wraps rather than crushes. On a 320px phone "A Roblox account" was being
   squeezed to 61px, one word per line, to keep its figure on the same row. */
.bill div {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 0 var(--step-4); flex-wrap: wrap;
}
.bill dt { color: var(--text-3); font-size: 13px; min-width: 11ch; }
.bill dd { margin: 0 0 0 auto; color: var(--text-2); font-size: 14px; }
[hidden] { display: none !important; }
.quote-out {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-top: var(--step-4); padding-top: var(--step-4);
  border-top: 1px solid var(--border-str);
}
.quote-out .k { font-size: 13px; color: var(--text-2); }
.quote-total {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 38px; font-weight: 600; color: var(--accent); letter-spacing: -0.04em;
}
/* 13px, for the same reason `.rent-why` below was raised off 12: this is the
   card's terms - the Robux the account has to hold, the bulk ladder rule, what
   the fee does and does not include - and 12px `--text-3` is the size and colour
   this site uses for a footnote. It was the smallest type on the site, inside
   the box that takes the money, under a 13px label and a 13px hint. */
.quote-note { margin-top: var(--step-4); font-size: 13px; color: var(--text-2); }

/* A line that explains the answer somebody just picked, with a FLOOR under its
   height. The card is the tallest thing in the page head, so the head's height
   is the card's height: without the floor, moving between a one line answer and
   a three line one resizes the card and visibly shoves the headline beside it
   up and down the page. Same fix, same reason, as the fixed height on `.hits`
   and the cap on `.qcart`, and the empty state costs a little air at 12px. */
/* 13.5px, not 12. This is the page's help text - it is what tells an owner
   which kind of account they have, and getting that wrong costs them a real
   Roblox sign in - and it was set at the size this site uses for its smallest
   footnotes. The 4.6em floor is kept rather than converted to pixels, so it
   still holds the tallest variant without the box changing height as the
   answer changes. */
.rent-why {
  margin-top: var(--step-3);
  font-size: 13.5px; line-height: 1.5; color: var(--text-3); max-width: 46ch;
}
/* The floor belongs to the ONE paragraph that swaps, not to the class. Two more
   carry `.rent-why` - the country explanation, which is fixed text of two or
   three lines, and the market line, which is served `hidden` - and both were
   reserving room for a fifth line that can never arrive. That is two dead bands
   in the middle of the form, and a gap in a form reads as a missing control. */
#r-why { min-height: 4.6em; }


/* rental price history ---------------------------------------------------- */
/* The one chart on the site. Classes only and no `style` attribute anywhere:
   the CSP carries no nonce, so an inline style is dropped with nothing on the
   page to say so and the lines would vanish silently. */
.chart { margin: var(--step-5) 0 0; }
.chart-svg {
  display: block; width: 100%; height: auto;
  overflow: visible;
}
/* Vector strokes that must not thicken when the svg is scaled up to the panel
   width, which is what `viewBox` does to everything inside it. */
.chart-cleared, .chart-asked {
  fill: none; stroke-linecap: round; stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.chart-cleared { stroke: var(--accent); stroke-width: 2; }
/* Asks sit BEHIND and read quieter: they include prices nobody accepted, so
   they are context rather than the answer. */
.chart-asked { stroke: var(--text-3); stroke-width: 1.5; stroke-dasharray: 4 4; }
.chart-axis {
  display: flex; justify-content: space-between;
  margin-top: var(--step-2);
  font-size: 0.85rem; color: var(--text-3);
}
