/* Element defaults and the shared shell (05-design-system.md §4).
   Every value spends a token; there is no literal here except 0, 100%, the
   one breakpoint, which a media query cannot read out of a custom property,
   the @font-face blocks below, whose descriptors are not properties and
   resolve no `var()` at all — a family name, a weight, a file path and a
   unicode-range have to be written out — and the three underline values on
   `a`, which are optical: they are measured against the glyph they sit under,
   so they are `em` and not a step of any ramp, and a token for them would be
   a token with one consumer (§15.9).

   The theme toggle sits inside the nav panel (§5) and the status strip below
   is shell rather than a control, so both are here and not in the vocabulary;
   r2-5 is what wires the strip to a request. */

/* The body face (§3's stack becomes the fallback; plans/deviations.md).
   Here and not in tokens.css, which §3 keeps to custom properties: an
   @font-face declares an element default, which is what this sheet is for.

   Two subsets and no more. Ukrainian's `і ї є ґ` are U+0454/0456/0457/0491,
   all inside Google's `cyrillic` range, and French's accents are all inside
   `latin`; `latin-ext` and `cyrillic-ext` would ship bytes nothing renders.
   `font-display: swap` because the fallback stack is a real stack: text is
   readable from the first paint, and the face swaps in when it lands.

   The family name is bare here and quoted in tokens.css: `font-family` is
   a real declaration, where stylelint reads quotes around a one-word name
   as unrecommended, while a custom property's value is where an unquoted
   name reads as a keyword. Same family either way. */
@font-face {
  font-family: Inter;
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/inter-latin-400.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;
}

@font-face {
  font-family: Inter;
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/inter-latin-600.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;
}

@font-face {
  font-family: Inter;
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/inter-cyrillic-400.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: Inter;
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/inter-cyrillic-600.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

/* The heading face. A serif IS the paper metaphor — and a serif UI at 16px is
   not modern, which is why this one stops at h1/h2/h3 and the brand while the
   body, the controls and the tables stay Inter. Same two subsets, same
   reasoning, same licence (OFL.txt beside them carries both notices). */
@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/source-serif-4-latin-400.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;
}

@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/source-serif-4-latin-600.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;
}

@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/source-serif-4-cyrillic-400.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/source-serif-4-cyrillic-600.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* The running text is --color-text and not --color-fg since r12-2: full ink
   on every glyph of a long page is what made it read flat, because a heading
   and the paragraph under it were the same colour and only size told them
   apart. Everything that inherits from `body` takes the softer step; the three
   things that mean "look here" — a heading, a `<strong>`, and every control in
   components.css, which sets `color` itself — re-assert the full ink. */
/* A column, so the shell ends where the viewport does and not before or after
   it (r12-4). `<body>`'s children are the skip link, the bar, `<main>` and —
   on the marketing shell alone — the footer; `.container` below takes the
   slack, so a page with little in it puts its footer ON the fold instead of
   halfway up the screen, and a long one still scrolls the ordinary way. On a
   phone "the fold" is approximate: `100vh` is the LARGE viewport, so the
   footer sits under the browser's own chrome until it retracts. `dvh` would
   be exact and is not in `.stylelintrc.js`'s `unit-allowed-list`; `100vh` is
   also what `.admin-shell` already spends, so this is the one unit the
   product measures a screen in.

   This is also what lets `.narrow-page` centre without measuring anything:
   it fills `<main>`, and `<main>` is whatever is left. The arithmetic that
   would otherwise be needed — viewport less the bar less the padding less the
   footer — cannot be written in CSS at all, because a footer's height is its
   content's.

   `#root` (the SPA) and `.admin-shell` are the other two children this rule
   ever sees, and neither takes the slack — both are flex ITEMS at the default
   `flex: 0 1 auto`, so they never grow. `#root` sizes to its content;
   `.admin-shell` declares its own `min-height: 100vh` and its own column, and
   `.admin-main` takes the slack from THAT rather than from here. The panel's
   rule is load-bearing, not a duplicate of this one: delete it and
   `.admin-shell` collapses from 800px to 197px. */
body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: var(--weight-normal);
  line-height: var(--leading-body);
}

/* The heading scale, all three levels in one rule: the serif is the channel
   that says "heading" and the size is what says which one. h3 gets a real
   rule here rather than being left to the UA sheet, which is what
   marketing.css was working around. */
h1,
h2,
h3 {
  margin: 0;
  color: var(--color-fg);
  font-family: var(--font-serif);
  line-height: var(--leading-tight);
  font-weight: var(--weight-medium);
}

h1 { font-size: var(--text-xl); }
h2 { font-size: var(--text-lg); }
h3 { font-size: var(--text-base); }

p {
  margin: 0;
  max-width: var(--measure);
}

