/* Orbitra component layer.
   Built 30 Jul 2026 against tokens.css, which is generated from tokens.json by build-tokens.mjs.
   RULE: every value in this file is a var(). A raw hex or an off-scale px value here is a defect.
   Exceptions are documented inline and each names the flag it is held under.
   3 Aug: the button and input 36px exceptions are RESOLVED, not re-held. Both are
   now var(--sp-40). The tag stays at 24px, on-scale, under flag 8. Two shadow
   values are held as exception 4, see .o-menu. */

/* ---------------------------------------------------------------- Button */
.o-btn {
  
  height: var(--sp-40);
  padding: 0 var(--sp-20);
  border: none;
  border-radius: var(--r-sm);
  font-family: var(--font-ui);
  font-weight: var(--fw-medium);
  font-size: var(--fs-base);
  line-height: var(--lh-base);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-8);
  cursor: pointer;
  transition: background-color var(--motion-fast) var(--motion-ease);
}
.o-btn:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* ================= THE ACCENT RULE, AND THE MINT RAMP UNDER IT =================
   Ruled by Shane, 6 August 2026. This is the canonical wording; anything that
   still says "one mint element per SCREEN" (TOKENS-PROPOSAL.md line 81, and the
   older header notes in the screens) predates the ruling and is superseded.

   ONE ACCENT ACTION PER SURFACE. Not per screen. A surface is what the reader
   is looking at on its own: the page, or a modal sitting over a scrim that has
   taken the page out of play. It is a CEILING, not a quota, so a surface with
   no single dominant act carries none. Per surface is what lets a modal keep
   its own mint while the page behind the scrim keeps its own, because with the
   scrim up the page is not live. Per surface is ALSO what stops a pinned panel
   spending a second mint: a panel that hangs over the page with no scrim is not
   its own surface, the reader sees both at once, and the count is one between
   them. That is why the setup guide's row action is a link, see .g-row__cta.
   An ACT GROUP counts as one accent, Shane 4 August: a button and the link
   beside it that name two halves of the same act are one accent, not two.

   THE RAMP, AND THE MIDDLE IS THE RESTING FILL. Six steps exist: mint-300,
   400, 500, 550, 600, 700. Measured by WCAG relative luminance, mint-550 sits
   54.6% of the way from the lightest step to the darkest, which is the middle
   of the ramp; mint-500 sits at 31.7% and is the accent and focus-ring value,
   never a large fill. So:
       REST    = the middle,   mint-550, through action.primary.bg
       HOVER   = brighter,     mint-400, two steps up,   action.primary.bgHover
       PRESSED = darker,       mint-700, two steps down, action.primary.bgActive
   Two steps each way rather than one, because one step was measured on 31 July
   as almost invisible in use. Symmetric, so press and hover read as equal and
   opposite. THE BUTTON READS THE SEMANTIC TOKENS, NEVER A MINT PRIMITIVE: a
   var(--mint-550) anywhere outside tokens.css is a defect, and there were two
   of them in this file until this morning.

   INK LABEL ON ALL THREE, AND ALL THREE PASS AA. neutral-900 on mint-550 is
   10.12:1, on mint-400 14.44:1, on mint-700 5.34:1, against the 4.5:1 that the
   15/22 label needs. No mint step passes with a white label at any size, which
   is why action.primary.text is ink and not white. The fill also clears WCAG
   1.4.11's 3:1 against every surface it sits on: mint-550 is 10.51:1 on the
   canvas, 10.12:1 on bg/surface and 9.45:1 on bg/raised.
   WHY REST DOES NOT MOVE TO mint-500, which is the other reading of "middle" if
   you count index positions rather than luminance: the focus ring is mint-500,
   and a mint-500 ring on a mint-500 fill measures 1:1 and disappears. The ring
   sits exactly one step lighter than its own button by design (Shane, 31 July),
   and mint-500 is also the brand accent, documented as marks only and never a
   large fill. Both rules would break for a step that is further from the middle
   by measurement than the one already in use. */
.o-btn--primary { background: var(--action-primary-bg); color: var(--action-primary-text); }
.o-btn--primary:hover:not(:disabled) { background: var(--action-primary-bgHover); }
.o-btn--primary:active:not(:disabled) { background: var(--action-primary-bgActive); }
.o-btn--secondary { background: var(--action-secondary-bg); color: var(--action-secondary-text); }
.o-btn--secondary:hover:not(:disabled) { background: var(--action-secondary-bgHover); }
.o-btn--secondary:active:not(:disabled) { background: var(--action-secondary-bgActive); }
.o-btn:disabled { background: var(--bg-raised); color: var(--text-disabled); cursor: default; }
/* Loading keeps the box and takes the disabled treatment. The label names the act, never a spinner. */
.o-btn--loading { background: var(--bg-raised); color: var(--text-disabled); cursor: default; }
/* Full width. Promoted 3 August: this was defined in the sign-up screen, which
   meant an o- prefixed class living outside the system. */
.o-btn--block { width: 100%; }

/* ----------------------------------------------------------------- Field */
.o-field { display: flex; flex-direction: column; gap: var(--sp-8); }
/* 14/20, ruled 3 August. At 13/18 the label tied with the helper text on BOTH
   size and weight, so a field's signpost was competing with its own guidance and
   separated only by one colour step. The ladder is now helper 13 < label 14 <
   value 15.
   COLOUR, second pass the same day: text/primary, not secondary. The argument for
   holding it back was that the label should not compete with the value it labels.
   That assumed there IS a value. In the state a trader actually meets first the
   form is empty, so the labels are the only content in the card and dimming them
   left nothing to read. Weight and size still separate label from value, and the
   input's own fill and border separate the value area. 15.32:1 on the card.
   WEIGHT, third pass: regular, not medium. 14/20 at 400 IS a named style, UI/SM,
   style 9 of the twelve. 14/20 at 500 is NOT: the system pairs regular and medium
   only at Base and XS, and there is no UI/SM Medium, so the medium version was
   off-scale. At primary white the colour already carries the emphasis and the
   extra weight made the label compete with the attestation line below it. */
.o-label {
  font-family: var(--font-ui); font-weight: var(--fw-regular);
  font-size: var(--fs-sm); line-height: var(--lh-sm);
  color: var(--text-primary);
}
/* FIELD SURFACE, ruled 3 August. The input, the checkbox and the upload dragger
   are one family: the surface a reader is invited to fill. All three moved from
   bg/inset, which is the correct semantic role and always was. The fields looked
   like cut-outs because prototype/tokens.css was STALE: it resolved bg/inset to
   neutral-950 #00060A, the value Shane rejected on 31 July. tokens.json has said
   neutral-925 #040C10 since that ruling. Regenerating the token layer fixed it at
   source, so the components go back to the role rather than routing around it. */

.o-input {
  height: var(--sp-40);              /* flag 7, same ruling as the button */
  padding: 0 var(--sp-12);
  background: var(--bg-inset);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  color: var(--text-primary);
  font-family: var(--font-ui); font-size: var(--fs-base); line-height: var(--lh-base);
  width: 100%;
}
.o-input::placeholder { color: var(--text-tertiary); }
.o-input:hover:not(:disabled):not(.is-error) { border-color: var(--border-hover); }
/* FOCUS IS THE BORDER ITSELF, NOT A SECOND RING (Shane, 2 September 2026,
   superseding the offset-outline treatment above this line's history). The
   field's own border turns mint, with a soft translucent mint halo hugging it
   (4px, no gap), the same construction the licence graphic's manager row
   already uses. The halo is the indicator area that keeps this past the
   WCAG 2.2 focus-appearance bar without moving layout. Buttons and links keep their offset ring,
   but only under :focus-visible, so a mouse user never sees a double border
   anywhere. */
.o-input:focus { outline: none; border-color: var(--mint-500);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--mint-500) 25%, transparent); }
.o-input.is-error { border-color: var(--status-danger-fill); }
.o-input:disabled { background: var(--bg-raised); color: var(--text-disabled); }
/* Ambient guidance: regular weight, text/tertiary. Ruled 3 August alongside the
   terms line, which does the same job and disagreed with this on two axes at
   once. One voice for "you may skip this", which is what makes text/secondary
   mean "the system is speaking to you" rather than nothing in particular. */
.o-help {
  font-family: var(--font-ui); font-weight: var(--fw-regular);
  font-size: var(--fs-xs); line-height: var(--lh-xs);
  color: var(--text-tertiary);
}
.o-help--error { color: var(--status-danger-text); }
/* Promoted out of the sign-up screen 3 August, where it had been defined locally
   against an o- prefixed class. Every field reserves its helper line whether or
   not it has one, so an error appearing never moves the fields below it. This is
   the Still Screen Rule at field level and it belongs to the component, not to a
   screen: four more screens inherit this form and would each have redefined it. */
.o-help { min-height: var(--lh-xs); display: block; }

/* Reveal control. Ruled 3 August, reversing an earlier call that left it out:
   the sign-up spec asked for a show-password control and the first pass missed it.
   It matters most in the case this product is built for, a trader typing on an
   iPad at a noisy conference table with no way to check what they typed.
   Tertiary at rest so it does not compete with the value, primary on hover. */
.o-input-wrap { position: relative; display: block; }
.o-input--reveal { padding-right: var(--sp-40); }
.o-reveal {
  position: absolute; right: var(--sp-4); top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--sp-32); height: var(--sp-32);
  color: var(--text-tertiary); background: none; border: none;
  border-radius: var(--r-sm); cursor: pointer;
}
.o-reveal svg { width: var(--icon-md); height: var(--icon-md); stroke-width: var(--icon-md-stroke); }
.o-reveal:hover:not(:disabled) { color: var(--text-primary); }
.o-reveal:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.o-reveal:disabled { color: var(--text-disabled); cursor: default; }
/* One glyph swapped, never two stacked: the state IS which icon shows. */
.o-reveal .icon-off { display: none; }
.o-reveal[aria-pressed="true"] .icon-on { display: none; }
.o-reveal[aria-pressed="true"] .icon-off { display: block; }

/* Select shares the input body. The chevron is a background image so no icon dependency lands in CSS. */
.o-select { appearance: none; padding-right: var(--sp-32); }
.o-select-wrap { position: relative; display: block; }
.o-select-wrap::after {
  content: ""; position: absolute; right: var(--sp-12); top: 50%;
  width: 8px; height: 8px; margin-top: -6px;
  border-right: 1.3px solid var(--text-tertiary); border-bottom: 1.3px solid var(--text-tertiary);
  transform: rotate(45deg); pointer-events: none;
}

/* --------------------------------------------------- Checkbox and radio */

.o-check {
  appearance: none; width: 16px; height: 16px; flex: none;
  background: var(--bg-inset);       /* field surface, see .o-input */
  border: 1px solid var(--border-strong);   /* field-border ruling, 5 Aug. See .o-input */
  border-radius: var(--r-xs);
  cursor: pointer; position: relative;
  transition: background-color var(--motion-fast) var(--motion-ease);
}
.o-check--radio { border-radius: var(--r-full); }
.o-check:hover:not(:disabled) { border-color: var(--border-hover); }
.o-check:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.o-check:checked { background: var(--action-secondary-bg); border-color: var(--action-secondary-bg); }
.o-check:checked::after {
  content: ""; position: absolute; left: 5px; top: 2px; width: 4px; height: 8px;
  border-right: 2px solid var(--text-onFill); border-bottom: 2px solid var(--text-onFill);
  transform: rotate(45deg);
}
.o-check--radio:checked::after {
  left: 4px; top: 4px; width: 6px; height: 6px; border: none;
  border-radius: var(--r-full); background: var(--text-onFill); transform: none;
}
.o-check.is-error { border-color: var(--status-danger-fill); }
.o-check:disabled { background: var(--bg-raised); border-color: var(--border-default); cursor: default; }
.o-check-row { display: flex; align-items: center; gap: var(--sp-8);
  font-family: var(--font-ui); font-size: var(--fs-base); line-height: var(--lh-base); color: var(--text-primary); }

/* -------------------------------------------------------------- Surfaces */
.o-card {
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-md);
  padding: var(--sp-24);
}
/* Raised variant. Promoted 5 August 2026, SYSTEM-DELTA-2026-08-05-signup-B.md
   item 3. For a card sitting over anything other than flat colour: over a
   picture, the 1px border alone reads as a rectangle drawn onto a photograph,
   and the drop shadow is what makes the card sit ON the surface rather than in
   it. elev-3 and not elev-2, deliberately: elev-2 opens with a 1px inset top
   highlight that stacks inside a full border and reads as a thicker top stroke
   (documented exception 4, see .o-menu). elev-3 is a pure drop shadow, so the
   collision does not apply. The pre-login shell applies this treatment to its
   card automatically, because its card always sits over the backdrop. */
