/* =============================================================================
   A-EMR — PORTAL CSS
   Aegis IT Advisors LLC

   WHY THIS FILE EXISTS, AND WHY IT IS NOT site.css.

   It carries the static cover surfaces — the reviewer sign-in page
   (site/signin.html, which `/` resolves to and which is therefore the first
   screen anybody meets) and the launch screen (site/portal/index.html) — which
   docs/02-product/portal-direction.md specifies as ONE document rendered on one
   dark ground. They share a masthead, a footer, a demonstration marker and an
   eyebrow, and any seam between them reads as two systems bolted together.

   There were THREE. site/index.html was the public landing page at `/` and it
   was removed when the sign-in page took that address; the comments below that
   still name it are describing history, not a surface that exists.

   IT IS A NEW FILENAME ON PURPOSE. .github/workflows/deploy.yml publishes
   site/assets/* with `public,max-age=31536000,immutable`, so anyone who has
   ever loaded any page on this host holds site.css for a year and a CloudFront
   invalidation does not reach their browser. Adding the portal rules to
   site.css would have rendered this design unstyled for exactly the people most
   likely to come back. portal-direction.md section 8.3 reaches the same
   conclusion and recommends the same fix.

   WHAT IT DOES NOT DO. It does not restyle site.css's components, it does not
   define a custom property (tokens.css is the one definition site), and it
   contains no hex literal or raw pixel colour — every colour below is a
   semantic alias. Structural lengths the token system does not model (a glow's
   radius, a connector's offset) are plain values, commented where the number is
   a decision rather than an arbitrary one.

   THE THEME IS PINNED IN THE MARKUP on all three pages rather than booted from
   a script, so --text-primary and the other flipping aliases cannot change
   underneath a fixed navy ground. Every new rule below reads a theme-invariant
   alias anyway — belt and braces, per portal-direction.md section 4.3 — so a
   future un-pinning breaks the inherited site.css rules loudly instead of
   breaking these silently.

   CONTRAST. Every ratio quoted in a comment below was computed in
   portal-direction.md section 6 against --surface-chrome (--color-navy-950
   #050b1f) or --portal-panel (--color-ink-850 #18202b). They are single numbers
   rather than pairs because every alias the portal reads is declared identically
   in both theme blocks of tokens.css.
   ========================================================================== */

/* -----------------------------------------------------------------------------
   THE DISPLAY FACE, SELF-HOSTED.

   The same declaration site/signin.html carries inline, restated here so the
   face travels with the cover. site.css's .wordmark and h1 have always read
   --font-display, and until this file existed the landing page asked for Space
   Grotesk and silently rendered the fallback, because the only @font-face on
   the static site was inside the sign-in page.

   It stays duplicated rather than moved: tests/checks/auth-gate.js mutates the
   sign-in page's own @font-face src to prove the third-party-subresource sweep
   can fail, and moving that declaration out of the page would leave the
   mutation injecting into nothing. A duplicated @font-face costs one extra
   parse and no extra fetch — the browser loads the file once.

   One weight, latin subset, 13 KB, served from this origin. No CDN and no font
   service: a third-party request from a healthcare-branded hostname is a
   different conversation from the same request on a marketing page (NF-4), and
   a room with no network gets the page and the face together or gets neither.
   Licence: SIL OFL 1.1; site/assets/fonts/OFL.txt travels with the file.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: "Space Grotesk";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/space-grotesk-600-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* -----------------------------------------------------------------------------
   PATTERN 01 — THE COVER BAND
   The portal surface: edge to edge, dark in both themes, and the thing that
   makes the three static pages read as one document.
   -------------------------------------------------------------------------- */
.portal-cover {
  background: var(--surface-chrome);
  color: var(--text-on-chrome-muted);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  margin: 0;
  /* The glows are fixed; without this a long viewport on a short page lets the
     page scroll sideways behind them. */
  position: relative;
  overflow-x: hidden;
}

/* The cover reads as a document, so it is narrower than site.css's .wrap at
   72rem, which is a marketing width. */
.portal-cover__band {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 60rem;
  margin-inline: auto;
  padding-block: var(--space-10) var(--space-16);
  padding-inline: var(--space-6);
}

/* THE THREE CHROME BANDS — marker, masthead, footer — share the cover's
   measure, so nothing on the page is aligned to a different edge. site.css's
   .wrap is 72rem (a marketing width) and it deliberately unconstrains the
   marker; both are corrected here rather than in the markup, so the three
   pages carry identical chrome and only this file knows the measure. */
.portal-cover .demo-marker .wrap,
.portal-cover .site-header .wrap,
.portal-cover .site-footer .wrap {
  max-width: 60rem;
}

/* site.css makes .site-header .wrap the flex row itself. On the cover the row
   is .portal-masthead inside it, because the masthead is closed by a rule that
   would otherwise become a third flex item beside the wordmark. */
.portal-cover .site-header .wrap {
  display: block;
  padding-block: 0;
}

/* site.css floats the footer on the canvas with a top rule and a margin. Here
   it closes a full-height brand surface, so it keeps the rule and drops the
   margin that would push it off the fold. */
.portal-cover .site-footer {
  flex-shrink: 0;
  margin-top: 0;
  border-block-start: var(--line-hairline) solid var(--portal-rule);
}

/* Everything that is not a decorative layer sits above both glows. The skip
   link is deliberately NOT in this list: site.css makes it position:absolute at
   --z-toast, and adding position:relative here would leave it in the flow as a
   visible line of text above the marker. */
.portal-cover .demo-marker,
.portal-cover .site-footer,
.portal-cover .portal-main {
  position: relative;
  z-index: var(--z-sticky);
}

.portal-main {
  flex: 1 0 auto;
}

/* The two glows. Decorative, aria-hidden, and static — there is nothing here
   for prefers-reduced-motion to switch off. The radii and offsets are the
   values already shipping on site/signin.html, adopted so the three static
   surfaces share one atmosphere. */
.portal-glow {
  position: fixed;
  z-index: var(--z-base);
  pointer-events: none;
  border-radius: var(--radius-pill);
  filter: blur(110px);
}

