/* /@handle, both tabs: the request form, the two mocks and the board block.
Loaded after base.css by three pages and no others: the listing page, the
second step of a request (/request/<token>) and a results page.

The builder page is the public half of the board system the dashboard owns the
other half of, and base.css writes the two together (body.dash .brow,
body.profile .brow). The month page /@handle/spots/YYYY-MM carries
body.profile too and does NOT load this file, so nothing both of those pages
draw with may be written here.

What only this page draws is written here; what the month page draws as well
is in base.css, which is the file that page loads by itself. So the tabs, the
two mocks, the payments strip, the banner card and the sponsor form are here,
and the header, the faces, the board line, the month heading and the board
table are not. A different value for this page is written here as an override
of the shared rule, which is what the load order is for. */

/* T0:start */

/* -------------------------------------------------- the page's own rules

These draw on /@handle and on no other page, so they are written here rather
than in base.css: the two tabs, both mocks, the newest-payments strip, the two
links under the board, the banner card and the sponsor form. They are first in
the file so that the overrides further down answer them. */

/* The two tabs. Which one is open is read off the page root. */
body.profile .tabs { display: flex; gap: 4px; margin: 24px 0 16px; border-bottom: 1px solid var(--hairline); }

body.profile .tab {
  display: inline-flex;
  align-items: center;
  height: 40px;
  padding: 0 16px;
  font-family: var(--sans);
  font-size: var(--btn);
  color: var(--muted);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

body.profile .tab:hover { color: var(--ink); text-decoration: none; }

body.profile .tab-banner .tab[data-tab="banner"],
body.profile .tab-pin .tab[data-tab="pin"] { color: var(--ink); font-weight: 500; border-bottom-color: var(--accent); }

body.profile .tab-banner .panelpin,
body.profile .tab-pin .panelbanner { display: none; }

/* One product for sale: its name, where the tabs would be, as a plain label
   (2026-09-25). The row keeps the tab row's height and hairline so the box
   under it does not move; there is no underline and nothing to press. */
body.profile .prodsolo {
  display: flex;
  align-items: center;
  height: 40px;
  margin: 0;
  padding: 0 16px;
  font-size: 13px;
  color: var(--muted);
  border-bottom: 1px solid var(--hairline);
}

body.profile .prodpanel { margin: 0; }
body.profile .panelnote { margin: 16px 0 0; }
body.profile .previewnote { margin: 0 0 16px; }

/* The mock. The banner is the picture the sponsors are drawn on; the SVG over
   it shares the same 1500x500 canvas, so a target sits exactly on its circle.
   The 3:1 box is reserved before the picture arrives, so nothing shifts. */
/* The mock is a panel inside the offer panel, so it is flat: the box comes
   from `.panel .panel--flat` in the markup rather than from a rule here. */

body.profile .mockbanner { position: relative; line-height: 0; }

body.profile .mockimg {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 1;
  object-fit: cover;
  border-radius: var(--panel-radius) var(--panel-radius) 0 0;
  transition: opacity var(--t-mid) linear;
}

/* A listing that sells no spots and has banked no header of its own. No grey
   slab: the ground is the site blue and the three places a logo would go are
   dashed on to it, in the same 1500x500 canvas the picture is drawn in. */
body.profile .mockempty { background: var(--accent); }

/* The plain header, and the same ground behind it: a header X will not hand
   over leaves the hero the colour it would have been rather than a broken
   picture on white. */
body.profile .mockplain { background: var(--accent); }

body.profile .mockemptyspots,
body.profile .mockspots { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }

body.profile .mockemptyspots circle {
  fill: none;
  stroke: var(--on-accent);
  stroke-width: 4;
  stroke-dasharray: 12 12;
  opacity: 0.7;
}

/* The hero changes over rather than blanking: public/spot.js loads the new
   picture off the page, fades this one out and puts it in. */
body.profile .mockimg.is-fading { opacity: 0; }
body.profile .mspot circle { fill: transparent; stroke: transparent; stroke-width: 5; }
body.profile .mspot-open { cursor: pointer; }
body.profile .mspot-open circle { stroke-dasharray: 10 10; }

body.profile .mspot-open:hover circle,
body.profile .mspot-open:focus circle,
body.profile .mspot-picked circle { stroke: var(--accent); }

body.profile .mockid { position: relative; padding: 0 20px 12px; line-height: 1.5; }

body.profile .mockavatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  border: 2px solid var(--surface);
  margin-top: -24px;
  display: block;
}

body.profile .mockname { margin: 8px 0 0; font-weight: 500; }
body.profile .mockhandle { margin: 4px 0 0; font-family: var(--mono); font-size: var(--help); color: var(--muted); }
body.profile .mockhandle a { color: inherit; }
body.profile .mockavatar + .mockhandle { margin-top: 8px; }
body.profile .mocklinks { margin: 8px 0 0; font-size: var(--help); }

body.profile .mockpin {
  margin: 0 20px 20px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 12px 16px;
}

body.profile .mockpinlbl {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--eyebrow);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

body.profile .mockpinhere { margin: 0; font-size: var(--body); display: none; }

body.profile .tab-pin .mockimg { opacity: 0.4; }
body.profile .tab-pin .mockspots { display: none; }
body.profile .tab-pin .mockpinlbl { display: none; }
body.profile .tab-pin .mockpinhere { display: block; }
/* A dashed outline, not the accent (paint7): a solid blue box read as a text
   field to type into. Dashed says "a place held for your post". */
body.profile .tab-pin .mockpin { border: 1px dashed var(--muted); background: var(--surface); }

/* The newest payments: one row of up to three, each the same width, each cut
   with an ellipsis rather than wrapped on to a second row. */
body.profile .latest {
  list-style: none;
  margin: 0 0 16px;
  padding: 0;
  display: flex;
  flex-wrap: nowrap;
  gap: 8px;
}

body.profile .latestrow {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 8px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
}

