/* Self-hosted (issue #984 - Inter/JetBrains Mono were declared but never
   shipped, so every visitor rendered an OS fallback). Inter is the variable
   family (one file covers the 400/600/700 weights base.css actually uses,
   incl. the browser-default bold on h1-h3), latin-subset per the
   page-weight priority. JetBrains Mono reuses the exact asset already
   vendored for the app (app/assets/fonts/JetBrainsMono-Regular.ttf) rather
   than sourcing a second copy. Licence text (SIL OFL 1.1, both) lives in
   shared/font-licenses/ - published only under the www-built site (served at
   the apex, bandbinder.app), the sole canonical host for these files (see
   www/.eleventy.js passthrough entry; docs deliberately does not
   passthrough-copy this dir, issue #2461; apex-not-www canonical, #2605).

   Two faces, and only two, site-wide (#1562): Inter for every heading and
   every word of body copy, JetBrains Mono for code/ChordPro samples alone.
   The variable axis is clamped to 400-700 - the only weights the ladder in
   tokens.css exposes - so a stray font-weight can't quietly summon a ninth
   rendered weight the design never sanctioned. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 400 700;
  src: url('/assets/fonts/Inter-Variable-latin.woff2') format('woff2-variations');
  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: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url('/assets/fonts/JetBrainsMono-Regular.ttf') format('truetype');
}
* { box-sizing: border-box; }
/* The HTML `hidden` attribute must actually hide (issue #2080).

   Two separate cascade rules conspire here, and both matter:

   1. The browser's own `[hidden] { display: none }` lives in the USER-AGENT
      origin, and author styles beat user-agent styles for normal
      declarations. So `.billing-auth-form { display: grid }` overrides it on
      ORIGIN alone - specificity never enters into it. That is how /billing/
      shipped with its two auth steps and the provider row all painted at
      once (.billing-auth-form and .billing-providers both set display: grid,
      while billing.js drives them purely via the `hidden` property).

   2. Re-declaring `[hidden]` here in the author origin is not enough by
      itself: `[hidden]` and `.billing-auth-form` are both specificity
      (0,1,0), so the later declaration in this file would win - and the
      component rules come later. `!important` is what settles it, since an
      important author declaration beats normal author declarations
      regardless of specificity or source order.

   Fixed once, globally, rather than per component: nothing errors when it
   happens, so it is a whole class of silent bug. */
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
body {
  color: var(--ink);
  background: var(--surface);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  line-height: 1.6;
  /* Sticky footer (issue #2246): without this, any page shorter than the
     viewport (docs home, /chordpro/, /404/, short docs pages) ends the
     footer partway up and leaves bare background below it. min-height keeps
     body at least a full viewport tall, the column flex stacks header/main/
     footer, and main's flex-grow (below) pushes the footer to the bottom
     without stretching it. 100vh first as a fallback for browsers without
     dvh support; the second declaration overrides it wherever dvh is
     understood, so it's progressive enhancement rather than an either/or. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
main { flex: 1 0 auto; }
a { color: var(--accent); }
/* #2990: --accent-strong (#4650e8) fails 4.5:1 as TEXT on --surface (3.24:1)
   now that it's the deep indigo fill, so hover can no longer swap to it the
   way it did under the old, lighter #7c6bff. --accent is the only token left
   that's both on-brand and readable here, so rest and hover read identically -
   a forced side effect of the contrast floor, not a design change. */
a:hover { color: var(--accent); }
img { max-width: 100%; display: block; }
.container { width: min(var(--max-width), calc(100% - 2rem)); margin: 0 auto; }
.docs-container { width: min(var(--docs-width), calc(100% - 2rem)); margin: 0 auto; }
/* Section rhythm (issue #2012, Jason's ruling: "things that are 5 page
   scrolls down aren't being seen by users"). --space-6 top AND bottom put
   96px of nothing between every pair of sections, which on a phone is more
   than a tenth of a screen per boundary and, compounded across /pricing/'s
   four sections and /features/'s eight, was the single biggest contributor
   to fold depth site-wide. --space-5 desktop, --space-4 on phones, where the
   screen is scarcest. This is the compact-UI standing veto from CLAUDE.md
   applied to the website: space is for the content. */
section { padding: var(--space-5) 0; }
@media (max-width: 800px) {
  section { padding: var(--space-4) 0; }
}
h1 { font-size: var(--text-display); line-height: 1.1; margin: 0 0 var(--space-3); color: var(--ink); overflow-wrap: break-word; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
h2 { font-size: var(--text-xl); line-height: 1.2; margin: 0 0 var(--space-3); color: var(--ink); }
h3 { font-size: var(--text-lg); margin: 0 0 var(--space-2); color: var(--ink); }
p { margin: 0 0 var(--space-3); }
.lead { font-size: var(--text-base); }
.btn {
  border-radius: var(--radius);
  padding: 0.55rem 1rem;
  text-decoration: none;
  display: inline-block;
  font-weight: var(--weight-semibold);
}
/* Disabled state (issue #1501): a dead CTA (config-gated sign-in, or a
   checkout/portal button mid-flight via billing.js's startAction()) was
   rendering pixel-identical to a live one - same fill, same hand cursor,
   the browser's own default disabled affordance overridden by the
   unconditional cursor:pointer below. Opacity dims the existing fill
   rather than inventing a new color; :disabled (0,2,0 specificity) always
   outranks :where(button.btn)'s 0-specificity cursor rule regardless of
   source order. */
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
/* .btn is applied to both <a> (its original use) and native <button> elements
   (e.g. pricing calculator stepper, support form submit). Without this reset the
   browser's default button chrome (background/border/font) wins over
   .btn-secondary/.btn-primary's own styling, rendering buttons as blank
   shapes with invisible text (#795). Wrapped in :where() to keep this at 0
   specificity: it still beats the UA button stylesheet (author origin always
   outranks UA origin, regardless of specificity), but stays LOWER specificity
   than a bare "button.btn" would be - a plain "button.btn { border: 0 }"
   is a class+type selector, which outranks the single-class ".btn-secondary"/
   ".btn-primary" selectors and would silently override their own border/
   background instead of just clearing the native chrome underneath them. */
:where(button.btn) { background: transparent; border: 0; font: inherit; cursor: pointer; }
/* Issue #1987: "hover over website buttons erases the button text". Every
   button on this site is an <a>, and the single-class .btn-primary /
   .btn-secondary selectors were losing specificity fights with the generic
   anchor rules above:

     a:hover            (0,1,1) > .btn-primary (0,1,0)
     .site-header nav a (0,1,2) > .btn-primary (0,1,0)

   so hovering an in-page primary button repainted its label to
   --accent-strong, which IS that button's background: contrast 1.00:1, text
   gone. The same leak was quietly costing the header CTA its contrast at
   REST too (--ink on --accent-strong, 3.28:1, under the 4.5:1 floor) because
   the header nav rule outranks both .btn-primary and a:hover.

   Fixed by doubling the class rather than reaching for !important:
   .btn.btn-primary is (0,2,0), which outranks every anchor rule above,
   and the :hover variants at (0,3,0) outrank that in turn. Keep both classes
   in these selectors - dropping back to a single class silently reopens all
   three defects. contrast-check.mjs now guards the hover pairs so a token
   change can't reintroduce them either.

   #2990: the hover fill used to lighten all the way to --accent (both were
   light violets). --accent is now the light chord tone (#929cff, kept light
   on purpose for on-dark text elsewhere) - a white-text button hovering onto
   it would drop to 2.49:1. The hover fill is re-derived from --accent-strong
   itself instead (88% mixed toward white, 4.65:1 for --on-accent), same
   "lighten on hover" relationship, new hue. */
.btn.btn-primary { background: var(--accent-strong); color: var(--on-accent); }
.btn.btn-primary:hover { background: color-mix(in srgb, var(--accent-strong) 88%, white); color: var(--on-accent); }
.btn.btn-secondary { border: 1px solid var(--accent); color: var(--ink); }
.btn.btn-secondary:hover { border-color: var(--accent); background: var(--surface-highest); color: var(--ink); }
.btn-sm { padding: 0.3rem 0.65rem; font-size: var(--text-sm); }
.grid-2 { display: grid; gap: var(--space-4); grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 800px) {
  .grid-2 { grid-template-columns: 1fr; }
}
/* #2563 item 14: the docs index's two section cards can hold very different
   amounts of content (User Guide: 12 pages; ChordPro Hub: 5) - the grid
   default (align-items: stretch) forced the shorter card to match the
   taller one's height, leaving ~130px of dead space below its last link.
   Scoped to this one grid so every other .grid-2 caller (card rows that
   WANT equal-height siblings) keeps the default. */
.docs-index-grid { align-items: start; }
/* 3-up icon card grid (issue #976 R2) - "Why BandBinder?" and any future
   3-card row. 3 columns desktop, 2 at mid widths, 1 on mobile. */
.grid-3 { display: grid; gap: var(--space-4); grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 900px) {
  .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 600px) {
  .grid-3 { grid-template-columns: 1fr; }
}
/* Card icon (issue #976 R2) - single-color stroke SVG, brand violet, sized
   small so it reads as a marker next to the headline, not a hero graphic.
   #2038: sized to 20px and set inline-left of the heading via .card-head -
   the standalone 28px block + bottom-margin layout (a full icon row above
   every heading) is retired. #2435 re-sized .platform-mark to 28px;
   .card-icon stays 20px, a beside-label marker, not an identity badge. */
/* #2990 retune (crew-reviewer gap 2): was --accent-strong. The retuned
   --accent-strong (#4650e8) is background-fill-only now (see tokens.css) -
   as this icon's own foreground color on --surface-raised it measured
   2.90:1, under both the 4.5:1 text floor and the 3:1 UI-component floor.
   --accent (#929cff) is the token every other on-dark text/icon/border role
   already moved to for the same reason (a:hover, .header-signin:hover,
   .card-recommended border) and clears 6.82:1 here. */
.card-icon { display: block; flex: none; color: var(--accent); width: 20px; height: 20px; }
/* Icon-inline-with-heading flex row (#2038, Maria's ruling), extracted from
   .platform-head below rather than duplicated - .platform-head now reuses
   this rule instead of restating it. Used to wrap an icon + <h3> so the
   heading sits beside its marker, not stacked under an icon row. */
.card-head, .platform-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.card-head h3 { margin: 0; }
/* Homepage hero (issue #542): the song-library shot sits beside the
   headline instead of stacked below it, on wide viewports. */
.hero-showcase { align-items: center; }
.stack { display: grid; gap: var(--space-3); }
/* /features/ jump strip (#2018, Maria's ruling): replaces the #1583 compare
   table's 2178px "16 of 17 rows identical checkmarks" section with five
   one-line anchor links, each naming what's behind it in one clause. Kept
   to plain text sizing (no card/border chrome) - the whole point is a small
   fraction of the table's footprint, so a compact-UI treatment beats a
   boxed one here. */
.jump-strip { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.35rem; }
.jump-strip li { font-size: var(--text-sm); line-height: 1.35; }
.jump-strip a { font-weight: var(--weight-bold); }
/* /faq/ table of contents (issue #3933, Maria's ruling): shrunk from the
   #2888 four-column, all-18-question grid to a single wrapping row of the
   four group titles, each with its muted count. Plain rectangular text
   links (R-A: never pills), no cards, no chrome, no JS. Wraps to at most
   two rows at 360px with no horizontal scroll. */
.faq-toc { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-4); }
.faq-toc-group-title { margin: 0; font-size: var(--text-sm); font-weight: var(--weight-bold); text-transform: uppercase; letter-spacing: 0.02em; color: var(--ink); text-decoration: none; }
.faq-toc-count { color: var(--muted); font-size: var(--text-sm); }
.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--surface);
}
/* Docs keeps the header/body seam - it's functional wayfinding once you're
   scrolling reference material. www's header and hero read as one canvas
   instead (see .hero below), so only docs restores the border. */