.portal-glow--accent {
  width: 29rem;
  height: 29rem;
  top: -11rem;
  right: -4rem;
  background: var(--accent-base);
  opacity: 0.16;
}

/* The alias, not the pink primitive underneath it. Under a 110px blur at 0.10
   the two stops are indistinguishable from either one alone, so nothing is
   gained by reaching past --brand-gradient — and reaching past an alias is how
   a second definition site starts. */
.portal-glow--brand {
  width: 22rem;
  height: 22rem;
  bottom: -12rem;
  left: -9rem;
  background: var(--brand-gradient);
  opacity: 0.1;
}

/* -----------------------------------------------------------------------------
   PATTERN 02 — EYEBROW, AND ITS THREE VARIANTS
   There is exactly one micro-label recipe in this system and .eyebrow in
   site.css is it. These are variants of it, never a second recipe.

   .eyebrow itself reads --text-muted, which FLIPS with the theme, so it is
   never used bare on the cover: every eyebrow on these three pages carries
   --cover or --accent.
   -------------------------------------------------------------------------- */
/* THE VARIANTS CARRY THE BLOCK BEHAVIOUR, NOT A DESCENDANT SELECTOR, AND THAT
   IS THE WHOLE OF A BUG WORTH RECORDING. This was written `.portal-cover
   .eyebrow`, which is (0,2,0) - so it beat every single-class variant below it
   AND every media query that tried to change one, because a media query adds no
   specificity. Two devices rendered wrong and neither threw: the tick rule
   became a zero-height inline box, and the co-brand label refused to hide at
   the narrow width and kept an 8px bottom margin that pushed it off the
   masthead's centre line. Both were found by screenshot.

   These are (0,1,0) and site.css's .eyebrow is (0,1,0), so this file simply
   loads later and wins - and anything that wants to override one of them can do
   it with one more class rather than three. */
.eyebrow--cover,
.eyebrow--accent {
  display: block;
  /* Tighter than site.css's --space-3, because the cover's headings are larger
     and the label should sit closer to what it labels. */
  margin: 0 0 var(--space-2);
}

.eyebrow--cover {
  color: var(--portal-text-quiet); /* 7.39 on the ground, 6.20 on the panel */
}

/* At most one per section, on the section that is the point. */
.eyebrow--accent {
  color: var(--portal-accent-text); /* 7.40 on the ground */
}

/* Compound, so it outranks the --cover and --accent display:block above it
   whichever order the classes are written in the markup. */
.eyebrow.eyebrow--tick {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* A BORDER, NOT A ONE-PIXEL BOX WITH A BACKGROUND. Forced colours discards
   author backgrounds and keeps borders, so the obvious spelling of this rule is
   the one that vanishes in high contrast. Every hairline in this file is
   written the same way for the same reason. */
.eyebrow--tick::before {
  content: "";
  inline-size: 1.375rem;
  block-size: 0;
  flex: none;
  border-block-start: var(--line-hairline) solid var(--accent-base); /* 3.63, non-text floor 3.0 */
}

/* -----------------------------------------------------------------------------
   PATTERN 03 — MASTHEAD AND CO-BRAND BLOCK
   Wordmark left, the Aegis lockup right. The same markup on all three surfaces.
   -------------------------------------------------------------------------- */
.portal-masthead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  /* The landing and launch pages put this inside a block .wrap; the sign-in
     page puts it inside site.css's flex .wrap, where a single child would
     otherwise shrink to its content and leave the co-brand mid-band. */
  width: 100%;
  padding-block: var(--space-4);
  background: transparent;
  color: var(--text-on-chrome);
}

.portal-cobrand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
}

.portal-cobrand__label {
  margin: 0;
  /* "Prepared by Aegis IT Advisors for <recipient>" is a longer line than the
     attribution it replaced, so it is allowed to wrap onto a second line
     rather than push the lockup off the band. Right-aligned, so both lines
     close on the hairline beside the mark. */
  max-width: 20rem;
  text-align: right;
}

/* The recipient. The name is PROVISIONAL and it is written in exactly one
   element per surface, so changing it is one edit per file. This rule exists
   to make that element findable and to hold anything the name later needs; it
   deliberately changes nothing about how the name renders, because it is part
   of the same sentence as the words around it. */
.portal-cobrand__recipient {
  color: inherit;
}

/* THE AEGIS LOCKUP. Mark, a rule, then the typeset name — the same three parts
   in the same order as site/signin.html and the aegisit.ai header.

   THE TYPE IS HERE, AND IT IS THE ONLY PLACE IT IS. This rule used to set
   display, gap and colour and no type at all, so the lockup inherited body
   size on the landing page and the launch screen while site/signin.html
   carried a second class, `wordmark`, that gave it --size-heading. Two
   renderings of one lockup is a seam; the smaller one was also a contrast
   failure. `.brand-it` is painted from --brand-gradient, and the two stops
   measure

     --color-accent-600     #9333ea   3.63 : 1  against --surface-chrome #050b1f
     --color-brand-pink-600 #db2777   4.25 : 1  against --surface-chrome #050b1f

   (computed from the token values, not quoted from the direction document).
   WCAG 1.4.3 sets 4.5:1 for normal text and 3.0:1 for large text, where large
   is 24px, or 18.66px at bold weight or heavier. At --size-heading (20px) and
   --weight-semibold (600) the string is large text, both stops clear 3.0, and
   the paint is permitted — which is precisely the argument portal-direction.md
   section 6.1 records. At body size it is normal text and BOTH stops fail. So
   the size is not a style choice here; it is what the measurement was taken at,
   and the `wordmark` class is removed from the sign-in page's markup so that
   this is the single definition of it.

   The values are site.css's .wordmark verbatim rather than a reference to it,
   because the lockup is not a wordmark: it is a name beside a mark, and a
   future change to the A-EMR wordmark must not silently move the Aegis one. */
.brand-lockup {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  text-decoration: none;
  color: var(--text-on-chrome); /* 19.55 */
  font-family: var(--font-display);
  font-size: var(--size-heading);
  line-height: var(--line-heading);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
}