.o-card--raised { box-shadow: var(--elev-3); }
/* THE MODAL NEVER REACHES THE EDGE OF THE WINDOW, AND IT SCROLLS ITSELF.
   Promoted 10 August 2026 from two screen-local copies, which is the same story
   as .modal-h below and the third time this file has had to end it. The booking
   modal carried `max-height:100%;overflow:auto` on .booking-modal and the
   home's share ask carried the identical pair on .share-scrim .o-modal, each
   written as if it were that screen's own problem. The verification prompt and
   the interruption set never got it, so a tall modal on either could run to the
   top of the window with nothing holding it off. Putting it here makes it true
   of every modal, including ones nobody has drawn yet.

   WHY IT GOES ON .o-modal AND NOT ON A NEW .o-scrim. Every modal in the system
   already carries this class, and the four scrims do not share one: they are
   .scrim, .share-scrim and .booking-scrim across four files. A new scrim class
   would mean a markup change in all four to gain a guarantee we can have
   without touching any of them.

   WHAT THE SCRIM MUST STILL DO, and all four already do it: centre its modal
   and carry `padding: var(--sp-40)`. That padding is the whole mechanism.
   max-height: 100% resolves against the scrim's CONTENT box, which is the
   window less both gutters, so the 40px survives at any window height rather
   than being the first thing a tall modal eats. A scrim with no padding gives
   a modal that touches the edge, so the padding is a requirement on the scrim
   and not a preference.

   overscroll-behavior: contain IS THE HALF THAT STOPS THE PAGE MOVING. Without
   it, a scroll that reaches the end of the modal chains to whatever is behind
   the scrim and the page creeps while a modal is open. With it, the scroll
   stops at the modal's own end. In the built product the page behind should
   also be made inert while a modal is open; that is a build concern rather than
   a stylesheet one, and it is written into the hand-off note rather than faked
   here, because these screens sit inside a fixed frame that never scrolls.

   ONE CONSTRAINT THIS PUTS ON EVERY MODAL. `overflow: auto` cannot apply to one
   axis alone: setting it vertically forces the horizontal axis out of visible
   too. So nothing may hang outside the modal's padding box. The corner close
   control is safe, because it overhangs the CONTENT COLUMN and not the card. */
.o-modal {
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--r-lg);
  padding: var(--sp-32);
  box-shadow: var(--elev-3);
  max-height: 100%;
  overflow: auto;
  overscroll-behavior: contain;
}
/* THE MODAL'S HEADING, BODY AND ACT ROW. Promoted 6 August 2026 from three
   screen-local copies that had drifted in four places: the booking modal, the
   verification prompt and the post-login home's share ask. The booking modal's
   own comment asserted its declarations were "the identical declarations the
   other two modals in the set already carry", and that was true of the
   verification prompt and false of the home. Three copies, and no two of
   .modal-acts alike.
   THE NAMES STAY UNPREFIXED FOR NOW. Renaming them to .o-modal__h and so on is
   the right end state and it is queued, because it touches markup and a
   data-driven state machine in three screens. The file already carries the
   .g-row* set and .wizard under the same temporary exception, so this is not a
   new kind of thing in here.

   WHAT WAS PICKED, AND WHY, VALUE BY VALUE.
   .modal-h max-width: 26ch, the verification prompt's and the booking modal's,
   over the home's padding-right: 40px. They were doing the SAME job by
   different means and only one of them says so: the booking modal's comment
   records that the cap exists to clear .o-x--corner, which overhangs the
   content column's right edge, and all three of these modals carry that close
   control. A measure is also the thing a heading actually wants; 40px of
   right padding is the clearance and nothing else, so it caps nothing at a
   wider card. Measured: the home's longest heading sets 311px against the
   397px the cap allows, so the home does not re-wrap.
   .modal-p max-width: 52ch, two copies of three, against the home's 56ch. 52ch
   is 460px and the home's card leaves 456px inside its padding, so the cap was
   never reached there and 56 was a number with no effect. It binds on the
   booking modal, whose card is 560 wide, and it is the measure the booking
   modal's banner parts were explicitly written to copy.
   .modal-acts gap: 16 and margin-top: 24, which is neither file wholesale: it
   is the majority value on each half, and each majority is also the system's
   own. 16 is the act-row gap .o-welcome__foot and the home's own act band both
   use, so the verification prompt's 20 was the outlier. 24 is the verification
   prompt's and the home's; the booking modal's 20 contradicts its own comment
   claiming it copied the others, so it is a slip and not a decision.
   TWO SCREENS MOVE, BY 4px EACH, AND BOTH ARE THE POINT OF THE PROMOTION. The
   verification prompt's two-act states close by 4px; the booking modal's act
   row and everything ordered after it drop by 4px. */
.modal-h { font: var(--fw-medium) var(--fs-xl)/var(--lh-xl) var(--font-display); max-width: 26ch; }
.modal-p { font: var(--fw-regular) var(--fs-sm)/var(--lh-sm) var(--font-ui);
  color: var(--text-secondary); margin-top: var(--sp-8); max-width: 52ch; }
.modal-acts { display: flex; align-items: center; gap: var(--sp-16); flex-wrap: wrap; margin-top: var(--sp-24); }

/* ------------------------------------------------------------------ Tag */
/* ============================================================================
   TAG: HELD, 6 August 2026. NO CLAIMANT IN V0.
   Its two named claimants both moved to the seal: the Orbitra ID issued screen
   on 5 August at Shane's instruction, and the public verification page, which
   was drawn with seals and never used a tag in its body. The last tag markup
   sat in verify-and-tamper-check-final.html, superseded 6 August by the
   redrawn v0-screens/verify-and-tamper-check.html, which carries no tag, so
   nothing holds the claim.
   The rules stay here because October may claim them back. Under the standing
   claimant rule they are not part of V0 and they are not in the Figma
   published set: see FIGMA-SYNC-QUEUE.md, item 2.
   Tag/Danger was separately cut long before this, because a failed document
   uses the verdict panel and a red label is not enough for a tamper result.
   ============================================================================ */
.o-tag {
  height: 24px;                      
  padding: 0 var(--sp-8);
  border-radius: var(--r-xs);
  display: inline-flex; align-items: center; gap: var(--sp-4);
  font-family: var(--font-ui); font-weight: var(--fw-medium);
  font-size: var(--fs-xs); line-height: var(--lh-xs);
  background: var(--bg-raised); color: var(--text-secondary);
  border: 1px solid transparent;
}
.o-tag--success { background: var(--status-success-bg); border-color: var(--status-success-border); color: var(--status-success-text); }
.o-tag--warning { background: var(--status-warning-bg); border-color: var(--status-warning-border); color: var(--status-warning-text); }
.o-tag--danger  { background: var(--status-danger-bg);  border-color: var(--status-danger-border);  color: var(--status-danger-text); }
.o-tag--info    { background: var(--status-info-bg);    border-color: var(--status-info-border);    color: var(--status-info-text); }

/* ----------------------------------------------------------- Status dot */
/* Filled with the status TEXT colour, not the fill (DESIGN.md Shapes). Corrected
   31 Jul: the fill tier is duller than the verdict word it sits beside, and this
   dot carries the Never Colour Alone load, so it cannot be the dimmest mark. */
.o-dot { width: 7px; height: 7px; border-radius: var(--r-full); flex: none; display: inline-block; }
.o-dot--success { background: var(--status-success-text); }
.o-dot--warning { background: var(--status-warning-text); }
.o-dot--danger  { background: var(--status-danger-text); }
.o-dot--info    { background: var(--status-info-text); }
.o-dot--neutral { background: var(--text-secondary); }

/* --------------------------------------------------------- Progress line */
/* The loading voice of the system. Always captioned, never a bare spinner. */
.o-progress { height: 2px; background: var(--border-subtle); overflow: hidden; border-radius: var(--r-full); }
/* Sweep timing, ruled by Shane 31 Jul: the 240ms alternating bounce read fast and jumpy.
   Now a single unbroken pass at 1400ms on a symmetric ease, so it glides instead of
   lurching at each end. 1400ms is deliberately off the motion token scale (fast 120,
   base 180, slow 240): those are interaction durations and this is an ambient loop.
   Add motion.sweep to tokens.json when design/ is next reconciled.
   Supersedes DESIGN.md Progress line, which still specifies 240ms per cycle. */
.o-progress__bar { height: 100%; width: 35%; background: var(--action-primary-bg);
  animation: o-sweep 1400ms cubic-bezier(.65,0,.35,1) infinite; }
@keyframes o-sweep { from { transform: translateX(-100%); } to { transform: translateX(286%); } }
.o-progress-caption { font-family: var(--font-ui); font-size: var(--fs-sm); line-height: var(--lh-sm);
  color: var(--text-secondary); margin-top: var(--sp-8); }
@media (prefers-reduced-motion: reduce) { .o-progress__bar { animation: none; width: 100%; } }

/* ----------------------------------------------------------- Ledger row */
.o-ledger { border-top: 1px solid var(--border-subtle); }
.o-ledger__row {
  display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-24);
  padding: 14px 0;                   
  border-bottom: 1px solid var(--border-subtle);
}
.o-ledger__key { font-family: var(--font-ui); font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--text-tertiary); }
.o-ledger__val { font-family: var(--font-ui); font-size: var(--fs-base); line-height: var(--lh-base); color: var(--text-primary); font-variant-numeric: tabular-nums; }

.o-ledger__val--id { font-family: var(--font-mono); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }

/* -------------------------------------------------------- Checklist row */
.o-checklist { display: flex; flex-direction: column; }
.o-checklist__row {
  display: flex; align-items: center; gap: var(--sp-12);
  padding: var(--sp-12) 0; border-bottom: 1px solid var(--border-subtle);
}
.o-checklist__mark { width: 16px; height: 16px; border-radius: var(--r-full);
  border: 1px solid var(--border-default); flex: none; display: grid; place-items: center; }
.o-checklist__label { font-family: var(--font-ui); font-size: var(--fs-base); line-height: var(--lh-base); color: var(--text-secondary); }
.o-checklist__row.is-done .o-checklist__label { color: var(--text-primary); }
.o-checklist__row.is-done .o-checklist__mark { border-color: transparent; }
.o-checklist__meta { margin-left: auto; font-family: var(--font-ui); font-size: var(--fs-xs);
  line-height: var(--lh-xs); color: var(--text-tertiary); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------- Upload dragger */
/* Two states only. The file row owns the per-file lifecycle (COMPONENT-ANATOMY). */
.o-dragger {
  background: var(--bg-inset);       /* field surface, see .o-input */
  border: 1px dashed var(--border-strong);  /* field-border ruling, 5 Aug. See .o-input */
  border-radius: var(--r-sm);
  padding: var(--sp-24);
  text-align: center;
  font-family: var(--font-ui); font-size: var(--fs-sm); line-height: var(--lh-sm);
  color: var(--text-secondary);
  transition: border-color var(--motion-fast) var(--motion-ease), background-color var(--motion-fast) var(--motion-ease);
}
.o-dragger.is-over { border-color: var(--border-hover); background: var(--bg-hover); }
.o-filerow { display: flex; align-items: center; gap: var(--sp-12); padding: var(--sp-12) 0;
  border-bottom: 1px solid var(--border-subtle); }
.o-filerow__name { font-family: var(--font-ui); font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--text-primary); }

.o-filerow__size { font-family: var(--font-ui); font-size: var(--fs-xs); line-height: var(--lh-xs);
  color: var(--text-tertiary); font-variant-numeric: tabular-nums; margin-left: auto; }

/* --------------------------------------------------------------- Stepper */
/* Four statuses including error, which the Ant audit found missing from ours. */
.o-stepper { display: flex; flex-direction: column; gap: 0; }
.o-step { display: flex; gap: var(--sp-12); }
.o-step__rail { display: flex; flex-direction: column; align-items: center; }
.o-step__mark { width: 16px; height: 16px; border-radius: var(--r-full); flex: none;
  border: 1px solid var(--border-default); display: grid; place-items: center; }
.o-step__line { width: 1px; flex: 1; min-height: var(--sp-24); background: var(--border-subtle); }
.o-step__body { padding-bottom: var(--sp-24); }
.o-step__title { font-family: var(--font-ui); font-size: var(--fs-base); line-height: var(--lh-base); color: var(--text-tertiary); }

.o-step.is-process .o-step__title { color: var(--text-primary); font-weight: var(--fw-medium); }
.o-step.is-process .o-step__mark { border-color: var(--text-primary); }
.o-step.is-finish .o-step__title { color: var(--text-secondary); }
.o-step.is-finish .o-step__mark { background: var(--status-success-fill); border-color: transparent; }
.o-step.is-error .o-step__title { color: var(--status-danger-text); }
.o-step.is-error .o-step__mark { background: var(--status-danger-fill); border-color: transparent; }

/* ---------------------------------------------------------------- Banner */
/* Ruled 3 August, promoted from the sign-up trial. No fill, no border, no padding:
   the banner is type, not a container. The status colour lives on the TITLE and the
   body stays text/secondary, which is the same move Rule 21 made on the verdict panel
   and the same move the inline confirm made the same day. Three status surfaces, one
   voice. BOTH variants change together, deliberately, so warning and info cannot
   drift apart. The screen owns placement and spacing; the component owns type. */
.o-banner { display: flex; flex-direction: column; gap: var(--sp-4); }
.o-banner__title { font-family: var(--font-ui); font-weight: var(--fw-medium);
  font-size: var(--fs-sm); line-height: var(--lh-sm); }
/* Weight declared, not inherited: this component is placed in cards, on the canvas
   and inside panels, and it must not read differently depending on where it lands. */
.o-banner__body { font-family: var(--font-ui); font-weight: var(--fw-regular);
  font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--text-secondary); }
.o-banner--warning .o-banner__title { color: var(--status-warning-text); }
.o-banner--info .o-banner__title { color: var(--status-info-text); }