html.docs .site-header {
  border-bottom: 1px solid var(--border);
}
.site-header-inner {
  min-height: 60px;
  display: flex;
  gap: var(--space-3);
  align-items: center;
  justify-content: space-between;
}
/* Brand and the docs "bandbinder.app" link keep their natural size at any
   viewport width; nav (and, above the 950px mobile-nav breakpoint below, the
   www CTA nested inside it) and the docs search widget are the only flex
   children allowed to shrink (min-width:0 overrides the flex default of
   min-width:auto, which otherwise floors a flex item at its content's
   min-content width and forces the row - and everything that inherits its
   width - wider than the viewport). Below their natural width, the link
   list scrolls horizontally on its own and the search box narrows, instead
   of either being clipped or pushing the header past the viewport edge. */
.site-header-inner > .brand,
.site-header-inner > .btn {
  flex-shrink: 0;
}
.site-header nav {
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex: 1 1 auto;
}
.site-header-inner > #search {
  min-width: 0;
  flex: 1 1 auto;
}
.site-header nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: var(--space-3);
  flex-wrap: nowrap;
  min-width: 0;
  overflow-x: auto;
}
.site-header nav .btn {
  flex-shrink: 0;
}
.site-header nav a { text-decoration: none; color: var(--ink); font-weight: var(--weight-semibold); }
/* Issue #2643: "Sign in" rides at the far right of the nav, immediately
   before Download - grouped with it in its own flex item so nav's
   space-between still resolves to exactly two children (link list, actions)
   instead of stranding "Sign in" in the middle of the gap. Reused as-is in
   the docs header below, outside <nav>, where .header-signin (not the
   nav-scoped `.site-header nav a` rule above) carries its look instead. */
.nav-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-shrink: 0;
}
.header-signin { text-decoration: none; color: var(--ink); font-weight: var(--weight-semibold); }
.header-signin:hover { color: var(--accent); } /* #2990: accent-strong no longer passes 4.5:1 as text, see a:hover */
/* Mobile nav toggle (issue #1176) - www's header had no rule collapsing the
   nav below any breakpoint, so at phone widths the desktop row was retained
   as-is: the CTA overlapped the links and Docs/About sat past the viewport
   edge, clipped by nav's own overflow-x:auto with no scrollbar hint. The
   toggle button is hidden entirely above 950px (nav stays the plain inline
   row above); below it, nav becomes a hidden dropdown panel that this
   button reveals. Panel visibility and the icon swap are both pure CSS,
   keyed off aria-expanded - shared/js/nav-toggle.js only flips that one
   attribute (see its own comment).

   #3022: the breakpoint was 600px, but the full row (7 links + Sign in +
   the Download button) needs its own min-content width to lay out flat -
   measured off the real link labels, .nav-toggle icon, and .btn padding at
   ~800-870px of nav+brand content plus the container's side inset, which
   lines up with Jason's ~860px tablet screenshot showing the link list's
   own overflow-x:auto kick in with a visible scrollbar and "Changelog" cut
   off. 950px clears that with headroom rather than sitting hairline-close
   to the measured minimum. */
.nav-toggle {
  display: none;
  background: transparent;
  border: 0;
  padding: 0.35rem;
  margin: -0.35rem 0;
  color: var(--ink);
  cursor: pointer;
}
.nav-toggle-icon { width: 22px; height: 22px; display: block; }
.nav-toggle .icon-close { display: none; }
.nav-toggle[aria-expanded='true'] .icon-open { display: none; }
.nav-toggle[aria-expanded='true'] .icon-close { display: block; }
@media (max-width: 950px) {
  .site-header .nav-toggle { display: block; }
  .site-header-inner { flex-wrap: wrap; }
  /* #primary-nav (id) outranks the plain ".site-header nav" rule above so
     this always wins regardless of source order. */
  .site-header nav#primary-nav {
    display: none;
    flex-basis: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-3);
    padding-top: var(--space-3);
  }
  .nav-toggle[aria-expanded='true'] ~ nav#primary-nav {
    display: flex;
  }
  .site-header nav#primary-nav ul {
    flex-direction: column;
    gap: var(--space-2);
    overflow-x: visible;
  }
  .site-header nav#primary-nav .nav-actions {
    align-self: flex-start;
  }
}
.brand { display: flex; gap: 0.6rem; align-items: center; text-decoration: none; color: var(--ink); }
/* 28px, up from 24 (issue #1562, item 9): the wordmark read undersized next
   to the nav links. Deliberately the only wordmark on the page - the hero
   headline carries the body, so no second, larger lockup goes below it. */
.brand img { height: 28px; width: auto; }
/* www is dark-always (no light wordmark ever renders there); docs swaps by
   OS preference, same html.docs scoping as the token overrides above. */
.brand .wordmark-light { display: none; }
@media (prefers-color-scheme: light) {
  html.docs .brand .wordmark-dark { display: none; }
  html.docs .brand .wordmark-light { display: block; }
}
/* b-note mark, left of the wordmark - same theme-swap pattern as the
   wordmark images above (issue #722: mark was footer-only, an omission). */
.brand .mark-light { display: none; }
@media (prefers-color-scheme: light) {
  html.docs .brand .mark-dark { display: none; }
  html.docs .brand .mark-light { display: block; }
}
.docs-badge {
  font-family: 'JetBrains Mono', Consolas, Menlo, 'Courier New', monospace;
  border: 1px solid var(--accent-support);
  color: var(--accent-support);
  border-radius: 6px;
  padding: 0.1rem 0.5rem;
  font-size: var(--text-xs);
}
.site-footer {
  background: var(--surface-low);
  color: var(--ink);
  padding: var(--space-4) 0;
}
.site-footer a { color: var(--ink); text-decoration: none; }
/* Inverse tile is for dark chrome; www is dark-always so it's the only tile
   www ever shows. Docs swaps to the primary tile on the light variant -
   same html.docs scoping as the wordmark and token overrides above. */
