/* The shop. Tokens, the reset and the interactive components come from
   design/tokens.css, design/base.css and design/controls.css, which the
   application layout loads ahead of this file.

   The shop has two registers, not a stylesheet per page:

   QUIET    the catalog, a product page, checkout, an unpaid order. Cream
            ground, serif headings, flat surfaces, one orange button. It is
            deliberately the register the buyer arrived from — geneva-drive.dev
            and the other product sites — so that crossing into the shop does
            not read as having been handed to a payment processor.

   ARRIVED  a paid order, and everything under /my. Sand ground, Archivo Black,
            a sand band, warm lift under the surfaces. This is what "you are
            inside Stanquette now" is made of, and it is spent exactly once:
            on the words "your license is ready".

   Everything below is written once and reads the register's variables. A page
   picks its register with one content_for in the template; nothing here
   selects a page by name. See doc/purchase-and-licensing-flows.md,
   "The gradient, page by page". */

/* --- The two registers ---------------------------------------------------
   Only these two blocks carry the difference. Buttons keep the shadow their
   component gives them in both registers: the buyer clicked that exact object
   on the way in, and object permanence is the strongest thread in the
   handoff. What "flat" withholds from the quiet register is depth on the
   page's own surfaces — no plates, no lifted bands. */

.shop {
  /* The bar's own height. The account panel is a popover, so while it is open
     it lives in the top layer and lays itself out against the viewport — it
     cannot be positioned against the bar the way an ordinary absolute box
     could. So the bar is given a height rather than left to find one, and
     min-height on .shop-masthead holds it to that. */
  --masthead-height: 3.875rem;

  @media (width >= 48rem) {
    --masthead-height: 4.5rem;
  }

  --ground: var(--cream);
  --heading-family: var(--font-body);
  --heading-weight: var(--weight-semibold);
  --heading-leading: var(--leading-title);
  --heading-tracking: 0;

  --masthead-plate: transparent;
  --masthead-ink: var(--ink);
  --masthead-rule: var(--rule-hair);
  --masthead-accent: var(--orange);

  --plate: transparent;
  --plate-pad: 0;
  --plate-lift: none;
}

.shop--arrived {
  --ground: var(--sand);
  --heading-family: var(--font-display);
  --heading-weight: var(--weight-regular);
  --heading-leading: var(--leading-display);
  --heading-tracking: var(--track-display);

  --masthead-plate: var(--ink);
  --masthead-ink: var(--cream);
  --masthead-rule: transparent;
  /* --orange disappears into an ink slab; --orange-tint is the accent that
     survives it. Everything in the bar reads this one variable. */
  --masthead-accent: var(--orange-tint);

  --plate: var(--paper);
  --plate-pad: clamp(1.5rem, 4vw, 2.25rem);
  --plate-lift: var(--lift-quiet);
}

body.shop {
  display: flex;
  flex-direction: column;
  background: var(--ground);
}

/* --- Masthead ------------------------------------------------------------
   The wordmark is on every shop page from the first one: continuity of mark,
   gradient of voice. Only its ground changes — quiet leaves it on the paper
   under a hairline, arrived puts it back on the ink slab the rest of
   Stanquette wears. */

.shop-masthead {
  min-height: var(--masthead-height);
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  align-items: center;
  justify-content: space-between;
  padding: 1rem var(--gutter);
  background: var(--masthead-plate);
  color: var(--masthead-ink);
  border-bottom: 1px solid var(--masthead-rule);

  & .wordmark {
    width: 6.875rem;

    @media (width >= 48rem) {
      width: 9.375rem;
    }
  }

  /* base.css rings focus in --orange, which the arrived register's ink slab
     swallows into a heavy dark rectangle. In the bar it takes the register's
     own accent instead. */
  & :focus-visible {
    outline-color: var(--masthead-accent);
  }
}

/* Seven items signed in, and on a phone they wrap onto two or three lines
   rather than scrolling sideways or shrinking below reading size. The gap
   closes as the viewport does, which is the whole of the accommodation. */

.shop-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem clamp(0.875rem, 3.5vw, 2rem);
  align-items: center;

  & a,
  & button {
    color: inherit;
    font-family: var(--font-body);
    font-size: var(--step-small);
    text-decoration: none;
    border-bottom: 1px solid transparent;
    padding-bottom: 0.1rem;
    transition: color var(--tempo) var(--ease),
      border-color var(--tempo) var(--ease);

    &:hover,
    &:focus-visible {
      color: var(--masthead-accent);
      border-bottom-color: var(--masthead-accent);
    }

    &[aria-current="page"] {
      border-bottom-color: var(--masthead-accent);
    }
  }

  & form {
    display: contents;
  }

  & button {
    margin: 0;
    padding: 0 0 0.1rem;
    background: none;
    border: 0;
    border-bottom: 1px solid transparent;
    cursor: pointer;
    line-height: inherit;
  }
}

/* --- The account menu -----------------------------------------------------
   A button that names a popover as its target, and a panel that says it is
   one. That is the whole mechanism: no script, and the browser gives back the
   thing the <details> this replaces could never have — light dismiss. Click
   anywhere else and the menu closes; Escape closes it; it is in the top layer
   so nothing on the page can crop it.

   The wrapper draws nothing. display:contents drops it out of the box tree so
   the trigger is a flex child of the bar exactly like Licenses and Orders are,
   and centers on the same line as they do — which is what an inline-flex
   summary, whose baseline is not its text's baseline, would not. */