/* The one mark inside running text, so it is the one thing in a paragraph
   that goes back to full ink. Weight alone said "emphasis" against a body that
   was already full ink; against --color-text it says it twice, which is what
   gives a paragraph somewhere for the eye to stop. */
strong {
  color: var(--color-fg);
}

/* The chrome is ink since r12-1, so a link is body text with a rule under it
   and has no second colour to move to. What moves is the rule: thin at rest,
   twice that under the pointer. In `em` so it tracks the type size — and so
   no `stylelint-disable` for the px unit list enters this sheet. */
a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 0.0625em;
  text-underline-offset: 0.2em;
}

/* `:not(.btn)` survives the chrome becoming ink: an anchor wearing .btn
   carries an ink ground, and thickening an underline inside a filled button
   is noise. Every variant is covered at once, which is why the exclusion is
   here rather than restated per modifier in components.css. */
a:not(.btn):hover { text-decoration-thickness: 0.125em; }

/* Focus is always visible, in one rule, so no control can opt out.
   `outline: none` appears nowhere in this codebase. */
:focus-visible {
  outline: var(--focus-ring) solid var(--color-fg);
  outline-offset: var(--focus-ring);
}

/* A three-column grid — gutter, measure, gutter — and children default to the
   middle one. It reads exactly as `max-width: var(--measure); margin: 0 auto`
   did, and buys one thing that recipe cannot: a child may say
   `grid-column: 1 / -1` and claim the full width, which r12-1's price band
   does. A child's `max-width` can never exceed its parent's, so the band had
   no other way out.

   THE CALC IS LOAD-BEARING. `box-sizing` is border-box, so the old column's
   CONTENT was --measure minus the inline padding. `min(var(--measure), 100%)`
   would widen every page by 2 x --space-4 and shift it half that to the left,
   while every `p` — capped separately, on the element — stayed put. */
.container {
  /* Grow into `<body>`'s spare height, never shrink below the content: the
     `0 auto` half is what keeps a long document its own height. Inert inside
     the SPA, whose `.container` is a child of `#root` rather than of a flex
     `<body>`. */
  flex: 1 0 auto;

  /* …but do not hand that height to the ROWS. A stretched grid resolves
     `align-content: normal` to `stretch`, which grows every auto row equally
     — so on `/`, whose two children are an `<h1>` and `.languages`, the
     language cards grew with the window (411px at an 800px viewport, 511px at
     1000px) and the title detached from them by 64px. Found at review, after
     `/` had already been approved. `start` is what a document grid wants:
     rows are their content's height and the slack collects at the bottom.

     The one page that DOES want the slack asks for it by name below. The
     combinator sits inside `:has()` because that is one compound to
     stylelint, and `.container > .narrow-page` plus the `:has` would be
     three. */
  align-content: start;
  display: grid;
  grid-template-columns:
    1fr min(calc(var(--measure) - 2 * var(--space-4)), 100%) 1fr;
  padding: var(--space-8) var(--space-4);
}

/* A page that is ONE narrow thing (r12-4): /login, /login/verify and 404 — a
   heading, a line of prose, and a single form or link. Everything else in the
   product is a document you read down; these are a destination you act on
   once, and left-aligned at the full measure they read as a document that
   ran out. Centred both ways, so the one control is where the eye already is.

   base.css and not marketing.css: `/login` and `/login/verify` extend
   base.html and never load that sheet, and 404 extends the marketing shell —
   so this is the only place all three can see it. It is shell by §4's rule
   anyway: it arranges a page, it renders no control.

   It measures NOTHING. `<body>` is a column and `.container` takes its slack,
   so this fills whatever `<main>` turned out to be — viewport less the bar,
   less the padding, less the footer where there is one — and centring in it
   is just `justify-content`. A first draft subtracted those from `100vh` by
   hand and pushed the marketing footer a screen below the fold, because a
   footer's height is its content's and no `calc` can know it.

   `text-align: center` is the 404's, whose whole content is a heading and one
   link; `.narrow-page__card` below turns it off again, because a form's
   labels read down a left edge. */
.narrow-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

/* The card inside it. `.card` (components.css) draws the outline and stacks
   the contents; what is here is this page's own packing and the width — 24rem,
   narrower than the reading measure on purpose, because the widest thing in it
   is one input and a 648px box round an email field is the flatness this rule
   came to fix. `min(…, 100%)` is what makes it the track on a phone.

   Its own CLASS and not `.narrow-page > .card`: `.card`'s contract is that
   every consumer sets its own gap and padding beside it — `class="card
   plan-card"` on the landing, `card language-card` on the door — and a
   descendant rule re-specifying the properties the component declined would
   be a third mechanism for one thing. It also DOM-couples: any wrapper
   inserted between the two would silently drop the width and the padding. */
.narrow-page__card {
  width: min(24rem, 100%);
  text-align: start;
  gap: var(--space-4);
  padding: var(--space-6);
}