.footer-icon-light { display: none; }
@media (prefers-color-scheme: light) {
  html.docs .footer-icon-dark { display: none; }
  html.docs .footer-icon-light { display: block; }
}
.footer-grid { display: grid; gap: var(--space-4); grid-template-columns: 180px repeat(4, minmax(0, 1fr)); }
/* v1.0 tagline (DECISIONS.md #49, issue #275) - short declarative line, kept
   separate from the hero's "Ditch the binder." headline and its positioning-
   statement subheadline; appears here (every page) and in the homepage
   <title>/og:title (website/www/src/index.njk titleOverride) - the complete
   v1.0 surface list. */
.footer-tagline { margin: var(--space-2) 0 0; font-size: var(--text-sm); opacity: 0.75; }
/* `auto`, not 0, on the horizontal axis: this element carries class
   "footer-credits container", and .container's own `margin: 0 auto` is what
   centers it. This rule sits later in the file at equal specificity, so a
   `margin: X 0 0` shorthand here silently overwrote that auto and pinned the
   credit line to x=0 while the footer grid above it started at x=180 - on
   every page of BOTH sites, and hard against the screen edge on a phone.
   Pre-existing, found while measuring #2012's footer. */
.footer-credits { margin: var(--space-3) auto 0; font-size: var(--text-sm); opacity: 0.6; }
.footer-credits a { text-decoration: underline; }
.footer-links h3 { color: var(--ink); font-size: var(--text-base); margin-bottom: var(--space-2); }
.footer-links ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-1); }
@media (max-width: 800px) {
  .site-header-inner { align-items: flex-start; padding: 0.75rem 0; }
  .site-header nav ul { gap: var(--space-2); }
  /* Issue #2012: collapsing straight to one column stacked the brand block
     and all three link lists end to end - a 923px footer on a 390px phone,
     more than a full screen of chrome under every page. Two columns fit the
     same links in half the height at any mainstream phone width. */
  .footer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Issue #2568: with four link columns, two-per-row leaves the brand block
     as an orphan in a five-cell grid. Span it full width so the four link
     lists pair up cleanly 2x2 underneath, preserving #2012's fix. */
  .footer-grid > div:first-child { grid-column: 1 / -1; }
}
@media (max-width: 360px) {
  .footer-grid { grid-template-columns: 1fr; }
}
/* Docs header only (same html.docs scoping as the wordmark/footer-icon
   overrides above) - brand + search + the bandbinder.app cross-link no
   longer fit on one row once the search widget hits its own padding-driven
   minimum width (it can't shrink past that, no matter what width/min-width
   says). Below that point the row wraps instead of the search box
   overlapping the link. www never has a #search element, so its header
   never needs to wrap - nav absorbs its own overflow via scroll (see
   above) and the row already fits at 390px without it. */
@media (max-width: 480px) {
  html.docs .site-header-inner { flex-wrap: wrap; row-gap: var(--space-2); }
}
/* www only (docs never uses .hero): flat against --surface so the header
   and hero read as one canvas (issue #722) - cards/panels below keep
   --surface-raised, contrast moves to where it earns something. */
/* Issue #2012 items 1 and 2, verbatim: "way too much padding BEFORE content
   heading" and "way too much padding AFTER content heading". A marketing
   hero here is one h1 plus one lead line - it needs breathing room, not a
   half-screen of it. The bottom padding is the tighter of the two on
   purpose: whatever follows the hero is the content the visitor came for,
   and .hero + section below adds its own --space-3 on top of this. */
.hero { background: var(--surface); padding: var(--space-4) 0 var(--space-2); }
/* A hero's own bottom padding plus the next section's top padding summed to
   ~96px of dead air (issue #1562). One shared rule, so the home page stops
   carrying per-section inline padding hacks (#976 R4) and every marketing
   page tightens the same way. /billing/ keeps its own tighter override. */
.hero + section { padding-top: var(--space-3); }
/* Small muted secondary line (issue #1562). Replaces the retired .eyebrow
   pill everywhere it was used - the home page's platform-availability note
   under the download CTA, the persona-page category lines, /faq/'s section
   label. Pills are gone site-wide: Jason's ruling on #1562, "rounded pills
   are last decade", and the pill was the loudest thing above the headline. */
.note { font-size: var(--text-sm); opacity: 0.75; margin: var(--space-1) 0 var(--space-2); }
.accent { color: var(--accent); }
.hook-line { text-align: center; }
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
/* Trust line (issue #976 R1, renamed by #1973 when the early-access signup
   form it lived in was retired) - an offer statement on the homepage hero
   and /download/, muted but slightly stronger than the microcopy tier
   since it's the page's strongest offer, not a legal disclosure. */
.trust-line { font-size: var(--text-sm); opacity: 0.85; margin: var(--space-1) 0 0; }
/* #2108: separate the platform/download link from the subscription qualifier
   above it, while keeping both lines compact within the homepage hero. */
.hero-availability { margin-top: var(--space-3); }
/* Support intake form (issue #1421) - /support/, functions/api/support.js
   forwards submissions to support@bandbinder.app. Cardless (#1484 S3 - the
   form is the page's only content, so a card frames nothing). */
.support-form { display: grid; gap: var(--space-3); max-width: 480px; margin-bottom: var(--space-3); }
.support-field { display: grid; gap: var(--space-1); }
.support-field label { font-size: var(--text-sm); font-weight: var(--weight-semibold); }
.support-optional { font-weight: var(--weight-regular); opacity: 0.6; }
.support-field input,
.support-field select,
.support-field textarea,
/* /billing/'s email sign-in fields (issue #1838) join this rule rather than
   restating it - one text-control treatment on the site, extended, never a
   second copy that can drift. */
.billing-auth-field input {
  padding: 0.55rem 0.85rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  /* Recessed, NOT --surface-raised (issue #2080). Both of these controls live
     inside a .card, whose background is --surface-raised itself - so the field
     was painting the exact same colour as the panel behind it, leaving a
     1px --surface-highest hairline as the only thing marking where you could
     type. That is the "invisible input fields" report. A well that is darker
     than its container reads as an input on sight, without needing the
     border to do all the work. */
  background: var(--surface-low);
  color: var(--ink);
  font: inherit;
  width: 100%;
}
/* The focused field is unmistakable - the same violet the rest of the site
   uses for focus, doubled in width so it reads at a glance. */
.support-field input:focus-visible,
.support-field select:focus-visible,
.support-field textarea:focus-visible,
.billing-auth-field input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
.support-field textarea { resize: vertical; min-height: 120px; }
.support-status, .support-fallback { font-size: var(--text-sm); margin: 0; }
.support-status[data-state='success'] { color: var(--accent-support); }
.support-status[data-state='error'] { color: var(--ink); font-weight: var(--weight-semibold); }
.support-form[data-submitted] .support-field,
.support-form[data-submitted] [data-support-submit] { display: none; }
.navy-strip { background: var(--surface-low); color: var(--ink); }
.navy-strip h2, .navy-strip h3 { color: var(--ink); }
/* Shared section-shell component (issue #976, R3) - centers the h2 + optional
   lead header block; content slotted below keeps its own natural alignment
   (see shared/includes/section-shell.njk). */
.section-shell-header { text-align: center; margin-bottom: var(--space-4); }
.section-shell-header .lead { margin-bottom: 0; }
.screenshot-placeholder {
  border: 2px dashed var(--border);
  border-radius: var(--radius);
  min-height: 220px;
  display: grid;
  place-items: center;
  padding: var(--space-3);
  text-align: center;
  background: var(--surface-raised);
  color: var(--ink);
}
.card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-3);
  background: var(--surface-raised);
}
/* Issue #2012: a card's last child carried its own bottom margin ON TOP of
   the card's bottom padding, and its first child a top margin on top of the
   top padding - so every card on the site was ~32px taller than it looked,
   multiplied by 9 cards on /pricing/ alone. The card's padding is the
   card's spacing; children don't get to add to it at the edges. */
.card > :first-child { margin-top: 0; }
/* List copy inside a card is the card's content, not a nested document:
   browser-default 40px indent and 1em block margins are both too loud at
   this density. */
.card ul, .card ol { margin: 0 0 var(--space-2); padding-left: 1.25rem; }
/* Must come after .card ul/ol above: same specificity, so source order
   decides, and this needs to win when a card's last child is a <ul>/<ol> -
   otherwise the list's own bottom margin survives (#2016 review). */
.card > :last-child { margin-bottom: 0; }
/* Type discipline (issue #1562, item 4): a card is a compact component, so
   its heading shares the body tier and earns its rank on weight alone. That
   is what holds the home page to four rendered sizes - h1, h2, body/lead,
   small - instead of the five it grew to. Site-wide on .card so cards read
   identically everywhere (reuse-first); a section's own <h3> subhead outside
   a card still steps up to --text-lg. */