/* Ruled 3 August: no tinted background, no pill. A success message is a
   sentence, not a badge. The tick carries the status colour and the words
   stay text/primary, so confirmation reads at the same weight as everything
   else on the screen and the green is a mark rather than a surface. */

.o-confirm { background: none; color: var(--text-primary); padding: 0;
  display: inline-flex; align-items: center; gap: var(--sp-8);
  font-family: var(--font-ui); font-size: var(--fs-sm); line-height: var(--lh-sm); }
.o-confirm__tick { width: var(--icon-sm); height: var(--icon-sm); flex: none;
  color: var(--status-success-text); stroke-width: var(--icon-sm-stroke); }

/* --------------------------------------------------------- Verdict panel */

.o-verdict { border-radius: var(--r-md); padding: var(--sp-24);
  background: var(--bg-surface); --verdict-rule: var(--border-default); }
.o-verdict__head { display: flex; align-items: center; gap: var(--sp-12); margin-bottom: var(--sp-16); }
.o-verdict__word { font-family: var(--font-display); font-weight: var(--fw-medium);
  font-size: var(--fs-lg); line-height: var(--lh-lg); }
.o-verdict--success .o-verdict__word { color: var(--status-success-text); }
.o-verdict--danger .o-verdict__word { color: var(--status-danger-text); }
/* --icon-md, 20px on a 1.65 stroke, which is the named step for a glyph sitting
   with type rather than standing alone. Against the 19/28 word it is optically
   level; --icon-lg at 24 read as an icon with a caption. */
.o-verdict__mark { width: var(--icon-md); height: var(--icon-md); flex: none;
  display: grid; place-items: center; }
.o-verdict__mark svg { width: 100%; height: 100%; stroke-width: var(--icon-md-stroke); display: none; }
.o-verdict--success .o-verdict__mark { color: var(--status-success-text); }
.o-verdict--danger .o-verdict__mark { color: var(--status-danger-text); }
/* MARKUP CONTRACT: .o-verdict__mark holds exactly two SVGs, .mark--danger
   carrying Tabler alert-triangle and .mark--success carrying Tabler check,
   the bare tick. The variant on the panel decides which one renders. */
.o-verdict--danger .o-verdict__mark .mark--danger { display: block; }
.o-verdict--success .o-verdict__mark .mark--success { display: block; }

/* ------------------------------------------------------------- App shell */

.o-shell {
  display: grid;
  /* THE RAIL WIDTH IS A VARIABLE WITH THE OLD CONSTANT AS ITS FALLBACK, so a
     screen that sets nothing renders byte-identically to before. 236 expanded,
     --sp-64 collapsed. The 220-to-400 DRAGGABLE RANGE that 3 August created is
     RETIRED with the splitter (SYSTEM-DELTA-2026-08-05.md section 4): no screen
     has to hold across a range any more, only across two named widths. */
  grid-template-columns: var(--rail-w, var(--size-nav-rail)) 1fr;
  border: 1px solid var(--border-default);
  border-radius: var(--r-md); overflow: hidden;
  /* ONE GUTTER AND ONE CAP, SHARED BY THE TOP BAR AND THE PAGE, which is the
     whole point: the bar hard-coded its own padding and said nothing about the
     content region's, so every screen picked its own and they drifted 44px
     apart. Both now read the same two variables and cannot disagree.
     48 AND 1200 ARE SHANE'S RULED VALUES, 4 August. --sp-48 rather than the
     literal 50 he asked for: the scale runs 40, 48, 64, and 2px is below the
     threshold of visible difference. At the 1440 reference the rail leaves
     1204px, four under the cap, so the cap only starts doing work above roughly
     1636px of window, which is the rehearsal machine fullscreen. */
  --gutter: var(--sp-48);
  --content-max: 1200px;
}
/* The rail stops being a surface: it takes the canvas and is separated by a
   single hairline instead of a border and a fill.
   SCOPED WITH A CHILD COMBINATOR, NOT LEFT OPEN, and this is not tidiness. The
   drawer reuses .o-rail wholesale, so an unscoped rule here reaches into it:
   the 1390 hide below was switching off the drawer's own navigation, which is
   the one thing the drawer exists to provide. */
.o-shell > .o-rail { position: relative;
  background: var(--bg-canvas);
  border-right: 1px solid var(--neutral-850);   /* exception 5, see above */
}
.o-rail { padding: var(--sp-16) 10px; }
/* Sentence case at regular weight, tracking off. Was 12px medium uppercase
   +0.09em, which shouted at a group heading nobody is looking for. */
.o-rail__group { font-family: var(--font-ui); font-weight: var(--fw-regular);
  font-size: var(--fs-caption); line-height: var(--lh-caption); text-transform: none;
  letter-spacing: 0; color: var(--text-tertiary); padding: 0 10px var(--sp-8); }

.o-rail__item { display: flex; align-items: center; gap: 11px; padding: 7px 10px; border-radius: 6px;
  font-family: var(--font-ui); font-weight: 300;
  font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--text-tertiary); cursor: pointer; }

.o-rail__item svg { width: 18px; height: 18px; stroke-width: 1.5; flex: none; }
.o-rail__item:hover { background: var(--bg-hover); }
/* SELECTED IS COMPLIANT AND FRAGILE, AND THIS SENTENCE IS THE GUARD RAIL.
   The fill is gone, so the two signals are hue and WEIGHT. Simulated across
   protanopia, deuteranopia, tritanopia and achromatopsia, the selected teal and
   the unselected grey land within 1.18:1 to 1.32:1 of each other, so colour
   carries NONE of the load and weight carries all of it: 300 against 500, which
   is 1.99x ink per character. ANYONE WHO LATER HARMONISES THOSE WEIGHTS BREAKS
   WCAG 1.4.1 WITHOUT TOUCHING A COLOUR. */
.o-rail__item.is-active { background: none; color: var(--teal-400); font-weight: var(--fw-medium); }
.o-rail__item.is-active svg { color: var(--teal-400); }
/* Border off, and the padding and cap come from the shell rather than from
   here. See the --gutter note on .o-shell for the defect this closes. */
.o-topbar { height: var(--size-top-bar);
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 var(--gutter);
  width: 100%; max-width: var(--content-max); margin-inline: auto; }
.o-topbar__search { font-family: var(--font-ui); font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--text-tertiary); }
/* The page beside the rail. Same cap and same gutter as the bar above it. */
.o-main { padding: var(--sp-48) var(--gutter) var(--sp-80);
  width: 100%; max-width: var(--content-max); margin-inline: auto; }
.o-avatar { width: 30px; height: 30px; border-radius: var(--r-full); background: var(--bg-raised); }

/* --------------------------------------------------- Rail collapse control */
/* Promoted 5 August from home-A-prov-right.html. Shane, 4 August: the splitter
   stops resizing the rail and becomes a collapse toggle. At rest it is a quiet
   2px bar. On hover it becomes a chevron, the rail's right edge steps 5px left
   on the overshoot curve, and a "Collapse" label appears beside it.
   WHAT THIS RETIRES, and it is worth saying because it was argued at length:
   the ARIA window-splitter pattern, aria-valuenow, the arrow-key steps, the
   derived 220-to-400 stops, and the click-to-cycle that existed only to satisfy
   WCAG 2.2 SC 2.5.7. NONE OF IT IS NEEDED: a button that toggles is already
   operable by pointer and by keyboard, so 2.5.7 and 2.1.1 fall out for free.
   The 24 by 40 target stays, per SC 2.5.8.
   THE GLYPH IS TABLER chevron-left and chevron-right, RULED 5 AUGUST. It was
   drawn with Ant Design's filled LeftOutlined and RightOutlined at Shane's
   instruction. Ant does not join the icon set: see ICON-INVENTORY.md for the
   full reasoning, and the short version is that a filled glyph cannot take part
   in a scale whose one rule is that stroke scales with size on Tabler's ratio.
   MARKUP CONTRACT: a <button> carrying aria-expanded and aria-controls pointed
   at the rail, with an accessible name that changes with the state, plus one
   .o-rail-collapse__bar and two .o-rail-collapse__chev, --in and --out. */
/* THE SELECTOR IS DOUBLED ON PURPOSE, AND IT IS A DEFECT FIX, 6 August 2026.
   This wrapper is always composed as class="o-tip o-rail-collapse-tip": it is
   the tooltip component, hung off the rail's right edge. Written as a single
   class it is (0,1,0) and so is .o-tip, which sets position: relative and is
   defined 450 lines further down, so .o-tip won on order and the control
   rendered in flow instead of pinned. It went unseen from the 5 August
   promotion until the home was switched off its local copy, because the
   screen's own copy sat later still and beat both. Measured effect when the
   local copy went: the collapse control moved 10px left and 127px down.
   Doubling the class makes it (0,2,0) and order stops mattering. */
.o-tip.o-rail-collapse-tip { position: absolute; left: 100%; top: 50%; transform: translateY(-50%); z-index: 5; }
.o-rail-collapse {
  width: var(--sp-24); height: var(--sp-40);
  display: grid; place-items: center;
  padding: 0 0 0 var(--sp-8); box-sizing: content-box;
  background: none; border: none; cursor: pointer;
}
.o-rail-collapse > * { grid-area: 1 / 1; }
/* text/disabled, 3.19:1 on the canvas. WCAG 1.4.11 wants 3:1 for an interactive
   control and the next two ramp steps both fail, so this is the quietest this
   control can be and stay conformant. "Quieter" has no moves left. */
.o-rail-collapse__bar {
  width: 2px; height: var(--sp-20);
  border-radius: var(--r-full); background: var(--text-disabled);
  transition: opacity var(--motion-fast) var(--motion-ease);
}
/* 14px on --icon-xs-stroke, which is 1.15. That is Tabler's native ratio at
   this size (14 x 0.083 = 1.162), so the chevron joins the four named icon
   steps rather than sitting beside them. It is the swap from the filled Ant
   glyph that made this possible: a filled path has no stroke to scale. */
.o-rail-collapse__chev {
  width: 14px; height: 14px; stroke-width: var(--icon-xs-stroke); color: var(--text-primary);
  opacity: 0; transition: opacity var(--motion-fast) var(--motion-ease);
}
.o-rail-collapse:hover .o-rail-collapse__bar,
.o-rail-collapse:focus-visible .o-rail-collapse__bar { opacity: 0; }
.o-rail-collapse:hover .o-rail-collapse__chev,
.o-rail-collapse:focus-visible .o-rail-collapse__chev { opacity: 1; }
.o-rail-collapse:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--r-sm); }
.o-rail-collapse__chev--out { display: none; }
.is-collapsed .o-rail-collapse__chev--in { display: none; }
.is-collapsed .o-rail-collapse__chev--out { display: block; }

/* THE HOVER NUDGE IS VISUAL, NOT STRUCTURAL, and that distinction was a real
   defect before it was a rule. It used to move --rail-w, which is a grid
   column, so the content column grew by 5px and everything inside it, being
   centred, slid 2.5px sideways: a hover on a control in the rail was moving the
   whole page. Now the rail's own box moves and the column does not. The 5px
   strip left behind is canvas-coloured, so nothing is visible except the border
   stepping, which is the whole intent. Collapsed, the margin goes negative and
   the border steps the other way, so the movement always points at what the
   click does. --rail-w still changes on the CLICK, because collapsing genuinely
   is a layout change; only the hover is decorative.
   THE CURVE IS --motion-easeBack, named in tokens.json 5 August. It was a raw
   cubic-bezier in a screen file until then. */
.o-shell { transition: grid-template-columns var(--motion-slow) var(--motion-easeBack); }
.o-shell > .o-rail { transition: margin-right var(--motion-slow) var(--motion-easeBack); }
.o-shell:has(.o-rail-collapse:hover) > .o-rail,
.o-shell:has(.o-rail-collapse:focus-visible) > .o-rail { margin-right: 5px; }
.o-shell.is-collapsed:has(.o-rail-collapse:hover) > .o-rail,
.o-shell.is-collapsed:has(.o-rail-collapse:focus-visible) > .o-rail { margin-right: -5px; }

/* THE COLLAPSED RAIL. Icons only, labels and group headings gone, the wordmark
   swapped for the mark. 64px is --sp-64: the 18px icon plus the item's 10px
   horizontal padding is 38, and 64 centres that on the rail's own rhythm rather
   than hugging the glyphs. The brand swap needs .brand-full and .brand-mark in
   the screen's own markup; those are screen names, not system ones. */
.o-shell.is-collapsed { --rail-w: var(--sp-64); }
.is-collapsed .o-rail__item span, .is-collapsed .o-rail__group { display: none; }
.is-collapsed .o-rail__item { justify-content: center; padding-left: 0; padding-right: 0; }
@media (prefers-reduced-motion: reduce) {
  .o-shell, .o-shell > .o-rail { transition: none; }
  .o-rail-collapse__bar, .o-rail-collapse__chev { transition: none; }
}