.account-menu {
  display: contents;
}

.shop-nav .account-menu-trigger {
  position: relative;

  /* The address is the whole point of the menu, so it gets the room — but an
     address long enough to push the bar sideways is trimmed instead. */
  max-width: min(18rem, 42vw);
  padding-right: 0.9em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;

  /* The marker, and the only thing that says this is a menu rather than a
     label. It is out of flow, so it cannot pull the address off the line the
     way a flex sibling did. */
  &::after {
    content: "";
    position: absolute;
    top: 50%;
    right: 0;
    width: 0.55em;
    aspect-ratio: 2 / 1.3;
    translate: 0 -70%;
    background: var(--masthead-accent);
    clip-path: polygon(0 0, 100% 0, 50% 100%);
    transition: rotate var(--tempo) var(--ease);
  }
}

.account-menu:has(> .account-menu-panel:popover-open) > .account-menu-trigger::after {
  rotate: 180deg;
}

/* Settings is the current page, so Settings is what carries the marking. The
   trigger, which is what you can see while the menu is shut, reads it off the
   link rather than being told separately. */
.account-menu:has([aria-current="page"]) > .account-menu-trigger {
  border-bottom-color: var(--masthead-accent);
}

/* An open popover is in the top layer, and a top layer box takes the viewport
   as its containing block whatever it is nested in — so there is no ancestor
   to hang this off. It is placed against the viewport instead.

   On a phone the bar wraps to two rows and its height stops being knowable, so
   there the panel does not try: it is a sheet across the foot of the screen,
   which is both robust and the thing a thumb can reach. Wide enough that the
   bar provably cannot wrap, it goes back to being a card under the bar's right
   end, on the gutter. position:absolute rather than fixed, so it scrolls with
   the bar it belongs to. */
.account-menu-panel {
  position: fixed;
  inset: auto 0 0;
  z-index: 10;

  /* the UA sheet gives every popover width: fit-content, which would keep the
     sheet from spanning the foot of the screen */
  width: auto;
  gap: 1.125rem;
  justify-items: end;
  margin: 0;
  padding: 1rem var(--gutter) calc(1rem + env(safe-area-inset-bottom, 0px));
  overflow: visible;
  background: var(--cream);
  color: var(--ink);
  border: 0;
  border-top: 1px solid var(--rule-hair);
  box-shadow: var(--lift-quiet);
  white-space: nowrap;

  /* The masthead may be an ink slab, but the panel is on the page's own paper
     — so the accent goes back to the one cream can carry. */
  --masthead-accent: var(--orange);

  & a,
  & button {
    color: var(--ink);
  }

  @media (width >= 48rem) {
    position: absolute;
    inset: calc(var(--masthead-height) + 0.4rem) var(--gutter) auto auto;
    width: max-content;
    gap: 0.625rem;
    padding: 0.875rem 1rem;
    border: 1px solid var(--rule-hair);
  }
}

/* display goes on the open state alone. Author styles beat the UA sheet
   whatever their specificity, so a plain `display: grid` here would override
   [popover]:not(:popover-open) { display: none } and leave the menu standing
   open for everyone. */
.account-menu-panel:popover-open {
  display: grid;
}

/* Popover shipped in Firefox 125 and ApplicationController still serves 121,
   so four Firefox releases inside this app's own supported range have no
   popover at all. There the attribute means nothing and the panel would simply
   sit open under the bar forever, which is worse than what it replaced. So
   where there is no popover the menu stops being a menu: the address is a
   label, and its two items join the bar — which is where they would have been
   all along if the bar had had room for them. */
@supports not selector(:popover-open) {
  .account-menu-panel {
    position: static;
    display: contents;
    color: inherit;
    --masthead-accent: inherit;

    & a,
    & button {
      color: inherit;
    }
  }

  .shop-nav .account-menu-trigger {
    padding-right: 0;
    cursor: default;

    &::after {
      content: none;
    }
  }

  /* Nothing opens, so nothing is behind the address — and an underline under a
     plain label would only claim it was somewhere you can be. */
  .account-menu:has([aria-current="page"]) > .account-menu-trigger {
    border-bottom-color: transparent;
  }
}

/* --- The column ----------------------------------------------------------
   One centered measure with a full-bleed escape hatch, so a band can span the
   viewport without any page needing its own wrapper.

   The measure is a named grid track, not a max-width and an auto margin. That
   distinction is the whole point: centering by margin is only ever a default,
   and any later rule that writes the `margin` shorthand — `.rule-list`,
   `.page-note`, a bare `& > p` — silently takes it back and drops that one
   element at the gutter while the rest of the page stays centered. A track
   cannot be overridden by a margin, so an element's horizontal place on the
   page is decided here and nowhere else.

   One track list, named once and read by everything that has to line up with
   the column: the column itself, the flash above it and a full-bleed band
   inside it. `100%` resolves against each of those boxes in turn, and all
   three are the width of the viewport, so the measure lands on the same two
   edges in all of them. */

.shop {
  --column-tracks:
    [full-start] minmax(var(--gutter), 1fr)
    [measure-start] min(100% - 2 * var(--gutter), var(--measure-prose))
    [measure-end] minmax(var(--gutter), 1fr) [full-end];
}

