/* One stylesheet for the whole apex site. No fonts, no third party anything --
   a legal page that depends on a CDN is a legal page that can be unavailable
   at the moment somebody needs to read it.

   There is now exactly one script, /theme.js, served from this origin. It is
   the theme toggle and nothing else. See the block below.

   TOKENS ARE COPIED FROM THE DASHBOARD, ON PURPOSE
   ------------------------------------------------
   The values below are the same ones as
   `src/dashboard/static/style.css` (issue #123). The two files cannot share a
   stylesheet -- different origins, different deploy pipelines, and the whole
   argument for this site being static is that it does not depend on the box
   the dashboard runs on. So the values are duplicated, and this comment is
   half of the pair that says so. The other half is in the dashboard's file.
   Undocumented duplication is how two surfaces drift into looking like two
   products.

   Change a colour in one, change it in the other.

   THERE IS NOW A THIRD COPY: status/public/style.css, the status page (issue
   #170). Same argument again, one step further -- that Worker exists to keep
   serving when this one is not, so it cannot fetch a stylesheet from here.
   Change a colour in one, change it in all three. tests/test_status_page.py
   compares its tokens against this file and fails on drift.

   WHAT WAS NOT COPIED, and where to find it if a later phase needs it: the
   toggle-switch tokens, --control-h, --logo-filter, the --notice-* set and
   --ok / --danger. Those are dashboard *component* tokens with nothing on
   this site to paint. Copy one across when a rule here actually needs it,
   rather than importing a vocabulary this file does not speak.

   --control-line came across that way and is the one token whose light value
   deliberately differs from the dashboard's. The reason is measured and
   written next to it in the light block. Everything else on both sides is the
   same hex.

   It now draws four things, not one: the theme picker, the flow diagram's
   number rings, the "Last updated" pill and the changelog's tags. Its light
   value was tuned against --chrome, the surface the picker sits on; the other
   three sit on --panel or --bg, where it measures wider still. Check the
   surface before reaching for it on a fifth.

   ONE NAME CHANGED MEANING, so read this before copying a rule between files.
   This stylesheet used to define `--accent-text` as the text drawn ON the
   accent -- white on blurple, inside a button. In the dashboard's vocabulary
   that is `--accent-ink`, and `--accent-text` is the opposite thing: blurple
   as a FOREGROUND, for a link on a page. They are now used the dashboard's
   way here too. A rule moved between the files without noticing would paint a
   button's label in blurple on blurple.

   THEMING: DARK BY DEFAULT, AND WHY THE BLOCKS ARE ORDERED THIS WAY
   -----------------------------------------------------------------
   Three states, same as the dashboard: explicit Dark, explicit Light, and
   "follow the OS". The mechanism has to differ because there is no server
   here to render the right one into the first paint.

   The dashboard can treat *no attribute* as "System", because its server
   always knows what to stamp. This site cannot: no attribute is also what a
   reader sees before /theme.js runs, and what they see forever with
   JavaScript off. So here the absence of an attribute means DARK -- the
   default -- and "System" is an explicit `data-theme="system"`.

   That inverts the dashboard's block order. Read these four in order:

     1. Dark values on bare `:root`, which every document matches, plus the
        explicit `[data-theme="dark"]`. This is the floor, so a first visit
        with no stored choice and no JavaScript is dark with no flash and no
        correction after paint.
     2. The light values, named once as `--light-*`. Nothing paints from
        these; they exist so the two light selectors can share them.
     3. An explicit `[data-theme="light"]`.
     4. `[data-theme="system"]` plus a light OS.

   Dark needs no equivalent of block 4: "System" on a dark OS is already the
   floor, so there is nothing to override. That asymmetry is the reward for
   putting the default at the bottom of the cascade. */

/* 1. Dark: the floor, and what an explicit Dark choice pins. */
:root,
:root[data-theme="dark"] {
  /* Per-theme rather than `light dark`, so form controls and scrollbars
     follow the theme the page actually chose rather than the one the OS
     prefers. */
  color-scheme: dark;

  --bg: #1e1f22;          /* page ground */
  --chrome: #2b2d31;      /* header and footer bars */
  --panel: #313338;       /* cards and call-out notes */

  --ink: #dbdee1;         /* body text */
  --ink-strong: #f2f3f5;  /* headings, anything you scan for */
  --muted: #b5bac1;       /* ledes, descriptions, footer text */
  --faint: #949ba4;       /* the least important thing that is still text */

  --line: #3f4147;
  --line-soft: #35373c;

  /* Blurple means exactly two things, here as in the dashboard: "this is the
     button that does the thing", and a link. Never a border for decoration,
     never a heading.

     Two tokens because one value cannot do both jobs on a dark surface. As a
     button background --accent must stay dark enough for white --accent-ink
     to read on it (4.6:1). As a foreground it must be light enough to read
     against the panel, which #5865f2 is not (2.7:1) -- hence --accent-text. */
  --accent: #5865f2;
  --accent-hover: #4752c4;
  --accent-ink: #ffffff;
  --accent-text: #8a93f6;

  /* EDGES THAT HAVE TO BE SEEN, which --line cannot carry.
     --line is a separator between things that are already distinguishable. An
     edge that IS the element -- a control's boundary, a pill's whole shape --
     needs 3:1 against what surrounds it (WCAG 1.4.11 for the control; simple
     visibility for the pills). --line manages 1.35:1 against the header bar
     and 1.62:1 on the ground, so the select had no visible edge and the pills
     did not render as pills until this was measured.

     This is the dashboard's --control-line, at the dark value unchanged
     (3.68:1 on --chrome, 3.38:1 on --panel). The light value below is the one
     that diverges -- see the light block. */
  --control-line: #80848e;

  /* COPIED ACROSS FOR THE THEME MENU (#195 phase 7), following the rule this
     file's header states: take a dashboard component token when a rule here
     actually needs it, rather than importing a vocabulary this site does not
     speak. These five are what a popover needs and nothing else here uses. */
  --hover: #35373c;
  --radius-control: 4px;
  --radius-card: 8px;
  --bar-control: 2rem;
  --shadow-overlay: 0 4px 12px rgb(0 0 0 / 0.40), 0 12px 32px rgb(0 0 0 / 0.36);
}