.card h3 { font-size: var(--text-base); }
/* /faq/ question headings (issue #2888, Maria's ruling): group headings now
   take the h2 level in the page outline, so each question steps down to h3 -
   but must stay pixel-identical to the h2 it replaces (same card, same
   reader expectation). Overrides the type-discipline rule directly above:
   deliberate exception for this one card, not a second heading scale.
   Two-class selector (.card .faq-question) so it wins on specificity alone,
   regardless of source order. */
.card .faq-question { font-size: var(--text-xl); line-height: 1.2; margin: 0 0 var(--space-3); color: var(--ink); }
/* Tips & tricks cards (/tips/, issue #3931). The page is a flat, NUMBERED
   list of cards in data order (Maria's ruling: at a cap of 8 tips, grouping
   is chrome). The outer <ol> carries the 1-8 numbering; each <li> is a
   standard .card, so a tip card reuses the site's one card look rather than
   inventing a snowflake. Kept a plain block list (not grid) so the ordinal
   ::marker still paints. */
.tip-list { list-style: decimal; padding-left: 2.25rem; margin: 0; }
.tip-list > li { margin-bottom: var(--space-3); }
.tip-list > li:last-child { margin-bottom: 0; }
.tip-list > li::marker { color: var(--accent); font-weight: var(--weight-bold); }
/* Headline reuses the .faq-question scale exactly (the one ratified
   card-heading exception directly above) - same reader expectation on the
   www card and the docs page. */
.card .tip-headline { font-size: var(--text-xl); line-height: 1.2; margin: 0 0 var(--space-3); color: var(--ink); }
.tip-steps { line-height: 1.5; }
.tip-payoff { font-weight: var(--weight-bold); margin-top: var(--space-2); }
/* ChordPro sample block (home page + /chordpro/), was an inline style
   duplicated on both pages (#1562). Mono is confined to this one card - it
   is the only place on a marketing page where the letterforms are the
   point. */
.chordpro-sample {
  font-family: 'JetBrains Mono', Consolas, Menlo, 'Courier New', monospace;
  white-space: pre-wrap;
}
/* /pricing/ recommended-tier marker (R5, issue #976) - an accent border and a
   small filled pill, not a banner (compact-UI veto applies to plan cards too).
   Border reuses --accent, already verified against surface-raised in
   contrast-check.mjs's UI_PAIRS; badge reuses the on-accent/accent-strong
   pair already verified there as btn-primary text. */
.card-recommended { border-color: var(--accent); border-width: 2px; }
.badge-recommended {
  display: inline-block;
  background: var(--accent-strong);
  color: var(--on-accent);
  border-radius: 6px;
  padding: 0.15rem 0.65rem;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  margin-bottom: var(--space-2);
}
/* /download/ platform card (issue #1983, Jason's ruling: "make them smaller,
   MUCH SMALLER"). Replaces the #1973 grid, which put four tall .card blocks
   in .grid-2 - stacked icon, heading, status pill, prose, button and a
   trailing link, roughly 300px of card each, so a phone got a full screen of
   scrolling to reach iOS. The per-platform status pill went with it (its rule
   used to live right here): all four platforms ship now, so four identical
   badges would be noise, and availability is stated once above the grid
   instead. design-ruling-1983.test.mjs greps this file for the retired
   selector name, so do not name it in this comment.

   The compact-UI standing veto in CLAUDE.md is the whole point here - the
   card is reduced to the four things a reader needs (which platform, what
   they get, the button, one footnote where a platform genuinely has a second
   step) and nothing else. */
.grid-4 { display: grid; gap: var(--space-3); grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* 4-up on desktop, 2-up from tablet down. The 1-col fallback is deliberately
   pushed to a very narrow floor (360px): two of these cards fit side by side
   on any mainstream phone, which is what keeps the whole grid on one screen
   instead of scrolling - the specific complaint in #1983. */
@media (max-width: 900px) {
  .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 360px) {
  .grid-4 { grid-template-columns: 1fr; }
}
/* #2563 item 12: five platform cards no longer fit .grid-4's 4-column
   contract without orphaning Linux alone on row 2. A modifier bumps the
   desktop/tablet column counts up by one for this one caller, at the same
   breakpoints as .grid-4 above, without touching .grid-4's own rules (which
   design-ruling-1983.test.mjs still pins to 4/2/1 for any future 4-item
   caller). */
.grid-4.grid-platforms { grid-template-columns: repeat(5, minmax(0, 1fr)); }
@media (max-width: 1100px) {
  .grid-4.grid-platforms { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
  .grid-4.grid-platforms { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* #3021 (Codex gap): the 360px floor below was set for the old
   intrinsically-sized CTAs, but the full-width .card-platform .btn rule
   above needs the LABEL to fit one line, not just the button. At 2 columns,
   available text width works out to (viewport/2) - 78.8px: half the
   container (viewport - 2rem side inset, gap 16px split across 2 columns)
   minus .card-platform's padding+border (2 * (16px --space-3 + 1px
   border) = 34px) minus .btn-sm's own horizontal padding (2 * 0.65rem =
   20.8px). The widest label, Android's "1. Join early access" at .btn-sm's
   14.4px/600-weight Inter, measures roughly 127-140px depending on hinting -
   so the 2-column layout needs upwards of (127..140 + 34 + 20.8) * 2 =
   ~363-390px of viewport just to break even, with zero margin for
   measurement error. That band sits squarely inside "roughly 361-420px",
   which is exactly where Codex caught the wrap. Moving the 1-column floor
   to 480px clears the widest-label math with real headroom (480/2 - 78.8 =
   161.2px available vs. ~140px needed) instead of sitting hairline-close to
   a font-metric estimate, the same margin-of-safety approach as #3022's
   950px header breakpoint. This trades 2-up density on the 360-480px band
   for the actual point of #3021 - a uniform, always-one-line CTA - which is
   the same tradeoff Maria's ruling made everywhere else on this page. */
@media (max-width: 480px) {
  .grid-4.grid-platforms { grid-template-columns: 1fr; }
}
/* #3021: real air between the trust line above and the grid - scoped to
   .grid-platforms alone, never a .trust-line margin change (the homepage
   hero reuses .trust-line too and must not move). */
.grid-platforms { margin-top: var(--space-3); }
/* Padding now matches the site-wide .card - issue #2012 brought every card
   down to --space-3, which is where this one already was. The rule stays
   because #1983's guard test names it, and the paragraph collapse below is
   still this card's own: a platform card is a button with a label, not a
   piece of prose. */
.card-platform { padding: var(--space-3); }
.card-platform p { margin: 0 0 var(--space-2); }
.card-platform p:last-child { margin-bottom: 0; }
/* #3021: every card's CTA becomes an identical-width, identical-height bar
   instead of five intrinsically-sized buttons (the iOS one wrapped to two
   lines). Scoped to .card-platform so it never touches .btn elsewhere on
   the site. Rectangular small-radius shape unchanged (R-A stands). */
.card-platform .btn { display: block; width: 100%; text-align: center; }
/* break-word, NOT break-all: the only long token in these cards is
   "bandbinder.exe", which fits a 2-up column on any mainstream phone.
   break-all shattered it mid-word ("bandbinder.ex / e.") even on desktop,
   where there was room to spare. */
.card-platform code { overflow-wrap: break-word; }
/* Mark and platform name share ONE line. The old card spent a full row on a
   stacked icon and another on the heading; collapsing those two rows into one
   is the single biggest height saving in the card.

   Still a real <h3> (Copilot review, PR #1988): the height came from the
   stacked layout, never from the element, so there is nothing to gain by
   demoting it to a <p> - and a screen reader loses the per-card landmark that
   makes this grid navigable. .card h3 already holds it to --text-base and the
   h3 element rule already supplies the bottom margin, so it needs no size or
   spacing of its own here. #2038: the flex-row rule itself now lives on the
   shared .card-head/.platform-head group above (extract, never duplicate) -
   the Why cards on the homepage reuse this same pattern. */
/* Sizing and color live here, never inline on the SVGs in
   shared/includes/platform-marks.njk, so one edit moves all four marks.
   Reuses --accent exactly as .card-icon does (#2990 retune, crew-reviewer
   gap 2: --accent-strong failed 2.90:1 here, see .card-icon's comment).
   #2435 (Jason: "make them larger"): 28px, up from 20px - these five are
   each card's platform identity, not bullet markers, and 20px starved all
   of them. */
.platform-mark { width: 28px; height: 28px; flex: none; color: var(--accent); }
.platform-note { font-size: var(--text-sm); opacity: 0.75; }
/* #2563 item 13: ragged CTA baseline - blurb length varies by platform (e.g.
   macOS's one-line "DMG - drag to Applications." vs Android's explicit
   two-line note), which pushed the Download button up or down per card.
   Reserving two lines' worth of height on the first note (the blurb, always
   the first <p> in the card) keeps every button starting at the same
   offset regardless of how many lines the blurb actually wraps to. */
.card-platform p:first-of-type { min-height: 3.2em; }
/* #2301: per-card trust row - one factual signing claim per platform.
   A quiet line, never a pill or seal (R-A; #1983).
   #3021: margin-top: auto pins the row to the card's bottom edge - paired
   with .card-platform's flex-column rule below, this is what lines the four
   trust rows up across the desktop row regardless of how many lines each
   card's own content wraps to.
   #3021 QA follow-up (Julia): every trust row is a <p>, and .card-platform p
   above is (0,1,1) - a class plus a type - which beats .platform-trust's
   (0,1,0) and, because it's the margin shorthand, re-declares margin-top: 0
   regardless of source order. Selector raised to .card-platform p.platform-trust
   (0,2,1) so it wins the cascade; the rendered row now actually pins to the
   bottom edge. */
.card-platform p.platform-trust { display: flex; align-items: flex-start; gap: 0.35rem; font-size: var(--text-sm); opacity: 0.75; margin-top: auto; }
/* #2990 retune (same --accent-strong-as-foreground defect as .card-icon/
   .platform-mark above, found in the same audit): identical 2.90:1 failure
   on --surface-raised, same --accent fix.
   #3021 (Jason: trust shields too small to read as shields): 14px -> 18px.
   margin-top trimmed to match - the bigger glyph needs less of a nudge to
   sit level with the text's cap height. */
.trust-mark { width: 18px; height: 18px; flex: none; color: var(--accent); margin-top: 0.1em; }
/* #3021: column flex so .platform-trust's margin-top: auto above can push
   the trust row to the bottom edge of every card, balancing the four rows
   across the desktop row regardless of each card's own content length. A
   second rule for this selector (not folded into the padding rule above)
   because design-ruling-1983.test.mjs pins that rule to padding alone. */
.card-platform { display: flex; flex-direction: column; }
/* #2192 scope item 7: Windows/macOS install-troubleshooting notes on
   /download/, native <details> so they read the same collapsed-by-default
   way the old releases-repo README's own <details> block did - a small gap
   above separates the closed summary from the download button so it never
   reads as part of that button's own caption. */
.card-platform details.platform-note { margin-top: var(--space-1); }
.card-platform details.platform-note summary { cursor: pointer; }
/* /pricing/ plan-card price line (#1300) - founder price is the big number
   per the ratified #1168 ruling, list price struck through beside it and
   visually de-emphasized. .price-founder reuses --text-xl (the h2 size,
   already the "big" step on the type scale) rather than inventing a new
   size - the scale in tokens.css is the only set of sizes this site may
   use. */
.price-display { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-2) 0; }
/* Issue #2659 S4-1 (Maria's ruling): the struck list price and the founder
   price are two separate <p class="price-display"> blocks - the markup shape
   #1480 and #1495 ratified, and their guard tests match it literally, so it
   stays untouched. #2012's inline-flex flowed both blocks onto one line, but
   a grid-3 card's inner width sits right at that pair's wrap threshold, so
   Band Plan, Solo Plus and the add-on card each wrapped differently. Back to
   `display: flex` (a block again) so the two lines stack in source order at
   every width; `flex-wrap: nowrap` (overriding base .price-display's `wrap`)
   welds "founder pricing" to the founder price it qualifies - that pair is
   ~236px and fits down to 320px. Direct-child selector on purpose: it catches
   exactly the four plan/add-on cards, and never the cost-comparison rows
   (price-display nested in .grid-2) or /billing/'s picker (nested in
   .billing-plan-option). */