.shop-main {
  flex: 1;
  display: grid;
  align-content: start;
  grid-template-columns: var(--column-tracks);
  gap: clamp(2.25rem, 5vw, 3.5rem) 0;
  padding-block: clamp(2.5rem, 6vw, 4.5rem) clamp(3.5rem, 8vw, 6rem);

  /* Package names and hostnames are unbreakable words wide enough to push a
     phone sideways. `anywhere` rather than `break-word` because only the
     former also shrinks the min-content width a grid track is sized from. */
  overflow-wrap: anywhere;

  & > * {
    grid-column: measure;
    min-width: 0;
  }

  /* A band spans the viewport by taking the outer tracks as well, then lays
     the very same track list out again inside itself. That is why a band's
     contents line up with the column above and below it to the pixel. */
  & > .full-bleed {
    grid-column: full;
    display: grid;
    grid-template-columns: var(--column-tracks);

    & > * {
      grid-column: measure;
      min-width: 0;
    }
  }

  /* A band that opens the page meets the masthead, with no stripe of ground
     between them — that seam is what makes the arrival read as a change of
     place rather than as a colored box. */
  &:has(> .full-bleed:first-child) {
    padding-block-start: 0;
  }
}

/* The flash sits above the column and has to land on the same measure, so it
   is the same grid rather than a second guess at the same arithmetic. */
.shop-flash {
  display: grid;
  grid-template-columns: var(--column-tracks);
  gap: 0.75rem 0;
  margin-top: 1.25rem;

  & > * {
    grid-column: measure;
    min-width: 0;
  }
}

.shop-footer {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  padding: 1.5rem var(--gutter);
  border-top: 1px solid var(--rule-hair);
  font-family: var(--font-label);
  font-size: var(--step-label);
  letter-spacing: var(--track-legal);
  text-transform: uppercase;
  color: var(--ink-soft);

  & a {
    color: inherit;
    text-decoration: none;
    border-bottom: 1px solid var(--rule-hair);

    &:hover {
      color: var(--orange-deep);
      border-bottom-color: currentcolor;
    }
  }
}

/* --- Page furniture ------------------------------------------------------ */

.page-head {
  display: grid;
  gap: 0.875rem;
}

.page-title {
  margin: 0;
  font-family: var(--heading-family);
  font-size: var(--step-display);
  font-weight: var(--heading-weight);
  line-height: var(--heading-leading);
  letter-spacing: var(--heading-tracking);
  text-wrap: balance;
}

.page-lede {
  margin: 0;
  max-width: var(--measure-lede);
  font-size: var(--step-lede);
  color: var(--ink-soft);
}

.page-section {
  display: grid;
  gap: 1rem;
  align-content: start;
}

.block-title {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: baseline;
  font-family: var(--heading-family);
  font-size: var(--step-name);
  font-weight: var(--heading-weight);
  line-height: var(--leading-title);
}

.section-note,
.page-note,
.step-note {
  margin-block: 0;
  font-size: var(--step-small);
  line-height: var(--leading-ui);
  color: var(--ink-soft);
}

.page-note {
  padding-top: 1rem;
  border-top: 1px solid var(--rule-hair);
}

.page-empty {
  margin: 0;
  color: var(--ink-soft);
}

/* A link inside running prose. It keeps the ink of the sentence it is in and
   is marked by a single orange underline, so a paragraph does not turn into a
   row of colored words. Rows that are entirely a link (.item-link) and
   anything wearing .button carry their own treatment and are left alone. */

.shop-main
  :is(
    .page-lede,
    .page-note,
    .section-note,
    .step-note,
    .item-body,
    .field-hint,
    .arrival-note
  )
  a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--orange-deep);
  transition: color var(--tempo) var(--ease);

  &:hover,
  &:focus-visible {
    color: var(--orange-deep);
  }
}

/* --- Rule lists ----------------------------------------------------------
   The same structure the marketing site uses: a heavy rule opens the list,
   hairlines separate the rows. Restated here because the shop does not load
   main.css — the vocabulary is shared, the stylesheets are not. */

.rule-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: var(--rule-heavy) solid var(--ink);
}

.rule-list-item {
  display: grid;
  gap: 0.375rem;
  padding: 1.125rem 0;
  border-bottom: 1px solid var(--rule-hair);
}

.item-name {
  margin: 0;
  font-family: var(--heading-family);
  font-size: var(--step-title);
  font-weight: var(--heading-weight);
  line-height: var(--leading-title);
  color: inherit;
}

.item-body {
  margin: 0;
  color: var(--ink-soft);
}

.item-package {
  display: block;
  margin-top: 0.375rem;
  font-family: var(--font-mono);
  font-size: var(--step-fine);
  color: var(--text-placeholder);
}

/* The package name as the door to the package's own page. It keeps the mono
   and the quiet ink — a row of orange package names would shout over the
   product names above them — and says it is a link by the hairline under it,
   the same device a link in running prose uses. `width: fit-content` because
   the element is a block: without it the underline runs the width of the
   column rather than the width of the name. */

a.item-package {
  width: fit-content;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 25%, transparent);
  transition: color var(--tempo) var(--ease), border-color var(--tempo) var(--ease);

  &:hover,
  &:focus-visible {
    color: var(--orange-deep);
    border-bottom-color: var(--orange-deep);
  }
}

/* The one act a version row offers. Sized and tracked like .item-cue, which is
   the other thing that sits in that column, so a list of downloads and a list
   of order states line up as the same column rather than as two. */