body.profile .latestrow .avatar { width: 20px; height: 20px; border-radius: 50%; flex: none; }
body.profile .latestrow .lwho { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
body.profile .latestrow .lamt { flex: none; margin-left: auto; }
body.profile .latestrow .lago { color: var(--muted); flex: none; }

/* On a phone a chip is the only cell on its row.

   Everything on a chip but the handle is fixed: the face is 20, the three
   gaps 24, the amount about 22, "just now" about 46, the padding 16. That is
   130 before a single letter of the name. Three hundred pixels shared between
   two chips leaves each of them sixteen, which is two characters, and the
   handle is the one thing on the chip a reader is there for. So the strip
   wraps below the same breakpoint the board rows use and each chip takes the
   whole width, which leaves the handle about 170 and no ellipsis at all. */
@media (max-width: 560px) {
  body.profile .latest { flex-wrap: wrap; }
  body.profile .latestrow { flex: 1 1 100%; }
}

/* The two links under the board: show the rest of this month, and the month
   before this one. */
body.profile .boardmore { margin-top: 16px; font-size: var(--help); }
body.profile .boardmore summary { cursor: pointer; color: var(--accent); }
body.profile .boardmonth { margin: 16px 0 0; font-size: var(--help); }
body.profile .boardmonth a { color: var(--muted); }

/* The sponsor's form. It is the one primary press on the tab. */
body.profile .spotcard { margin: 0; }

/* The price line on this card is the pin card's line, word for word (prompt
   Y3), so it is drawn the same way: one size, and each piece holds together
   so a wrap lands between the pieces and never inside one. */
body.profile .spotcard .price {
  margin: 0 0 16px;
  font-family: var(--mono);
  font-size: var(--h1);
  line-height: 1.25;
}

body.profile .spotcard .price .pricepart { white-space: nowrap; }
body.profile .spotcard h2 { font-size: var(--h2); margin: 0 0 16px; }
body.profile .panelbanner .error { margin: 0 0 16px; }

/* ------------------------------------------------ the sponsor's form (Y4)

   One form on both tabs, so everything particular to it is written once here
   and the two cards only place it. The order it is read in is the order it is
   answered in: the dates, what is being placed, who is placing it, what it
   comes to, the tick, the press. */
body.profile .reqform { margin: 0; }
body.profile .reqform textarea { width: 100%; }

/* The three steps, each with a small label over it, so a reader can see where
   they are in a long form without having to read it. */
body.profile .reqstep { margin: 0 0 24px; }
body.profile .reqsteph {
  margin: 0 0 8px;
  font-family: var(--sans);
  font-size: var(--label);
  font-weight: 500;
  line-height: 1.5;
  color: var(--muted);
}

/* The calendar IS the picker. Start and Days under it are the readout of what
   was pressed - and the whole form for a reader with no script, which is why
   they are quieter rather than gone. A date needs the width of a date. */
body.profile .reqform .dateread { margin-top: 8px; }
body.profile .reqform .dateread label { color: var(--muted); }
body.profile .reqform .dateread input { max-width: 160px; }

/* No run of open days to offer: the sentence sits under the grey grid, where
   the dates would have been. */
body.profile .reqform .reqshut { margin: 8px 0 0; }

/* What the run comes to, directly over the press (prompt Y3): the last thing
   read before the button is what the button costs, so it sits closer to the
   press under it than to the tick above it. */
body.profile .reqform .reqtotal {
  margin: 24px 0 0;
  font-size: var(--body);
  font-variant-numeric: tabular-nums;
}

body.profile .reqform label.check { margin: 16px 0 0; }

/* One press, and it is the primary one: full width at every size, because
   nothing beside it shares the row. */
body.profile .reqform .reqbtns { margin-top: 24px; }
body.profile .reqform .reqbtns .btn { width: 100%; }
body.profile .reqform .reqnote { margin: 8px 0 0; }

/* Fixed mode has no amount to type. The cell beside Category holds the price
   as a line rather than a field nobody can use, on the label and the control
   height every other cell on the grid takes. */
body.profile .amountfixed .flabel {
  margin: 0 0 8px;
  font-family: var(--sans);
  font-size: var(--label);
  font-weight: 500;
  line-height: 1.5;
  color: var(--ink);
}

/* The gap is the row's own, not a space in the markup: the price is a flex
   item and a flex container drops the whitespace between its items, so
   "$100 · fixed" would otherwise read "$100· fixed". */
body.profile .amountfixed .famount {
  display: flex;
  align-items: center;
  gap: 4px;
  height: var(--ctl-h);
  margin: 0;
  font-size: var(--body);
}

/* The pin card. It is a card like every other section on these pages; only
   what is particular to it is written here. */
body.profile .buycard { margin: 0; }

body.profile .buycard .price { margin: 0 0 4px; font-family: var(--mono); font-size: var(--h1); line-height: 1.25; }

/* Two prices on one line, and the column is not always wide enough for both.
   Each price holds together, so the line breaks between them and never
   between a number and the length it is for. */
body.profile .buycard .price .pricepart { white-space: nowrap; }
body.profile .buycard .rule { margin: 0 0 16px; }
body.profile .buycard .help { margin: 0 0 16px; }

body.profile .buycard form { margin: 0 0 16px; }
body.profile .buycard select { margin: 0 0 16px; }
body.profile .buycard .chips { margin: 0 0 8px; }
body.profile .buycard .chiplegend { margin: 0 0 16px; }

/* Six months, one word each, the same width, and the state is the style: an
   outline is open, dashed is held, grey and struck through is booked. Three
   and three on a phone, where six will not fit. */
body.profile .monthchips > .chip { flex: 1 1 0; min-width: 48px; max-width: none; padding: 0 4px; }
body.profile .chips .chip-booked { text-decoration: line-through; }

/* Six will not fit across a phone, so the floor is raised to the width that
   makes the row break three and three rather than five and one. */
@media (max-width: 560px) {
  body.profile .monthchips > .chip { min-width: 88px; }
}
body.profile .buycard .ptrustline { margin: 0 0 16px; }
body.profile .buycard > :last-child { margin-bottom: 0; }
body.profile .buylinks { margin: 16px 0 0; font-size: var(--help); line-height: 1.5; }
body.profile .pmeta { font-size: var(--help); color: var(--muted); margin: 8px 0 0; }

/* How long: one bar of three, a month or one of the two halves.

   The bar writes the `half` field. The other field the booking route reads,
   `duration`, rides on the submit button, and there are two of those with one
   shown: a form sends the name and value of the button that was pressed and of
   no other. So the two fields arrive exactly as the booking route reads
   them and nothing here needs a script. Without :has() the month button is the one
   shown, which is the same fallback the two prices had before. */
body.profile .lengthfield { border: 0; padding: 0; margin: 0 0 16px; min-width: 0; }

body.profile .lengthfield legend {
  padding: 0;
  margin: 0 0 8px;
  font-family: var(--sans);
  font-size: var(--label);
  font-weight: 500;
  color: var(--ink);
}

body.profile .seg.lengthseg { display: flex; width: 100%; }
body.profile .seg.lengthseg .segopt { flex: 1 1 0; min-width: 0; padding: 0 8px; }


/* What the builder posts, under the facts. Read at the last handshake and
   printed as text: nothing here loads from x.com. */
body.profile .xposts { margin-top: 32px; }
body.profile .xposts-head { font-size: var(--body); margin: 0 0 12px; }

/* The heading is a plain line above the stack, not the head of a box: the
   posts are not inside a panel of their own, so each card is a panel and
   nothing here is a box inside a box. The hairlines that used to separate the
   rows are gone with the boxes: a gap says the same thing. */
body.profile .xpostlist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

body.profile .xpost { padding: 16px 20px; }

/* The whole card is the link to the post on X (2026-09-25). The link is the
   date on the handle line; its ::before is stretched over the card, so a
   press anywhere on it opens the post. The links inside the words are lifted
   above that layer and stay pressable on their own. The ground under the
   pointer is the directory row's. The lift for the words' own links is on
   the .xpost-text a rule below. */
body.profile .xpost {
  position: relative;
  transition: background var(--t-quick) ease-out;
}

body.profile .xpost:hover { background: var(--row-hover); }

body.profile a.xpost-go { color: inherit; text-decoration: none; }

body.profile a.xpost-go::before {
  content: "";
  position: absolute;
  inset: 0;
}

/* The face and the words, the way X sets them: a column that does not move
   and a column that takes the rest. min-width on the second is what lets a
   long unbroken address wrap instead of pushing the card wide. */
body.profile .xpost-row { display: flex; align-items: flex-start; gap: 12px; }
body.profile .xpost-av { width: 40px; height: 40px; }
body.profile .xpost-col { flex: 1 1 auto; min-width: 0; }

/* Who posted it and when, on the line the face is level with. */
body.profile .xpost-who {
  margin: 0 0 4px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--ink);
  overflow-wrap: anywhere;
}