.card > .price-display { display: flex; flex-wrap: nowrap; margin: 0 0 var(--space-1) 0; }
/* Zeroes the gap between the struck list line and the founder-price line so
   the two blocks read as one stacked lockup; the founder line keeps its own
   --space-1 before the following "or $X/year" note. */
.card > .price-display:has(+ .price-display) { margin-bottom: 0; }
.price-display-stack { display: flex; flex-direction: column; align-items: flex-start; gap: 0; }
.price-founder { font-size: var(--text-xl); font-weight: var(--weight-bold); color: var(--ink); }
.price-list { font-size: var(--text-sm); opacity: 0.6; text-decoration: line-through; }
.price-label { font-size: var(--text-sm); opacity: 0.75; }
/* #1495 (Maria's ruling): point-of-decision "founder pricing" caption, rides
   the same line as .price-founder - not .price-label, which now names the
   price source (OnSong/BandBinder) elsewhere on this page, a different job. */
.price-caption { font-size: var(--text-sm); font-weight: var(--weight-regular); opacity: 0.7; }
/* /billing/ (#1462) - trivial glue only, reuses .card/.stack/.price-display
   everywhere else. One compact row per band, one inline status/error line
   shared by every row state, and a two-option plan picker inside a native
   <details> (no modal).

   align-items: flex-start (issue #1516 S4-1, was center) - when the manager+
   none row's picker <details> opens, the right column grows to ~250px while
   the left column stays two short lines; centering floated the band name to
   the row's vertical middle with ~100px of dead air above it, reading as a
   rendering glitch. The other three row states are unaffected either way. */
.billing-row { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
/* Issue #2636: a flex item's default min-width is its content's min-content
   width, not 0 - once the picker's price-label grew real badge chrome, its
   longest unbroken phrase ("Additional band - yearly") became wide enough
   to refuse to shrink at phone widths, so the whole row overflowed the
   viewport instead of wrapping. min-width: 0 on the row's own direct
   children lets the grid/flex wrapping already in place (.billing-plan-
   option's minmax(0, 1fr) track, .price-display's flex-wrap) actually take
   over below that width. */
.billing-row > div { min-width: 0; }
.billing-row-status { font-size: var(--text-sm); margin: 0; opacity: 0.85; }
.billing-row-status[data-state='error'] { color: var(--ink); font-weight: var(--weight-semibold); opacity: 1; }
/* Row-state badges + left rail (issue #2636): an Active paid band used to
   render as plain muted text, identical in weight to a "no plan yet" row -
   the four states (active/lapsed/free-sync-none/member-managed) now carry a
   badge next to the band name, so state scans at a glance instead of
   requiring the status prose to be read. billing-view.js's `stateBadge`
   emits ONE class family (`.state-badge` + a modifier that only sets the
   tint custom property) - the same recipe specced for the admin usage
   report's badges (#2634): ~15% alpha tinted background, tinted text, 1px
   tinted border, 4px radius, uppercase micro-label. */
.billing-row-head { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-1); }
.state-badge {
  display: inline-block;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  line-height: 1.5;
  padding: 0 0.4em;
  border-radius: 4px;
  background: color-mix(in srgb, var(--badge-tint) 15%, transparent);
  color: var(--badge-tint);
  border: 1px solid color-mix(in srgb, var(--badge-tint) 45%, transparent);
}
/* active = green, lapsed = amber, free/none = gray (gate-aware the same way
   the status line is, #1500), tier = a second, neutral violet badge. */
.state-badge--active, .state-badge--save { --badge-tint: var(--ok); }
.state-badge--lapsed { --badge-tint: var(--warn); }
.state-badge--free, .state-badge--none { --badge-tint: var(--muted); }
.state-badge--tier { --badge-tint: var(--accent); }
/* 3px left rail in the state tint, visible before any text is read - a
   .billing-row is already a .card, so this narrows just its left edge
   rather than restating the whole border. Free/none rows intentionally
   have no override here, so their rail stays transparent per spec. */
.billing-row { border-left: 3px solid transparent; }
.billing-row--active { border-left-color: var(--ok); }
.billing-row--lapsed { border-left-color: var(--warn); }
/* Plan row layout (#1540 S3-1): a grid, not a flex row, because the radio
   must line up with the founder price line (child 3) while the struck list
   price (child 1) and blurb (child 4) share its left edge one row above/
   below - a flex row with align-items can only align the radio to the WHOLE
   stack's edge or center, never to one interior line. billing-view.js's
   planOptionRow() always emits this exact 4-child shape; nth-child below
   places each piece by that fixed position, not by class, since child 1 and
   3 share the .price-display class (#1462's single-source price markup). */
/* minmax(0, 1fr), not a bare 1fr (issue #2636): once the price-label grew
   real badge chrome, its content set the grid track's min-content width and
   pushed the whole card past the viewport at phone widths instead of
   wrapping - minmax(0, ...) lets the track (and the price-display flex row
   inside it) shrink and wrap normally again. */
.billing-plan-option { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; column-gap: var(--space-2); row-gap: 2px; }
.billing-plan-option > :nth-child(1) { grid-column: 2; grid-row: 1; }
.billing-plan-option > input[data-plan-radio] { grid-column: 1; grid-row: 2; }
.billing-plan-option > :nth-child(3) { grid-column: 2; grid-row: 2; }
.billing-plan-option > .billing-plan-blurb { grid-column: 2; grid-row: 3; }
/* #1540 S4-1: .price-display's site-wide vertical margin (--space-2 top and
   bottom, sized for /pricing/'s spacious cards) doubled the row-gap above on
   this dense two-line-per-plan picker - 482px tall at 320px. Zeroed here
   only (scoped to .billing-plan-option, /pricing/'s cards untouched);
   spacing comes from row-gap alone. */