.item-download {
  font-family: var(--font-label);
  font-size: var(--step-label);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--orange-deep);
  text-decoration: none;
  border-bottom: 1px solid var(--orange-deep);

  &:hover,
  &:focus-visible {
    color: var(--ink);
    border-bottom-color: var(--ink);
  }
}

/* A row that is entirely a link carries the columns itself, so the row must
   not also be a grid — the link would be confined to the first track. */
.rule-list-item:has(> .item-link) {
  display: block;
  padding: 0;
}

.item-link {
  display: grid;
  gap: 0.375rem;
  padding: 1.125rem 0;
  color: inherit;
  text-decoration: none;

  &:hover .item-name,
  &:focus-visible .item-name {
    color: var(--orange-deep);
  }

  @media (width >= 44rem) {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.375rem 2rem;
    align-items: baseline;

    & .item-side {
      grid-row: 1 / span 2;
      grid-column: 2;
      text-align: right;
      align-self: start;
    }
  }
}

.item-side {
  display: grid;
  gap: 0.25rem;
}

.item-price {
  font-family: var(--font-label);
  font-size: var(--step-title);
  font-weight: var(--weight-semibold);
}

.item-cue {
  font-family: var(--font-label);
  font-size: var(--step-label);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--orange-deep);
}

/* A way on from a page that has nothing else to offer. Set like .item-cue, so
   it reads as the same voice as the cue on a shelf row; the arrow steps
   forward on hover rather than the link changing color. */
.onward-link {
  font-family: var(--font-label);
  font-size: var(--step-label);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--orange-deep);
  text-decoration: none;

  & span {
    display: inline-block;
    transition: translate var(--tempo) var(--ease);
  }

  &:hover span,
  &:focus-visible span {
    translate: 0.25em 0;
  }
}

/* --- The offer -----------------------------------------------------------
   The one block on a product page that is allowed to raise its voice: a heavy
   rule to open it, the price large, the facts as badges, one orange button. */

.offer {
  display: grid;
  gap: 1.125rem;
  padding-top: 1.5rem;
  border-top: var(--rule-heavy) solid var(--ink);
}

.offer-price {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem 1rem;
  align-items: baseline;
  margin: 0;
}

.offer-amount {
  font-family: var(--font-label);
  font-size: var(--step-section);
  font-weight: var(--weight-semibold);
  line-height: 1;
}

/* The empty state for a list whose whole point is to have something in it.
   The sentence and the button that fixes it belong together, rather than a
   stranded line of gray text and a button somewhere below it. */
.empty-slot {
  display: grid;
  gap: 0.75rem;
  justify-items: start;
  padding: 1.25rem;
  /* A width, not `var(--rule-hair)` — that token is a color, and the whole
     shorthand was being thrown away as invalid. The slot drew no border at
     all, so its padding read as an unexplained indent off the measure. */
  border: 1px dashed color-mix(in srgb, var(--ink) 25%, transparent);
}

.offer-action {
  margin: 0.375rem 0 0;
}

.badge-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* --- Order lines --------------------------------------------------------- */

.lines {
  width: 100%;
  border-collapse: collapse;
  text-align: left;

  & th,
  & td {
    padding: 0.875rem 0;
    font-weight: var(--weight-regular);
    vertical-align: baseline;
  }

  & tbody tr:first-child > * {
    border-top: var(--rule-heavy) solid var(--ink);
  }

  & tbody tr + tr > * {
    border-top: 1px solid var(--rule-hair);
  }

  & tfoot > tr > * {
    border-top: 1px solid var(--rule-hair);
    font-family: var(--font-label);
    font-size: var(--step-small);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--track-caps);
    text-transform: uppercase;
  }
}

.line-name {
  display: block;
}

.line-package {
  display: block;
  margin-top: 0.25rem;
  font-family: var(--font-mono);
  font-size: var(--step-fine);
  color: var(--text-placeholder);
}

.line-amount {
  text-align: right;
  white-space: nowrap;
  font-family: var(--font-label);
  font-variant-numeric: tabular-nums;
}

/* The VAT breakdown sits under the total, because the total is what the price
   is and the split is a note about it. It borrows the table's tabular alignment
   and gives back the total's uppercase weight: only one of the three rows is
   the number being agreed to, and it is the first. */

.lines tfoot .lines-sub > * {
  font-weight: var(--weight-regular);
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-soft);
}

.lines tfoot .lines-sub + .lines-sub > * {
  border-top: 0;
  padding-top: 0;
}

.vat-reason {
  max-width: var(--measure-lede);
}

/* --- Checkout form ------------------------------------------------------- */

.checkout-form {
  display: grid;
  gap: 1.5rem;
}

/* Sign-in is two pages with one field each, so the form is the page and wants
   no plate, no card and no box around it — only the rhythm every other stack of
   fields in the shop has. */
.sign-in-form {
  display: grid;
  gap: 1.5rem;
}

/* A nine-digit code, typed once and never read again. Mono and tracked so the
   digits are countable against the ones on the screen they were copied from,
   and only as wide as nine of them: a field running the full measure for nine
   characters asks to be filled with something longer. */
.code-entry {
  max-width: 15rem;
  font-family: var(--font-mono);
  font-size: var(--step-title);
  letter-spacing: 0.24em;

  /* Shown in capitals because the mail shows it in capitals, and a code read in
     capitals is read faster. It is a transform and not a value: what gets
     submitted is whatever was typed, and MagicLink::Code settles the case — so
     nobody is fighting their own keyboard to match what is on the screen. */
  text-transform: uppercase;
}