body.profile .xpost-when { color: var(--muted); }

body.profile .xpost-tag {
  font-family: var(--sans);
  font-size: var(--eyebrow);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  margin: 0 0 12px;
}

/* The post itself. A timeline is read down, not across, so three posts a
   sponsor can take in beat three posts they have to scroll past. The size is
   the bio's: it is the same kind of reading. */
body.profile .xpost-text {
  margin: 0;
  font-size: var(--body);
  line-height: 1.5;
  overflow-wrap: anywhere;
  /* Three lines at most on this page, long post or short (2026-09-25): the
     card is a way to the post, not the post. The whole text stays in the
     markup; the clamp only hides what is past the third line. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}

body.profile .xpost-text a { position: relative; z-index: 1; color: inherit; }

/* On a phone the card keeps the face at 40 and gives back the side air. */
@media (max-width: 640px) {
  body.profile .xpost { padding: 12px 16px; }
}

body.profile .blurb { color: var(--muted); }

/* T0:end */


/* ------------------------------------------------------------- the width

The listing is the page with the most on it: a mock, a two-column form and a
board. That is a layout rather than a reading column, so it gets the width the
layout needs. main is border-box and carries 20 of padding either side, so the
content is 1040 and the form card inside it is 992. */
body.profile main,
body.profile header.top,
body.profile footer { max-width: 1080px; }

/* Every card on this page runs the full width of it. base.css caps a card at
   --content-max (880), which is the right cap for a page of prose and the
   wrong one for a form laid out in two columns. */
body.profile .card { max-width: none; }

/* ------------------------------------------------------------ the header

The builder's own line to sponsors. It is a fact about the builder rather
than about one product, so it sits under the facts and outside the tabs, in
the same quiet type the facts are in. */
body.profile .pnote {
  margin: 8px 0 0;
  font-size: var(--help);
  line-height: 1.5;
  color: var(--muted);
}

/* -------------------------------------------------------------- the mock

No line under the picture saying where the placement is: the picture is that.

The mock is the full width of the container, the same width as the form card
under it, so the two read as one column rather than as a narrow picture with a
wide form beside nothing. The cap that holds the artwork down is on the
ARTWORK and not on the mock: 280, which is width / 3 up to 840 wide and a crop
above it.

A crop moves sponsor targets off their logos unless both halves of it agree,
so they are made to: the picture is object-fit: cover, and the SVG of circles
over it carries preserveAspectRatio="xMidYMid slice", which is the same crop
of the same 1500x500 canvas. The avatar, the handle and the pinned box under
it stay the width of the thing they sit on. */
body.profile .mockbanner { max-height: 260px; overflow: hidden; }
body.profile .mockimg { max-height: 260px; }

/* The ring around the picture's own ring on an open spot, and the word under
   the mock that says what it is. The picture draws a 28-unit circle in a
   1500-unit canvas, which comes out 21px at 1280 and 13px at 390: the spot is
   there, but it does not read as an offer and the "open" printed inside it is
   not legible at either width. The overlay ring is 18 units outside it, in
   the brand colour because the spot sits on the strip's light ground, and it
   is not a bigger spot: the target, the position and the radius the picture
   drew are all unchanged. */
body.profile .mspot-open circle.mopen {
  fill: none;
  stroke: var(--accent);
  stroke-width: 6;
  stroke-dasharray: 14 12;
}

body.profile .mspot-open:hover circle.mopen,
body.profile .mspot-open:focus circle.mopen { stroke-dasharray: none; }

/* The "Open from <day>" line is the tail of the 90-day strip's own title, at
   the bottom of the same pane, so the day this listing opens on is printed
   once and nothing here draws it under the picture. */

/* On a phone the mock is the first thing under the tabs, so the artwork is
   held shorter still: it is the picture of the product, not the page. */
@media (max-width: 560px) {
  body.profile .mockbanner { max-height: 200px; }
  body.profile .mockimg { max-height: 200px; }
}

/* --------------------------------------------------------- the price line

One line, mono, 18, and the same line in the same size on both tabs. base.css
gives it --h1 (28), which is a price shouting over the form it belongs to. The
refund promise under it on the pin tab is a .help line and stays one. */
/* 15, not 18: the fold's left column is about 407 wide at 1280 and the whole
   sentence is 36 mono characters, which needs about 410 at 18 and about 325
   at 15. At 18 it wraps, and a price that takes two lines is not a price
   line. 15 is the size the third product's own price line (.pricelead) is set
   at, so this is the site's size for the sentence rather than a new one. */
body.profile .spotcard .price,
body.profile .buycard .price { font-size: var(--body); }

/* The day price leads (paint7): it is the number the charge is worked out in,
   so it is the one piece in the ink and at the heavier weight. The month and
   the window after it are context and step back to muted. Weight and colour,
   not size. */
body.profile .spotcard .price,
body.profile .buycard .price { color: var(--muted); }
body.profile .spotcard .price .pricepart:first-child,
body.profile .buycard .price .pricepart:first-child { color: var(--ink); font-weight: 600; }

/* ------------------------------------------------ the offer panel (P6)

One bordered container under the header, in the site's own box style: the
values are base.css's `.ibx, .pofferbox`, which is the inbox's box, so the
site has one container and not two. The tab row is the header row inside it,
and under the row two panes split by a hairline.

The panes are the two cells of one grid with `align-items: stretch`, so both
are the full height of the row and every tab ends on the same edge: the left
pane holds the total, the press and the two quiet lines against the bottom of
the box, and the right pane holds the strip (or the posts tab's impressions
line) against it. On the posts tab, which has the least in it, the air is left
in the middle rather than at a ragged bottom.

1 / 1.1, the owner's numbers: the mock is the wider of the two because it is
the picture of the product. No gap between the panes, because there are no
loose cards to separate: the hairline is what divides them.

Below 900 the panes stack INSIDE the same box and the MOCK GOES FIRST, so the
phone order is header, tabs, mock, price, calendar, readout, total, button.
That is the one place source order and reading order differ, and it is written
here rather than in the markup because the desktop order (form first) is the
one a screen reader should hear on a wide window too. The right pane is
`display: contents` while they are stacked, so its two blocks take their own
places in the one column - the mock over the form, the strip under it. No auto
margins there: a column as tall as its own content has no free space to push a
block into, and the button follows the calendar the way it reads.

16 is the padding of the inbox's own panes. */
body.profile .pofferbox { margin: 16px 0 0; }

/* The tab row is the box's header row: no margin of its own, and the tab's
   own 16px of padding is what starts its text on the pane's own left edge. */
body.profile .pofferbox .tabs { margin: 0; padding: 0; }

body.profile .pofferpanes { display: flex; flex-direction: column; }

body.profile .pofferleft,
body.profile .pofferright,
body.profile .poffermock,
body.profile .pofferend { min-width: 0; }

body.profile .pofferleft {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  order: 0;
  padding: 16px;
}

body.profile .pofferright { display: contents; }
body.profile .poffermock { order: -1; padding: 16px; }
body.profile .pofferend { order: 1; padding: 16px; }

/* Hairlines, never a second box: while the panes are stacked the three blocks
   are separated the way the two panes are. */
body.profile .poffermock { border-bottom: 1px solid var(--hairline); }
body.profile .pofferend { border-top: 1px solid var(--hairline); }

/* No card inside the box. The pane's own padding is the padding and the
   box's own border is the border, so the three panels, the strip and Past
   sponsors are plain blocks with hairlines between them. */
body.profile .pofferbox .card {
  max-width: none;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  /* A card carries `.panel`, and a panel inside a panel is flat. This is
     the same reset one step further. */
  box-shadow: none;
}

/* The impressions line is the posts tab's own bottom block, in the place the
   strip has on the two dated tabs. One of the two is drawn, never both, off
   the root's tab class - the same switch the two mocks over them use. */
body.profile .postmockfacts { display: none; }
body.profile .tab-posts .postmockfacts { display: block; }

@media (min-width: 900px) {
  body.profile .pofferpanes {
    display: grid;
    grid-template-columns: 1fr 1.1fr;
    align-items: stretch;
  }

  /* The hairline between the panes. It is the left pane's own right edge, so
     it runs the full height of the row whichever pane is the taller. */
  body.profile .pofferleft { border-right: 1px solid var(--hairline); }

  body.profile .pofferright {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    padding: 16px;
  }

  /* The pane carries the padding and the blocks inside it carry none, so the
     two panes' contents start on the same line. */
  body.profile .poffermock,
  body.profile .pofferend { padding: 0; border: 0; }

  /* The bottom edge, both panes. */
  body.profile .pofferend { margin-top: auto; }
  body.profile .pofferleft .reqrest { margin-top: auto; }

  /* What makes that auto margin have somewhere to go: the panel that is open,
     the card inside it and the form inside that are each as tall as the pane.
     Only the OPEN panel is named, because the other two are display: none off
     the root's tab class and a rule in this file would win that tie.

     align-items: stretch on every step, because .reqform is a grid with
     `align-items: start` on it and a flex column that inherited start would
     shrink every field to the width of its own text. */
  body.profile .tab-banner .pofferleft .panelbanner,
  body.profile .tab-pin .pofferleft .panelpin,
  body.profile .tab-posts .pofferleft .panelposts,
  body.profile .pofferleft .bformcol,
  body.profile .pofferleft .card,
  body.profile .pofferleft .reqform {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    flex: 1 1 auto;
  }

  /* The one form that is not stretched: no day on the grid can be pressed, so
     the column is a sentence and the way to ask about it. Stretching that to
     the height of the mock beside it leaves a column of empty paper with a
     line marooned at the bottom of it, so the form ends where the words end
     and the rest of the pane stays empty. The auto margin that pushes the
     bottom block down has nothing to push here. */
  body.profile .pofferleft .reqform.reqshutform { flex: 0 0 auto; }

  body.profile .pofferleft .reqform.reqshutform .reqrest { margin-top: 0; }
}

/* The form is ONE column. The fold above owns the page's two columns, and
   the fields that would fill a second one are on the second screen of a
   request. What the form holds is a short stack: price, what it buys,
   calendar, readout, the two plain fields, the total, the button, two quiet
   lines. */
body.profile .reqform {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 24px;
  align-items: start;
}

body.profile .reqform > .reqdates,
body.profile .reqform > .reqrest { min-width: 0; }

/* The grid gap is the gap between the two boxes, so the last step inside one
   does not add a second one under itself. */
body.profile .reqform .reqdates .reqstep:last-child { margin-bottom: 0; }

/* One month at a time in this column (views/cal.js shown: 1), so the pair of
   month arrows is the only way through the rest and is never hidden by a
   width. */
body.profile .reqform .cal .calmonths { gap: 16px; }

/* The month and the two arrows are ONE header row: the name at the left, the
   presses at the right, the way every month picker is drawn. As two rows, a
   40 tall bar of arrows over a 26 tall caption, that is 82px of calendar
   before the first day.

   The nav is taken out of the flow and laid over the caption's own row rather
   than the markup being reordered, because views/cal.js draws one nav for a
   pair of months and the caption belongs to its table. The caption is given
   the nav's height so the two line up and nothing overlaps the day names. */
body.profile .reqform .cal { position: relative; }

body.profile .reqform .cal .calnav {
  position: absolute;
  top: 0;
  right: 0;
  margin: 0;
}

body.profile .reqform .cal .calmonth caption {
  height: var(--ctl-h);
  margin: 0;
  padding-right: 96px;
  line-height: var(--ctl-h);
}

/* No padding of a card's own inside the offer panel: the pane's 16 is the
   padding and the box's own border is the border. */

/* The rhythm at the foot of the card: the total sits close over the button
   and the button close under it, so the three things a sponsor reads last -
   what it costs, the press, what happens after - are one block rather than
   three spaced apart. base.css gives each of them 24, which is the spacing of
   a form with six more fields in it. */
body.profile .reqform { gap: 8px; }
body.profile .reqform .reqtotal { margin: 8px 0 0; }
body.profile .reqform .reqbtns { margin-top: 8px; }

/* The readout is the last line of the calendar and the total is the first of
   what follows it, so the space between them is the gap of the form and
   nothing else. The calendar's own bottom margin and the empty status line's
   stack under it otherwise and read as a hole in the card. */
body.profile .reqform .cal { margin-bottom: 0; }
body.profile .reqform .cal .calnote:empty { margin: 0; }

/* ------------------------------------------------------- the banner tab

The form is the card and it holds its own two columns, so the board has nowhere
to sit beside it. It goes under, where it reads as what it is: what this banner
has actually sold, not the offer. */
body.profile .bformcol { margin: 0; }
body.profile .bboard { margin: 24px 0 0; }

/* ------------------------------------------------- the full banner (Z7)

What a sponsor is booking, as one radio row at the top of the one form. It is
the `placement` field itself rather than a control that swaps two forms, so
the choice changes the price line, the total, two fields and the calendar's
dataset without leaving the page.

Rows, not cards: each option is one 40px-high line, the tap floor every
pressable thing on this site keeps, with the native radio as the mark. The one
that is on takes the brand colour and the card's own quiet ground, so the row
that is chosen is the row that is legible. */
body.profile .placerow {
  /* The form is a two-column grid from 900 up and this row is a direct child
     of it, so it is told to run the whole width. Left in one track it would
     take the dates' place and push the calendar into the other. */
  grid-column: 1 / -1;
  margin: 0 0 16px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  overflow: hidden;
}

body.profile .placerow .placeopt {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 8px 16px;
  font-size: var(--body);
  line-height: 1.4;
  color: var(--ink);
  cursor: pointer;
}

body.profile .placerow .placeopt + .placeopt { border-top: 1px solid var(--hairline); }
body.profile .placerow .placeopt:hover { background: var(--row-hover); }
body.profile .placerow .placeopt.is-on { background: var(--row-new); }
body.profile .placerow .placeopt.is-on .placename { font-weight: 600; color: var(--accent); }
/* The radio itself is never drawn on this site: the label is the control, and
   which one is on is said by the label's own colour and ground. It stays in
   the page, focusable and keyed through with the arrows, so the row is a real
   radio group; it is the same hiding base.css gives every other .segradio. */
body.profile .placerow .placeradio {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  margin: 0;
  padding: 0;
  pointer-events: none;
}

body.profile .placerow .placeopt:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* The name of the product never breaks in half. The price and the count after
   it may wrap under it on a phone, which is where the row runs out of room. */
body.profile .placerow .placename { white-space: nowrap; }
body.profile .placerow .placemeta { font-size: var(--help); color: var(--muted); }

/* The price line and the one line under it saying what the money buys, in
   variant B's rhythm: the two read as one block, and the calendar is the next
   thing rather than the third. The same two lines open all three tabs, so the
   pin's card is given the same numbers below.

   (The grid-column does nothing on a one-column form. It is kept for the day
   a full-width row is put back into it.) */
body.profile .reqform > .reqprice { grid-column: 1 / -1; }
body.profile .reqform > .reqprice .price { margin: 0 0 8px; }
body.profile .reqform > .reqprice .help { margin: 0; }

body.profile .buycard > .price { margin: 0 0 8px; }
body.profile .buycard > .help { margin: 0 0 16px; }
body.profile .postscard > .pricelead { margin: 0 0 8px; }
body.profile .postscard > .postsstay { margin: 0 0 16px; }

/* The readout under the grid, in place of the two date fields (prompt M2).
   public/cal.js unhides it and hides them; with no script it is never shown
   and the fields are what a sponsor types into. */
body.profile .reqform .calread {
  margin: 8px 0 0;
  font-size: var(--body);
  color: var(--ink);
}

/* The two month arrows. A square each, the same 40 every control on this page
   is, pushed to the end of the row so they sit over the right-hand month
   rather than in front of the first one. */
body.profile .reqform .cal .calnav { justify-content: flex-end; }

body.profile .reqform .cal .calstep {
  width: var(--ctl-h);
  padding: 0;
  font-size: var(--h2);
}

/* The answer to a picture that was just uploaded, under the file input. It is
   a help line, and the refusal is the site's own error tone. */
body.profile .reqform .artworknote { margin: 8px 0 0; }


/* ================================================== results (prompt L6)

   /results/<token>: what one finished deal did. Rows and hairlines, no cards,
   and every number set in the mono face the rest of the site sets numbers in.

   These selectors carry no body class. The page names 'public-profile' as its
   area and nothing else on the site uses these names, so the area file is
   still the one stylesheet that owns them. */

.rhead {
  margin: 0 0 8px;
  font-size: var(--h1);
  line-height: 1.2;
}

.rfacts {
  margin: 0 0 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--hairline);
  font-size: var(--body);
  color: var(--muted);
}