.billing-plan-option .price-display { margin: 0; }
/* #1540 S4-2: own class instead of reusing .billing-row-status, which also
   carries the unrelated [data-state='error'] variant - a marketing blurb
   inheriting an error-status hook is a trap for the next edit. */
.billing-plan-blurb { font-size: var(--text-sm); margin: 0; opacity: 0.85; }
/* Annual saver row (#1492): show only the row for the currently-selected plan.
   Solo Plus value is hidden by default (band is the default-checked plan
   radio). CSS :has() toggles values when plan selection changes without JS. */
.billing-cadence-annual { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); }
/* Same phone-width overflow fix as .billing-plan-option's grid track above
   (issue #2636): a flex item's default min-width is its content's
   min-content width, which the price-label badge inflated - min-width: 0
   lets this column shrink and its own price-display rows wrap instead of
   pushing the card past the viewport. */
.billing-cadence-annual [data-for-plan] { min-width: 0; }
.billing-cadence-annual [data-for-plan='solo_plus'] { display: none; }
.billing-plan-picker:has(input[value='solo_plus'][data-plan-radio]:checked) .billing-cadence-annual [data-for-plan='band'] { display: none; }
.billing-plan-picker:has(input[value='solo_plus'][data-plan-radio]:checked) .billing-cadence-annual [data-for-plan='solo_plus'] { display: inline; }
/* The yearly option's own visible content (issue #2636) - independent of the
   outer [data-for-plan] span's own show/hide `display` value above, so that
   toggle can stay a plain inline/none pair while this inner element still
   lays out its three lines (struck price, founder price + label, blurb)
   exactly like planOptionRow's cards do. */
.billing-yearly-plan { display: flex; flex-direction: column; gap: 2px; }
/* The three plan-picker options read as real cards, permanently, whether
   selected or not (issue #2636 problem 2): background one step lighter than
   the row (.card's own --surface-raised -> --surface-highest), a visible
   1px border, and real padding/radius instead of the old "just enough
   negative-margin padding to host a selection tint" treatment. One recipe
   for lead tier, Solo Plus monthly (both .billing-plan-option) AND the
   yearly option (.billing-cadence-annual) - never a per-card snowflake. */
.billing-plan-option,
.billing-cadence-annual {
  border-radius: 6px;
  padding: var(--space-2);
  margin: 0;
  background: var(--surface-highest);
  border: 1px solid color-mix(in srgb, var(--ink) 15%, transparent);
}
/* 8px gap between cards (issue #2636 spec: 8-10px) - its own wrapper so the
   outer .stack keeps a full --space-3 gap before "Continue to checkout",
   which needs clear air below the card stack, not the cards' own tight one. */
.billing-plan-cards { display: grid; gap: var(--space-1); }
/* Plan-name label reads as a small neutral badge inside a picker card only
   (issue #2636) - scoped here rather than restyling .price-label itself,
   which /pricing/'s OnSong-vs-BandBinder comparison rows also use as a
   plain, unboxed caption. */
.billing-plan-option .price-label,
.billing-cadence-annual .price-label {
  display: inline-block;
  opacity: 1;
  padding: 0 0.4em;
  border-radius: 4px;
  background: color-mix(in srgb, var(--ink) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--ink) 25%, transparent);
}
/* Founder price is the big number a manager scans for first - tabular digits
   keep it from jittering as the plan/cadence selection swaps in a different
   width of number (issue #2636). */
.billing-plan-option .price-founder,
.billing-cadence-annual .price-founder {
  font-variant-numeric: tabular-nums;
}
/* #1543: selection is row chrome, not opacity - the web port of the app's
   _SelectableRow (plans_sheet.dart:495-540). Exactly one row carries the
   chrome: the checked plan row at monthly cadence, the annual saver row at
   annual. Clearing a row's selection makes it ORDINARY AND EQUAL to its
   sibling (the app's _planRowSelected contract, :367-372) - never dimmer
   than it. Selection changes the card's border to violet and adds a faint
   violet fill (issue #2636) - it never ADDS chrome an unselected card
   lacks, since every card already carries its own border/background above. */