/* 2. The light values, named once and painted by nothing.
      Kept on `:root` so both light selectors can reference them, rather than
      the same hexes typed twice with no mechanism to keep them equal. */
:root {
  --light-bg: #e3e5e8;
  --light-chrome: #f2f3f5;
  --light-panel: #ffffff;

  --light-ink: #313338;
  --light-ink-strong: #060607;
  --light-muted: #5c5e66;
  --light-faint: #63666f;

  --light-line: #dfe0e3;
  --light-line-soft: #ebedef;

  --light-accent: #5865f2;
  --light-accent-hover: #4752c4;
  --light-accent-ink: #ffffff;
  /* Darker than --accent rather than lighter: a foreground has to move away
     from its background, and on light the background is the pale one. */
  --light-accent-text: #4150f0;

  /* THE ONE VALUE THAT DELIBERATELY DIFFERS FROM THE DASHBOARD.
     The dashboard's light --control-line is #8d919b, tuned against the white
     cards its inputs sit on, where it gives 3.16:1. This site's only control
     sits on the header bar -- --chrome, #f2f3f5, which is darker than white --
     and against that the same grey gives 2.84:1, under the 3:1 a UI component
     needs. The dashboard was never asked for that pairing, so its value is not
     wrong; it is measured against a surface this site does not use here.

     Darkening the token is the same fix the dashboard applied to --ok when the
     plan cards put it on a surface it had not been measured against: move the
     token, not the element. Pinned in test_site.py.

     DARKENED AGAIN by #227, for the third time, and the same reason each time:
     a control turned out to sit on a surface the token had never been measured
     against. #848892 cleared the bar (3.20:1) and the fill (3.55:1), but the
     `Last updated` pill on the legal pages is a direct child of <main> and so
     sits on --bg, which is darker than --chrome. There it gave 2.81:1, under
     the 3:1 WCAG 1.4.11 asks of a component boundary -- and that pill IS its
     boundary, since its whole shape is the border. #7c7f89 clears all three:
     --bg 3.17:1, --chrome 3.60:1, --panel 4.00:1.

     The test now pins all three surfaces rather than the two that happened to
     have controls on them, which is the actual lesson of it having moved three
     times. The next control to land somewhere new should fail loudly instead
     of rediscovering this. */
  --light-control-line: #7c7f89;

  /* The light halves of the theme-menu tokens. --radius-* and --bar-control do
     not change with the theme, so they are declared once above. */
  --light-hover: #e3e5e8;
  --light-shadow-overlay: 0 4px 12px rgb(0 0 0 / 0.12), 0 12px 32px rgb(0 0 0 / 0.10);
}

/* 3. Light, chosen explicitly. Unaffected by what the OS prefers, which is
      the point of choosing. */
:root[data-theme="light"] {
  color-scheme: light;

  --bg: var(--light-bg);
  --chrome: var(--light-chrome);
  --panel: var(--light-panel);

  --ink: var(--light-ink);
  --ink-strong: var(--light-ink-strong);
  --muted: var(--light-muted);
  --faint: var(--light-faint);

  --line: var(--light-line);
  --line-soft: var(--light-line-soft);

  --accent: var(--light-accent);
  --accent-hover: var(--light-accent-hover);
  --accent-ink: var(--light-accent-ink);
  --accent-text: var(--light-accent-text);
  --control-line: var(--light-control-line);
  --hover: var(--light-hover);
  --shadow-overlay: var(--light-shadow-overlay);
}

/* 4. Light, because the OS asked and the reader chose to follow it.
      The whole of "System" mode -- the dark half needs no block, because dark
      is the floor. */
@media (prefers-color-scheme: light) {
  :root[data-theme="system"] {
    color-scheme: light;

    --bg: var(--light-bg);
    --chrome: var(--light-chrome);
    --panel: var(--light-panel);

    --ink: var(--light-ink);
    --ink-strong: var(--light-ink-strong);
    --muted: var(--light-muted);
    --faint: var(--light-faint);

    --line: var(--light-line);
    --line-soft: var(--light-line-soft);

    --accent: var(--light-accent);
    --accent-hover: var(--light-accent-hover);
    --accent-ink: var(--light-accent-ink);
    --accent-text: var(--light-accent-text);
    --control-line: var(--light-control-line);
    --hover: var(--light-hover);
    --shadow-overlay: var(--light-shadow-overlay);
  }
}