.rprivate,
.rnone {
  margin: 0 0 24px;
  font-size: var(--help);
  color: var(--muted);
}

.rhd {
  margin: 24px 0 8px;
  font-size: var(--help);
  line-height: 1.5;
  color: var(--muted);
  font-weight: 400;
}

.rwhat {
  margin: 0 0 24px;
  padding: 16px;
  border-left: 2px solid var(--accent);
  font-size: var(--body);
  white-space: pre-wrap;
}

/* One bar per day. The list is the chart: each item is a column, the fill
   inside it is the day's share of the tallest day, and the day number sits
   under it. No script and no style attributes, so the height is one of
   twenty-one classes below. */
.rbars {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  margin: 0 0 24px;
  padding: 0;
  list-style: none;
  overflow-x: auto;
}

.rbar {
  display: flex;
  flex: 1 1 0;
  min-width: 16px;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-end;
  height: 152px;
  margin: 0;
}

.rbarfill {
  display: block;
  background: var(--accent);
  border-radius: 2px 2px 0 0;
}

.rbarday {
  display: block;
  margin-top: 8px;
  text-align: center;
  font-size: 12px;
  line-height: 1;
  color: var(--muted);
  min-height: 12px;
}

/* A day with no clicks keeps a hairline of a bar, so the run reads as a run
   of days rather than as a gap in one. */