/* The two ways to settle. A choice between two things is a choice, not two
   commands — and the labels are in one voice now, which two submit buttons
   reading "Pay by card" and "Send me an invoice" were not. */
.settle {
  margin: 0 0 1rem;
  padding: 0;
  border: 0;
}

.settle-legend {
  padding: 0;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  letter-spacing: var(--track-label, 0.06em);
  text-transform: uppercase;
  color: var(--ink-soft);
}

.settle-choice {
  display: flex;
  gap: 0.625rem;
  align-items: baseline;
  margin-top: 0.5rem;
  cursor: pointer;

  & input {
    flex: none;
  }
}

/* The two mailing lists. The fieldset borrows .settle, because it is the same
   shape of question, but the rows have a class of their own: a settlement is a
   choice between two things and the checkout test counts them, so a checkbox
   that is not one of them must not answer to .settle-choice.

   A checkbox has no baseline to sit on, and these labels run to two lines on a
   phone, so the box is pinned to the top of the row rather than floating to
   the middle of a wrapped sentence. */
.subscription-choice {
  display: flex;
  gap: 0.625rem;
  margin-top: 0.5rem;
  cursor: pointer;

  & input {
    flex: none;
    align-self: flex-start;
    margin-top: 0.25em;
  }
}

.checkout-submit {
  display: grid;
  gap: 0.75rem;
  justify-items: start;
  padding-top: 0.5rem;
}

/* Two ways to pay, one of them orange. The quiet register allows exactly one
   saturated object per page and this is where it is spent, so the second
   choice is present at the same size and reads as a choice, not as a lesser
   button. On a phone they stack full-width rather than shrink. */

.checkout-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;

  & .button {
    flex: 1 1 auto;
  }

  @media (width >= 30rem) {
    & .button {
      flex: 0 0 auto;
    }
  }
}

/* The no-JavaScript way to re-price the page. Not a button in the system's
   sense — it undoes nothing and takes no money — so it is set as the same
   underlined text link the masthead uses, and the checkout Stimulus controller hides it
   the moment it can press it on the customer's behalf. */

.checkout-reprice {
  justify-self: start;
  margin: -0.75rem 0 0;
  padding: 0 0 0.1rem;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--ink-soft);
  font-family: var(--font-body);
  font-size: var(--step-small);
  color: var(--ink-soft);
  cursor: pointer;
  transition: color var(--tempo) var(--ease),
    border-color var(--tempo) var(--ease);

  &:hover,
  &:focus-visible {
    color: var(--orange);
    border-bottom-color: var(--orange);
  }
}

/* --- The VIES detour -----------------------------------------------------
   VIES could not answer, so the page stops being a checkout for a moment and
   becomes an explanation. A heavy rule opens it the way the offer block opens
   a product page: this is a different kind of section, and it says so without
   a color, a panel or an icon. */

.checkout-detour {
  display: grid;
  gap: 1rem;
  padding-top: 1.5rem;
  border-top: var(--rule-heavy) solid var(--ink);
}

.detour-title {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--step-name);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-title);
}

.detour-body {
  margin: 0;
  max-width: var(--measure-lede);
  line-height: var(--leading-body);

  & code {
    font-family: var(--font-mono);
    font-size: 0.875em;
  }
}

.detour-choices {
  display: grid;
  gap: 0.875rem;
  margin: 0;
  padding: 0;
  max-width: var(--measure-lede);
  list-style: none;

  & li {
    padding-left: 1rem;
    border-left: 1px solid var(--rule-hair);
    line-height: var(--leading-snug);
    color: var(--ink-soft);
  }

  & strong {
    display: block;
    font-weight: var(--weight-semibold);
    color: var(--ink);
  }
}

/* --- Code slabs ----------------------------------------------------------
   The most recognizable object a Ruby developer's eye lands on, and the same
   slab on both sites. slab-select makes one click take the whole line, which
   is the entire point of the setup page.

   The token colors inside one come from the Rouge palette in prose.css, which
   is the same palette a fenced block in a case study is painted with. There
   were three hand-rolled classes here for a while and a template tagging its
   own keywords by hand; SlabHelper runs the real lexer instead. */

.slab {
  margin: 0;
  padding: 0.5rem 0.625rem;
  background: var(--code-plate);
  color: var(--code-text);
  font-family: var(--font-mono);
  font-size: var(--step-fine);
  line-height: var(--leading-code);
  tab-size: 2;

  /* A command you cannot see is a command you cannot check, so a long line
     wraps rather than hiding off the right edge. Copying still yields the
     single line it was written as. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;

  /* JetBrains Mono ligates === into a single glyph, which misreads Ruby's
     case-equality operator as an identity sign. Code shows its own bytes. */
  font-variant-ligatures: none;

  & code {
    font: inherit;
  }
}

.slab-select {
  user-select: all;
  -webkit-user-select: all;
}

/* Inline code in running prose is a word, not a slab */
:is(.item-body, .page-lede, .section-note, .step-note, .arrival-note) code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  color: var(--orange-deep);
}

/* --- Setup steps --------------------------------------------------------- */

.steps {
  display: grid;
  gap: 1.75rem;
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}

.step {
  display: grid;
  gap: 0.625rem;
  counter-increment: step;
}