/* The mark is the shield alone, cropped from the full Aegis lockup at the
   artwork's own empty band. Height fixed and width derived, both also on the
   element, so the masthead reserves the box before the image arrives and
   nothing shifts. Intrinsic size of the file is 126x144. */
.brand-mark {
  height: 2.25rem;
  width: auto;
}

.brand-rule {
  width: 0;
  height: 1.5rem;
  border-left: var(--line-hairline) solid var(--portal-border); /* 4.31 */
}

.brand-it {
  background: var(--brand-gradient);
  background-clip: text;
  -webkit-background-clip: text;
}

/* The paint is applied only where it can be undone. A browser without
   background-clip:text, a printer, and forced colours all keep the inherited
   --text-on-chrome and lose nothing but the gradient. This is the ONE string on
   the portal painted from the gradient, and it is permitted because it renders
   at --size-heading / --weight-semibold, clearing WCAG 1.4.3's large-text
   threshold — the two stops measure 3.63 and 4.25 against the ground. */
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .brand-it {
    color: transparent;
  }
}

/* -----------------------------------------------------------------------------
   PATTERN 04 — RULE, FLUSH RULE, ACCENT RULE AND STUB
   Three shapes, one job: closing something without drawing a box.

   --portal-rule measures 2.75 against the ground and 2.31 against the panel.
   Both are below 3.0 and both are CORRECT: a divider is decoration, not a
   control boundary, and SC 1.4.11 does not reach it. Same verdict
   ui-direction.md section 3.3 already records for the table row rule.
   -------------------------------------------------------------------------- */
.portal-rule {
  border: 0;
  border-block-start: var(--line-hairline) solid var(--portal-rule);
  margin-block: var(--space-6);
}

.portal-rule--flush {
  margin-block-start: 0;
}

/* The one ornament on the page. It is a gradient and therefore a background, so
   it disappears under forced colours — acceptable and deliberate: it carries no
   meaning and nothing is positioned by it. */
.portal-rule--accent {
  block-size: var(--line-hairline);
  border: 0;
  background: linear-gradient(
    90deg,
    var(--accent-base),
    var(--portal-border),
    transparent
  );
  margin-block: var(--space-6);
}

/* A border, so it survives forced colours where the rule above does not. */
.portal-stub {
  inline-size: 2.5rem;
  block-size: 0;
  border-block-start: var(--line-rule) solid var(--accent-base);
  margin-block: var(--space-4);
}

/* -----------------------------------------------------------------------------
   PATTERN 05 — DISPLAY HEADING AND LEAD
   -------------------------------------------------------------------------- */
.portal-display {
  font-family: var(--font-display);
  font-size: var(--size-display);
  line-height: var(--line-display);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--text-on-chrome); /* 19.55 */
  margin: 0 0 var(--space-2);
}

.portal-lead {
  font-size: var(--size-body-lg);
  line-height: var(--line-body-lg);
  color: var(--text-on-chrome-muted); /* 11.59 */
  max-width: var(--measure-prose);
  margin: 0 0 var(--space-6);
}

/* -----------------------------------------------------------------------------
   PATTERN 06 — SECTION HEAD

   align-items: flex-end is load-bearing: it puts the actions cluster on the
   same baseline as the bottom of the title whether the left side is one line or
   three. The unclassed left <div> is required — it is the flex child that
   groups the eyebrow and the title.
   -------------------------------------------------------------------------- */
.portal-section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-6);
  padding-block-end: var(--space-3);
  border-block-end: var(--line-hairline) solid var(--portal-rule);
  margin-block-end: var(--space-6);
}

.portal-section-head__title {
  font-family: var(--font-display);
  font-size: var(--size-heading);
  line-height: var(--line-heading);
  font-weight: var(--weight-semibold);
  color: var(--text-on-chrome); /* 19.55 */
  margin: 0;
}

.portal-section-head__actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: none;
}

.portal-section-head__note {
  font-size: var(--size-caption);
  line-height: var(--line-caption);
  color: var(--portal-text-quiet); /* 7.39 */
  margin: 0;
  max-width: 24rem;
}

/* -----------------------------------------------------------------------------
   PATTERN 07 — LINK CARD ON THE DARK GROUND
   The whole card is the link. The arrow inside it is a <span>, never a nested
   <a>: a link inside a link is not a thing a keyboard can describe.
   -------------------------------------------------------------------------- */
.portal-card {
  display: block;
  background: var(--portal-panel); /* a 1.19 lift off the ground */
  border: var(--line-hairline) solid var(--portal-border); /* 4.31 ground, 3.61 fill */
  border-radius: var(--radius-md);
  box-shadow: var(--elevation-1); /* which is none — level 1 is a border */
  padding: var(--space-5);
  text-decoration: none;
  color: var(--text-on-chrome-muted);
  transition: border-color var(--dur-fast) var(--ease-standard);
}

/* No lift, no shadow, no scale. ui-direction.md section 10: there are no
   decorative animations in this demonstration at all, so hover changes colour
   and nothing else. */
.portal-card:hover {
  border-color: var(--portal-accent-text); /* 7.40 */
}

/* PATTERN 07a - THE SAME CARD WITH NOTHING TO CLICK.

   The walkthrough on the launch screen names the three things worth seeing and
   does NOT link to them: those routes are served by the instance, the instance
   may be stopped, and a link that answers with a raw CloudFront error page is
   worse than no link. The card keeps its shape and gives back the one thing
   that promises a click - the hover border. Same specificity as the rule above
   and later in the file, so it wins on document order. */
.portal-card--static:hover {
  border-color: var(--portal-border);
}

.portal-card__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
}

.portal-card__title {
  font-size: var(--size-heading-sm);
  line-height: var(--line-heading-sm);
  font-weight: var(--weight-semibold);
  color: var(--text-on-chrome); /* 16.40 on the fill */
}

.portal-card__text {
  font-size: var(--size-body);
  line-height: var(--line-body);
  color: var(--text-on-chrome-muted); /* 9.72 on the fill */
}