/* ONE TYPEFACE ACROSS BOTH SURFACES (#195 phase 2).
   Vendored, same-origin, and the same file the dashboard serves --
   src/dashboard/static/fonts/inter-latin-var.woff2, copied rather than linked
   because the two hosts share no origin and no deploy. Latin subset, variable
   100-900, 47KB.

   THIS AMENDS site/README.md's "no fonts", and the reason that line gave is
   why it could be amended: "a legal page that needs a CDN is a legal page that
   can be unavailable at the moment somebody needs to read it." That argument
   is about a THIRD PARTY, and it still stands -- nothing here reaches one. A
   file served by the same Worker as the page cannot be down while the page is
   up, so the availability risk the rule was written to prevent does not exist
   in this shape.

   `font-display: swap` is what makes it safe rather than merely defensible:
   if the font is slow or never arrives, the page renders in the stack below,
   which is EXACTLY WHAT THIS SITE LOOKED LIKE BEFORE THIS COMMIT. There is no
   state this can produce that is worse than the status quo it replaces.

   The stack was already the dashboard's minus its first entry, so this is one
   name added rather than a typeface swapped. */
@font-face {
  font-family: "Inter var";
  src: url("fonts/inter-latin-var.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

* { box-sizing: border-box; }

/* THE TYPE SCALE, NAMED (#195 phase 1).
   Every value below is what this file already used; nothing here changes how
   a byte of this site renders. What changes is that the ramp -- the heading
   and prose ladder every page walks -- stops being magic numbers and gains a
   role per step.

   THIS NAMES THE RAMP, NOT YET EVERY SIZE IN THE FILE. Component sizes are
   still literals: the nav's 0.94rem, the flow diagram's 0.9375rem, the plan
   card's 1.5rem, and the `.policy` overrides that deliberately run a step
   larger than the shared ramp for long-form reading. Each is adopted as its
   component is restyled in a later phase, which is the same bargain the
   dashboard's token block struck for the same reason -- converting a whole
   stylesheet in one commit is a large diff in which a single mistyped digit
   changes the layout and nothing catches it.

   THE OTHER HALF OF THIS PAIR IS IN src/dashboard/static/style.css, exactly
   as the colour tokens are, and for the same reason -- two origins, two
   deploys, no shared stylesheet possible. Read that file's block before
   changing anything here.

   The two ramps are NOT identical, and that is deliberate rather than drift:

     --text-display   IS identical, and is the only step that is. It is the
                      marketing size. This site's hero already used it, and
                      the dashboard now declares the same clamp so /pricing
                      can reach it -- which is what makes a visitor crossing
                      from the landing page to the price meet one product.

     everything else  differs, because a console is denser than a document on
                      purpose. The dashboard's largest step is 22.4px and
                      belongs on Overview; this site's h1 is 30.4px and
                      belongs on Terms. Forcing one number on both would make
                      the legal pages smaller to no one's benefit.

   So: shared vocabulary, shared display size, different density. A step that
   means the same thing in both files carries the same name. */
:root {
  --text-display: clamp(2rem, 1.3rem + 2.6vw, 3rem); /* 32-48px, the hero */
  --text-title: 1.9rem;    /* 30.4px  h1 on a document page */
  --text-section: 1.2rem;  /* 19.2px  h2 */
  --text-lede: 1.15rem;    /* 18.4px  the sentence under a hero */
  --text-blurb: 1.08rem;   /*  17.3px the sentence under a document title */
  --text-subhead: 1rem;    /*    16px h3, and body */
  --text-note: 0.9375rem;  /*    15px the notes under a plan card */
  --text-micro: 0.9rem;    /*  14.4px the footer, the smallest thing here */

  /* 1.65 HERE, 1.55 IN THE DASHBOARD, and that is the last deliberate
     divergence between the two files -- #195's acceptance criteria asked for
     it to agree or be explained, so: it is the same argument as the ramp
     above. A document is read and a console is operated. Long-form prose at
     16px wants the looser leading, a settings row three lines high does not,
     and matching them would make one of the two worse to no one's benefit.
     --leading-tight is identical in both, because a heading is a heading. */
  --leading-body: 1.65;
  --leading-tight: 1.25;

  /* A MEASURE. This site already held one at the layout level -- `main.wrap`
     caps at 42-60rem depending on the page -- which is why its longest line
     is 78 characters against the dashboard's 120. Named here so the pair of
     files says the same word for the same idea; the dashboard needs it as a
     per-element rule because its cards, not its layout, are what run wide. */
  --measure: 52ch;

  /* Named to match the dashboard's `--font`, and holding the same value. The
     fallbacks are what this site used before #195 phase 2, so a browser that
     refuses the woff2 lands exactly where it started. */
  --font: "Inter var", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  /* Longhand, not the `font` shorthand. The shorthand is legal with var()
     but is parsed at computed-value time, so a single bad substitution drops
     the whole declaration -- including font-family -- rather than one part of
     it. Three lines that fail independently are worth more than one that
     fails together. */
  font-family: var(--font);
  font-size: 16px;
  line-height: var(--leading-body);
  -webkit-text-size-adjust: 100%;
}

.wrap { max-width: 46rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }

/* PROSE TAKES THE MEASURE, and this site needed it as much as the dashboard
   did -- which was not what #195 originally claimed.

   That issue reported this site holding 78 characters against the dashboard's
   120, and both numbers were wrong: the first metric divided text length by
   rendered line count, which the bell's `-webkit-line-clamp` and any
   author-placed <br> both break. Measured properly -- canvas text metrics at
   each element's computed font, counting only leaf prose that actually wraps
   -- this site was running 111 characters on the landing page's plans note and
   91 on Terms, i.e. WIDER than the dashboard on some pages.

   The per-page `main.wrap` widths above are layout measures and they stay:
   they set how far a card grid or a flow diagram may spread. What they cannot
   do is hold a paragraph to a readable line, because 42rem of 16px text is 91
   characters. So the layout keeps its width and the prose inside it takes its
   own.

   This overrides a deliberate choice in #137 phase 3, which set `.policy` to
   42rem "tuned for reading long-form terms". That reasoning was right and the
   number was not measured; 45-75 is the norm and 91 is outside it.

   Card interiors are unaffected -- a plan card is ~430px and a flow step
   ~260px, both already inside 55ch -- so this cannot squeeze a component. */
main.wrap p,
main.wrap li { max-width: var(--measure); }

/* TWO THINGS IN main THAT ARE NOT PROSE, and the measure broke both.
   `.trust-strip` is a row of three claims joined by middots -- a label, read
   in one glance, not a sentence read left to right. Clamped, it wrapped and
   left "OVERRIDE" alone on a second line, which reads as a layout fault
   rather than a deliberate break.
   `.flow-note` is a one-sentence callout against an accent rule; at 52ch it
   broke after "yes or a" and orphaned "no." on its own line under the rule.
   Both are short enough that no measure was protecting them anyway. */
/* SPECIFICITY, and phase 1 shipped this rule too weak to do its job.
   `.home main.wrap > p` is (0,2,2); a bare `.trust-strip` is (0,1,0), so the
   measure kept winning and both elements kept wrapping -- the strip orphaning
   "OVERRIDE" and the note orphaning "no.", which is exactly what this was
   written to prevent. The CPL numbers looked right because a wrapped strip
   still has short lines; only the screenshot showed it.
   Scoped to the landing page's own container so it outranks (0,3,1 > 0,2,2)
   rather than relying on source order. */
.home main.wrap > .trust-strip,
.home main.wrap > .flow-note { max-width: none; }

/* THE CHROME IS ONE WIDTH ON EVERY PAGE, and this rule is what guarantees it.
   `.wrap` wraps the header, the main content AND the footer, so any per-page
   measure written as `.home .wrap` captures the chrome too. That shipped: the
   header rendered at four different widths across the six pages and the brand
   moved 144px between the landing page and Terms, while
   test_the_header_nav_is_identical_on_every_page passed the whole time --
   it compares markup, and the markup was identical. The divergence was
   entirely in CSS keyed off the body class.

   So the per-page rules above are scoped to `main.wrap`, and the chrome is
   pinned here. Element+class beats the body-class selectors on specificity,
   which is belt and braces rather than the mechanism. */
header.site .wrap,
footer.site .wrap { max-width: 60rem; }

header.site {
  border-bottom: 1px solid var(--line);
  background: var(--chrome);
}
header.site .wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  align-items: baseline;
  padding-top: 1.25rem;
  padding-bottom: 1.25rem;
}
header.site a.brand {
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--ink-strong);
  text-decoration: none;
  margin-right: auto;
}
header.site nav { display: flex; flex-wrap: wrap; gap: 1.25rem; }
header.site nav a { color: var(--muted); text-decoration: none; font-size: 0.94rem; }
header.site nav a:hover, header.site nav a:focus { color: var(--ink-strong); text-decoration: underline; }