.step-title {
  display: flex;
  gap: 0.75rem;
  align-items: baseline;
  margin: 0;
  font-family: var(--heading-family);
  font-size: var(--step-title);
  font-weight: var(--heading-weight);
  line-height: var(--leading-title);

  &::before {
    content: counter(step);
    flex: none;
    font-family: var(--font-label);
    font-size: var(--step-label);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--track-caps);
    color: var(--orange-deep);
  }
}

.aside {
  border-top: 1px solid var(--rule-hair);
  padding-top: 1rem;

  & summary {
    cursor: pointer;
    list-style: none;
    font-family: var(--font-label);
    font-size: var(--step-label);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--track-caps);
    text-transform: uppercase;
    color: var(--ink-soft);

    &::-webkit-details-marker {
      display: none;
    }

    &::before {
      content: "+";
      display: inline-block;
      width: 1rem;
      color: var(--orange-deep);
    }

    &:hover {
      color: var(--orange-deep);
    }
  }

  &[open] > summary::before {
    content: "–";
  }

  & > * + * {
    margin-top: 0.75rem;
  }
}

.setup-license + .setup-license {
  padding-top: 1rem;
  border-top: var(--rule-heavy) solid var(--ink);
}

/* --- The arrival ---------------------------------------------------------
   The moment the shop stops sounding like the product and starts sounding
   like the shop. The register has already flipped the ground, the heading
   face and the depth; this band is the sand it lands on. */

.arrival {
  padding-block: clamp(2.75rem, 7vw, 4.5rem);
  background: var(--sand-deep);
}

.arrival-inner {
  display: grid;
  gap: 1.125rem;
  justify-items: start;
}

.arrival-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--step-display);
  font-weight: var(--weight-regular);
  line-height: var(--leading-display);
  letter-spacing: var(--track-display);
  text-wrap: balance;
}

.arrival-lede {
  margin: 0;
  max-width: var(--measure-lede);
  font-size: var(--step-lede);
  color: var(--ink-soft-on-dune);
}

.arrival-actions {
  margin-top: 0.375rem;
}

.arrival-note {
  margin: 0;
  max-width: var(--measure-lede);
  font-size: var(--step-small);
  line-height: var(--leading-ui);
  color: var(--ink-soft-on-dune);
}

.arrival-email {
  font-family: var(--font-mono);
  font-size: 0.9em;
}

/* A plate is the arrived register's one piece of depth. In the quiet register
   its variables collapse to nothing and it is an ordinary section. */
.plate {
  display: grid;
  gap: 1rem;
  align-content: start;
  padding: var(--plate-pad);
  background: var(--plate);
  box-shadow: var(--plate-lift);
}

.detail-list {
  display: grid;
  gap: 0.875rem;
  margin: 0;

  & div {
    display: grid;
    gap: 0.1875rem;
  }

  & dt {
    font-family: var(--font-label);
    font-size: var(--step-label);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--track-caps);
    text-transform: uppercase;
    color: var(--ink-soft);
  }

  & dd {
    margin: 0;
  }
}

/* --- Pages still on plain markup ----------------------------------------
   Sign-in has not been rewritten yet. It renders inside this shell, so give
   bare elements a sane default rather than letting them arrive unstyled.

   Every selector here is :not([class]). Without it these bare-element rules
   would also reach the classed elements on every other page and undo their
   measure and their spacing. */

.shop-main {
  & > h1:not([class]) {
    margin: 0;
    font-family: var(--heading-family);
    font-size: var(--step-display);
    font-weight: var(--heading-weight);
    line-height: var(--heading-leading);
    letter-spacing: var(--heading-tracking);
  }

  & > p:not([class]) {
    margin: 0;
    max-width: var(--measure-lede);
  }

  & > form div + div {
    margin-top: 1rem;
  }

  & > table:not([class]) {
    width: 100%;
    border-collapse: collapse;
    text-align: left;

    & th,
    & td {
      padding: 0.75rem 0.75rem 0.75rem 0;
      border-bottom: 1px solid var(--rule-hair);
      font-weight: var(--weight-regular);
      vertical-align: baseline;
    }

    & thead th {
      border-bottom: var(--rule-heavy) solid var(--ink);
      font-family: var(--font-label);
      font-size: var(--step-label);
      letter-spacing: var(--track-caps);
      text-transform: uppercase;
    }
  }
}

/* --- The signed-in pages -------------------------------------------------
   /my is the arrived register throughout: sand ground, Archivo Black, plates
   that cast a warm lift. Nothing below re-states any of that — it comes from
   .shop--arrived — these are only the shapes those pages need. */

/* A row that is a fact on the left and a state on the right. A grid rather
   than a wrapping flex row, so that every row in a list breaks at the same
   width: with flex-wrap the badge sits beside a short package name and under a
   long one, and a column of rows disagreeing with itself reads as a mistake. */

.window-row {
  display: grid;
  gap: 0.5rem;
  justify-items: start;

  & > * {
    min-width: 0;
  }

  @media (width >= 30rem) {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.5rem 1.5rem;
    align-items: baseline;

    /* `:not(:first-child)`, because a row whose right-hand side is absent —
       a token with no badge on it — is one element, and an only child that is
       also the last child would otherwise be flung to the right column on its
       own. */
    & > :last-child:not(:first-child) {
      justify-self: end;
    }
  }
}

.block-title-sub {
  font-family: var(--font-body);
  font-size: var(--step-small);
  font-weight: var(--weight-regular);
  color: var(--ink-soft);
}