.rbars .rbarh-0 { height: 2%; }
.rbars .rbarh-1 { height: 5%; }
.rbars .rbarh-2 { height: 10%; }
.rbars .rbarh-3 { height: 15%; }
.rbars .rbarh-4 { height: 20%; }
.rbars .rbarh-5 { height: 25%; }
.rbars .rbarh-6 { height: 30%; }
.rbars .rbarh-7 { height: 35%; }
.rbars .rbarh-8 { height: 40%; }
.rbars .rbarh-9 { height: 45%; }
.rbars .rbarh-10 { height: 50%; }
.rbars .rbarh-11 { height: 55%; }
.rbars .rbarh-12 { height: 60%; }
.rbars .rbarh-13 { height: 65%; }
.rbars .rbarh-14 { height: 70%; }
.rbars .rbarh-15 { height: 75%; }
.rbars .rbarh-16 { height: 80%; }
.rbars .rbarh-17 { height: 85%; }
.rbars .rbarh-18 { height: 90%; }
.rbars .rbarh-19 { height: 95%; }
.rbars .rbarh-20 { height: 100%; }

/* ------------------------------------------- past sponsors (prompt L6)

   The Booked line's sibling on a builder's own page: the finished deals both
   sides published. Same quiet type and the same one-line-each shape, because
   it answers the same question one step further on. */