.portal-chip {
  inline-size: 2.75rem;
  block-size: 2.75rem;
  border: var(--line-hairline) solid var(--portal-accent-text);
  border-radius: var(--radius-pill);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--portal-accent-text); /* 6.21 on the fill */
  flex: none;
}

/* -----------------------------------------------------------------------------
   PATTERN 08 — NUMBERED WALKTHROUGH

   The numeral span is EMPTY and the number is a CSS counter, so reordering or
   inserting an item renumbers automatically and there is no number that can go
   stale. That is Constitution VI's discipline applied to a list.

   minmax(0, 1fr) rather than a bare 1fr throughout, deliberately: a bare 1fr is
   minmax(auto, 1fr) and refuses to shrink below min-content, which is how a
   grid pushes past the viewport.
   -------------------------------------------------------------------------- */
.portal-walk {
  list-style: none;
  margin: 0 0 var(--space-12);
  padding: 0;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(3, minmax(0, 1fr));
  counter-reset: portalwalk;
}

.portal-walk__item {
  display: flex;
  counter-increment: portalwalk;
}

.portal-walk__item > .portal-card {
  flex: 1 1 auto;
}

.portal-walk__num {
  inline-size: 1.75rem;
  block-size: 1.75rem;
  border: var(--line-hairline) solid var(--portal-accent-text);
  border-radius: var(--radius-pill);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: var(--size-caption);
  line-height: var(--line-caption);
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--portal-accent-text); /* 6.21 on the card fill */
  flex: none;
}

.portal-walk__num::before {
  content: counter(portalwalk, decimal-leading-zero);
}

/* -----------------------------------------------------------------------------
   PATTERN 09 — HONESTY PANEL
   The highest-value block on the portal, specified to be the strongest thing on
   the page. Strength comes from placement, size and word — never from ornament.
   It is never collapsed, never behind a disclosure control, and never below the
   fold on the launch screen — which, since site/index.html was removed, is its
   only static carrier. The sign-in page in front of it carries the three claims
   an unauthenticated visitor is owed, not all eleven.
   -------------------------------------------------------------------------- */
.portal-honesty {
  background: var(--portal-panel);
  border: var(--line-hairline) solid var(--portal-border);
  border-radius: var(--radius-md);
  padding: var(--space-8);
  /* More air than any other block on the page. */
  margin-block: var(--space-12);
}

/* ONE STEP LARGER than every other section head on the portal, which is the
   whole of its emphasis. */
.portal-honesty__title {
  font-family: var(--font-display);
  font-size: var(--size-title);
  line-height: var(--line-title);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--text-on-chrome); /* 16.40 on the fill */
  margin: 0;
}

.portal-honesty__list {
  display: grid;
  gap: 0;
  margin: 0;
}

.portal-honesty__row {
  display: grid;
  grid-template-columns: 12rem minmax(0, 1fr);
  gap: var(--space-4);
  padding-block: var(--space-4);
  border-block-start: var(--line-hairline) solid var(--portal-rule);
}

.portal-honesty__row:first-child {
  border-block-start: 0;
}

/* The eyebrow recipe, on a <dt>. */
.portal-honesty__row dt {
  font-size: var(--size-micro);
  line-height: var(--line-micro);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-micro);
  color: var(--portal-text-quiet); /* 6.20 on the fill */
}

.portal-honesty__row dd {
  font-size: var(--size-body-lg);
  line-height: var(--line-body-lg);
  color: var(--text-on-chrome-muted); /* 9.72 on the fill */
  max-width: var(--measure-prose);
  margin-inline-start: 0;
}

/* -----------------------------------------------------------------------------
   PATTERN 10 — LINK ARROW
   -------------------------------------------------------------------------- */
.portal-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--size-micro);
  line-height: var(--line-micro);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-micro);
  color: var(--portal-accent-text); /* 7.40 ground, 6.21 fill */
  transition: color var(--dur-fast) var(--ease-standard);
}

/* A border so it survives forced colours, and currentColor so it follows the
   hover shift for free. THE RULE DOES NOT GROW on hover. */
.portal-arrow::after {
  content: "";
  inline-size: 1.375rem;
  block-size: 0;
  flex: none;
  border-block-start: var(--line-hairline) solid currentColor;
}

.portal-card:hover .portal-arrow {
  color: var(--text-on-chrome);
}

/* -----------------------------------------------------------------------------
   PATTERN 11 — QUOTE

   Weight 400 against the headings' 600 is the point. The display subset ships
   only weight 600, so the quote is set in the TEXT face at regular weight
   instead: it reads quieter while being larger, which is what a pull quote is
   for. No italic, no oversized quotation glyph, no decorative punctuation.
   -------------------------------------------------------------------------- */
.portal-quote {
  font-family: var(--font-sans);
  font-size: var(--size-heading);
  line-height: var(--line-heading);
  font-weight: var(--weight-regular);
  color: var(--text-on-chrome); /* 19.55 */
  border-inline-start: var(--line-rule) solid var(--accent-base); /* 3.63 */
  padding-inline-start: var(--space-5);
  /* Narrower than --measure-prose (68ch), so a quote beside a lead reads as the
     quieter element despite being the larger type. */
  max-width: 60ch;
  margin: 0;
}

.portal-quote__attr {
  display: block;
  margin-block-start: var(--space-4);
  font-size: var(--size-micro);
  line-height: var(--line-micro);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-micro);
  color: var(--portal-text-quiet); /* 7.39 */
}

/* -----------------------------------------------------------------------------
   PATTERN 13 — STATE BADGE
   Three channels, always: a word, a glyph and a colour. The word is
   --text-on-chrome in EVERY state, so it never gets harder to read as the news
   gets worse; colour is the third channel and is expected to be discarded under
   forced colours.

   The fill is transparent and there is no tint token. A composited fill would
   invalidate every ratio quoted in this file.
   -------------------------------------------------------------------------- */
.portal-state {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  border: var(--line-hairline) solid var(--portal-tone-idle);
  background: transparent;
  margin: 0;
}

.portal-state__word {
  font-size: var(--size-micro);
  line-height: var(--line-micro);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-micro);
  color: var(--text-on-chrome); /* 19.55, in every state */
}