/* ------------------------------------------- Narrow-window navigation */
/* THE RAIL MAY ONLY STAND DOWN WHERE THERE IS ANOTHER ROUTE, WHICH IS WHY THIS
   IS AN OPT-IN MODIFIER AND NOT SHELL DEFAULT BEHAVIOUR.
   The breakpoint and the drawer are one behaviour, not two. Between 4 and
   5 August the rail-hide existed without the drawer and the gap it opened, no
   route to any navigation item at all below the breakpoint, sat open on an
   ordinary laptop window. Promoting the hide unconditionally would reopen that
   gap on every screen that has not drawn a drawer, silently, which is exactly
   the class of breakage this promotion exists to stop. So the shell opts in
   with .o-shell--with-drawer, and carrying that class is the screen's assertion
   that a .o-drawer exists in its markup.
   1390 IS MEASURED, NOT CHOSEN. It is the point where "Brazilian" drops off the
   first line of the chamber attribution in the home screen's provenance column;
   rendered at 20px steps, the jump falls between 1350 and 1370, and working
   back through the width chain puts the true threshold at 1381. 1390 clears it
   with 9px. Hiding the rail hands the content region 236px back, which lifts
   the sidebar to its cap and holds it clear all the way down to the 1280
   reflow, so there is no gap between the two rules. */
@media (max-width: 1390px) {
  .o-shell--with-drawer > .o-rail { display: none; }
  .o-shell--with-drawer { grid-template-columns: 1fr; }
  /* THE CAP COMES OFF THE BAR AND THE PAGE TOGETHER. Uncapping only the bar
     would put the top bar's first control at 48px and the page title at 143px
     on a 1390 window, a 95px drift between two things that have been aligned
     all along. Below the breakpoint the window is never wide enough for a 1200
     cap to be doing anything anyway. */
  .o-shell--with-drawer .o-topbar, .o-shell--with-drawer .o-main { max-width: none; }
}

/* ONE CONTROL, NOT TWO. The hamburger and the wordmark sit inside a single
   button, which is why one low-contrast fill appears behind BOTH on hover
   rather than a fill behind the icon and a logo sitting beside it. Two adjacent
   hover surfaces read as two controls doing different things; these do the same
   thing, so they are one control.
   SIZED TO THE RAIL, NOT GUESSED. The wordmark takes --logo-h, the same 22.5px
   the rail's brand block uses, so the logo is identical whichever navigation is
   on screen. The glyph takes --icon-lg, 24px on a 1.6 stroke, the step defined
   for standalone wayfinding controls, which is what a hamburger is. At 20px it
   sat visibly under the logo. */
.o-navbar-trigger {
  display: none; align-items: center; gap: var(--sp-12);
  height: var(--sp-40); padding: 0 var(--sp-12); margin-left: calc(var(--sp-12) * -1);
  background: none; border: none; border-radius: var(--r-md); cursor: pointer;
  color: var(--text-secondary);
  --logo-h: 22.5px;
  transition: background-color var(--motion-fast) var(--motion-ease);
}
.o-navbar-trigger:hover { background: var(--bg-hover); color: var(--text-primary); }
.o-navbar-trigger:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.o-navbar-trigger svg { width: var(--icon-lg); height: var(--icon-lg);
  stroke-width: var(--icon-lg-stroke); flex: none; }
.o-navbar-trigger img { height: var(--logo-h); width: auto; display: block; }
/* THE SHOW RULE HAS TO SIT AFTER THE HIDE RULE, and until 6 August 2026 it did
   not. It was written inside the 1390 block above with the rail-hide, 13 lines
   BEFORE .o-navbar-trigger { display: none }. A media query adds no
   specificity, so at every width the later rule won and the trigger was never
   displayed: below the breakpoint the rail was gone and nothing opened the
   drawer, which is exactly the gap the opt-in modifier exists to prevent. It
   was invisible because the only screen with a drawer carried its own copy of
   both rules in the right order. Found when that copy was deleted. It stays a
   separate block down here rather than moving back up, so the ordering is
   obvious to the next reader. */
@media (max-width: 1390px) {
  .o-navbar-trigger { display: inline-flex; }
}

/* ------------------------------------------------------------------ Drawer */
/* The rail's content, over the page rather than beside it. It reuses .o-rail
   wholesale, so the items, groups, spacing and active state are the same
   objects: there is ONE navigation in a screen, shown two ways.
   THE SCRIM IS WHAT MAKES IT A DRAWER rather than a panel. It also carries the
   dismiss: clicking anywhere outside closes, which with Escape gives the two
   exits an overlay owes the reader.
   OPEN STATE IS AN ATTRIBUTE ON AN ANCESTOR, deliberately unscoped: the mockup
   carries data-drawer on its presentation frame and a build carries it on the
   page wrapper, and neither has to know about the other.
   ONE REQUIREMENT ON THE SCREEN, because the scrim is position: absolute and
   there is nothing here that can guarantee its containing block: the element
   holding data-drawer must be position: relative, or the scrim resolves against
   the page and covers more than the shell. */
.o-drawer-scrim {
  position: absolute; inset: 0; z-index: 40;
  background: rgba(0,0,0,.5);          /* exception 4, see .o-menu */
  opacity: 0; pointer-events: none;
  transition: opacity var(--motion-base) var(--motion-ease);
}
.o-drawer {
  position: absolute; top: 0; left: 0; bottom: 0; z-index: 41;
  width: 280px; max-width: 86%;
  background: var(--bg-canvas); border-right: 1px solid var(--neutral-850);  /* exception 5 */
  transform: translateX(-100%);
  transition: transform var(--motion-slow) var(--motion-easeBack);
  display: flex; flex-direction: column;
}
[data-drawer="open"] .o-drawer-scrim { opacity: 1; pointer-events: auto; }
[data-drawer="open"] .o-drawer { transform: none; }
.o-drawer__head { display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-12) var(--sp-12) var(--sp-24) 10px; }
.o-drawer__head img { height: 22.5px; display: block; }
.o-drawer__close {
  width: var(--sp-32); height: var(--sp-32); display: grid; place-items: center;
  background: none; border: none; padding: 0; cursor: pointer; color: var(--text-tertiary);
  transition: color var(--motion-fast) var(--motion-ease);
}
.o-drawer__close:hover { color: var(--text-primary); }
.o-drawer__close:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--r-xs); }
.o-drawer__close svg { width: var(--icon-sm); height: var(--icon-sm); stroke-width: var(--icon-sm-stroke); }
/* The rail inside the drawer drops its own surface and rule: the drawer is
   already the surface, and a second edge inside one would read as two panels. */
.o-drawer .o-rail { background: none; border-right: none; padding: 0 var(--sp-12); }
@media (prefers-reduced-motion: reduce) {
  .o-drawer { transition: none; } .o-drawer-scrim { transition: none; }
}

/* ------------------------------------------------------- Pre-login shell */

.o-prelogin {
  position: relative;
  display: flex; flex-direction: column;
  min-height: 900px;   /* the 1440 by 900 reference frame. The built page takes
                          the viewport height instead (min-height: 100vh) */
  background: var(--bg-canvas);
  overflow: hidden;
  
}

.o-prelogin .frame-bg {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image: url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAA0JCgsKCA0LCgsODg0PEyAVExISEyccHhcgLikxMC4pLSwzOko+MzZGNywtQFdBRkxOUlNSMj5aYVpQYEpRUk//2wBDAQ4ODhMREyYVFSZPNS01T09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0//wAARCAEsAeADASIAAhEBAxEB/8QAGQABAQEBAQEAAAAAAAAAAAAAAAECAwQH/8QAGxABAQEBAQEBAQAAAAAAAAAAAAEREgITAyH/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAQL/xAAWEQEBAQAAAAAAAAAAAAAAAAAAEQH/2gAMAwEAAhEDEQA/APmsjXK+WlZcrGK6+nOgyAKAAAAAAAAAAAAAoChBFigqKCiCiqgouCIq4YJQXDFSoNYYFZRrDArKNYYiso1iCoSKsFxZF5I0jTPJy3pouOV8sWOvqudRpnDFNESR0kYlblBuRcSUvoCxj0t9MevQOfpit+qxQdJW9c41FZ0rnXRmwM1hGqiNIAAAAAAAAAAAAoAqpGhCKLFZFgogosVkiiiC4RVQwxVETDFFSs4KIrOI0lFZStIisi1BpdOmU1Fb6Tti1NGsbvpi1LWaitamogrfSz05mg69l9uWmg3fSX0yAWiAOkajMagkVK0lCOdZbsYoqCoAAAAAAAAAACgAsaZiiNKysVlpWWlRVjMUZaVFVFVlRGlZXRFE0VFQ1NRSpRBRBEUqCUUSlRGkqKlFRKqI0gIKAAAAAAAA3G4xG4CxSAM1it1igyi1AAAAAAAAAAAFQBpYyojSsqqNKyojSsistrGY6eJokWReXXx411n5BHl5OXpv5p81SPPhjv8AM4COGHLv81+aEeaxmvR68OXrzgscqi1kICag0rIIoglFSgiKIAoAAAAAAADcbjEalBuKzKuglc63WaDFRagAAAAAAAAAAAAKIoKsSNQRcXFkXFSJg1i4JEkdfzYkb8/wI9f5u8x4/PvHSfoEei4n8cfofQSOuRmyMfRL7COkxf44/Q+gRv28/wCjpfbl6uixx9Rix1rNgRzTHTEwWMYljpjNgOdRqsVFEAUAAAAAAAAABpZWWoDWmoAus1rEwGUasSgiKgAAAAAAApgAuGAguGARqJiyA3K1rmug3q656aI6avTnpoOva9uOmqO/0X6PPq6I9H0S+3Do6B27Ttx00HbtOnLpNFdejXLo6QdNTWNTQdNZtZ0tFT0xWqmAguGAguGAyKAgAAAAANRqRJG5AQXEoAgBWaqUERUAAAAAVFBY1IkagEi8rGoqM8nLYDHJy2AxyvLQDGGNgM8mNAM4Y0AzhjQDOGNAMWJWqzQQAEFAQUBBQEwxQEwxQExMaQEZaSoqIqAAAAA6RuRiOnkDGbHTDAcsOXXDkHLlL5dsZsBxsR09RigyKgAACoA1FlZNB0lWenPVlB06XWI1gLpqYYC6amGAumphgLpqYYC6mmJgLprIDWmpi4BazWsMBlGsMBkaxMBAXAQawwGRvEwGRqxmgampU0F1AAQAAAAAdI6+XKOvkGgAUABmtM0HP05109OdBEUBAAAAAAVYhAdI3HKVroGxjo6BsY6OgdBz6XoHQxmempQMSxrUoMWEjRAJFwigmGKAmJjQDFjNbrFBI1GNOgdBjo6BsY6OgarNS+ktBKhUAAAAAAAAB0jcrlK1KDrprn0dA66a5dHQOupa59HQHpiragIACAAAAAAKgC6agC6agC6agC6srKg3K1K56ug69HTl0dA6auuXR0Drp059HQOnR05dHQOumufR0DdrFqaloGpogLpqALpqALogCoAAAAAAAAAKagC6agC6usgNamoAuiAAAAoCCgIKAgoCCgIKAgoCCgAAAAAAAAAAAAAAIKAgoCCgIKAgoCCgIKAgoCAAAAAAAAAAAAKLAMMWRqQGMMdZ5XgHHDHbg4Bxwx24OAccMdb5ZsBjDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnDGsMBnEbxLAZRpAQAAAAAAAAAFWIsBuN+YzG/Ko35jc8sxrQOTldNA5S+WtS0HP1HOx09VzoMoqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJVSgzUWoioAAAAAAAAACrEWA6RuOcbio3KusKDWmsqC6WomAlrNbsZsBgasQEFxcBkaw5Bkb5TkGRvlOQZGsTAQUBBrDAZGsOQZG+TkGBrDAZFwBBVwGRrDAZGuV5Bgb5TAZGsQEStM0Gai1EVAAAAAAAAAAVYiwG41GY3FRqRueU8x18wRmeDh2kSwVy5XlsEc+Gb5dqx6FcbGMdPTAEjUiR08wEnlrhuRqQRy4OHbDBXHg4d8SwR575Ysej1HH1Ac8WQWCrI1PK+Y6SCOfC8Osi8g48HDviYDhfDN8u9jn6gONiN1kUkakSOnmCJPK8NyNSA58HDti4K48M3w9GMeoDz2M2OvqOdEYrNbrFFZqLURUAAAAAAAAABViLAbjp5c/Lr5VHTzHTyz5jcEa1m1UoMmqYCWs2tWM0HP0y16ZBfLr5co6eaDrGoxKaDemsdL0DWlrPSX0Ceq5em/Vc/VBlfLLUFdfLpHPzW5RG4rGnQNaaz0nQNVz9NWseqDn6Yb9MA15dfLlHTzQdI0xKug1p0xfTN9A6dM2sdJ0C+nOtWs0GKxW6xRWai1EVAAAAAAAAAAVYiwG/Lt4cfLr5VHfy1rn5rWqlatS1i1m0HTTpy06QdNZtZ6S0CsLUBY3KxGoDfR0zhipWujpnDArXSX0mGBS1itYlRWViLAbla6YiqVro6YTUHTpOmNNBvpLWdQFrKoCxuViNQGujplAbvpm1EBdNQBdShQZrFbrFFZqLURQxVwGcMbwsBhGrEBAAAAVYiwG46eXONyqzrrK05yr0rLVYq2s2hghqajSiaaCoagNRuOcrcqpra4z0dDLWGJ0dKi4idHSKVmraxaLiVYmkqNOkaYlXpWSs1bWbRcBNNRVE00FQ1Aajcc5WpVTWmaus6GCo1EVMMaxcBjDG8MBysZsdrGb5FcLGbHX1GLEVI1GFBtKgBWVqAgAAAKsRYDUalZiqjWrrAI3qayCtamoAumoAogC6usgN9HTAJG+jpgCN9HTAEb1NZBV01AGtXWARrTWQVdNQBdNQBRAF1dZAa01kBuVqVzjUB0lXWFBrTWdNBrWaazQT051v051Ff/Z");
  background-size: cover; background-position: center top;
}
/* The wash: radial, derived from --neutral-925 with color-mix so it tracks the
   canvas token rather than freezing a copy of it. Darkest under the card,
   lightest at the corners, retuned 5 August to 92/80/38 against a source
   brightened before encoding. */