/* THREE LINKS ON A PHONE (#200).
   A `@media (max-width: 26rem)` block used to live here, tightening the gap
   and shrinking the label so that Pricing, three legal links and Dashboard
   fitted on one line at 360px. It said outright that it did not fix 320px,
   because five links do not fit there.

   #200 took the legal links out of the header. The nav is now 250px wide
   against 280px of content box at 320px, so it holds one line at every width
   with no override at all -- including the 320px case the tightening could
   not reach. The block went with the links it was written for.

   What is unchanged: below ~500px the brand, the nav and the theme picker
   still stack into three rows, because `.wrap` is a wrapping flex row and the
   three together need ~416px. That was true before this change and is not
   what the media query was fixing. */

/* THE THEME MENU (#195 phase 7).

   ONE AFFORDANCE ACROSS BOTH HOSTS. This was a labelled <select> while the
   dashboard used an icon opening a popover -- two controls for the same
   three-way choice, met by the same person within one click of each other.
   The dashboard's is the model because it is the one that works with
   JavaScript off; this site's never did, and could not, because there is no
   server here to post to.

   That is also why the markup for this is BUILT BY theme.js rather than
   written into the six pages. The control is JavaScript-only either way, so
   markup describing it would be markup describing something that may never
   exist -- and the header is byte-identical across six hand-maintained files,
   one of them generated, so every line of it is a line to keep in step six
   times. The <div class="theme-picker" hidden> placeholder is all the markup
   needs to carry.

   The class names and the shape follow the dashboard's `.bar-menu` /
   `.menu-item`, so the two read as one control. The tokens it needs were
   copied across at the top of this file. */
.theme-menu { position: relative; }
.theme-menu[hidden] { display: none; }

/* The default disclosure triangle has to go from both engines: `list-style`
   covers Firefox and current Chrome, the pseudo-element covers older WebKit.
   The icon is the marker. */
.theme-button { list-style: none; }
.theme-button::-webkit-details-marker { display: none; }

.theme-button {
  display: grid;
  place-items: center;
  width: var(--bar-control);
  height: var(--bar-control);
  border-radius: var(--radius-control);
  color: var(--muted);
  cursor: pointer;
}
.theme-button:hover { background: var(--hover); color: var(--ink-strong); }
/* Stays lit while its menu is open, so it is obvious which control produced
   the panel now covering part of the page. */
.theme-menu[open] .theme-button { background: var(--hover); color: var(--ink-strong); }

/* Anchored to its own right edge, so a panel wider than the button it hangs
   from opens inwards and never off the side of the window. */