.portal-state__glyph {
  display: inline-flex;
  color: var(--portal-tone-idle);
}

.portal-state__glyph .icon {
  width: 0.875rem;
  height: 0.875rem;
}

/* The five states. UNKNOWN is not optional and it is not a fallback that never
   fires: if the portal cannot observe the environment it says so, because
   rendering Stopped for an unobserved state is a claim the page has not
   earned. */
.portal-state[data-portal-state="running"] {
  border-color: var(--portal-tone-ready); /* 9.65 */
}

.portal-state[data-portal-state="running"] .portal-state__glyph {
  color: var(--portal-tone-ready);
}

.portal-state[data-portal-state="starting"],
.portal-state[data-portal-state="stopping"] {
  border-color: var(--portal-tone-working); /* 7.40 */
}

.portal-state[data-portal-state="starting"] .portal-state__glyph,
.portal-state[data-portal-state="stopping"] .portal-state__glyph {
  color: var(--portal-tone-working);
}

.portal-state[data-portal-state="stopped"] {
  border-color: var(--portal-tone-idle); /* 7.39 */
}

.portal-state[data-portal-state="unknown"] {
  border-color: var(--portal-tone-problem); /* 9.62 */
}

.portal-state[data-portal-state="unknown"] .portal-state__glyph {
  color: var(--portal-tone-problem);
}

/* -----------------------------------------------------------------------------
   PATTERN 14 — PROGRESS STEPS
   A vertical list of named steps that really happen. Not a bar, not a
   percentage, not a spinner standing in for a step.

   THE REFRESH GLYPH DOES NOT ROTATE. A spinner is a decorative animation, and
   it additionally implies work is progressing when all the page knows is that
   it has not finished.
   -------------------------------------------------------------------------- */
.portal-steps {
  list-style: none;
  margin: 0;
  padding: 0;
}

.portal-steps__item {
  display: grid;
  grid-template-columns: 1.5rem minmax(0, 1fr) auto;
  column-gap: var(--space-3);
  row-gap: var(--space-1);
  align-items: start;
  padding-block: var(--space-3);
  position: relative;
}

/* The connector is a BORDER, so forced colours keeps the sequence visible.
   1.75rem clears the mark above it; -0.25rem meets the next row's mark. */
.portal-steps__item:not(:last-child)::after {
  content: "";
  position: absolute;
  inset-block-start: 1.75rem;
  inset-block-end: -0.25rem;
  inset-inline-start: 0.75rem;
  inline-size: 0;
  border-inline-start: var(--line-hairline) solid var(--portal-rule);
}

.portal-steps__mark {
  grid-column: 1;
  grid-row: 1;
  inline-size: 1.5rem;
  block-size: 1.5rem;
  border: var(--line-hairline) solid var(--portal-tone-idle);
  border-radius: var(--radius-pill);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--portal-tone-idle);
  /* Over the connector, not through it. */
  background: var(--surface-chrome);
  position: relative;
  z-index: 1;
}

.portal-steps__mark .icon {
  width: 0.75rem;
  height: 0.75rem;
}

.portal-steps__name {
  grid-column: 2;
  grid-row: 1;
  font-size: var(--size-body);
  line-height: var(--line-body);
  font-weight: var(--weight-semibold);
  color: var(--text-on-chrome); /* 19.55 */
}

.portal-steps__state {
  grid-column: 2;
  grid-row: 2;
  font-size: var(--size-micro);
  line-height: var(--line-micro);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-micro);
  color: var(--portal-tone-idle);
}

/* THE TIMESTAMP IS THE EVIDENCE. Every Done carries the wall-clock time the
   condition was observed true. A step with no timestamp has not happened, which
   is why Waiting carries none. */
.portal-steps__at {
  grid-column: 3;
  grid-row: 1;
  justify-self: end;
  font-family: var(--font-mono);
  font-size: var(--size-caption);
  line-height: var(--line-caption);
  font-variant-numeric: tabular-nums;
  color: var(--portal-text-quiet); /* 7.39 */
}

.portal-steps__reason {
  grid-column: 2 / -1;
  font-size: var(--size-body);
  line-height: var(--line-body);
  color: var(--text-on-chrome-muted); /* 11.59 */
  max-width: var(--measure-prose);
  margin: var(--space-2) 0 0;
}

.portal-steps__item[data-portal-step="active"] .portal-steps__mark {
  border-color: var(--portal-tone-working);
  color: var(--portal-tone-working);
}

.portal-steps__item[data-portal-step="active"] .portal-steps__state {
  color: var(--portal-tone-working);
}

.portal-steps__item[data-portal-step="done"] .portal-steps__mark {
  border-color: var(--portal-tone-ready);
  color: var(--portal-tone-ready);
}

.portal-steps__item[data-portal-step="done"] .portal-steps__state {
  color: var(--portal-tone-ready);
}

.portal-steps__item[data-portal-step="failed"] .portal-steps__mark {
  border-color: var(--portal-tone-problem);
  color: var(--portal-tone-problem);
}

.portal-steps__item[data-portal-step="failed"] .portal-steps__state {
  color: var(--portal-tone-problem);
}

/* A settled step that is neither a success nor a failure — the demonstration
   data carried over from the previous session (demo-lifecycle.md section 2.3).
   It is a statement rather than a stall, and it must not render like a Done:
   two settled outcomes that look identical have told the visitor nothing. */
.portal-steps__item[data-portal-step="settled"] .portal-steps__mark {
  border-color: var(--portal-text-quiet);
  color: var(--portal-text-quiet);
}

.portal-steps__item[data-portal-step="settled"] .portal-steps__state {
  color: var(--portal-text-quiet);
}