.o-prelogin .frame-bg::after {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(115% 85% at 50% 38%,
    color-mix(in srgb, var(--neutral-925) 92%, transparent) 0%,
    color-mix(in srgb, var(--neutral-925) 80%, transparent) 42%,
    color-mix(in srgb, var(--neutral-925) 38%, transparent) 100%);
}
.o-prelogin .frame-head, .o-prelogin .wizard { position: relative; z-index: 1; }

/* Pre-login header: logo and language switcher only. No rail, no top bar,
   because no navigation exists before an account does. */
.o-prelogin .frame-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-24) var(--sp-40); gap: var(--sp-24);
}
/* 28px renders the trimmed logo asset's visible wordmark at the approved
   26.7px. THE APPROVED SIZE IS THE VISIBLE GLYPH, NOT THE BOX; the 3 August
   asset repair is written up in DESIGN-DECISIONS-home-2026-08-03.md. */
.o-prelogin .frame-head img { height: 28px; display: block; max-width: 100%; }
/* The trigger and the panel are system components (.o-icon-btn, .o-menu). The
   shell owns only where the panel hangs and whether it is open. */
.o-prelogin .lang { position: relative; }
.o-prelogin .lang-menu { position: absolute; top: calc(100% + var(--sp-8)); right: 0; z-index: 20; display: none; }
.o-prelogin .lang-menu[data-open="true"] { display: block; }

/* One centred column, VERTICALLY CENTRED (Shane's 1 September ruling; the
   supersession note sits on .o-prelogin above). Free space splits 45/55 via
   the two spacers so the card sits at the optical centre; the 24px padding is
   the hard minimum gap, and past it the page scrolls rather than clipping.
   The 560px track is the card's width, one number for every pre-login card,
   so the card never changes width as a trader crosses between these
   screens. */
.o-prelogin .wizard {
  flex: 1 0 auto; display: flex; flex-direction: column; align-items: center;
  padding: var(--sp-24) var(--sp-48);
}
.o-prelogin .wizard::before { content: ""; flex: 45 0 0; }
.o-prelogin .wizard::after { content: ""; flex: 55 0 0; }
.o-prelogin .formcol { min-width: 0; width: min(560px, 100%); flex: none; }

@media (max-width: 656px) {
  .o-prelogin .wizard { padding-left: var(--sp-24); padding-right: var(--sp-24); }
}

.o-prelogin .page-title { font: var(--fw-medium) var(--fs-3xl)/var(--lh-3xl) var(--font-display); }
.o-prelogin .page-sub {
  font: var(--fw-regular) var(--fs-sm)/var(--lh-sm) var(--font-ui);
  color: var(--text-secondary); margin-top: var(--sp-4); max-width: 52ch;
}

/* The shell's card is the whole screen, so it takes the raised treatment
   (.o-card--raised above: the backdrop is never flat colour) and 32px padding,
   because a Display/3XL title 24px off a card edge reads cramped. */
.o-prelogin .formcol .o-card { padding: var(--sp-32); box-shadow: var(--elev-3); }

/* The banner region inside the card takes 24 above, and the action row's own
   24 sits below, so the card's rhythm does not change when a banner appears.
   The card grows; its top edge does not move. */
.o-prelogin .card-alert { margin-top: var(--sp-24); }
.o-prelogin .actions { margin-top: var(--sp-24); }


.o-prelogin .o-field .o-help:empty { display: none; }
.o-prelogin .o-field .o-help--live:empty { display: block; }

/* ---------------------------------------------------------- Icon scale */

:root {
  --icon-xs: 14px;  --icon-xs-stroke: 1.15;   /* marks inside a row: the menu tick */
  --icon-sm: 16px;  --icon-sm-stroke: 1.3;    /* THE DEFAULT. Inline with text     */
  --icon-md: 20px;  --icon-md-stroke: 1.65;   /* controls sitting inside a field   */
  --icon-lg: 24px;  --icon-lg-stroke: 1.6;    /* standalone wayfinding controls    */
}

/* THE MODAL GUTTER, AND IT IS ONE NUMBER ON PURPOSE. Added 10 August 2026.
   The four scrims in this system each carried their own `padding: var(--sp-40)`,
   so the gap between a modal and the edge of the window was four copies of one
   decision and could not be changed anywhere. It is now this token, and every
   scrim reads it.
   WHAT IT CONTROLS, and it is more than padding: .o-modal caps itself at 100%
   of the scrim's content box, so this number sets BOTH the gap at the edge and
   the tallest a modal may ever be. Raise it and every modal gets more air and
   a lower ceiling at the same time; a modal that then does not fit scrolls
   inside itself rather than pushing into the gap.
   64 AND NOT 40. At 40 the booking modal, which is the tallest in the system by
   a wide margin because it carries a calendar, filled 812 of a 900px window and
   read as full-bleed rather than as a card sitting over the page. Every other
   modal in the system is 590px or shorter, so this number never binds on them
   and costs them nothing.
   THE NARROW OVERRIDE STAYS. The interruption set drops its scrim to --sp-20
   at 375px wide, which is correct: at phone width the gutter is competing with
   the modal for room rather than giving it air. */
:root { --modal-gutter: var(--sp-64); }

/* --------------------------------------------------------- Menu */
/* The panel any trigger opens: the language switcher, a select, any short list
   of choices. Added 3 August, when the sign-up screen became the first screen to
   claim it. The treatment is not new: COMPONENT-SET-PLAN already specified it as
   an inferred variant of the select's open state, and this is that spec built.
   bg/surface, 1px border/default, radius.md, elevation 2. */
.o-menu { min-width: 180px; padding: var(--sp-4);
  background: var(--bg-surface); border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  /* DOCUMENTED EXCEPTION 4, ruled 3 August. elev-2's first layer is a 1px inset
     top highlight in neutral-700, which exists to give a raised surface a light
     top edge when it has NO border. On a fully bordered panel it stacks directly
     inside the top border and reads as a thicker top stroke than the other three
     sides. This component takes elevation's drop shadow only. The same collision
     exists on .o-modal with elev-3 and is logged, not fixed here. */
  box-shadow: 0 2px 8px rgba(0,0,0,.35); }
.o-menu__item { display: flex; align-items: center; gap: var(--sp-8); width: 100%;
  font-family: var(--font-ui); font-size: var(--fs-sm); line-height: var(--lh-sm);
  font-weight: var(--fw-regular); color: var(--text-secondary); text-align: left;
  background: none; border: none; border-radius: var(--r-sm);
  padding: var(--sp-8) var(--sp-12); cursor: pointer; }
.o-menu__item:hover { background: var(--bg-hover); color: var(--text-primary); }
/* Selected is RECESSED, not raised. Ruled 3 August, and it corrects a real
   collision rather than a preference: bg/raised and bg/hover are both
   neutral-800, so a selected row and a hovered row were the same colour and
   could not be told apart. Selected now takes bg/inset, which is darker than
   the panel, so hover reads as "you could pick this" and selected as "this one
   is already pressed in". This supersedes COMPONENT-SET-PLAN, which specified
   bg/raised for the select's open state before the collision was known.
   Softened 3 August from bg/inset (neutral-950) to bg/canvas (neutral-900):
   one rung below the panel rather than two. Inset is the floor of the ramp and
   on a small row it read as a hole punched in the panel. */
.o-menu__item[aria-checked="true"],
.o-menu__item[aria-current="true"] { background: var(--bg-canvas); color: var(--text-primary); }
.o-menu__item:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
/* The tick reserves its space in every row, so choosing one does not shift the
   labels of the others. Same Still Screen reasoning as the field helper lines. */
.o-menu__tick { width: var(--icon-xs); height: var(--icon-xs); flex: 0 0 var(--icon-xs); stroke-width: var(--icon-xs-stroke); visibility: hidden; }
.o-menu__item[aria-checked="true"] .o-menu__tick { visibility: visible; }

/* --------------------------------------------------- Profile menu */

.o-menu--profile { min-width: 208px; }
.o-menu__id { display: flex; flex-direction: column; gap: 2px;
  padding: var(--sp-8) var(--sp-12) var(--sp-12);
  border-bottom: 1px solid var(--neutral-850);   /* exception 5, see the app shell */
  margin-bottom: var(--sp-4); }
.o-menu__id-name { font: var(--fw-medium) var(--fs-sm)/var(--lh-sm) var(--font-ui); color: var(--text-primary); }

.o-menu__id-co { font: var(--fw-medium) var(--fs-caption)/var(--lh-caption) var(--font-ui);
  color: var(--text-secondary); text-transform: uppercase; letter-spacing: .07em; }

.o-menu__id-orb { margin-top: 2px; font: var(--fw-regular) var(--fs-xs)/var(--lh-xs) var(--font-mono);
  color: var(--teal-400); font-variant-numeric: tabular-nums; }
/* THE GUARD. Any glyph in a menu item takes the inline icon step unless it is
   the tick, which carries its own size. Without this, an unclassed SVG in a
   menu item renders at the browser's replaced-element default, which is the
   defect that broke the profile menu on 6 August. Scoped with :not() so the
   tick's XS sizing is not overridden by this rule's higher specificity. */
.o-menu__item svg:not(.o-menu__tick) { width: var(--icon-sm); height: var(--icon-sm);
  stroke-width: var(--icon-sm-stroke); flex: none; }

/* The trigger. An icon button that opens a menu, sized for wayfinding rather
   than for the 16px icon standard, because a reader who cannot read the
   interface language has to find it. */
.o-icon-btn { display: inline-flex; align-items: center; justify-content: center;
  width: var(--sp-40); height: var(--sp-40);
  color: var(--text-primary); background: none; border: none;
  /* Circular. An icon has no reading direction, so a square hover patch reads as
     a button that lost its label. A disc reads as a target. */
  border-radius: var(--r-full); cursor: pointer; }
.o-icon-btn svg { width: var(--icon-lg); height: var(--icon-lg); stroke-width: var(--icon-lg-stroke); }
/* One ramp step off the canvas (neutral-850), not two. action/ghost/bgHover is
   neutral-800, which on a 40px disc reads as a lit button rather than a hover.
   The token itself is unchanged, because the ghost ACTION button shares it and
   a text button needs the stronger step to register. If this softer treatment
   spreads beyond icon buttons it earns its own token. */
.o-icon-btn:hover { background: var(--bg-surface); }
.o-icon-btn[aria-expanded="true"] { background: var(--bg-raised); }
.o-icon-btn:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* ------------------------------------------------------------- Tooltip */
/* The label an icon-only control owes the reader. Added 3 August with the icon
   trigger, because an icon button without one is a guess. Sits BELOW the control
   so it never covers the thing you are pointing at, and it is aria-hidden: the
   button already carries the same words as its accessible name, and announcing
   them twice is worse than not announcing them at all. */
.o-tip { position: relative; display: inline-flex; }
.o-tip__label {
  position: absolute; top: calc(100% + var(--sp-4)); left: 50%; transform: translateX(-50%);
  padding: var(--sp-4) var(--sp-8);
  background: var(--bg-raised); color: var(--text-primary);
  border-radius: var(--r-sm);
  box-shadow: 0 2px 8px rgba(0,0,0,.35);   /* exception 4, see .o-menu */
  font-family: var(--font-ui); font-weight: var(--fw-medium);
  font-size: var(--fs-xs); line-height: var(--lh-xs);
  white-space: nowrap; pointer-events: none;
  opacity: 0; transition: opacity var(--motion-fast) var(--motion-ease);
}
/* SITS RIGHT INSTEAD OF BELOW. Promoted 5 August from home-A-prov-right.html,
   where the rail collapse control claimed it: below would drop the label over
   the page content, because that control hangs off the rail's own right edge
   with the page immediately under it. A positional MODIFIER rather than a
   second tooltip, so the padding, fill, shadow, type and fade stay one object.
   Use it wherever the control sits on a vertical edge; keep the default
   anywhere the control sits in a row. */
.o-tip__label--right { top: 50%; left: calc(100% + var(--sp-4)); transform: translateY(-50%); }
.o-tip:hover .o-tip__label,
.o-tip:focus-within .o-tip__label { opacity: 1; }
/* While the menu is open the tooltip is noise: the panel already answers it. */
.o-tip:has([aria-expanded="true"]) .o-tip__label { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .o-tip__label { transition: none; } }

/* ================================================================
   THE CREDENTIAL PAGE
   Promoted 5 August 2026 from mockups/home-A-prov-right.html, section 2 of
   SYSTEM-DELTA-2026-08-05.md, filtered through V0-REDUCTION-home.md. The
   reduction is what decides which of these exist: a component is here because
   a shipping V0 screen composes it, and the two that a V0 screen cannot
   support on its own carry their fallback in the comment rather than in a
   separate document nobody reads at build time.
   THE COMPOSITION IS NOT PROMOTED, ONLY THE PARTS. The page grid, the act
   band, the QR block and the credential identifier stay in the screen file,
   because they are that screen's arrangement rather than objects another
   screen could pick up. If a second screen claims one, it promotes then.
   ================================================================ */