/* Under the 90-day strip, as a flat list on every listing, in a card of its
   own the way the strip above it is. Mono, one to a line, a hairline between
   them and none under the last. */
body.profile .ppastcard { margin: 0 0 24px; }

body.profile .ppastcard .pbookedh {
  margin: 0 0 8px;
  font-family: var(--mono);
  font-size: var(--eyebrow);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

body.profile .ppast {
  display: flex;
  flex-direction: column;
  list-style: none;
  margin: 0;
  padding: 0;
}

body.profile .ppastrow {
  margin: 0;
  padding: 4px 0;
  border-bottom: 1px solid var(--hairline);
  font-family: var(--mono);
  font-size: var(--help);
  line-height: 1.5;
  color: var(--ink-2);
}

body.profile .ppastrow:last-child { border-bottom: 0; }

body.profile .ppastrow a { color: inherit; }

/* ------------------------------------------ the pinned post's image (N2)

The picture a pin sponsor attached, in the mock under the words it goes up
with. It is the sponsor's own file, so its shape is theirs: the thumbnail is
held to 200 tall and left at its own width rather than cropped into a box the
picture was never made for. 8pt off the line above it, the card's own radius,
and the hairline every quiet block on this page wears. */
body.profile .mockpinimg { margin: 8px 0 0; }
body.profile .mockpinimg img {
  display: block;
  max-width: 100%;
  max-height: 200px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}
/* ------------------------------------------- the heart (prompt N4)

   The listing page draws it too and does not load app-shell.css either, so
   the same rules stand here. It is the last thing on the header row, after
   the facts and before the line that says how long this builder has had
   sponsors, and it takes only the width it needs. */
.heartform { display: inline; margin: 0; }

body.profile button.heart {
  padding: 0 4px;
  text-decoration: none;
  font-size: 18px;
  line-height: 1;
  color: var(--muted);
  background: none;
  border: 0;
  cursor: pointer;
}

body.profile button.heart.on { color: var(--saved); }
body.profile button.heart:hover { color: var(--ink); }
body.profile button.heart.on:hover { color: var(--saved); }

body.profile .pheadsave { flex: none; margin-top: 4px; }


/* ================================================== social proof (prompt O6)

   The header's face stack, the availability strip over the form, and the past
   sponsors as a line once there are three of them. */

/* 24 rather than base.css's 20. These are favicons as often as they are
   photographs and a favicon needs the extra room to be a mark rather than a
   smudge. Not 28: that is not on the site's px scale
   (scripts/design-lint.js PX_OK) and 24 is the step above 20 that is, so the
   scale is kept rather than waived. tests/avatar-cache.test.js reads this
   number. */
body.profile .pfaces .avatar.tiny { width: 24px; height: 24px; }

/* The stack is the way into the list of what those sponsors bought, which
   sits under the 90-day strip rather than directly beneath the stack. */
body.profile .pfaceslink { display: block; color: inherit; text-decoration: none; }

/* The header carries the faces and no caption under them. The words are
   still in the page and still the accessible name of the link the faces are -
   a stack of four circles linking to somewhere unnamed is not a link anybody
   can follow by ear - so they are taken off the screen rather than out of the
   markup. Same clip base.css's own .sr-only uses. */
body.profile .pfacecount {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}

/* ------------------------------------------------ the availability strip

   Ninety squares, today first. Light is open, grey is not for sale, brand is
   sold. The squares share the width equally rather than carrying one of
   their own, so the same strip reads on a phone and on a desktop and there is
   no width in here to fall off the scale. */
/* Inside the RIGHT column of the fold, under the mock, on the two tabs that
   sell days: there it is beside the calendar it describes, and it is not a
   full-width card putting ninety squares between the button and the foot of
   the page. It is the block held against the BOTTOM of that pane, with Past
   sponsors over it: the strip is what is for sale next, so it is what the
   pane ends on, closest to the bottom edge the button on the other side of
   the hairline lands on. The posts tab has no days to sell and shows the
   impressions line in its place. */
body.profile .pfoldproof { margin: 0; }
body.profile .tab-posts .pfoldproof { display: none; }

body.profile .pfoldproof .availstrip,
body.profile .pfoldproof .ppastcard { margin: 0 0 16px; }

body.profile .pfoldproof > :last-child { margin-bottom: 0; }

body.profile .availstrip { margin: 32px 0 24px; }

body.profile .availhd {
  margin: 0 0 8px;
  font-size: var(--help);
  color: var(--muted);
}

/* One title per dated product, and only the open tab's is drawn: the day the
   strip says it is open from is that product's own first open run. */
body.profile .availhd-banner,
body.profile .availhd-pin { display: none; }
body.profile .tab-banner .availhd-banner { display: block; }
body.profile .tab-pin .availhd-pin { display: block; }

body.profile .availrow {
  display: flex;
  gap: 1px;
  align-items: stretch;
}

body.profile .avday {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  height: 20px;
  border-radius: 2px;
  background: var(--hairline);
}

/* The key under the squares. Without it the words are in the title of every
   square and nowhere a reader can see them, and ninety squares in three greys
   say nothing on their own. Each mark is an .avday, so a colour can never be
   named here that the strip does not draw.

   12 rather than the mock's 10: 10px is not on the site's px scale
   (scripts/design-lint.js PX_OK) and 12 is the step it rounds to. */
body.profile .availkey {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 16px;
  margin: 8px 0 0;
  font-size: var(--eyebrow);
  color: var(--muted);
}

body.profile .availkeyone {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

body.profile .availkey .avday {
  flex: none;
  width: 12px;
  height: 12px;
}

body.profile .avday.avopen { background: var(--row-new); }
body.profile .avday.avclosed { background: var(--hairline); }
body.profile .avday.avbooked { background: var(--accent); }

/* The hover: the sponsor's own face and the dates of the run. Every square
   also carries the same words in a title, so a reader with no pointer, or
   with this rule unread, still gets the answer from the browser itself. */
body.profile .avtip {
  display: none;
  position: absolute;
  bottom: 100%;
  left: 50%;
  z-index: 2;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
  padding: 4px 8px;
  white-space: nowrap;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  transform: translateX(-50%);
}

body.profile .avday.avbooked:hover .avtip { display: flex; }

body.profile .avatar.avtipface {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  flex: none;
}

body.profile .avtipw { font-size: 12px; color: var(--ink-2); }

/* Past sponsors are the flat list above on every listing, not a dotted line
   from three sponsors up: no rule here draws a line, its position stops or
   the month ticks under it (views/public.js pastSponsors). */

/* --------------------------------------------- the posts tab (O5)

The third panel. base.css hides the other two off the root class; the third
is hidden the same way from here, because a stylesheet for one area is where
that area's rules belong.

The tab strip and the panels are the ones already on the page: nothing here
restyles either, it only says which of the three is showing. */
body.profile .tab-banner .panelposts,
body.profile .tab-pin .panelposts { display: none; }

body.profile .tab-posts .panelbanner,
body.profile .tab-posts .panelpin { display: none; }

/* One right column, three products. The profile mock is what the banner and
   the pin are bought against; the post mock is what a post is. Whichever tab
   is open, the column holds exactly one of them. */
body.profile .postmockwrap { display: none; }
body.profile .tab-posts .postmockwrap { display: block; }
body.profile .tab-posts .mock { display: none; }

body.profile .tab-posts .tab[data-tab="posts"] {
  color: var(--ink);
  font-weight: 500;
  border-bottom-color: var(--accent);
}

/* The mock: one post in a timeline, which is what is for sale. Rows, not a
   card inside a card, so it reads as the thing rather than as a picture of
   an interface. */
/* The post mock is a panel inside the offer panel, so the box comes from
   base.css and it is flat. Padding and the air under it are written here. */
body.profile .postmock {
  padding: 16px;
  margin: 0 0 16px;
}

body.profile .postmock-head {
  display: flex;
  align-items: center;
  gap: 8px;
}

body.profile .postmock-av {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  flex: none;
}

body.profile .postmock-who { margin: 0; font-size: 14px; color: var(--muted); }

/* The label X itself draws over a paid post (prompt P5). Small, mono and
   quiet, the way X sets it, because it is the platform's mark rather than the
   builder's words or the sponsor's. */
body.profile .postmock-lbl {
  margin: 8px 0 0;
  font-size: var(--eyebrow);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

body.profile .postmock-body { margin: 4px 0 0; }

/* The card X draws under a link in a post: the picture, the address and the
   title of the page. It is a placeholder, so it holds no words of the
   sponsor's beyond the address their handle suggests. */
body.profile .postmock-card {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 12px 0 0;
  padding: 8px;
  border: 1px solid var(--hairline);
  border-radius: 8px;
}

body.profile .postmock-thumb {
  width: 56px;
  height: 40px;
  border-radius: 4px;
  background: var(--hairline);
  flex: none;
}

body.profile .postmock-cardw {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

body.profile .postmock-cardd { font-size: var(--eyebrow); color: var(--muted); }
body.profile .postmock-cardt { font-size: var(--help); }

/* What a post on this timeline gets, under the mock. Printed only where the
   listing has banked its impressions. */
body.profile .postmockfacts { margin: 8px 0 0; }

/* The price line over the form, in the same weight the other two tabs give
   their price. */
body.profile .pricelead { margin: 0 0 16px; }

/* One group per post: a hairline block, no card, so five of them stack as a
   list rather than as five boxes. */
body.profile .postgroup {
  border: 0;
  border-top: 1px solid var(--hairline);
  padding: 16px 0 0;
  margin: 16px 0 0;
}

body.profile .postgroup > legend {
  padding: 0;
  font-family: var(--mono);
  font-size: 13px;
}

body.profile .postgroup .help { margin: 4px 0 0; }

/* The groups above the smallest order the builder takes are still on the
   page with no script: the count select says how many are read. */
body.profile .postgroup-extra > legend .help { display: inline; margin-left: 8px; }

body.profile .postsreq textarea { width: 100%; }

/* The count, as a stepper (prompt P5). Narrow: the control is a number
   between one and five, so it is sized to the number rather than to the
   column. The field IS a number input, which is what a reader with no script
   types into and what the two presses drive; the presses are quiet squares
   the size of the field, never the card's own primary. */
body.profile .stepfield .stepper {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--hairline);
  border-radius: 8px;
  overflow: hidden;
}

body.profile .stepfield .stepper input[type="number"] {
  width: 56px;
  border: 0;
  border-radius: 0;
  text-align: center;
  font-family: var(--mono);
  -moz-appearance: textfield;
  appearance: textfield;
}

body.profile .stepfield .stepper input[type="number"]::-webkit-outer-spin-button,
body.profile .stepfield .stepper input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

body.profile .stepctl {
  width: 40px;
  border: 0;
  background: var(--ground);
  color: var(--ink);
  font-size: var(--body);
  line-height: 1;
  cursor: pointer;
}

body.profile .stepctl:hover { background: var(--row-hover); }
body.profile .stepctl[disabled] { color: var(--muted); cursor: default; }

/* The optional picture on one post of an order (prompt O5). The pin's field
   in the pin's shape, once per group: the thumbnail sits under the picker at
   the width the group has, and is hidden until one is stored. */
body.profile .postpic { margin-top: 16px; }

body.profile .postpicshot {
  margin: 8px 0 0;
}

body.profile .postpicshot img {
  max-width: 160px;
  border: 1px solid var(--hairline);
  border-radius: 8px;
}

/* One row per post on a results page (prompt O5 B). The directory's own row
   rhythm: hairline rules, no card, numbers right. */
body.profile .rposts { width: 100%; border-collapse: collapse; margin: 16px 0 0; }
body.profile .rposts th,
body.profile .rposts td { text-align: left; padding: 8px 8px 8px 0; border-bottom: 1px solid var(--hairline); vertical-align: top; }
body.profile .rposts th { font-size: 13px; color: var(--muted); font-weight: 400; }
body.profile .rposts .num { text-align: right; }
body.profile .rposts .cellsub { display: block; font-size: 13px; color: var(--muted); }

/* ------------------------------------------- step two of a request (P5)

/request/<token> loads this stylesheet, because it is the second half of the
card the sponsor pressed on. It does NOT carry body.profile - that class is
the listing page's - so these four rules are scoped by their own classes,
which exist on no other page. Three things: the summary of what the sponsor
picked, drawn as a bordered row with the way back to the dates on the right of
it; the refund promise under it, which is what the press is worth; and one
full-width button, because there is one press on the page and nothing to weigh
it against. */
/* The summary row is a panel; what is written here is its layout and its
   padding. */
.reqsum {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px 16px;
  margin: 0 0 8px;
  padding: 12px 16px;
}

.reqsum .reqsummary {
  margin: 0;
  font-size: var(--body);
}

.reqsum .reqsumchange { font-size: var(--help); }

.reqsumnote { margin: 0 0 24px; color: var(--muted); font-size: var(--help); }

.reqstep2 .reqbtns .btn { width: 100%; }

/* ============================================= Q2: the restrained set

   Four things move on this page: the tab underline slides between the two
   tabs, the calendar answers the pointer before the press, the sponsor faces
   in the header lift, and a button darkens and presses. All of them are
   timed by --t-fast (150ms), and --t-fast is zero under the reduced-motion
   block at the top of base.css. The reduced-motion block in this file zeroes
   the transforms, which a duration token cannot reach. */

/* 1. The underline that slides. The two tabs are switched by public/spot.js
      without a page load, so the mark under them was jumping from one to the
      other; public/ui.js lays one span over the open tab and moves it. With
      no script, or with reduced motion asked for, ui.js adds nothing, the
      `has-slide` class is never written and the per-tab border is what the
      reader sees, which is the page's own underline. */
body.profile .tabs { position: relative; }

body.profile .tabs.has-slide .tab { border-bottom-color: transparent; }

body.profile .tabslide {
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 2px;
  background: var(--accent);
  transition: transform var(--t-fast) ease-out, width var(--t-fast) ease-out;
}

/* 1b. The panes under the tabs (paint7). public/spot.js pins the box at its
      old height while the tab switches and lets it run to the new one, so
      the page under it slides rather than jumps. The class is only on the
      box for the length of the run; with reduced motion asked for spot.js
      never adds it. */
body.profile .pofferpanes.is-sizing {
  overflow: hidden;
  transition: height var(--t-mid) ease-out;
}

/* 2. The calendar, answering the pointer. An open day under the pointer, and
      the run a press would take with it, wear the picked run's fill at half
      strength: near enough to say "this is what you are about to pick", far
      enough that the picked run stays the one that is picked. The readout
      and the total follow it (public/cal.js preview), so the price is on the
      page before the click rather than after it. */
body.profile .cal .calday.is-hov {
  background: color-mix(in srgb, var(--row-new) 50%, transparent);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
}

body.profile .cal .calday {
  transition: background var(--t-fast) ease-out, border-color var(--t-fast) ease-out;
}

/* 3. The sponsor faces in the header. They are one stack inside one link to
      the Past sponsors block below, so each face is not a link of its own -
      it lifts under the pointer and keeps the title it has always had. */
body.profile .pfaces .avatar.tiny {
  transition: transform var(--t-fast) ease-out;
}

body.profile .pfaces .avatar.tiny:hover { transform: translateY(-2px); }

/* 4. The press. base.css darkens a button on this page; this adds the pixel
      it goes down by while it is held and takes the focus ring to the brand
      at full strength, which is a ring for a keyboard and not a box. At 40%
      the ring is there and not visible, which is the worst of both. */
body.profile .btn,
body.profile button,
body.profile input[type="submit"] {
  transition: background var(--t-fast) ease-out, border-color var(--t-fast) ease-out,
    transform var(--t-fast) ease-out;
}

body.profile .btn:active,
body.profile button:active,
body.profile input[type="submit"]:active { transform: translateY(1px); }

body.profile a:focus-visible,
body.profile button:focus-visible,
body.profile input:focus-visible,
body.profile select:focus-visible,
body.profile textarea:focus-visible,
body.profile summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  body.profile .pofferpanes.is-sizing,
  body.profile .tabslide,
  body.profile .cal .calday,
  body.profile .pfaces .avatar.tiny,
  body.profile .btn,
  body.profile button,
  body.profile input[type="submit"] {
    transition: none;
  }

  body.profile .pfaces .avatar.tiny:hover,
  body.profile .btn:active,
  body.profile button:active,
  body.profile input[type="submit"]:active { transform: none; }
}