/* NOT OBSERVED — a state that had no rule at all, which is the worst kind to
   leave out: with nothing here the row rendered in the default tone, which is
   --portal-tone-idle, which is exactly what WAITING renders in. So the one row
   saying "this page never saw this step" was pixel-identical to a row saying
   "this step has not happened yet", and those are different facts about the
   machine (portal-launch.js NOT_OBSERVED, demo-lifecycle.md section 2.1).

   It shares the quiet tone with SETTLED, because neither is a success or a
   failure, and it is separated from settled by a DASHED edge rather than by a
   second colour. A border style survives forced colours, greyscale and paper;
   a second grey does not survive any of them. The words differ too — "Not
   observed" against "Carried over" — so the row carries three channels like
   every other state on this page. */
.portal-steps__item[data-portal-step="not-observed"] .portal-steps__mark {
  border-style: dashed;
  border-color: var(--portal-text-quiet); /* 7.39 */
  color: var(--portal-text-quiet);
}

.portal-steps__item[data-portal-step="not-observed"] .portal-steps__state {
  color: var(--portal-text-quiet); /* 7.39 */
}

/* -----------------------------------------------------------------------------
   PATTERN 15 — COUNTDOWN, AND THE EXTEND AFFORDANCE BESIDE IT

   The value is derived from the deadline the API returns, never counted down in
   the browser: a browser timer drifts and then lies, and a reloaded page has to
   read the same number as the page before it. Granularity is minutes and there
   is no per-second tick — a ticking clock is an animation, and it would imply a
   precision the value does not have.
   -------------------------------------------------------------------------- */
.portal-countdown {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.portal-countdown__value {
  font-family: var(--font-mono);
  font-size: var(--size-heading);
  line-height: var(--line-heading);
  font-variant-numeric: tabular-nums;
  color: var(--text-on-chrome); /* 19.55 */
  margin: 0;
}

.portal-countdown__unit {
  font-family: var(--font-sans);
  font-size: var(--size-body);
  line-height: var(--line-body);
  color: var(--portal-text-quiet); /* 7.39 */
  margin-inline-start: var(--space-3);
}

/* -----------------------------------------------------------------------------
   PATTERN 16 — ACTIONS ON THE DARK GROUND

   .btn-secondary as shipped reads --surface-raised, which is white in the light
   theme. A white button on the navy cover is not a design choice, it is the
   alias flipping underneath a fixed ground. The correction removes the fill
   entirely rather than substituting a mixed one, because a composited fill
   invalidates its own measurement.
   -------------------------------------------------------------------------- */
.portal-cover .btn-secondary {
  background: transparent;
  color: var(--text-on-chrome); /* 19.55 */
  border-color: var(--portal-border); /* 4.31 */
}

.portal-cover .btn-secondary:hover {
  background: transparent;
  border-color: var(--portal-accent-text); /* 7.40 */
  color: var(--text-on-chrome);
}

.portal-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  /* 24px. The Start control changes state shared with every other viewer, which
     makes it destructive under ui-direction.md section 9, and a destructive
     control keeps its distance from whatever sits beside it. */
  gap: var(--space-6);
  margin-block: var(--space-6) 0;
}

/* The message beneath the controls - an error sentence from the control plane,
   or the transport-failure line - needs its own air; the actions row above it
   carries no bottom margin. */
.portal-actions + .portal-note {
  margin-block-start: var(--space-4);
}

.portal-actions__note {
  font-size: var(--size-caption);
  line-height: var(--line-caption);
  color: var(--portal-text-quiet); /* 7.39 */
  margin: 0;
  max-width: var(--measure-prose);
}

/* 36px in BOTH densities rather than --control-height, because a destructive
   control keeps the comfortable target size even where the rest of the
   interface shrinks. The portal does not offer the density switch, so this is
   belt and braces against a future one. */
.portal-start {
  block-size: 2.25rem;
}

/* -----------------------------------------------------------------------------
   THE SENTENCE THAT SITS UNDER A STATE
   Every badge is accompanied by a sentence saying what the state means for the
   reader. A bare coloured pill is not a status.
   -------------------------------------------------------------------------- */
.portal-note {
  font-size: var(--size-body-lg);
  line-height: var(--line-body-lg);
  color: var(--text-on-chrome-muted); /* 11.59 */
  max-width: var(--measure-prose);
  margin: 0 0 var(--space-6);
}

/* Panels that group the launch controls: the same fill and edge as a card,
   without the link behaviour. */
.portal-panel {
  background: var(--portal-panel);
  border: var(--line-hairline) solid var(--portal-border);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  margin-block: var(--space-6);
}

/* NO SCRIPT, OR A BROWSER TOO OLD TO REACH THE GATE: show the reason and take
   the controls off the page rather than leave something that cannot act. Both
   the sign-in page and the launch screen depend on script for their one job, so
   both fail loudly in the same words. site/signin.html carried its own copy of
   these two rules inline before this file existed; they live here now, once. */
html.js .no-js-only {
  display: none;
}

html:not(.js) .js-only {
  display: none;
}

/* The visually hidden live region, under the same utility name the application
   uses so the two do not diverge. It is the ONE live region on the launch
   screen: the step list itself is not one, because a live <ol> re-announces on
   every DOM write and a page that polls writes on every poll. */
.a11yHidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* -----------------------------------------------------------------------------
   FOCUS — AND A COMPUTED DEFECT THE PORTAL DOES NOT INHERIT.

   ui-direction.md section 9 specifies a two-tone ring built from --border-focus
   and --focus-halo. Both FLIP with the theme, and on the navy ground the
   specification fails in one theme in each direction:

     --border-focus  light  pur-700 #7e22ce   2.80 on the ground — below 3.0
     --focus-halo    dark   ink-950 #0b1119   1.03 on the ground — invisible

   Neither theme fails outright, because in each case one of the two rings
   survives — but that is luck rather than design, and on the panel fill the
   light ring is worse still at 2.35. The portal therefore pins both rings to
   theme-invariant aliases. This is a portal-scoped rule, not a token change:
   the application's ring is unaffected. portal-direction.md section 6.2 owns
   the arithmetic and carries the [ACTION: A-EMR build] for the gate check that
   would compute it.
   -------------------------------------------------------------------------- */