/* --------------------------------------------------------------- Copy field */
/* Stripe's API-keys pattern: a bordered field holding a value in mono with a
   copy glyph, the whole box a button. TWO CLAIMANTS, which is what earned it
   promotion: the home screen's public link and the Orbitra ID issued screen's
   (screen-08). It pairs with .o-tip for the copied confirmation.
   NO TRUNCATION, deliberately. Stripe elides a secret key because nobody reads
   one; this is a link a trader checks before sending it to a customer.
   THE BORDER IS NOT ASKED TO CARRY IDENTIFICATION. border/subtle is 1.13:1 and
   nothing on this ramp reaches WCAG 1.4.11's 3:1, which is a logged system gap
   rather than a defect in this control: the copy glyph carries 4.53:1 and the
   mono treatment marks the box as a value, so the control is identifiable
   without its outline. Any control that relies on its outline ALONE is not. */
.o-copyfield {
  display: inline-flex; align-items: center; gap: var(--sp-8);
  max-width: 100%; height: var(--sp-40); padding: 0 var(--sp-12);
  background: none;
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-sm);
  font-family: var(--font-mono); font-size: var(--fs-sm); line-height: var(--lh-sm);
  color: var(--text-primary); font-variant-numeric: tabular-nums;
  cursor: pointer; text-align: left;
  transition: background-color var(--motion-fast) var(--motion-ease),
              border-color var(--motion-fast) var(--motion-ease);
}

.o-copyfield--sm { height: var(--sp-32); padding: 0 var(--sp-8);
  font-size: var(--fs-xs); line-height: var(--lh-xs); }
.o-copyfield__val { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.o-copyfield svg { width: var(--icon-sm); height: var(--icon-sm);
  stroke-width: var(--icon-sm-stroke); color: var(--text-tertiary); flex: none; }
.o-copyfield:hover { background: var(--bg-hover); border-color: var(--border-strong); }
.o-copyfield:hover svg { color: var(--text-primary); }
.o-copyfield:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* ---------------------------------------------------- Seal, ring and status */

/* CENTRED 6 August, Shane: icon and word sit on one optical line. This is the
   common case now that the status is a glyph plus a word. Use --block where the
   seal carries a description paragraph under the label, so the icon aligns to the
   first line rather than to the middle of the block. */
.o-seal { display: flex; align-items: center; gap: var(--sp-8); }
.o-seal--block { align-items: flex-start; }
/* RESTYLED 5 August, Shane: the ring is gone. The status is the icon and the word
   beside it, small gap, no border, no background. The ring carried the non-colour
   signal for WCAG 1.4.1; that job now sits with the glyph itself, which differs
   between states (Tabler id for Registered, shield-check for Verified) rather
   than by a tick inside an otherwise identical shape. */
/* ALIGNMENT, standardised 6 August. The ring takes the LABEL'S LINE HEIGHT as its
   own, not the icon's height, so the glyph and the word sit in the same vertical
   rhythm instead of two boxes of different heights being centred against each
   other. That is what was reading as misaligned: the boxes centred at exactly
   0px apart while the optical masses did not. It also fixes --block for free,
   where the ring now aligns to the first line rather than to the top of a
   multi-line stack. Fix it here, never per screen. */
.o-seal__ring { display: inline-flex; align-items: center; justify-content: center;
  flex: none; height: var(--lh-md); color: var(--teal-400); }
.o-seal__ring svg { width: 22px; height: 22px; stroke-width: 1.5; }
.o-seal__ring .ring-check { display: none; }
.o-seal--verified .ring-plain { display: none; }
.o-seal--verified .ring-check { display: block; }
.o-seal__txt { min-width: 0; }
.o-seal__txt strong { display: block;
  font: var(--fw-medium) var(--fs-md)/var(--lh-md) var(--font-display); color: var(--text-primary); }
/* 44ch IS THE TWO-LINE MEASURE and it was measured, not chosen. At 37ch the
   home screen's status sentence set on THREE lines with four words stranded on
   the last. The sentence is 109 characters and its balanced break falls near
   55; 44ch carries about 57, which clears that break and does not reach far
   enough to pull a third word up. In ch rather than pixels, per the rule on
   text containers. A screen whose sentence is materially shorter or longer
   re-measures rather than inheriting this number blind. */
.o-seal__txt p { font: var(--fw-regular) var(--fs-sm)/var(--lh-sm) var(--font-ui);
  color: var(--text-secondary); margin-top: var(--sp-2); max-width: 44ch; }
/* Padded to a 24px target, WCAG 2.2 SC 2.5.8. It is a STANDALONE link, not one
   sitting inside a sentence, so the criterion's inline exception does not cover
   it. Same defect class that caught the QR download link on 4 August. */
.o-seal__txt a {
  display: inline-flex; align-items: center; gap: var(--sp-8); min-height: var(--sp-24); margin-top: var(--sp-4);
  font: var(--fw-regular) var(--fs-sm)/var(--lh-sm) var(--font-ui);
  color: var(--link-default); text-decoration: none;
}
/* THE INFORMATION GLYPH. Added 10 August 2026, Shane, and it is defined here so
   any future use of the seal's link gets it without the screen asking.
   --icon-sm AND NOT --icon-xs. The scale's own note calls sm "THE DEFAULT.
   Inline with text", and xs is reserved for marks sitting inside a row, which
   is the menu tick. This glyph sits beside a sentence, so it takes the inline
   step at its matching 1.3 stroke.
   gap: --sp-8 IS THE SYSTEM'S ICON GAP AND NOT A JUDGEMENT. Every icon-to-text
   pair in this file already uses it: .o-seal itself, .o-menu__item, .o-confirm
   and .o-copyfield. There is no 6px step and inventing one here is how the
   next person ends up measuring four different gaps.
   NO COLOUR IS SET, DELIBERATELY. The markup's stroke is currentColor, so the
   glyph takes the link's own --link-default and follows it to --link-hover on
   hover. Hard-coding teal here would leave the glyph behind on hover, which is
   the exact defect the copy control had before its own hover rule was written.
   IT IS DECORATIVE AND CARRIES aria-hidden. The link already says what it does,
   so a screen reader announcing an icon after it would say the same thing
   twice. */
.o-seal__txt a svg { width: var(--icon-sm); height: var(--icon-sm);
  stroke-width: var(--icon-sm-stroke); flex: none; }
.o-seal__txt a:hover { color: var(--link-hover); }
.o-seal__txt a:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--r-xs); }

/* ---------------------------------------------------------- Provenance row */
/* Held by / Issued by / Issued: who this credential belongs to, who issued it,
   and when. All three are captured at sign-up or at the merge, so V0 supports
   it with nothing new (reduction row 5).
   IT IS NOT AN .o-ledger VARIANT, and that was checked rather than assumed.
   The 08-05 delta suggested it might be. A ledger is a stack of key-and-value
   ROWS separated by rules, read one line at a time; this is a set of facts read
   across, with no rules, that reflows between two and three columns and stacks
   on mobile. They share a key-over-value pair and nothing else.
   LOW SOUGHT, HIGH UNSOUGHT VALUE, which is why it goes on the screen and goes
   quiet: nobody logs in to find out who issued their credential, and a
   credential with no issuer on it is a card. No heading, no fold priority.
   TWO COLUMNS IS THE NARROW CASE, THREE IS THE WIDE ONE, which reads backwards
   until you see where it sits: two columns is the capped SIDEBAR, three across
   is what it wants once the sidebar drops under the credential and goes full
   width. Equal tracks capped in ch, because content-sized items gave three
   columns three different widths and read as a mistake rather than as columns.
   WHETHER THE ISSUER LINE MAY NAME A CHAMBER OF COMMERCE AS CO-ISSUER IS NOT A
   DESIGN QUESTION (reduction M-1, due 7 Aug). Without it the block still works
   on the issuer and the place and date alone. */
.o-prov { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-20); align-items: start; }
@media (max-width: 1280px) {
  .o-prov { display: grid; grid-template-columns: repeat(3, minmax(0, 24ch));
    gap: var(--sp-40); justify-content: start; }
}
@media (max-width: 768px) {
  .o-prov { grid-template-columns: 1fr; gap: var(--sp-24); }
}
/* LEFT-ALIGNED VARIANT, added 6 August 2026 on Shane's instruction, claimed by
   verify-and-tamper-check.html. NOTHING IN IT IS NEW: it is the arrangement the
   1280 rule above already describes, promoted to a modifier so a row can ask for
   it by width of its CONTAINER rather than by width of the WINDOW.
   THE DEFECT IT FIXES. The default splits the row into equal fractions, which is
   right in the roughly 165px sidebar it was designed for and wrong across a
   1104px content column: two items land 550px apart and read as two unrelated
   blocks that happen to share a rule, not as a set of facts read across.
   BOTH NUMBERS ARE THE COMPONENT'S OWN, not chosen here. 24ch is its track cap
   and --sp-40 is its gap, both taken from the rule above, which is what makes
   this a modifier rather than a second opinion about the same object.
   WHEN TO USE IT: the row sits full width with fewer items than the container
   has room for. Keep the default anywhere the row is in a capped column.
   COLUMN FLOW RATHER THAN A COUNT, and that is the one thing here the 1280 rule
   does not do. That rule hardcodes repeat(3), which is correct for the one row
   it was written for and wrong for a row with two items or four. Auto-flow takes
   whatever the screen puts in it, which is what a modifier owes. */
.o-prov--start { grid-template-columns: none;
  grid-auto-flow: column; grid-auto-columns: minmax(0, 24ch);
  gap: var(--sp-40); justify-content: start; }
@media (max-width: 768px) {
  /* auto-flow has to be put back as well as the tracks, or the items keep
     running sideways into implicit columns and the stack never happens. */
  .o-prov--start { grid-auto-flow: row; grid-template-columns: 1fr; gap: var(--sp-24); }
}
.o-prov__k { font: var(--fw-regular) var(--fs-caption)/var(--lh-caption) var(--font-ui); color: var(--text-tertiary); }
.o-prov__v { font: var(--fw-medium) var(--fs-sm)/var(--lh-sm) var(--font-ui); color: var(--text-primary); margin-top: var(--sp-4); }
.o-prov__v small { display: block; font: var(--fw-regular) var(--fs-caption)/var(--lh-caption) var(--font-ui);
  color: var(--text-tertiary); margin-top: 2px; }

/* --------------------------------------------------------- Welcome card */
/* The orientation card a trader meets on their first sign-ins and never again.
   It scores 10 on the visits it exists for and 0 on every visit after, so it
   has to be both early and temporary; nothing else is possible.
   THE ONLY BORDERED CARD ON THE CREDENTIAL PAGE, deliberately. Every other
   block on that screen lost its card on 4 August, which is what lets this one
   read as a greeting rather than as one module among several. It uses a teal
   border rather than .o-card's neutral one for the same reason.
   ITS VISIBILITY RULE IS THE PART V0 CANNOT SUPPORT (reduction row 12,
   dependency J-2, due 13 Aug). Shane's rule is the first three sign-ins or
   until the trader clears it, whichever comes first, and a boolean "dismissed"
   flag is no longer enough for that: it needs a sign-in COUNT as well, or one
   field storing whichever of the two ended it. FALLBACK IF A COUNTER IS
   REFUSED: show the card while the account is under seven days old, which
   needs no new field because the issue date already exists, and keep the X
   writing the dismissed flag. Either way the card is a two-state object and
   both states are drawn.
   ITS CONTENT IS FREE AND STATIC (reduction row 13). The figures in it are
   Maher's to approve, not the system's.
   PADDING IS UNIFORM AT 32, matching .o-prelogin's card rather than .o-card's
   24: it is a large content card, not a small module. It was the only
   asymmetric inset on its screen and that was an oversight, not a decision. */
.o-welcome { position: relative;
  border: 1px solid var(--teal-800); border-radius: var(--r-md);
  padding: var(--sp-32); }
.o-welcome__eyebrow { font: var(--fw-medium) var(--fs-caption)/var(--lh-caption) var(--font-ui);
  color: var(--teal-400); letter-spacing: .04em; text-transform: uppercase; }
.o-welcome__h { font: var(--fw-medium) var(--fs-2xl)/var(--lh-2xl) var(--font-display); margin-top: var(--sp-8); }
/* THE TRACKS ARE CAPPED, NOT FRACTIONAL, which is the whole mechanism. At 1fr
   each the columns ate every pixel available and the gap never moved. Capped,
   they stop growing and justify-content hands the surplus to the gutters.
   --sp-40 IS THE FLOOR, NOT THE GAP: below the cap the tracks shrink and the
   gap holds at 40; above it the tracks hold and the gap opens.
   36ch RATHER THAN 30ch because at 30 the growth ran away: a 1920 window put
   130px between columns sitting at 285. At 36ch the columns take most of the
   surplus and only the remainder reaches the gaps. */
.o-welcome__cols { display: grid;
  grid-template-columns: repeat(3, minmax(0, 36ch));
  justify-content: space-between;
  gap: var(--sp-40); margin-top: var(--sp-24); }