.theme-panel {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: 20;
  min-width: 15rem;
  padding: 0.5rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-overlay);
  text-align: left;
}
.theme-panel ul { list-style: none; margin: 0; padding: 0; }

.theme-panel-label {
  margin: 0.25rem 0.5rem 0.5rem;
  font-size: var(--text-micro);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--faint);
}

/* Always a button, never a radio: a theme is a one-click decision, and a
   picker needing a second click to confirm is a worse control than the select
   it replaced. */
.theme-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 0.5rem;
  border: 0;
  border-radius: var(--radius-control);
  background: none;
  font: inherit;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.theme-item:hover { background: var(--hover); }
.theme-item.current { color: var(--ink-strong); }

/* The icon keeps its column when the tick is absent, so the three rows do not
   shift depending on which one is current. */
.theme-item .theme-mark { flex: 0 0 auto; color: var(--muted); }
.theme-item.current .theme-mark { color: var(--accent-text); }

.theme-item-text { display: flex; flex-direction: column; flex: 1 1 auto; }
.theme-item-label { font-size: var(--text-subhead); line-height: var(--leading-tight); }
.theme-item-hint {
  font-size: var(--text-micro);
  color: var(--faint);
  line-height: var(--leading-tight);
}
.theme-tick { flex: 0 0 auto; color: var(--accent-text); }

h1 { font-size: var(--text-title); line-height: var(--leading-tight); margin: 2rem 0 0.35rem; text-wrap: balance; color: var(--ink-strong); }
h2 { font-size: var(--text-section); margin: 2.25rem 0 0.5rem; text-wrap: balance; color: var(--ink-strong); }
h3 { font-size: var(--text-subhead); margin: 1.5rem 0 0.35rem; color: var(--ink-strong); }
p, li { color: var(--ink); }
ul, ol { padding-left: 1.35rem; }
li { margin: 0.3rem 0; }

.updated { color: var(--muted); font-size: 0.9rem; margin: 0 0 1.5rem; }

/* --- "On this page" contents list on the legal pages (#190) ---
 *
 * IT IS NAVIGATION, NOT A SUMMARY. A tidy list at the top of a Terms of
 * Service can be mistaken for a precis of the agreement, and a reader who
 * believes they have read the terms because they read the list has been misled
 * by the layout. So: announced as <nav>, labelled "On this page", drawn as a
 * block of links, carrying no descriptions. It says where things are, never
 * what they say.
 *
 * NO DISCLOSURE WIDGET, AND THAT WAS MEASURED RATHER THAN PREFERRED.
 *
 * The first build made it a <details>, collapsed on phones and forced open by
 * CSS above the breakpoint. That cannot work. A closed <details> does not lay
 * its content into the flow whatever CSS says, so the forced-open list was
 * painted over by the document beneath it -- in Chromium, Firefox AND WebKit,
 * both directly and via `display: contents`. It is worth knowing that
 * `getBoundingClientRect()` still reports a box for that content, so a test
 * asking only about height passes while the page is visibly broken; the
 * question to ask is whether the NEXT element is pushed below it.
 *
 * So the list is always present, and the height problem is solved by shape
 * instead: two columns, at every width.
 *
 * TWO COLUMNS EVERYWHERE, WHICH PHASE 1 GOT WRONG. Phase 1 measured the phone
 * and stopped there, leaving `display: grid` with no track count on desktop.
 * One column of twelve is 689px, so the agreement began below the fold on a
 * 900px-tall screen -- the same defect the phone rule existed to prevent, just
 * further down the viewport. There is no width at which a tall single column
 * of links is the right answer, so the breakpoint no longer changes the shape.
 * It only tightens the type.
 *
 * Multi-column rather than a grid. Grid sizes each row to its tallest item,
 * and the longer titles wrap, so every short neighbour left a hole. Multicol
 * packs, and it fills down then across, which reads better for a list the
 * document order matters in: 1-6 beside 7-12 rather than odds beside evens.
 * The DOM order is untouched either way, so a screen reader hears the sections
 * in document order whatever the columns do.
 *
 * PANEL, NOT CHROME. Phase 1 filled the box with --chrome and bordered it
 * with --line, which is the one place on the site those two are paired. --line
 * is tuned against --panel, so in the light theme the result was a 1.05:1
 * hairline around a fill 1.13:1 from the page: a smudge rather than a box.
 * --panel is what every other card and note here uses, and it costs nothing in
 * legibility -- links go 5.21 to 5.78 in light and 4.98 to 4.57 in dark, both
 * still above AA, and both pairs are already declared in test_contrast.py.
 *
 * It does not thereby become the .note above it on two of these pages: .note
 * has no border, a 3px accent bar down its left edge and square left corners,
 * and it holds a paragraph rather than a column of links.
 *
 * The label is a <p>, deliberately not a heading. These pages give every
 * heading an id and a contents entry, so a real <h2> here would have to list
 * itself.
 *
 * No list markers. Terms carries its numbering in the heading text itself
 * ("1. What the service does"), so a marker would render it twice; Privacy and
 * Refunds do not number their sections at all. The <ol> stays on all three
 * because the document order is meaningful. */
.toc {
  margin: 0 0 2.5rem;
  padding: 1rem 1.25rem;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  background: var(--panel);
}

.toc-label {
  margin: 0 0 0.6rem;
  font-weight: 600;
  color: var(--ink);
}

.toc-list {
  list-style: none;
  margin: 0;
  padding: 0;
  columns: 2;
  column-gap: 1.5rem;
}