/* The exception, and the whole reason `.narrow-page` can measure nothing: its
   row takes the grid's slack, so "fill what is left of the page" is true. */
.container:has(> .narrow-page) {
  align-content: stretch;
}

.container > * {
  grid-column: 2;
}

/* The one way out of the middle track (r12-5). A child that wears this spans
   all three columns and centres itself at the band width — the escape hatch
   the rule directly above otherwise closes, which is why it is declared here
   and after it: the two selectors tie at 0-1-0 and source order is what
   decides.

   It is SHELL and not the band's own class, for the reason §4 gives: it
   arranges a page and renders no control. It moved out of `.pricing` because
   two surfaces render that band now, and the escape works ONLY on a direct
   child of `.container` — geometry that silently depends on nesting is a trap,
   and `class="pricing bleed"` puts the requirement in the call site's class
   list where a reader meets it.

   The width is the card arithmetic written out — three 16rem columns and two
   `--space-8` gaps. `.pricing__cards` does NOT read it from here: it sets
   `repeat(var(--plan-columns), 1fr)` and derives its own 55rem query from the
   same 848px. So `16rem` is written in two sheets and this is the one place
   the band's arithmetic can drift — components.css's `.pricing__cards` comment
   says the same thing from the other side. `min(…, 100%)` is what makes it the
   viewport minus `.container`'s padding on anything narrower. */
.bleed {
  grid-column: 1 / -1;
  justify-self: center;
  width: min(calc(3 * 16rem + 2 * var(--space-8)), 100%);
}

/* The one layout primitive round 0 needs: vertical rhythm between siblings. */
.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* First focusable element on every surface; visible only while focused. */
.skip-link {
  position: absolute;
  left: var(--space-2);
  top: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--color-bg);
  border: var(--border-width) solid var(--color-line-strong);
  border-radius: var(--radius);
  transform: translateY(-200%);
}

.skip-link:focus-visible {
  transform: none;
}

/* `min-height` and `flex-wrap`, not a fixed `height`, for the fallback below:
   where popover is unsupported the nav stays in normal flow and takes its own
   row, and a fixed height would clip it. Where popover IS supported the panel
   is out of flow and the bar is exactly --topbar-height, which is the value
   the panel's own `inset` is measured from. */
.topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--topbar-height);
  padding: 0 var(--space-4);
  border-bottom: var(--border-width) solid var(--color-line);
}

.topbar__brand {
  font-family: var(--font-serif);
  font-weight: var(--weight-medium);
  text-decoration: none;
}

/* §5: sticky inside /app, static on marketing and legal pages. One modifier
   rather than a surface-scoped selector — the two renderers emit the same
   classes, and this is the one difference between them the parity test allows
   on the header itself. */
.topbar--sticky {
  position: sticky;
  top: 0;
  background: var(--color-bg);   /* it overlaps content; §5 asks for a hairline, not a see-through bar */
}

/* The navigation (§5), mobile-first.

   EVERY declaration that shows the hamburger or turns the nav into a panel is
   inside a `@supports (selector(:popover-open))` block — there are two, the
   one directly below and one nested in the `min-width` query. A browser
   without popover support ignores that block and is left with these rules: no
   hamburger at all, and the links stacked in normal flow on their own row
   under the bar, reachable by tab order alone. There is no state in which a
   control is present but inert.

   NOTHING CHECKS THIS. Popover support is decided by the browser's CSS
   parser, so no test can simulate its absence — a postcss walk asserting the
   structure was written and deleted as heavier than what it protected
   (plans/deviations.md). Adding a declaration below: if it only makes sense
   when `popover` works, it goes in the block.

   The block is load-bearing, not tidiness. `.nav`'s `display: none` is
   authored here because `display: flex` is what stacks the links in the
   fallback, and an author `display` beats the UA sheet's
   `[popover]:not(:popover-open)` rule. Unconditional, it would leave an
   unsupporting browser an invisible nav behind a dead hamburger. */
.nav {
  display: flex;
  flex-basis: 100%;   /* its own row under the bar, which .topbar wraps for */
  flex-direction: column;
  gap: var(--space-6);
}

.nav__link {
  text-decoration: none;
}

/* The theme toggle (§5): a bare glyph carrying its label as `aria-label`
   (plans/deviations.md). It is panel content, so it is here rather than in
   the popover block — a browser without popover support still shows it, in
   the row the links stack into.

   Not folded into the glyph pair below, which is otherwise the same six
   declarations: those two are `display: none` until popover support is
   confirmed and this one is shown always, so a shared rule would need the
   difference back as an override (§15.13).

   `text-align` because a `<button>` centres its own label and this one is a
   full-width flex item inside the stacked panel, where every link beside it
   starts at the same edge. `align-self` would say the same thing on mobile
   and undo the bar's vertical centring on desktop. */