/* Titles are UI/MD, an existing named style, not a new size-and-face pairing.
   Regular rather than medium: at medium the three titles competed with the
   card's own heading, and at regular the size alone carries them. */
.o-welcome__col strong { display: block;
  font: var(--fw-regular) var(--fs-md)/var(--lh-md) var(--font-display); }
.o-welcome__col p { font: var(--fw-regular) var(--fs-xs)/var(--lh-xs) var(--font-ui);
  color: var(--text-secondary); margin-top: var(--sp-8); }
/* THE THREE COLUMNS HOLD UNTIL MOBILE, Shane 4 August, promoted 6 August with
   the home's switch-over. They used to stack at 1280, which is the SIDEBAR's
   reflow point and has nothing to do with them: at 1280 the card still has
   roughly 1184px, easily three columns. Borrowing another component's
   breakpoint is how a layout collapses for no reason.
   768 IS WHERE THEY GENUINELY CANNOT HOLD. Three columns need about 180px each
   to stay readable plus two --sp-40 gutters, which is 620px, and 768 less the
   96px of gutter leaves 672. Below that they stack. */
@media (max-width: 768px) {
  .o-welcome__cols { grid-template-columns: 1fr; gap: var(--sp-24); }
}
/* 32 above the act row where the heading-to-columns gap is 24. At 24 the card
   had no hierarchy and the button read as another paragraph rather than as the
   end of the card. A filled control needs more air above it than a line of text
   does, because its fill starts where its box starts. */
.o-welcome__foot { display: flex; align-items: center; gap: var(--sp-16); margin-top: var(--sp-32); flex-wrap: wrap; }
.o-welcome__dismiss { background: none; border: none; cursor: pointer;
  min-height: var(--sp-40); padding: 0 var(--sp-4);
  font: var(--fw-regular) var(--fs-sm)/var(--lh-sm) var(--font-ui); color: var(--text-tertiary); }
.o-welcome__dismiss:hover { color: var(--text-secondary); }
.o-welcome__dismiss:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--r-xs); }
/* THE DISMISS X IS AS QUIET AS IT IS ALLOWED TO BE, and that is a measured
   ceiling rather than a taste call. text/disabled is 3.19:1 on the canvas;
   WCAG 1.4.11 wants 3:1 for an interactive control and the next two steps down,
   border/strong at 2.13:1 and border/default at 1.54:1, both fail. So "not
   prominent" had exactly one move available. Same value and same reasoning as
   the rail collapse control's bar. THE TARGET IS 32px, not the 16px glyph:
   SC 2.5.8 wants 24 by 24 and the padding gets there without the icon growing.
   It brightens to text/primary on hover and focus, so the quietness costs
   nothing once the reader goes looking for it. */
.o-welcome__x {
  position: absolute; top: var(--sp-12); right: var(--sp-12);
  width: var(--sp-32); height: var(--sp-32);
  display: grid; place-items: center;
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--text-disabled);
  transition: color var(--motion-fast) var(--motion-ease);
}
.o-welcome__x svg { width: var(--icon-sm); height: var(--icon-sm); stroke-width: var(--icon-sm-stroke); }
.o-welcome__x:hover { color: var(--text-primary); }
.o-welcome__x:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--r-xs); }

/* --------------------------------------------------- Buyer's-eye miniature */
/* A browser frame around the trader's own public verification page, so the
   screen can answer "what does a buyer actually see" without the trader leaving
   it.
   THE FRAME IS THE COMPONENT. THE PAGE INSIDE IT IS NOT, and that is the
   ruling, not an omission. The honest build EMBEDS the real public page
   (reduction row 10); a hand-drawn copy is a second artifact describing one
   thing, and it goes stale the first time the public page changes. Embedding
   also keeps the miniature correct in every credential state for free. So
   .o-buyer, .o-buyer__bar and its parts are promoted, and everything that
   renders page CONTENT inside the frame stays in the screen file, where it is
   honestly labelled as a mime of a page that exists elsewhere.
   WHAT IT COSTS, and it is the one item here that is not free in a different
   way from the other two: it COUPLES this screen to the public verification
   page, whose own specification is still an unreviewed draft. Taken
   deliberately on 4 August rather than discovered in September.
   ONE COLLISION LEFT OPEN. A link labelled "Preview your page" sitting beside a
   picture of the page reads as redundant even though it is not: the miniature
   is a thumbnail and the link opens the real URL so the trader can check it
   resolves. The one-word fix is "Open it full size", and renaming a control is
   a copy decision. */
.o-buyer { border: 1px solid var(--neutral-850); border-radius: var(--r-md); overflow: hidden; }  /* exception 5 */
.o-buyer__bar { display: flex; align-items: center; gap: var(--sp-8);
  padding: var(--sp-8) var(--sp-12); background: var(--bg-surface); border-bottom: 1px solid var(--neutral-850); }
.o-buyer__dots { display: flex; gap: 5px; }
.o-buyer__dots i { width: 7px; height: 7px; border-radius: var(--r-full); background: var(--border-strong); display: block; }
.o-buyer__url { font-family: var(--font-mono); font-size: var(--fs-caption); color: var(--text-tertiary);
  background: var(--bg-canvas); border-radius: var(--r-xs); padding: 2px var(--sp-8); }
.o-buyer__page { padding: var(--sp-24) var(--sp-32) var(--sp-32); background: var(--bg-canvas); }
/* THE PAGE PARTS, PROMOTED 6 AUGUST 2026 when the post-login home was switched
   off its local .buyer* copies. The 4 August note above says the frame is the
   component and the page inside it is not, and that still holds for the honest
   build, which embeds the real public page. These four are the MIME the home
   draws while the embed does not exist, and they were sitting in the screen
   with the frame's own parts in the library, which is the split that made the
   inventory read .o-buyer as fully promoted when a third of it was not. They
   come here so there is one copy of the miniature, not one and a bit.
   .o-buyer__mark IS THE PUBLIC PAGE'S STATUS LINE, and it carries two glyphs
   toggled by the surface, exactly as .o-seal__ring does: mark-plain is the bare
   id glyph, mark-check the shield with a tick. The base rule hides the check;
   the surface's own state selector reveals it. That division is deliberate and
   it is a defect fix, not a style: the glyph used to be hard-coded to the tick
   in both states, so an unverified company's buyer-facing miniature showed a
   verification check. THE REAL PUBLIC PAGE OWES THE SAME FIX; this component
   only governs the thumbnail. */
.o-buyer__mark { display: inline-flex; align-items: center; gap: var(--sp-8);
  font: var(--fw-medium) var(--fs-xs)/var(--lh-xs) var(--font-ui); color: var(--teal-400); }
.o-buyer__mark svg { width: 16px; height: 16px; stroke-width: 1.6; }
.o-buyer__mark .mark-check { display: none; }
.o-buyer__co { font: var(--fw-medium) var(--fs-xl)/var(--lh-xl) var(--font-display); margin-top: var(--sp-8); }
.o-buyer__id { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-secondary); margin-top: var(--sp-2); }
.o-buyer__rows { margin-top: var(--sp-16); }
/* The miniature's rows are tighter than a full-size ledger's: it is a thumbnail
   of a page, so the rows compress with everything else in it. */
.o-buyer__rows .o-ledger__row { padding: var(--sp-8) 0; }
/* THREE UNSTYLED VALUE SOCKETS live on this family and owe no rule, the same
   arrangement .o-held__act has: .o-buyer__lei, .o-buyer__mark-txt and
   .o-buyer__note-next are names the surface's script writes text into. They are
   NOT Figma parts and must not be built as any. */
/* A COMMENT ON A THING SHOULD BE THE WIDTH OF THE THING. These notes carried
   their own 62ch measure while the frame above them filled the column, so two
   left-aligned blocks sat 75px apart on the right and read as a mistake. No
   cap: the column sets the width and the two can never disagree again. */
.o-buyer__note { font: var(--fw-regular) var(--fs-xs)/var(--lh-xs) var(--font-ui);
  color: var(--text-tertiary); margin-top: var(--sp-12); }
.o-buyer__note strong { color: var(--text-secondary); font-weight: var(--fw-regular); }

/* ------------------------------------------------------------ Feedback strip */

.o-seen__head { display: block; }
.o-seen__count { display: block; margin-top: var(--sp-4);
  font: var(--fw-regular) var(--fs-xs)/var(--lh-xs) var(--font-ui);
  color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
.o-seen__empty { font: var(--fw-regular) var(--fs-xs)/var(--lh-xs) var(--font-ui);
  color: var(--text-tertiary); margin-top: var(--sp-8); max-width: 44ch; }
.o-seen__list { margin-top: var(--sp-12); }
.o-seen__row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-16);
  padding: var(--sp-12) 0; border-bottom: 1px solid var(--border-subtle); }
.o-seen__row:first-child { border-top: 1px solid var(--border-subtle); }
.o-seen__what { font: var(--fw-regular) var(--fs-sm)/var(--lh-sm) var(--font-ui); color: var(--text-tertiary); }
.o-seen__when { font: var(--fw-regular) var(--fs-sm)/var(--lh-sm) var(--font-ui);
  color: var(--text-primary); font-variant-numeric: tabular-nums; }



/* ----------------------------------------------------------- Person row */
/* The named caller: a round face carrying initials, a name, and one line under
   it saying who they are. Claimed by the booking modal, the post-login home's
   share ask, the Orbitra ID issued screen's booking block, and both
   verification prompts. Five claimants, the most of anything promoted today.
   NO MARGIN HERE, AND THAT IS THE ONE REAL DISAGREEMENT BETWEEN THE FIVE
   COPIES. Three of them baked margin-top: var(--sp-24) into the class; the
   booking modal set it inline and the ID screen sets 16 through .nextstep. A
   value three files agree on and two contradict is placement, not anatomy, so
   it belongs to the screen and the three keep a one-line hang.
   THE FACE IS 44px AND IT IS OFF-SCALE, held under the same flag as the tag's
   24: it is an avatar sized to two lines of text beside it, not a spacing step,
   and every one of the five copies drew it at 44. */
.o-person { display: flex; align-items: center; gap: var(--sp-16); }
.o-person__face {
  width: 44px; height: 44px; border-radius: var(--r-full); flex: none;
  background: var(--bg-raised); border: 1px solid var(--border-default);
  display: grid; place-items: center;
  font: var(--fw-medium) var(--fs-sm)/var(--lh-sm) var(--font-ui); color: var(--text-secondary);
}
/* min-width: 0 so a long company name wraps inside the flex item instead of
   pushing the face off the card. Three of the five copies carried it and the
   two verification prompts did not; carried here because the failure it
   prevents is silent and the guard costs nothing at any drawn width. */
.o-person__txt { min-width: 0; }
.o-person__txt strong { display: block; font: var(--fw-medium) var(--fs-sm)/var(--lh-sm) var(--font-ui); }
/* var(--sp-2), not the raw 2px four of the five copies wrote. Same value, and
   the file's own rule is that every value here is a var(). */
.o-person__txt span {
  display: block; font: var(--fw-regular) var(--fs-xs)/var(--lh-xs) var(--font-ui);
  color: var(--text-tertiary); margin-top: var(--sp-2);
}

/* ------------------------------------------------------------ Held slot */
/* One time, pre-chosen off a fixed rota, offered as the primary act: one
   decision instead of two, and nothing built behind it. The act sits opposite
   the time and wraps under it when the card is narrow.
   Claimed by book a verification call and the home's share ask. The two copies
   were identical, so nothing was resolved to promote this one. One thing has
   been resolved since, and it is the flex basis below. */
.o-held { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-16);
  flex-wrap: wrap; margin-top: var(--sp-20); padding: var(--sp-12) var(--sp-16);
  background: var(--bg-inset); border: 1px solid var(--border-subtle); border-radius: var(--r-md); }
/* THE WHEN BLOCK ABSORBS THE SQUEEZE, AND flex: 1 1 0 IS WHAT MAKES IT.
   Without a basis the row breaks on the label's max-content width. At English
   length it fits and nothing shows; at Portuguese length "Terça-feira, 26 de
   agosto, 14:00" beside a translated button does not, so the row wrapped, the
   button dropped under the time, and the slot grew by the button's 40 plus the
   row's 16 of gap, 74 to 130. A basis of 0 gives the block a hypothetical size
   of 0, so the line never breaks on it: the block takes the space the button
   leaves and the LABEL wraps instead. min-width: 0 is what lets it shrink that
   far, which is why the two declarations belong together.
   WHERE THE LABEL BREAKS IS THE CARD'S BUSINESS, NOT THE COMPONENT'S, and it
   is measured rather than promised. On the booking modal's 560 the break lands
   at the string's own comma, "Terça-feira, 26 de agosto," then "14:00". On the
   home's 520 there is not room for that and it lands inside the date, "Terça-
   feira, 26 de" then "agosto, 14:00". Both are two lines at 102, both keep the
   button beside the time, and neither loses a character. The narrower break is
   a taste call left open rather than a defect: closing it means a non-breaking
   space or a balance rule, and either repaints a Portuguese render the booking
   screen's register has already measured and recorded.
   NOTHING IS TRUNCATED, and that is a decision rather than an omission. A time
   a trader has to turn up for is the last string in the product that may be
   cut, and spec 05 section 8 rules that status and error text never truncates.
   THE ROW KEEPS flex-wrap: wrap above it, so a genuinely narrow surface can
   still stack the button under the label. What it no longer does is stack at
   560 and at 520, where there was room all along.
   ENGLISH IS A NO-OP, measured on both claimants and not assumed: the English
   label fits on one line either way, and against justify-content:
   space-between a basis of 0 leaves the text and the button on the pixels they
   already had.
   PROMOTED 6 August, the same day it was written. Book a verification call
   carried it scoped to .booking-modal for one session while this file was
   another session's to write. The home's share ask never had it and still
   carried the defect on a 520 card, where the booking modal's 560 had 28px
   more to lose, so the screen-scoped copy fixed one claimant of two. */