.portal-cover :where(a, button, input, select, summary, [tabindex]):focus-visible {
  outline: var(--line-rule) solid var(--portal-accent-text); /* 7.40 ground, 6.21 panel */
  outline-offset: var(--line-rule);
  box-shadow: 0 0 0 var(--line-rule) var(--text-on-chrome); /* 19.55 ground, 16.40 panel */
  /* NO border-radius HERE. It used to set --radius-sm on the focused element
     itself, at (0,2,0) against .portal-card's (0,1,0) — so a walkthrough card
     visibly changed shape the moment it took focus, shrinking from --radius-md
     to --radius-sm and back again. A focus indicator that reshapes what it is
     indicating is telling the reader something about the element that is not
     true. Both the outline and the box-shadow ring already follow the
     element's own border-radius, so removing this loses nothing: a square
     control still gets a square ring and a rounded one still gets a rounded
     ring. */
}

/* THE SKIP LINK, SAID EXPLICITLY, AND NOT LEFT TO SOURCE ORDER.

   site.css:97 gives .skip-link:focus-visible a 4px --focus-halo ring, which is
   the two-tone specification the rule above exists to replace on this ground.
   Both selectors compute to (0,2,0) — one class plus one pseudo-class each —
   so which ring the skip link gets is decided purely by the fact that
   portal.css is linked after site.css on all three surfaces. That is true
   today and it is not a design: reordering two <link> tags, or inlining
   site.css, would quietly put the failing ring back with nothing to catch it.
   One more class makes the answer explicit at (0,3,0) and the same rule now
   reaches the sign-in page, which until its <body> gained .portal-cover was
   getting site.css's halo outright rather than by accident. */
.portal-cover .skip-link:focus-visible {
  outline: var(--line-rule) solid var(--portal-accent-text); /* 7.40 */
  outline-offset: var(--line-rule);
  box-shadow: 0 0 0 var(--line-rule) var(--text-on-chrome); /* 19.55 */
}

/* -----------------------------------------------------------------------------
   RESPONSIVE
   One value per token; the COMPONENT switches which token it reads at a
   breakpoint. Nothing here re-declares a custom property — that would be a
   second definition site, which is the thing the token file exists to prevent.
   -------------------------------------------------------------------------- */
@media (max-width: 56rem) {
  .portal-walk {
    grid-template-columns: minmax(0, 1fr);
  }

  .portal-section-head {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
  }
}

@media (max-width: 48rem) {
  /* The attribution is context; the mark is identity, and identity is what
     survives the narrow width. */
  .portal-cobrand__label {
    display: none;
  }

  .portal-honesty__row {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-2);
  }
}

@media (max-width: 40rem) {
  .portal-cover__band {
    padding-block: var(--space-8) var(--space-10);
    padding-inline: var(--space-4);
  }

  .portal-display {
    font-size: var(--size-title);
    line-height: var(--line-title);
  }

  /* Three short lines are a composition at full width and a stutter at this
     one, so the heading is allowed to wrap. */
  .portal-display br {
    display: none;
  }

  .portal-quote {
    font-size: var(--size-body-lg);
    line-height: var(--line-body-lg);
  }

  .portal-honesty {
    padding: var(--space-5);
  }

  .portal-glow--brand {
    display: none;
  }

  .portal-glow--accent {
    width: 18rem;
    height: 18rem;
    top: -8rem;
    right: -6rem;
  }

  /* The timestamp moves onto its own row rather than squeezing the step name. */
  .portal-steps__at {
    grid-column: 2;
    grid-row: 3;
    justify-self: start;
  }

  .portal-countdown {
    align-items: flex-start;
  }
}

/* -----------------------------------------------------------------------------
   FORCED COLOURS
   The OS takes the palette; the structure has to survive it. Everything
   structural on this surface is a border and survives on its own — what needs
   saying here is the handful of things that paint with a background.
   -------------------------------------------------------------------------- */
@media (forced-colors: active) {
  /* `color: inherit` and NOT `forced-color-adjust: none`. Opting the span out
     of forced colours would make it inherit --text-on-chrome, which is white,
     on a white Canvas. */
  .brand-it {
    background: none;
    color: inherit;
  }

  .portal-glow,
  .portal-rule--accent {
    display: none;
  }

  .portal-cover :where(a, button, input, select, summary, [tabindex]):focus-visible {
    outline-color: Highlight;
  }

  /* The skip link's own rule is (0,3,0) and would otherwise outrank the line
     above and keep --portal-accent-text, which forced colours rebinds to
     CanvasText - the ring would then be the same colour as the text it sits
     beside. Restated at matching specificity. */
  .portal-cover .skip-link:focus-visible {
    outline-color: Highlight;
  }
}

/* The mark is white artwork and forced colours does not touch an image. On a
   light high-contrast theme that is white on white and simply gone. Windows
   reports which high-contrast theme is active through prefers-color-scheme, so
   a light one gets the same inversion the printer gets. The two features have
   to be ANDed, which is why this is its own at-rule. */
@media (forced-colors: active) and (prefers-color-scheme: light) {
  .brand-mark {
    filter: invert(1);
  }
}

/* -----------------------------------------------------------------------------
   PRINT
   The token layer already turns --surface-chrome white and --text-on-chrome
   near-black under both :root and :root[data-theme="dark"], and every
   --portal-* alias is rebound there too. Five things it cannot do from there.
   -------------------------------------------------------------------------- */