.billing-plan-picker:has(input[data-cadence-radio][value='monthly']:checked) .billing-plan-option:has(input[data-plan-radio]:checked),
.billing-cadence-annual:has(input:checked) {
  background: color-mix(in srgb, var(--accent-strong) 16%, transparent);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
/* Brand the radios (they render browser-default blue today - the loudest,
   most off-brand pixel in the picker). Violet demotes them to secondary
   indicators beneath the row chrome, where they belong. */
.billing-plan-picker input[type='radio'] {
  accent-color: var(--accent);
}
/* The three sign-in gateways (issue #1838, Maria's unified ruling; Jason's
   #1841 overrule). One geometry class, three dresses - extracted rather than
   copied three times, so Google, Apple and Email can never drift apart on
   height, radius, gap or type. Each button keeps ONLY its own official
   fill/stroke/label colour, which is the whole point of an IDP button.

   Icon-only equal squares as of issue #2080, which brings www into line with
   the app's own ratified presentation (issue #1921, Maria's redesign): ONE
   "Sign in with:" label said once, then three equal branded squares. The app
   had already moved; www was still on #1838's stacked full-width buttons, so
   the two surfaces read as two different products at the same moment in the
   same flow.

   The 4px radius is not a taste call either - it is Jason's standing veto on
   stadium/pill shapes, already ratified for the app on issue #1851 ("no
   stadium/pill button shapes anywhere in the app, ever") and mirrored here.
   The old 999px was a literal pill that sidestepped #1562's site-wide pill
   ban by hardcoding the value after that ruling deleted the pill token. */
.btn-provider {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Square, exact - matching the app's 34dp desktop square. Height and width
     are set together so a longer label can never stretch one button wider
     than its neighbours; equal weight is structural, not hand-tuned. */
  width: 34px;
  height: 34px;
  min-height: 0;
  padding: 0;
  border-radius: 4px;
  font-size: var(--text-sm);
}
/* The app uses a 44dp square on touch-first platforms. Same reason here: a
   34px target is fine for a mouse and too small for a thumb. */
@media (pointer: coarse) {
  .btn-provider { width: 44px; height: 44px; }
}
.btn-provider svg { flex: none; }
/* One label for the row, replacing the three repeated "Sign in with X"
   labels - the app's #1921 layout verbatim. Each button keeps its full label
   as .sr-only text, so the accessible name is unchanged from when it was
   visible (the app does the same via Semantics(label:)). */
.billing-signin-label { font-size: var(--text-sm); color: var(--muted); }
/* Official Google-branded sign-in button (issue #1692; app precedent #1630).
   The colours are Google's dark-neutral variant, NOT brand tokens - an IDP
   button deliberately wears Google's chrome, not ours; that recognisability
   is the whole point. www is dark-always so only the dark variant ships.
   If a light surface ever gains a sign-in: fill #FFFFFF, stroke #747775,
   label #1F1F1F. Pill radius is literal and local - it is Google's (and the
   app's) button shape, not a revival of the retired site pill token (#1562). */
.btn-google {
  background: #131314;
  border: 1px solid #8e918f;
  color: #ffffff;
}
.btn-google:hover:not(:disabled) { background: #262627; }
/* Official Sign in with Apple button, WHITE variant - one of Apple's exactly
   two permitted fills, never tinted or recoloured. Switched from the black
   variant on issue #2080 to match the app, which made the same switch on
   issue #1921 for a concrete reason: black-on-dark is the invisibility bug
   #1924, and white already reads as a button against a dark surface with no
   border needed. www is dark-always, so it has exactly that problem.
   Border is transparent, not absent, so the button occupies the identical
   box as the bordered Google one. */
.btn-apple {
  background: #ffffff;
  border: 1px solid transparent;
  color: #000000;
}
.btn-apple:hover:not(:disabled) { background: #e6e6e6; }
/* Our own brand slot - the only one of the three that wears BandBinder indigo,
   matching the app's Email FilledButton on scheme.primary. Label and mark are
   --on-accent - white as of #2990's #4650e8 retune (5.88:1; the token's own
   note has the history), and this is the one button whose colours we owe to
   our own contrast rules rather than to a third party's brand book. */
.btn-email-signin {
  background: var(--accent-strong);
  border: 1px solid transparent;
  color: var(--on-accent);
}
.btn-email-signin:hover:not(:disabled) { background: color-mix(in srgb, var(--accent-strong) 88%, white); } /* #2990: see .btn.btn-primary:hover */
/* Email sign-in on /billing/ (issue #1838). The gateways stack, equal width
   and equal height, in the app's own order (Google, Apple, Email) - the
   billing counter and the app's signed-out card have to read as the same
   product. Capped well under the form width below it: three full-card-width
   slabs on a desktop card is the dead space the tight-UI veto exists to stop.
   Input fields in the two auth steps are capped at the support form's
   440-480px band so no field stretches across a desktop card. */
/* A centered row, not a stack (issue #2080). Equal widths no longer need a
   single-column grid to enforce them: the squares are a fixed size, so they
   are equal by construction at any container width and the row never needs a
   width/stack fork. `flex-wrap` is belt-and-braces for a very narrow viewport
   with the label in front. */
.billing-providers { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: var(--space-2); }
/* #2702 round 2: the 440px cap belongs to the INPUT, not the form. On the
   form it also caged the "We emailed a code to ..." status sentence, which
   wrapped mid-line on a desktop card with hundreds of px to spare. The cap
   moves to .billing-auth-field so the email input keeps its 440px band while
   text lines run the card width and only wrap when the viewport truly is
   narrow. */
.billing-auth-form { display: grid; gap: var(--space-2); }
.billing-auth-heading { font-size: var(--text-sm); font-weight: var(--weight-semibold); margin: 0; }
.billing-auth-field { display: grid; gap: var(--space-1); max-width: 440px; }
.billing-auth-field label { font-size: var(--text-sm); font-weight: var(--weight-semibold); }
.billing-auth-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
/* The 6 digits are the content here, so they get the room: wide tracking,
   centered, and only as wide as the code needs (the address field above it
   still fills the form). #2702: 12ch was measuring the digits alone - the
   0.4em letter-spacing adds a gap after every character too, and that gap
   ate into the same box, so the trailing digits clipped/scrolled on some
   displays. 16ch leaves room for the tracking as well as the "NNN NNN"
   content. */
.billing-code-input { max-width: 16ch; text-align: center; letter-spacing: 0.4em; font-size: var(--text-lg); }
/* /billing/ hero (issue #1516 S4-2) - the default `section { padding:
   var(--space-6) 0 }` on both the hero and the section that follows it
   stacks to ~96px of dead air above the band list. Fine on /pricing/'s
   multi-card grid; wasted on this page's dense list of rows - the site's
   tightest-content page had its loosest top. Halved on both sides so the
   hero reads tight, like /pricing/'s. */
.billing-hero { padding-bottom: var(--space-3); }
.billing-hero + section { padding-top: var(--space-3); }
/* No product shot is ever CSS-stretched past its own 1x pixels (issue #1562,
   item 5 - "no upscaling ever"). This rule used to be width:100% alone, so
   every shot filled its grid column: a 436px-wide asset painted across a
   528px column, and at DPR 1 the browser picked the 1x source and upscaled
   it 21%. That, not the captures, is why every product image on the site
   read blurry. The cap is per-image (screenshot.njk emits max-width at the
   asset's own intrinsic width), so a shot downscales on narrow viewports
   and stops growing at 1:1 on wide ones. Centering absorbs the slack the
   cap leaves in a wider column; it also subsumes the old fixed-width
   variant class, which existed only to stop the one no-2x asset from
   stretching - now nothing stretches. */
.screenshot-shot {
  width: 100%;
  height: auto;
  margin-inline: auto;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}
.data-table { width: 100%; border-collapse: collapse; }
.data-table th, .data-table td { text-align: left; padding: var(--space-1) var(--space-2); border-bottom: 1px solid var(--border); vertical-align: top; line-height: 1.4; }
.data-table th { color: var(--ink); font-size: var(--text-sm); }
/* contain: paint (issue #1193) - without it, a table with position: sticky
   cells inside this scroll container leaks its unclamped layout width into
   documentElement's scrollWidth, dragging the whole page sideways instead of
   scrolling only inside this box. contain: paint gives the container its own
   containing block for the sticky descendants and clips their paint/layout
   contribution to this box, so the leak can't reach html. Harmless on
   non-sticky tables (e.g. /page-turners/) - same container, same class. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; contain: paint; }
/* Sticky first column (issue #1162, Maria's ruling on #1161; widened from
   .table-compare to every .table-scroll .data-table by issue #3467's
   ruling - a scrolled-in cell is never unlabelled, on /pricing/'s compare
   table or /page-turners/'s and /chordpro/'s plain data tables alike).
   z-index keeps the pinned cell painted above scrolling siblings. */
.table-scroll .data-table th:first-child:not(.table-group th),
.table-scroll .data-table td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--surface-raised);
}
/* Group subhead rows (The app / Your data / Your devices / Your band) span
   every column via colspan, so their lone <th> - though first-child of its
   row - is excluded from the sticky rule above and scrolls with the rest
   of the row instead of pinning to the left. */
.table-compare .table-group th {
  background: var(--surface-raised);
  font-weight: var(--weight-semibold);
}
.table-compare { font-size: var(--text-sm); }
/* Phone-width fix (issue #1192, Maria's ruling on #1161 item 6; widened
   from .table-compare to every .table-scroll .data-table by issue #3467's
   ruling): the sticky first column above (unconstrained width) can be
   wider than the whole .table-scroll viewport at phone widths (273-383px
   at 320-430px), hiding every other column at every scroll position with
   no cue that more content exists. Table stays a table on phones
   (card-stacking and a JS plan-switcher were both rejected for
   .table-compare - see #1192) - the first column just gets capped and
   other cells get a floor so nothing shatters illegibly, plus a right-edge
   shadow signalling there's more to scroll to. The sitewide nowrap rule
   this fix used to have to undo per-table is gone (#3467) - it hid content
   on plain .data-table scrollers with zero affordance - so there's nothing
   left to override back to normal. */
@media (max-width: 800px) {
  .table-scroll .data-table th:first-child:not(.table-group th),
  .table-scroll .data-table td:first-child { max-width: min(45vw, 16rem); }
  .table-scroll .data-table td { min-width: 7em; }
  .table-scroll .data-table th:first-child:not(.table-group th),
  .table-scroll .data-table td:first-child {
    box-shadow: 8px 0 8px -8px color-mix(in srgb, var(--ink) 30%, transparent);
  }
}
/* Long qualitative summary text (the merged "every plan, forever" row)
   should wrap for readability instead of inheriting the mobile nowrap rule
   that keeps short data cells aligned. A prose cell opts out of the 7em
   floor above and gets the #1192 readability floor instead - selector must
   stay one class heavier than .table-scroll .data-table td (0,3,0 beats
   its 0,2,1) so #3467's generalised floor can never outrank it again
   (issue #3484, Maria's ruling). */
.table-scroll .data-table .cell-wrap { white-space: normal; min-width: 220px; }
/* Band Plan column: same accent treatment as .card-recommended (border,
   not a fill) so the table and the cards above it read as one language. */
.table-compare .mark-yes { color: var(--ok); font-weight: var(--weight-bold); }
.table-compare .mark-no { opacity: 0.45; }
.table-compare .col-recommended { border-left: 2px solid var(--accent); border-right: 2px solid var(--accent); }
.table-compare thead .col-recommended { border-top: 2px solid var(--accent); }
.table-compare tbody tr:last-child .col-recommended,
.table-compare tfoot .col-recommended { border-bottom: 2px solid var(--accent); }
/* Markdown-rendered tables (docs prose) get no .data-table/.table-scroll
   wrapper from the markdown source - display:block turns the table itself
   into the scroll container so a wide table narrows the page instead of
   forcing horizontal overflow on it. */
.docs-main table {
  display: block;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 100%;
  border-collapse: collapse;
}
.docs-main th, .docs-main td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); vertical-align: top; }
.docs-main th { color: var(--ink); font-size: var(--text-sm); }
/* Fenced code blocks (ChordPro/tab examples) can run far wider than any
   viewport - scroll the block instead of blowing out the page width. */
pre {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 100%;
  padding: var(--space-3);
  border-radius: var(--radius);
  background: var(--surface-raised);
  border: 1px solid var(--border);
}
code, kbd, samp { font-family: 'JetBrains Mono', Consolas, Menlo, 'Courier New', monospace; overflow-wrap: break-word; }
pre code { overflow-wrap: normal; }
/* Invite band code (download.njk) - was an inline em-sized style
   (issue #986 AC3); letter-spacing stays an em (tracks the code's own size,
   not banned - only font-size em is). */
.invite-code { font-size: var(--text-lg); letter-spacing: 0.15em; }
/* Key chiclets (issue #1000; matches app KeyCap, #928 ruling):
   face with a top-light bevel on a solid side wall, one soft
   ambient shadow. Semantic <kbd> is the only hook. */