.o-held__when { min-width: 0; flex: 1 1 0; }
/* Display face and tabular figures: a time is read at a glance and the columns
   must not shift between one slot and the next. */
.o-held__when strong { display: block; font: var(--fw-medium) var(--fs-lg)/var(--lh-lg) var(--font-display);
  color: var(--text-primary); font-variant-numeric: tabular-nums; }
.o-held__when span { display: block; font: var(--fw-regular) var(--fs-xs)/var(--lh-xs) var(--font-ui);
  color: var(--text-tertiary); margin-top: var(--sp-2); }
/* .o-held__act is a markup hook with no styling of its own: it holds whatever
   button the surface puts there. Named so the slot's two halves read as a pair
   in the markup, and listed here so nobody adds rules to it by reflex. */

/* ------------------------------------------- Framed scheduler embed */
/* An Orbitra-owned frame around somebody else's scheduler. The seam is
   admitted rather than hidden: a caption above saying what the reader is
   looking at, the third-party surface in the middle, and a foot saying what
   happens after they pick. Claimed by book a verification call and the home's
   share ask, identical in both.
   THE BODY IS NOT STYLED HERE. What sits inside the frame is the embed, and
   giving it a look in this file would be describing a surface we do not own.
   The screens hold their own placeholder for it. */
.o-embed { margin-top: var(--sp-20); border: 1px solid var(--border-default);
  border-radius: var(--r-md); overflow: hidden; background: var(--bg-inset); }
.o-embed__cap { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-16);
  padding: var(--sp-8) var(--sp-16); border-bottom: 1px solid var(--border-subtle); background: var(--bg-surface); }
.o-embed__cap strong { font: var(--fw-medium) var(--fs-xs)/var(--lh-xs) var(--font-ui); color: var(--text-secondary); }
.o-embed__cap span { font: var(--fw-regular) var(--fs-caption)/var(--lh-caption) var(--font-ui); color: var(--text-tertiary); }
.o-embed__foot { padding: var(--sp-8) var(--sp-16); border-top: 1px solid var(--border-subtle);
  background: var(--bg-surface); font: var(--fw-regular) var(--fs-xs)/var(--lh-xs) var(--font-ui); color: var(--text-tertiary); }

/* -------------------------------- Setup guide panel, and its strip form */

.o-guide {
  position: absolute; right: var(--sp-32); bottom: var(--sp-32); z-index: 20;
  width: 336px;
  background: var(--bg-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-3);
  overflow: hidden;
}
.o-guide__head {
  display: flex; align-items: center; gap: var(--sp-12);
  padding: var(--sp-16) var(--sp-20) var(--sp-12);
}
.o-guide__head strong { flex: 1; min-width: 0; font: var(--fw-medium) var(--fs-sm)/var(--lh-sm) var(--font-ui); }
/* The minimise control is the close control at its small size: .o-x.o-x--sm.
   It was a bare `button` rule here until 6 August, which is exactly the shape
   of drift the close-control merge below exists to end. */
.o-guide__head svg { width: var(--icon-sm); height: var(--icon-sm); stroke-width: var(--icon-sm-stroke); }
/* THE METER IS TEAL, NOT MINT, and that is the whole point of it. Mint marks
   the one act group a screen exists for, which on the home is the share. A
   standing task must not outrank it, so the progress line takes the same teal
   the welcome band already carries. */
.o-guide__meter {
  height: var(--sp-2); margin: 0 var(--sp-20);
  background: var(--border-subtle); border-radius: var(--r-full); overflow: hidden;
}
.o-guide__meter i { display: block; height: 100%; background: var(--teal-500); border-radius: var(--r-full); }
.o-guide__count {
  padding: var(--sp-8) var(--sp-20) 0;
  font: var(--fw-regular) var(--fs-caption)/var(--lh-caption) var(--font-ui);
  color: var(--text-tertiary); font-variant-numeric: tabular-nums;
}
.o-guide__list { padding: var(--sp-8) var(--sp-20) var(--sp-16); }

.g-row {
  display: flex; align-items: center; gap: var(--sp-12);
  padding: var(--sp-12) 0; border-bottom: 1px solid var(--border-subtle);
}
.g-row:last-child { border-bottom: none; }
.g-row__mark {
  width: var(--icon-sm); height: var(--icon-sm); flex: none;
  display: grid; place-items: center; color: var(--text-tertiary);
}
.g-row__mark svg { width: var(--icon-sm); height: var(--icon-sm); stroke-width: var(--icon-sm-stroke); }
.g-row--done .g-row__mark { color: var(--status-success-text); }
.g-row__label {
  flex: 1; min-width: 0;
  font: var(--fw-regular) var(--fs-sm)/var(--lh-sm) var(--font-ui);
  color: var(--text-primary);
}
.g-row--done .g-row__label { color: var(--text-tertiary); }
/* THE ROW ACTION IS A LINK, NEVER MINT. link/default is teal-400. The screen
   already holds exactly one mint act group and a standing task must not become
   a rival to it. This survived the promotion deliberately: it is the rule the
   sync queue says must survive. */
.g-row__act {
  font: var(--fw-regular) var(--fs-xs)/var(--lh-xs) var(--font-ui);
  color: var(--link-default); text-decoration: none; flex: none;
  border-radius: var(--r-xs);
  transition: color var(--motion-fast) var(--motion-ease);
}
.g-row__act:hover { color: var(--text-primary); }
.g-row__act:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.g-row__meta {
  font: var(--fw-regular) var(--fs-caption)/var(--lh-caption) var(--font-ui);
  color: var(--text-disabled); flex: none;
}
/* THE ROW'S OWN CTA IS A TEXT CTA, Shane 6 Aug. The bordered button made the
   panel claustrophobic: three rows in a 336px card cannot each carry a boxed
   control, so emphasis comes from weight and colour rather than a container.
   That half of the ruling stands: it is still a text CTA and still the heaviest
   thing in the panel.
   AND IT IS A LINK, NOT MINT. Ruled by Shane later the same day, 6 August, and
   it is the second half of the ruling that reworded the accent rule at the top
   of this file. It was mint for one morning on the reading that a pinned panel
   is its own surface. Counted per SURFACE it is not one: the panel hangs over
   the page with no scrim, the reader sees the panel and the page at the same
   moment, and a standing task must not be handed a control that outranks the
   act the screen exists for. The home now paints exactly one mint act group,
   the share, in every state.
   link/default is teal-400, measuring 5.35:1 on the panel's bg/raised fill,
   past the 4.5:1 the 14/20 label needs. Medium weight is what still separates
   it from .g-row__act above, which is regular at 13/18, so the two row actions
   are one family at two volumes rather than two colours.
   The old rule also reached past the token layer for var(--mint-550) and a
   var(--mint-400, ...) fallback. Both are gone: a mint primitive in this file
   is a defect whatever colour the rule ends up. */
.g-row__cta {
  flex: none; background: none; border: none; padding: 0; cursor: pointer;
  font: var(--fw-medium) var(--fs-sm)/var(--lh-sm) var(--font-ui);
  color: var(--link-default); border-radius: var(--r-xs);
  transition: color var(--motion-fast) var(--motion-ease);
}
.g-row__cta:hover { color: var(--text-primary); }
.g-row__cta:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }

/* The strip: the panel minimised. Revised 6 August by Shane, because the first
   form was not reading as something to press. It carries the same teal mark as
   the expanded head and sets its label at medium weight in primary text, so
   the pill reads as an invitation rather than a status chip. The dismiss
   control is gone from both forms: minimise only. */
.o-guide__icon, .o-guide-strip .o-guide__icon {
  width: var(--icon-md); height: var(--icon-md); stroke-width: var(--icon-md-stroke);
  color: var(--teal-400); flex: none;
}
.o-guide-strip {
  position: absolute; right: var(--sp-32); bottom: var(--sp-32); z-index: 20;
  display: inline-flex; align-items: center; gap: var(--sp-12);
  padding: var(--sp-12) var(--sp-16);
  background: var(--bg-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--r-full);
  box-shadow: var(--elev-3);
  cursor: pointer;
  font: var(--fw-regular) var(--fs-sm)/var(--lh-sm) var(--font-ui);
  color: var(--text-secondary);
  transition: color var(--motion-fast) var(--motion-ease),
              border-color var(--motion-fast) var(--motion-ease);
}
/* An author display declaration outranks the user agent's [hidden] rule, so
   the form that sets display needs the guard written out. .o-guide sets no
   display and does not need one. */
.o-guide-strip[hidden] { display: none; }
.o-guide-strip:hover { color: var(--text-primary); border-color: var(--border-strong); }
.o-guide-strip:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.o-guide-strip__label {
  font: var(--fw-medium) var(--fs-sm)/var(--lh-sm) var(--font-ui);
  color: var(--text-primary);
}
.o-guide-strip__count {
  font: var(--fw-regular) var(--fs-xs)/var(--lh-xs) var(--font-ui);
  color: var(--text-tertiary); font-variant-numeric: tabular-nums;
}
.o-guide-strip__chev { width: var(--icon-sm); height: var(--icon-sm); stroke-width: var(--icon-sm-stroke);
  color: var(--text-tertiary); flex: none; }
.o-guide-strip:hover .o-guide-strip__chev { color: var(--text-secondary); }
.o-guide-strip svg {
  width: var(--icon-sm); height: var(--icon-sm); stroke-width: var(--icon-sm-stroke);
  color: var(--text-tertiary); flex: none;
}
/* The panel is the first thing to give way when the frame gets short or
   narrow, because the corner it takes belongs to the share card. */
@media (max-width: 900px) {
  .o-guide, .o-guide-strip { right: var(--sp-20); bottom: var(--sp-20); }
  .o-guide { width: min(336px, calc(100% - var(--sp-40))); }
}
@media (prefers-reduced-motion: reduce) {
  .o-guide-strip, .g-row__act, .g-row__cta { transition: none; }
}

/* --------------------------------------------------------- Close control */
/* ONE CONTROL, AND THERE WERE FIVE OF IT. Standardised 6 August 2026 working
   the FIGMA-SYNC-QUEUE.md item "The close control needs standardising". The
   five were .modal-x on the booking modal and both verification prompts,
   .card-x on the post sign-up card, .o-welcome__x on the home's welcome band,
   .o-drawer__close on the navigation drawer, and the setup guide's minimise.
   THE QUEUE SAID THEY ALL ALREADY MATCHED THE SPEC AND TWO OF THEM DID NOT.
   That is worth writing down rather than quietly averaging away:
     .o-welcome__x rests at --text-disabled, not --text-tertiary. It is not
       drift. Shane ruled it quiet on 4 August and text/disabled is the last
       ramp step that still clears WCAG 1.4.11 at 3.19:1 on the canvas, so the
       ruling had exactly one value available. It is kept as .o-x--quiet.
     The guide's minimise is a 24px box, not 32. Also kept, as .o-x--sm: it
       sits in a 336px panel head where 32 crowds the title, and 24 is still on
       WCAG 2.2 SC 2.5.8's floor rather than under it.
   Everything else was a rename. The base below is the queue's spec exactly.
   A CLOSE IS NEVER A SKIP, and that rule travels with the component. A close
   means "this surface is finished with". Where a card still has an outstanding
   act the skip link carries the exit and there is no cross; where there is
   nothing left to do the cross carries it and there is no skip. Both never
   appear together. */
.o-x {
  width: var(--sp-32); height: var(--sp-32);
  display: grid; place-items: center;
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--text-tertiary); border-radius: var(--r-xs);
  transition: color var(--motion-fast) var(--motion-ease);
}
.o-x:hover { color: var(--text-primary); }
.o-x:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.o-x svg { width: var(--icon-sm); height: var(--icon-sm); stroke-width: var(--icon-sm-stroke); }
/* The hang four of the five share: top right inside a card or a modal. Kept a
   modifier rather than folded into the base, because the drawer's close sits
   in a flex row and must not be taken out of flow. */
.o-x--corner { position: absolute; top: var(--sp-16); right: var(--sp-16); }
/* Shane's quiet treatment, 4 August. See the note above before changing it. */
.o-x--quiet { color: var(--text-disabled); }
/* The small box, for a control inside a narrow panel head. */
.o-x--sm { width: var(--sp-24); height: var(--sp-24); }
@media (prefers-reduced-motion: reduce) { .o-x { transition: none; } }
/* .o-welcome__x and .o-drawer__close above predate this and still carry their
   own copies of the same anatomy. They are the same control and Figma builds
   ONE component with the variants named here. They are left in place rather
   than rewritten because both render correctly today and .o-welcome__x carries
   a ruling in its own comment that a merge would bury. Folding them onto .o-x
   is queued, not done. */