/* The document sets `li { margin: 0.3rem 0 }` for prose lists. Navigation is
 * not prose, and inheriting that spacing is half of why phase 1 ran to 689px.
 * An entry split across the column break would read as two half-titles. */
.toc-list li {
  margin: 0 0 0.4rem;
  break-inside: avoid;
}

.toc-list li:last-child { margin-bottom: 0; }

.toc-list a {
  display: inline-block;
  padding: 0.1rem 0;
  color: var(--accent-text);
}

@media (max-width: 46rem) {
  /* The shape is already right. This only tightens it for a narrow column. */
  .toc { padding: 0.85rem 1rem; }

  .toc-list {
    column-gap: 1.25rem;
    font-size: 0.9rem;
    line-height: 1.35;
  }
}

.lede { font-size: var(--text-blurb); color: var(--muted); }

a { color: var(--accent-text); }

table { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: 0.95rem; }
th, td { text-align: left; padding: 0.55rem 0.7rem; border-bottom: 1px solid var(--line); vertical-align: top; }
th { color: var(--muted); font-weight: 600; }
.scroll { overflow-x: auto; }

.note {
  background: var(--panel);
  border-left: 3px solid var(--accent);
  padding: 0.9rem 1.1rem;
  margin: 1.25rem 0;
  border-radius: 0 6px 6px 0;
}
.note p:first-child { margin-top: 0; }
.note p:last-child { margin-bottom: 0; }

.cta {
  display: inline-block;
  background: var(--accent);
  color: var(--accent-ink);
  padding: 0.6rem 1.1rem;
  border-radius: 6px;
  text-decoration: none;
  font-weight: 600;
  margin: 0.35rem 0.5rem 0.35rem 0;
}
.cta:hover, .cta:focus { background: var(--accent-hover); }
/* THE OUTLINED BUTTON, and two contrast fixes it needed.
   The border is --accent-text, not --accent. This button also appears inside
   a plan card, where the fill behind it is --panel, and --accent on --panel is
   2.74:1 in dark -- under the 3:1 a control boundary needs, and it is this
   button's ONLY edge since the background is transparent. The token block
   above already documents that exact number as the reason --accent-text
   exists, and this rule was using --accent anyway. --accent-text clears it on
   both surfaces in both themes.

   Hover fills rather than tinting. It used to set `background: --line-soft`,
   which dropped the --accent-text label to 4.30:1 in dark -- so the control
   became LESS readable at the moment of interaction, and on keyboard focus,
   which are the two moments it most needs to be readable. Filling with the
   accent and switching the label to --accent-ink is 4.61:1 and unambiguous. */
.cta.secondary {
  background: transparent;
  color: var(--accent-text);
  border: 1px solid var(--accent-text);
}
.cta.secondary:hover, .cta.secondary:focus {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

/* ==========================================================================
   THE LANDING PAGE (#137 phase 2)
   Everything below is index.html only. The legal pages use none of it.
   ========================================================================== */

/* A landing page wants more room than a policy does. The .wrap measure is
   tuned for reading long-form terms; the hero and the two-card comparison
   both suffocate at 46rem, so index widens and the prose inside it keeps a
   measure of its own. */
.home main.wrap { max-width: 60rem; }
/* The heading may span the wider landing measure; the prose under it may not
   -- this rule used to cap all three at 46rem, which is 111 characters of
   16px text and outranked `main.wrap p` on specificity. */
.home main.wrap > h2 { max-width: 46rem; }
.home main.wrap > p,
.home main.wrap > ul { max-width: var(--measure); }

.home h1 {
  font-size: var(--text-display);
  letter-spacing: -0.015em;
  margin-top: 3rem;
}
.home .lede { font-size: var(--text-lede); max-width: 42rem; }

.cta-row { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 1.75rem 0 0; }
.cta-row .cta { margin: 0; }

/* Small, quiet, and the last thing under the buttons -- the Zaro pattern, in
   --faint rather than the accent, because the accent is spoken for. */
.trust-strip {
  margin: 1.25rem 0 0;
  color: var(--faint);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* THE FLOW.
   A row on a desktop, a column on a phone, with the arrows rotating a quarter
   turn rather than being hidden -- an arrow that disappears takes the "and
   then" with it, and three unconnected boxes do not read as a sequence. */
.flow {
  display: flex;
  align-items: stretch;
  gap: 0.75rem;
  list-style: none;
  padding: 0;
  margin: 1.25rem 0 0;
}
.flow-step {
  flex: 1;
  margin: 0;
  padding: 1.15rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
}
.flow-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 999px;
  border: 1px solid var(--control-line);
  color: var(--muted);
  font-size: 0.875rem;
  font-weight: 700;
}
.flow-step h3 { margin: 0.7rem 0 0.3rem; font-size: 1rem; }
.flow-step p { margin: 0; color: var(--muted); font-size: 0.9375rem; }
.flow-arrow {
  display: flex;
  align-items: center;
  margin: 0;
  color: var(--faint);
  flex: 0 0 auto;
}

.flow-note {
  margin: 1rem 0 0;
  color: var(--muted);
  font-size: 0.9375rem;
  border-left: 3px solid var(--accent);
  padding-left: 0.9rem;
}

/* THE TWO PLANS.
   .plan-card and .plan-featured are the dashboard's names on purpose (#141,
   #158) so the two surfaces stay recognisably one product -- and so nobody
   reintroduces a bare `.plan`, which is the collision #158 was filed for.
   The dashboard draws these on --inset, which this site does not carry; the
   equivalent surface here is --panel.

   NO .plan-badge, unlike the dashboard. It needs one because it renders three
   cards and a `highlight` flag decides which is featured. Two cards, one of
   them outlined in the accent, is already unambiguous -- and the badge sat
   directly above an <h3> saying the same word. */
.plans {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1rem;
  margin: 1.25rem 0 0;
  align-items: start;
}
.plan-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
  padding: 1.35rem 1.25rem 1.25rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  position: relative;
  height: 100%;
}
/* Outlined rather than filled: the accent states which card is the paid one,
   and it is still doing one of its two permitted jobs. */