kbd {
  --kbd-sheen: 8%; /* top-light strength */
  --kbd-face: var(--surface-raised);
  --kbd-face-low: color-mix(in srgb, var(--surface) 55%, var(--surface-raised));
  --kbd-shadow: rgb(0 0 0 / 0.35);
  display: inline-block;
  min-width: 1.7em; /* single chars read square */
  padding: 1px 7px 2px;
  text-align: center;
  font-size: var(--text-xs); /* intentional --text-xs exception (tokens.css):
    kbd is a short UI label, not sentence text - see tokens.css comment */
  font-weight: var(--weight-bold); /* app legend weight */
  line-height: 1.3;
  white-space: nowrap;
  color: var(--ink);
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, #fff var(--kbd-sheen), var(--kbd-face)) 0%,
    var(--kbd-face) 28%,
    var(--kbd-face-low) 100%);
  border: 1px solid var(--border);
  border-radius: 5px;
  box-shadow:
    0 2px 0 var(--border), /* solid side wall */
    0 3px 4px var(--kbd-shadow); /* soft ambient shadow */
}
@media (prefers-color-scheme: light) {
  html.docs kbd {
    --kbd-sheen: 45%; /* app: 0.45 white overlay */
    --kbd-face: var(--surface-low); /* reads against white page */
    --kbd-face-low: color-mix(in srgb, var(--surface-highest) 40%, var(--surface-low));
    --kbd-shadow: rgb(0 0 0 / 0.18); /* app: 0.18 light shadow */
  }
}
/* Issue #1018 (Maria's ruling): a combo is bare adjacent <kbd> tags (no "+"
   text between them) - a small gap is all that separates them, matching the
   app's KeyCap Wrap(spacing: 3). */
kbd + kbd { margin-left: 3px; }
/* Dim connector text between key runs - "or" for alternatives, "/" for a
   mirrored pair - same grammar as the app's KeyCapsLabel connector style.
   Deliberately not kbd-prefixed (design-ruling-1000 AC1 forbids that class
   family) and not a kbd override - it styles the plain-text separator
   beside the chiclets, not a chiclet itself. */
.key-sep { opacity: 0.65; font-style: italic; }
/* Blog scaffold (issue #1308 phase 1, Maria's ruling on #1204). Index and
   post pages both run through marketing.njk (shared/layouts/blog-post.njk
   chains onto it) - h1/h2/p/.lead/.card/.stack/pre/code are already shared
   with the rest of the site; these are the only blog-specific rules. */
.blog-post-date { font-size: var(--text-sm); opacity: 0.75; margin: 0 0 var(--space-2); }
.blog-index { list-style: none; margin: 0; padding: 0; }
.blog-index-item h2 { margin-bottom: var(--space-1); }
.blog-index-item .blog-post-date { margin: var(--space-1) 0 0; }
.blog-empty { opacity: 0.75; }
/* Long-form prose rules (issue #2053, first real posts). No new fonts, colors
   or sizes - every value below is an existing token. The only thing a post
   page needs that a marketing page does not is a reading measure: .container
   is 1080px, which is right for a row of cards and about twice the comfortable
   line length for a paragraph of body copy. */
.blog-post-column { max-width: 68ch; }
.blog-post-body h2 { margin-top: var(--space-5); }
.blog-post-body ul { margin: 0 0 var(--space-3); padding-left: var(--space-3); }
.blog-post-body li { margin-bottom: var(--space-1); }
.blog-post-author { margin-top: var(--space-5); padding-top: var(--space-3); border-top: 1px solid var(--border); }
.blog-post-byline { font-weight: var(--weight-semibold); margin-bottom: var(--space-1); }
.blog-post-bio { font-size: var(--text-sm); opacity: 0.75; margin: 0; }
/* Changelog (issue #2192 Phase A, Maria's ruling): reuses .stack/.card, same
   as the blog index above - the only page-specific rules are the date line
   and the sticky-header anchor offset so a #v0-9-1 jump doesn't land the
   heading under .site-header (position: sticky, ~60px tall). The outer <ul>
   needs the same marker suppression .blog-index carries: its <li>s are cards,
   and default bullets render beside every card (#3239). */
.changelog-index { list-style: none; margin: 0; padding: 0; }
.changelog-date { font-size: var(--text-sm); opacity: 0.75; margin: 0 0 var(--space-2); }
.changelog-entry { scroll-margin-top: calc(60px + var(--space-3)); }
.changelog-empty { opacity: 0.75; }
/* #2715: /download/'s invitee-state header CTA now retargets to #platforms
   (an in-page anchor, not a page reload) - same sticky-header offset as
   .changelog-entry above, so the jump doesn't land the kicker under
   .site-header. */
#platforms { scroll-margin-top: calc(60px + var(--space-3)); }
/* /faq/ deep links (issue #2888, Maria's ruling): the TOC and any external
   page link straight at a question's #slug or a group's #anchor - same
   sticky-header offset as .changelog-entry/#platforms above, so the jump
   never lands the card or heading under .site-header. */
.faq-group-heading,
.faq-card,
/* /tips/ deep links (#3931): each tip card anchors on its #slug, same
   sticky-header offset so a jump never lands the card under .site-header. */
.tip-card { scroll-margin-top: calc(60px + var(--space-3)); }
/* Platform-availability line (issue #2251, Maria's ruling): silence in a
   changelog entry means "shipped on all four platforms" - an entry for a
   version cut on fewer than four carries this line first, full-opacity ink
   (already verified >= 4.5:1 on surface-raised) so it reads as fact, not a
   dimmed footnote. No new color pair to re-check in contrast-check.mjs. */
.changelog-availability { font-weight: var(--weight-semibold); margin: 0 0 var(--space-2); }
.docs-shell {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: 240px minmax(0, 1fr);
  width: min(1200px, calc(100% - 2rem));
  margin: 0 auto;
  padding: var(--space-5) 0;
}
.docs-sidebar { border-right: 1px solid var(--border); padding-right: var(--space-3); }
.docs-sidebar ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-1); }
.docs-sidebar h3:not(:first-child) { margin-top: var(--space-3); }
/* Current-page marker (issue #1915): bold + accent so the sidebar always
   shows where you are, matching the aria-current="page" attribute set by
   docs-page.njk when a nav.json entry equals the rendered page's url. */
.docs-sidebar a[aria-current="page"] { font-weight: var(--weight-bold); color: var(--accent, inherit); }
/* Docs index page card lists (R8, issue #976): the theme itself is not
   changing (docs stays prefers-color-scheme adaptive, DECISIONS.md row 40) -
   the actual bug is that these link lists render with raw browser-default
   bullets and underlines, which reads as unstyled/broken once the light
   surface stops hiding it. Scoped to html.docs .card so it never touches
   .card usage elsewhere on www (pricing calculator, feature cards, etc.).
   Applies in both themes - a deliberate list beats a raw one either way. */
html.docs .card ul {
  list-style: none;
  margin: 0 0 var(--space-2);
  padding: 0;
  display: grid;
  gap: var(--space-1);
}
html.docs .card a { text-decoration: none; }
html.docs .card a:hover { text-decoration: underline; }
.docs-main { min-width: 0; }
/* Heading permalinks (issue #3466, Maria's ruling): same sticky-header
   scroll offset as .changelog-entry/#platforms/.faq-card above, so a deep
   link into a docs section doesn't land the heading under .site-header.
   The `#` affordance is an EMPTY <a class="header-anchor"> (markdown-it-
   anchor's linkInsideHeader, aria-label for its name) - no text node at
   all, or Pagefind puts it in every search-result title (PR #3475). It is
   invisible until the heading is hovered or the link is keyboard-focused,
   and it sits OUT OF FLOW in the gutter to the LEFT of the heading, pinned
   to the first line:
   - out of flow, so it takes no space at rest and reveal is paint only,
     never layout (#3480's ruling, kept);
   - to the left, because any box past a heading's right edge - in flow or
     not, opacity 0 or not (opacity never removes a box from the scrollable
     overflow region) - reaches the viewport edge whenever the heading's
     last line runs long, and the whole document scrolls sideways (Julia,
     PR #3475, 440/480px). Start-side overflow is unscrollable by spec, and
     the glyph is one --text-base mark at every heading level (about 10px
     wide plus the 0.25rem gap), so it fits whole in the 1.5rem .docs-shell
     gap on desktop AND in the page's 1rem gutter under 1000px.
   The glyph is generated content sized on the pseudo, not the <a>, so the
   anchor keeps the heading's font metrics and the glyph sits on line one's
   baseline (#986: font-size consumes a token, never an em literal). */
.docs-main :is(h2, h3, h4) {
  position: relative;
  scroll-margin-top: calc(60px + var(--space-3));
}
.header-anchor {
  position: absolute;
  top: 0;
  right: 100%;
  margin-right: 0.25rem;
  opacity: 0;
  text-decoration: none;
  color: var(--accent);
  font-weight: var(--weight-regular);
}
.header-anchor::before {
  content: "#";
  font-size: var(--text-base);
}
.docs-main :is(h2, h3, h4):hover .header-anchor,
.header-anchor:focus-visible {
  opacity: 1;
}
.prev-next { display: flex; justify-content: space-between; margin-top: var(--space-5); gap: var(--space-3); }
@media (max-width: 1000px) {
  .docs-shell { grid-template-columns: 1fr; }
  .docs-sidebar { border-right: 0; border-bottom: 1px solid var(--border); padding-bottom: var(--space-3); }
}