@media print {
  .portal-glow,
  .portal-rule--accent {
    display: none;
  }

  .portal-cover {
    min-height: 0;
  }

  /* White artwork on white paper is nothing at all. The file has an alpha
     channel, so the inversion touches the mark and not a box around it. */
  .brand-mark {
    filter: invert(1);
  }

  /* Gradient-filled text prints as nothing, twice over: printers drop
     backgrounds by default, and color:transparent has no fallback. */
  .brand-it {
    background: none;
    color: inherit;
  }

  /* THE PRIMARY ACTION PRINTS AS AN OUTLINE, NOT AS A FILL. Browsers drop
     background colours from a print by default, and site.css fills this button
     with --accent-base and sets its label to white - so on paper it comes out
     as white text on nothing. It becomes an outlined control that still reads
     as the control it is, which is what the sign-in page's print block already
     does with its gradient version. */
  .portal-cover .btn-primary,
  .portal-cover .btn-primary:hover,
  .portal-cover .btn-primary:active {
    background: none;
    color: var(--text-primary);
    border: var(--line-hairline) solid var(--border-control);
  }

  .portal-cover .btn-primary .icon {
    color: var(--text-primary);
  }

  /* Two-up on paper, where three columns of card would each be too narrow to
     read. */
  .portal-walk {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .portal-walk__num {
    color: var(--text-primary);
    border-color: var(--text-primary);
  }

  /* A print stylesheet that hides the disclosure is the defect this line exists
     to make impossible to introduce quietly. */
  .demo-marker {
    display: block;
  }
}

/* ---------------------------------------------------------------------------
   THE LAUNCH BANNER (GT section 52).

   What a start leads with. A provisioning card in the shape a platform console
   uses - one ring, one heading, one supporting line, one live status - built out
   of the portal's own tone tokens rather than a new palette, so it reads as part
   of this surface and not as a widget dropped onto it.

   THE RING IS THE ONLY ANIMATION IN THIS DEMONSTRATION AND IT IS EARNED.
   portal-direction.md section 7.2 X4 permits it on one condition: it turns while,
   and only while, the page is polling. The controller adds .is-watching when it
   starts and removes it when it stops, and the rotation lives ENTIRELY on that
   class - so a stopped watch cannot leave a spinning ring behind, which would be
   the page asserting it is still asking when it is not.

   X3 still binds it. It is a ring, not a bar, not a shimmer and not a sweep:
   nothing here grows, fills or travels, because those read as measured progress
   and nothing about this is measured.
   ------------------------------------------------------------------------- */

.launch-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-5);
  margin-block: var(--space-6);
  padding: var(--space-6);
  background: var(--portal-panel);
  border: 1px solid var(--portal-border);
  border-radius: var(--radius-lg);
}

/* The ring. Three sides transparent and one coloured is what makes an arc read
   as an arc at 2px without a second element or an SVG. */
.launch-banner__ring {
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
  margin-block-start: var(--space-05);
  border: 2px solid var(--portal-rule);
  border-top-color: var(--portal-tone-working);
  border-radius: var(--radius-pill);
}

.launch-banner.is-watching .launch-banner__ring {
  animation: launch-ring-spin 1.1s linear infinite;
}

@keyframes launch-ring-spin {
  to { transform: rotate(360deg); }
}

/* REDUCED MOTION TAKES THE ROTATION, NOT THE INDICATOR. Removing the ring
   entirely would leave a viewer who has asked for less motion with no signal
   that the page is still watching, which is the one thing the ring is for. The
   arc holds still and the status line below carries the change instead - and
   that line was always the honest half. */
@media (prefers-reduced-motion: reduce) {
  .launch-banner.is-watching .launch-banner__ring {
    animation: none;
  }
}

.launch-banner__body {
  min-width: 0;
}

.launch-banner__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--size-heading-sm);
  line-height: var(--line-heading-sm);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
}

.launch-banner__lede {
  margin: var(--space-2) 0 0;
  max-width: var(--measure-prose);
  font-size: var(--size-body);
  line-height: var(--line-body);
  color: var(--text-secondary);
}

.launch-banner__now {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
  /* Monospace, because this line changes under the reader's eye and a
     proportional face makes every change reflow the words beside it. */
  font-family: var(--font-mono);
  font-size: var(--size-caption);
  line-height: var(--line-caption);
  color: var(--text-primary);
}

.launch-banner__now-label {
  font-family: var(--font-sans);
  font-size: var(--size-micro);
  line-height: var(--line-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-micro);
  text-transform: uppercase;
  color: var(--portal-text-quiet);
}

/* ---------------------------------------------------------------------------
   THE OBSERVATIONS, AS A DISCLOSURE.

   Closed while a start is going well and opened by the controller when one does
   not finish. The summary is a control, so it carries the same focus treatment
   as every other control on this surface rather than the browser default.
   ------------------------------------------------------------------------- */

.portal-steps__summary {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding-block: var(--space-3);
  cursor: pointer;
  color: var(--text-secondary);
  border-radius: var(--radius-sm);
  /* The default triangle is suppressed by display:flex in most engines and by
     this rule in the rest. The chevron below replaces it, because a summary
     with NO marker reads as a heading and nobody clicks a heading - which on
     this surface would hide the failure state behind an affordance that does
     not exist. */
  list-style: none;
}

.portal-steps__summary::-webkit-details-marker {
  display: none;
}

/* A chevron drawn from two borders rather than an SVG or a glyph: it inherits
   currentColor, so it follows the summary's hover and both themes for free. */
.portal-steps__summary::before {
  content: '';
  flex: none;
  align-self: center;
  width: 0.45rem;
  height: 0.45rem;
  margin-inline-end: calc(var(--space-1) * -1);
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transform-origin: center;
  transition: transform var(--dur-fast) var(--ease-standard);
}

[open] > .portal-steps__summary::before {
  transform: rotate(45deg);
}

/* The chevron is the one place on this surface where a transition would fight
   a stated preference for less motion, and it carries no information the
   rotation is needed for - the open state is already obvious from the rows
   appearing underneath. */
@media (prefers-reduced-motion: reduce) {
  .portal-steps__summary::before {
    transition: none;
  }
}

.portal-steps__summary:hover {
  color: var(--text-primary);
}

.portal-steps__summary:focus-visible {
  outline: none;
  box-shadow: var(--focus-halo);
}

.portal-steps__summary-title {
  font-family: var(--font-display);
  font-size: var(--size-body);
  line-height: var(--line-body);
  font-weight: var(--weight-medium);
}

/* PAPER HAS NO DISCLOSURE. A printed page cannot be opened, so the observations
   print whether or not somebody expanded them - the same argument the demo
   marker's print rule above makes. */
@media print {
  .launch-banner__ring {
    display: none;
  }

  #launch-steps-wrap {
    display: block !important;
  }

  #launch-steps-wrap > *:not(summary) {
    display: revert !important;
  }
}