.plan-card.plan-featured {
  border-color: var(--accent-text);
  box-shadow: 0 0 0 1px var(--accent-text);
}
.plan-card h3 { margin: 0; font-size: 0.9375rem; font-weight: 600; color: var(--muted); }
.plan-price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.15rem 0 0;
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--ink-strong);
}
.plan-price span { font-size: 0.875rem; font-weight: 400; color: var(--muted); }
.plan-blurb { margin: 0.5rem 0 0; color: var(--muted); font-size: 0.9375rem; }
.plan-list { margin: 0.9rem 0 1.25rem; padding: 0; list-style: none; }
.plan-list li {
  position: relative;
  padding-left: 1.5rem;
  margin: 0.35rem 0;
  font-size: 0.9375rem;
}
/* A tick drawn in text rather than an image.
   The empty alt string is what actually keeps it out of the accessibility
   tree, so a screen reader reads the feature and not "check mark" six times.
   This comment used to claim that was happening while the plain `content`
   below did nothing of the sort -- the ticks were being announced.

   Guarded by @supports because the `content: x / alt` syntax is recent
   (Chrome 77, Safari 17.4, Firefox 129); where it is unsupported the whole
   declaration would be dropped and the ticks would vanish entirely, which is
   worse than announcing them. So the plain form is the floor. */
.plan-list li::before {
  content: "\2713";
  position: absolute;
  left: 0;
  color: var(--accent-text);
  font-weight: 700;
}
@supports (content: "x" / "y") {
  .plan-list li::before { content: "\2713" / ""; }
}
.plan-card .cta { margin: auto 0 0; align-self: stretch; text-align: center; }

.plans-note { margin: 1rem 0 0; color: var(--faint); font-size: var(--text-note); }

@media (max-width: 46rem) {
  .flow { flex-direction: column; align-items: stretch; }
  .flow-arrow { justify-content: center; transform: rotate(90deg); }
}

/* ==========================================================================
   THE LEGAL PAGES (#137 phase 3)
   Typography and layout only. No wording was touched -- the prose on Terms,
   Privacy and Refunds is byte-identical to what it was before this phase, and
   the only markup change is an `id` and an anchor link per heading.
   ========================================================================== */

/* THE HEADING IDS ARE A PUBLIC CONTRACT NOW.
   They are derived from the heading text with the leading section number
   stripped, so renumbering a clause does not break a link into it. Once one of
   these is published -- in a support reply, a Stripe portal field, a Discord
   message -- it lives outside this repository forever, exactly like the
   changelog entry ids in changelog.py. Renaming a section is free; changing
   its id silently breaks every link anybody ever saved to it. */

/* A measure tuned for reading, not for fitting. 46rem at 16px is roughly 75
   characters, which is the long end of comfortable; policy prose is read under
   duress by people looking for one clause, so it gets the shorter end. */
.policy main.wrap { max-width: 42rem; }
.policy p,
.policy li { line-height: 1.7; }

/* Hierarchy by size and spacing, not by colour. There is more space above a
   heading than below it, so a heading belongs to what follows rather than
   floating between two sections -- the single most useful thing typography can
   do for a document somebody is scanning. */
.policy h1 { font-size: 2rem; letter-spacing: -0.01em; margin: 2.5rem 0 0.5rem; }
.policy h2 {
  font-size: 1.3rem;
  margin: 3rem 0 0.75rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line-soft);
}
.policy h3 { font-size: 1.05rem; margin: 2rem 0 0.5rem; }

/* The first h2 needs no rule above it: the lede already separates it from the
   title, and a line there reads as an underline for the wrong thing.

   MATCHED ON POSITION, not on what precedes it. This was two adjacent-sibling
   selectors -- `.updated + h2` and `.lede + h2` -- and between them they fired
   on Terms alone. Privacy and Refunds both put a `.note` callout before their
   first heading, so neither matched and both drew the rule anyway, directly
   under the note box. `.updated + h2` matched nothing on any page at all. */
.policy main.wrap > h2:first-of-type { border-top: 0; padding-top: 0; margin-top: 2rem; }

/* Jumping to #billing must not land the heading hard against the viewport
   edge with its section already scrolled past. */
.policy :target { scroll-margin-top: 1.5rem; }
.policy h2, .policy h3 { scroll-margin-top: 1.5rem; }

/* A jumped-to section marks itself, so somebody arriving from a link into a
   specific clause can see which one they were sent to rather than counting
   headings from the top of the viewport.

   THIS IS A LEGITIMATE USE OF THE ACCENT, and worth saying why given the rule
   that it means exactly two things. "This is the page you are on" is the first
   of the two, and a `:target` heading is the same statement at a finer grain --
   it is where you are, stated by the navigation that brought you. It is not
   decoration and it is not a link. */
.policy h2:target, .policy h3:target { border-left: 3px solid var(--accent); padding-left: 0.75rem; }