.theme-toggle {
  /* The box is gone, the target is not: with `padding: 0` and no border, the
     button is the advance width of one glyph, which is under §6/WCAG 2.5.8's
     24x24 CSS px floor. `--control-height` is §9's control box, already
     declared, so the floor is the same 36px the button used to BE. */
  min-width: var(--control-height);
  min-height: var(--control-height);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}

/* The bar's two other bare glyph buttons. Neither wears .btn: §9's control is
   a 36px box with a border and a ground, and these are chrome. */
.hamburger,
.nav__close {
  display: none;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

@supports (selector(:popover-open)) {
  /* The UA sheet's `[popover]` defaults are `position: fixed; inset: 0;
     width: fit-content; height: fit-content; margin: auto; border: solid;
     padding: .25em; color: CanvasText; background-color: Canvas` — a centred,
     shrink-wrapped box painted in system colours. §5 asks for a panel instead:
     full width, under the bar, links stacked, on the palette's own surface.
     Every one of those defaults is answered here, `width` and `color`
     included: left and right at 0 do nothing while width is `fit-content`,
     and CanvasText is not --color-fg.

     TODO: `inset`'s top is --topbar-height, which assumes the bar is at the
     viewport top. Only `.topbar--sticky` guarantees that, and today only /app
     wears it — on a scrolled marketing page the panel would detach from the
     bar. No misbehaviour yet: both Jinja pages are short enough not to
     scroll. r3-1 introduces the long marketing pages and owns the fix, which
     is either sticky everywhere or `inset: 0` with the bar inside the
     panel. */
  .nav {
    display: none;
    position: fixed;
    inset: var(--topbar-height) 0 auto 0;
    width: auto;
    max-height: calc(100vh - var(--topbar-height));
    overflow-y: auto;
    margin: 0;
    color: inherit;
    padding: var(--space-6);
    border: 0;
    border-bottom: var(--border-width) solid var(--color-line);
    background: var(--color-surface);
  }

  .nav:popover-open {
    display: flex;
  }

  .hamburger {
    display: inline-flex;
  }

  .nav__close {
    display: inline-flex;
    align-self: flex-end;
  }
}

/* Additive, and the only breakpoint (§5). A media query cannot resolve a
   custom property, which is why tokens.css declares none for it. */
@media (min-width: 700px) { /* stylelint-disable-line unit-allowed-list -- the breakpoint, not a spacing value */
  .nav:not(.nav--static) {
    flex-basis: auto;
    flex-direction: row;
    align-items: center;
    gap: var(--space-4);
  }

  @supports (selector(:popover-open)) {
    .hamburger,
    .nav:not(.nav--static) .nav__close {
      display: none;
    }

    /* Back into the bar's own row: the panel geometry above is undone rather
       than re-stated, so the two are one description of one element. */
    .nav:not(.nav--static) {
      display: flex;
      position: static;
      max-height: none;
      padding: 0;
      border: 0;
      background: none;
    }
  }
}

/* /_styleguide alone (§14), which has to show the panel OPEN. Nothing
   declarative starts a popover open — `popover="manual"` starts closed too —
   so that page renders a static copy of the markup rather than a second
   `{% include %}`, and this is what puts the copy back in flow.

   Two declarations, not ten, because the DESKTOP UNDO EXCLUDES IT rather than
   being cancelled: `.nav:not(.nav--static)` above. Restating the panel's
   geometry here instead means re-stating everything the `@media` block and its
   nested `@supports` strip — direction, alignment, gap, padding, border,
   ground, max-height — and getting one of them wrong shows a bare row on the
   one page that exists to show the panel. What is left is the pair the
   top-level `@supports` sets and no `:popover-open` will ever clear, since a
   static copy carries no `popover` attribute. Declared after it at equal
   specificity, which is what lets these two win. */
.nav--static {
  display: flex;
  position: static;
}

/* The status strip (05-design-system.md §7): full width under the top bar,
   one hairline top and bottom, never covering content, never auto-dismissing.
   It is shell, not a control, which is why it is here and not in the
   vocabulary. r2-5 wires it to a request; this slice ships it and shows it. */
.strip {
  padding: var(--space-2) var(--space-4);
  border-top: var(--border-width) solid var(--color-line);
  border-bottom: var(--border-width) solid var(--color-line);
}

.strip--loading {
  background: var(--color-warning-weak);
}

/* Two rules on one ground, deliberately NOT merged (§15.13): "the request
   failed" and "there is no network" are unrelated conditions that r2-5 has to
   order against each other — error > offline > loading — and a shared
   selector would make that priority unstateable. */
.strip--error {
  background: var(--color-danger-weak);
}

.strip--offline {
  background: var(--color-danger-weak);
}