.detail-ref {
  font-family: var(--font-label);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-caps);
}

/* An address, an id, anything typed rather than read. Mono, and NOT tracked:
   the wide letter-spacing that suits a seven-character license ref pulls an
   email address apart. */

.detail-mono {
  font-family: var(--font-mono);
  font-size: 0.9em;
}

.detail-aside {
  display: block;
  margin-top: 0.25rem;
  font-size: var(--step-fine);
  color: var(--ink-soft);
}

/* The right-hand column of a package row: what kind of thing it is, and — when
   the row goes somewhere — that it does. Orange, which is what every other cue
   that a row is a door already uses: .item-cue on the storefront, the arrows on
   the marketing lists. Flex rather than the grid .item-side is, because these
   sit beside each other rather than stacking. */

.item-side--icons {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--orange-deep);
}

.icon--package,
.icon--arrow {
  flex: none;
}

/* The name answers the hover; the icons stay put. Two things changing color at
   once for one pointer reads as the row flinching. */
.item-link:hover .item-side--icons,
.item-link:focus-visible .item-side--icons {
  color: var(--orange);
}

/* On a license, a package's row is titled by the name you type into a Gemfile
   and by nothing else — there is no editorial name over it. So `.item-package`
   is not the quiet second line it is in a list of products here; it is the row,
   and it is sized and inked like one while keeping the mono it is typed in. */

.package-row .item-package {
  margin-top: 0;
  font-size: var(--step-title);
  line-height: var(--leading-title);
  color: inherit;
}

/* A package row is one line of mono and its icons, so the two are centered on
   each other rather than sharing a baseline. Baseline is the right rule for
   the two-line rows .item-link is built for, but an svg's baseline is its
   bottom edge — under it, 18px of icon stands on the text baseline and reads
   as floating above the name. The stroked gem got away with that; a filled
   tile does not. */
.item-link.package-row {
  align-items: center;

  & .item-side {
    align-self: center;
  }
}

.item-link.package-row:hover .item-package,
.item-link.package-row:focus-visible .item-package {
  color: var(--orange-deep);
}

/* What a list shows of a token: the public ref, an ellipsis, its last four
   characters. Set as code, because it is what a customer holds against
   ~/.bundle/config. It is the whole of a token's identity here — nothing on a
   token is named. */

.token-shape {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.9em;
  overflow-wrap: anywhere;
}

/* Reveal and Revoke, side by side on the row the token is on, wrapping rather
   than squeezing on a narrow phone. Stretch, so the two are the same height
   even though one is a link and the other a form button — the pair reads as
   one control or as two things that do not belong together, and nothing in
   between. */

.token-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0.5rem;
}

/* Revoke is one button and the second step is the browser's own confirm — see
   the confirm controller. `button_to` wraps it in a form, so the form is the flex
   item and the button fills it. NOT `display: contents`, which would make the
   button the item directly and is exactly the case browsers have historically
   dropped out of the accessibility tree. */

/* Two acts on one table row. `button_to` wraps each in a form, and a form is a
   block: left alone they stack, and a row of stacked buttons makes every row in
   the table twice as tall as it needs to be. */

.row-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.5rem;
}

.row-action {
  margin: 0;
  display: flex;
}

.token-revoke-form {
  margin: 0;
  display: flex;
}

/* --- Dialogs --------------------------------------------------------------
   One dialog, everywhere: `shared/_dialog` is the markup and this is the look
   of it. A <dialog> with `open` written by the server, closed by a link back to
   the page underneath, because no page in this shop loads JavaScript and
   `showModal()` is the only thing that would give a dialog the top layer and a
   real ::backdrop.

   The scrim is the dialog's parent, and that is the whole point. It used to be
   a ::before on the dialog itself with a negative z-index, which paints inside
   the dialog's own stacking context — over its background, under its text — so
   the card came out dimmed along with the page behind it. A parent paints
   first, and the dialog's own background covers it.

   Mobile first: the scrim is the scrolling box, so a dialog taller than the
   viewport scrolls rather than growing a second scrollbar inside a card. */

.dialog-scrim {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  /* `safe`, because a dialog taller than the phone it is on gets its top
     clipped by plain centering and there is no scrolling back up to it. */
  place-items: safe center;
  padding: var(--gutter);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: color-mix(in srgb, var(--ink) 55%, transparent);
}

.dialog {
  /* The UA gives a dialog `position: absolute` and `width: fit-content`, which
     would take it out of the grid that is centering it. */
  position: static;
  display: grid;
  gap: 0.875rem;
  width: min(34rem, 100%);
  max-width: none;
  /* The UA also gives it `height: fit-content` and `overflow: auto`, which
     together would grow a second scrollbar inside the card. The scrim is the
     one thing that scrolls. */
  height: auto;
  overflow: visible;
  margin: 0;
  overflow-wrap: anywhere;

  /* The heavy orange rule a token used to get for being readable once. It is
     the mark of a dialog now: the one thing on the page that is asking to be
     dealt with before the page is left. */
  border: 0;
  border-top: var(--rule-heavy) solid var(--orange);

  /* Not the register's plate variables: the quiet register collapses those to
     a transparent box with no padding, and this has to be a card over a scrim
     wherever it is rendered — the order page is in the quiet register. */
  padding: clamp(1.5rem, 4vw, 2.25rem);
  background: var(--paper);
  color: var(--ink);
  box-shadow: var(--lift-quiet);
}