/* THE ANCHOR AFFORDANCE.
   Hidden until the heading is hovered or the link itself is focused, so it
   never competes with the text. `aria-hidden` and `tabindex="-1"` in the
   markup: a screen reader user navigates by heading and does not need a "#"
   announced fourteen times, and a keyboard user should not have to tab past
   one before every section. It is a convenience for someone who wants to copy
   a link to a clause, and it is only ever that. */
.policy .anchor {
  margin-left: 0.4rem;
  color: var(--faint);
  text-decoration: none;
  opacity: 0;
  font-weight: 400;
  /* Transparent is not gone. Without this the anchor keeps a 13x24px live hit
     box inside every heading, so a reader selecting or clicking heading text
     can land on an invisible link and get an unexplained hash navigation. */
  pointer-events: none;
}
.policy h2:hover .anchor,
.policy h3:hover .anchor,
.policy .anchor:focus { opacity: 1; pointer-events: auto; }

/* The date a policy was last changed is the first thing anybody checks when
   they are trying to establish what was in force on a given day, and it was
   rendering as small grey text below the title like a caption. */
.policy .updated {
  display: inline-block;
  margin: 0 0 2rem;
  padding: 0.25rem 0.6rem;
  /* --control-line, not --line. This element's entire shape is its border, and
     --line is 1.62:1 on the ground in dark and 1.05:1 in light -- so the pill
     did not render as a pill at all, just small grey text where a chip was
     drawn. --line is for separating things that are already distinguishable. */
  border: 1px solid var(--control-line);
  border-radius: 999px;
  font-size: 0.8125rem;
  color: var(--muted);
}

.policy .lede { font-size: 1.05rem; color: var(--ink); }
.policy ul { padding-left: 1.5rem; }
.policy li { margin: 0.5rem 0; }
.policy .note { margin: 1.75rem 0; }
.policy table { font-size: 0.9375rem; }
.policy th, .policy td { padding: 0.7rem 0.8rem; }

/* ==========================================================================
   THE CHANGELOG (#137 phase 4)
   site/changelog.html is GENERATED by scripts/gen_changelog.py. Style it
   here; never edit the markup there.
   ========================================================================== */

.changelog-page main.wrap { max-width: 48rem; }
.changelog-page .lede { margin-bottom: 2.5rem; }

.entries { list-style: none; padding: 0; margin: 0; }

/* Plain's shape: the date in a left gutter, the entry beside it, so a reader
   scanning for "what changed recently" runs their eye down one column instead
   of hunting a date inside each block.

   It collapses to a single column on a phone rather than shrinking the gutter,
   because a 5rem date column next to a 15rem text column is two bad measures
   instead of one good one. */
.entry {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: 0 1.5rem;
  padding: 1.5rem 0;
  border-top: 1px solid var(--line-soft);
}
.entry:first-child { border-top: 0; padding-top: 0; }

.entry-meta {
  grid-column: 1;
  grid-row: 1 / span 2;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
  margin: 0;
}
.entry-meta time { color: var(--faint); font-size: 0.875rem; }

/* "New" and "Premium". Quiet in both cases -- the dashboard makes the same
   choice for the same reason: a loud Premium tag on a page reachable by
   anybody reads as an advert, and the tag is meant to say what kind of thing
   shipped, not to sell it. */
.entry .tag {
  padding: 0.1rem 0.45rem;
  /* Same reason as the .updated pill: the chip is its border. With --line the
     "NEW" tag rendered as bare small-caps beside a crisp blurple "PREMIUM"
     one, which is the opposite of the even-handedness below. */
  border: 1px solid var(--control-line);
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--muted);
}
.entry .tag.premium { border-color: var(--accent-text); color: var(--accent-text); }

.entry h2 {
  grid-column: 2;
  margin: 0;
  font-size: 1.0625rem;
  line-height: 1.35;
}
/* The zero-entry fallback spans both columns. As a plain .entry-body it sat
   in column 2, indented past an empty 7.5rem date gutter with nothing in it. */
.entry.empty { color: var(--muted); }
.entry.empty, .entry.empty > * { grid-column: 1 / -1; }

.entry-body {
  grid-column: 2;
  margin: 0.35rem 0 0;
  color: var(--muted);
  line-height: 1.65;
}

/* #138: "get these in your own server", under the entries.
 *
 * Quiet on purpose -- a rule and muted text, not a card and not a CTA. This
 * page's job is credibility; a surface that reads as an advert stops being
 * read as news, which costs more than the follow is worth. The dashboard's
 * .changelog-follow makes the same argument about the same sentence. */
.entry-follow {
  margin: 3rem 0 0;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  max-width: var(--measure);
  color: var(--muted);
  line-height: 1.65;
}

@media (max-width: 34rem) {
  .entry { grid-template-columns: 1fr; gap: 0.5rem 0; }
  .entry-meta {
    grid-column: 1;
    grid-row: auto;
    flex-direction: row;
    align-items: center;
    gap: 0.6rem;
  }
  .entry h2, .entry-body { grid-column: 1; }
}

footer.site {
  border-top: 1px solid var(--line);
  background: var(--chrome);
  margin-top: 3rem;
  color: var(--muted);
  font-size: var(--text-micro);
}
footer.site .wrap { padding-top: 1.5rem; padding-bottom: 2.5rem; }
footer.site nav { display: flex; flex-wrap: wrap; gap: 1.25rem; margin-bottom: 0.75rem; }
footer.site a { color: var(--muted); }

a:focus-visible, .cta:focus-visible,
.theme-button:focus-visible, .theme-item:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}