.dialog-title {
  margin: 0;
}

.dialog-body {
  display: grid;
  gap: 0.875rem;
}

.dialog-close {
  justify-self: end;
  margin: 0;
}

/* `display: grid` above is an author rule, and so outranks the UA's
   `dialog:not([open]) { display: none }`. Nothing closes a dialog in the
   browser today — they close by navigating — but if one ever does, the scrim
   has to go with it rather than be left lying over the page on its own. */
.dialog:not([open]) {
  display: none;
}

.dialog-scrim:not(:has(dialog[open])) {
  display: none;
}

/* The link that sits beside a heading — "(configuration)" — in the body face
   rather than the heading's, so it reads as an aside to the title. */
.block-title-link {
  font-family: var(--font-body);
  font-size: var(--step-small);
  font-weight: var(--weight-regular);
  color: var(--ink-soft);
  text-decoration: underline;
  text-underline-offset: 3px;

  &:hover,
  &:focus-visible {
    color: var(--orange-deep);
  }
}

/* --- Issuing ------------------------------------------------------------
   A plain offer at the foot of a license, not a panic button. A hairline
   opens it rather than a heavy rule: making a token is ordinary. */

.issue-token {
  display: grid;
  gap: 0.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule-hair);
}

.issue-token-title {
  margin: 0;

  &::before {
    content: none;
  }
}

.issue-token-form {
  display: grid;
  gap: 0.875rem;
  justify-items: start;
  max-width: var(--measure-lede);

  & .field {
    width: 100%;
  }
}

/* Revoking is a disclosure, not a dialog: nothing on these pages loads
   JavaScript, so the second step has to be something HTML can do on its own.
   It sits inside a row that already has a hairline under it, so it gives back
   the one .aside would draw above it. */

.revoke {
  border-top: 0;
  padding-top: 0.5rem;
}

/* --- Licenses ------------------------------------------------------------ */

.license-card + .license-card {
  margin-top: 0;
}

.license-ref {
  color: inherit;
  text-decoration: none;
  letter-spacing: var(--track-display);

  &:hover,
  &:focus-visible {
    color: var(--orange-deep);
  }
}

.setup-license + .setup-license {
  padding-top: 1.5rem;
  border-top: var(--rule-heavy) solid var(--ink);
}

/* --- Orders -------------------------------------------------------------
   The invoice line hangs under the row it belongs to rather than beside it:
   the row is already a link, and a link inside a link is not a thing. */

.order-invoice {
  padding: 0 0 1rem;

  & a {
    color: var(--orange-deep);
  }
}

/* A row whose body is a link still has to hold the non-link line under it, so
   the padding the link was carrying comes off its bottom edge. */
.rule-list-item:has(> .item-link):has(> .order-invoice) > .item-link {
  padding-bottom: 0.375rem;
}

/* --- Admin forms ---------------------------------------------------------
   One column on a phone and one column on a desk: these are typed once in a
   while, by one person, and a two-column layout would only make the eye travel
   further for no gain. The width cap is the readable-measure one the prose
   already uses, so a text area does not stretch to the width of a monitor. */

.admin-form {
  display: grid;
  gap: 1.5rem;
  max-width: 34rem;
}

/* A set of checkboxes or radios reads as one question with several answers,
   so the legend carries the question and the control never repeats it. */
.choice-set {
  margin: 0;
  padding: 0;
  border: 0;
}

.choice-legend {
  padding: 0;
  font-family: var(--font-label);
  font-size: var(--step-label);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-soft);
}

.choice {
  display: flex;
  gap: 0.625rem;
  align-items: baseline;
  margin-top: 0.5rem;
  cursor: pointer;

  & input {
    flex: none;
  }
}

.admin-form-actions {
  display: grid;
  justify-items: start;
}

/* --- Discount code --------------------------------------------------------
   One label over one row of control height in both states — the box with its
   Apply, or the applied code with its Remove — so swapping one for the other
   in place moves nothing below. */

/* Each keeps the gap of the block it was lifted out of, so the region reads
   as if it were never there. */
.offer-pricing {
  display: grid;
  gap: 1.125rem;
}

.checkout-pricing {
  display: grid;
  gap: 1rem;
}

.discount-field {
  max-width: 30rem;
}

.discount-entry {
  display: flex;
  align-items: center;
  gap: 0.5rem 0.75rem;
  min-height: var(--control-height);

  & .field-control {
    flex: 1;
    min-width: 0;
    text-transform: uppercase;
  }

  & .button {
    flex: 0 0 auto;
  }
}

.discount-applied {
  margin: 0;
}

.offer-was {
  color: var(--ink-soft);
  font-size: var(--step-ui);
}

/* A price, a total or an applied code that has just been swapped in by the
   discount controller. 120ms is the controller's ARRIVE_BEFORE_PROCEED_MS: a
   Buy that was held for the code goes on as this ends. Opacity only, so
   nothing moves. */
.is-arriving {
  animation: discount-arrive 120ms ease-out;
}

@keyframes discount-arrive {
  from {
    opacity: 0.15;
  }
}

/* A discount code and the badge saying why it cannot be used, on one line.
   The line is held to the badge's height whether or not there is one, so the
   row does not grow when Disable puts a badge on it. */
.code-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.75rem;
  min-height: 1.8125rem;

  & .token-shape {
    display: inline;
  }
}
